id, mimetype, kind and content

This commit is contained in:
2025-05-21 08:00:35 +01:00
parent a6f33f5de4
commit 90e68264d1
16 changed files with 791 additions and 0 deletions

13
chk/chk.go Normal file
View File

@@ -0,0 +1,13 @@
// Package chk is a convenience shortcut to use shorter names to access the lol.Logger.
package chk
import (
"github.com/mleku/transit/lol"
)
var F, E, W, I, D, T lol.Chk
func init() {
F, E, W, I, D, T = lol.Main.Check.F, lol.Main.Check.E, lol.Main.Check.W, lol.Main.Check.I,
lol.Main.Check.D, lol.Main.Check.T
}

61
content/content.go Normal file
View File

@@ -0,0 +1,61 @@
package content
import (
"bytes"
"encoding/base64"
"github.com/mleku/transit/chk"
"github.com/mleku/transit/errorf"
)
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"
type C struct {
Val []byte
Binary bool
}
// New creates a new mime.Type based on input string or bytes of value.
func New[V []byte | string](c V, binary bool) (content *C) {
content = &C{Val: []byte(c), Binary: binary}
return
}
func (c *C) MarshalJSON() (b []byte, err error) {
if c.Binary {
b = make([]byte, base64.URLEncoding.EncodedLen(len(c.Val))+2+len(BinaryPrefix))
copy(b[1:], BinaryPrefix)
base64.URLEncoding.Encode(b[1+len(BinaryPrefix):len(b)-1], c.Val)
} else {
b = make([]byte, len(c.Val)+2)
copy(b[1:], c.Val)
}
b[0] = '"'
b[len(b)-1] = '"'
return
}
func (c *C) UnmarshalJSON(b []byte) (err error) {
b = b[1:]
if bytes.HasPrefix(b, BinaryPrefix) {
c.Binary = true
// remove quotes and binary prefix
b = b[len(BinaryPrefix) : len(b)-1]
var n int
// we use AppendDecode to avoid needing to trim pad bytes
if c.Val, err = base64.URLEncoding.AppendDecode(c.Val, b); chk.E(err) {
err = errorf.E("error: decoding failed at %d: %s", n, err.Error())
return
}
c.Binary = true
} else {
b = b[:len(b)-1]
c.Val = b
c.Binary = false
}
return
}

50
content/content_test.go Normal file
View File

@@ -0,0 +1,50 @@
package content
import (
"bytes"
"encoding/json"
"testing"
"lukechampine.com/frand"
"github.com/mleku/transit/chk"
"github.com/mleku/transit/log"
)
func TestNew(t *testing.T) {
content := "this is some plain text"
log.I.F("%s", content)
c := New(content, false)
var err error
var b []byte
if b, err = json.Marshal(&c); chk.E(err) {
t.Fatal(err)
}
log.I.F("%s", b)
c2 := &C{}
if err = json.Unmarshal(b, c2); chk.E(err) {
t.Fatal(err)
}
log.I.F("%s", c2.Val)
if !bytes.Equal(c.Val, c2.Val) {
t.Fatal("failed to encode/decode")
}
bin := make([]byte, frand.Intn(100)+16)
if _, err = frand.Read(bin); chk.E(err) {
t.Fatal(err)
}
bin1 := New(bin, true)
if b, err = json.Marshal(bin1); chk.E(err) {
t.Fatal(err)
}
log.I.S(bin1)
log.I.S(b)
bin2 := &C{}
if err = json.Unmarshal(b, bin2); chk.E(err) {
t.Fatal(err)
}
log.I.S(bin2)
if !bytes.Equal(bin1.Val, bin2.Val) {
t.Fatal("failed to encode/decode")
}
}

12
errorf/errorf.go Normal file
View File

@@ -0,0 +1,12 @@
// Package errorf is a convenience shortcut to use shorter names to access the lol.Logger.
package errorf
import (
"github.com/mleku/transit/lol"
)
var F, E, W, I, D, T lol.Err
func init() {
F, E, W, I, D, T = lol.Main.Errorf.F, lol.Main.Errorf.E, lol.Main.Errorf.W, lol.Main.Errorf.I, lol.Main.Errorf.D, lol.Main.Errorf.T
}

26
event/event.go Normal file
View File

@@ -0,0 +1,26 @@
package event
import (
"github.com/mleku/transit/content"
"github.com/mleku/transit/id"
"github.com/mleku/transit/kind"
"github.com/mleku/transit/mime"
)
// E is an event on the transit protocol.
type E struct {
// Id is the hash of the canonical format of the event.E.
Id *id.I `json:"id" doc:"hash of canonical form of event in unpadded base64 URL encoding"`
// Mimetype is the type of data contained in the Content field of an event.E.
Mimetype *mime.Type `json:"mimetype" doc:"standard mimetype descriptor for the format of the content field"`
// Kind is a 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 *content.C
// Tags are a set of descriptors formatted as a key, value and optional further descriptors.
}
// C is the canonical format for an event.E that is hashed to generate the id.I.
type C struct {
}

17
go.mod Normal file
View File

@@ -0,0 +1,17 @@
module github.com/mleku/transit
go 1.24.3
require (
github.com/davecgh/go-spew v1.1.1
github.com/fatih/color v1.18.0
)
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
)

23
go.sum Normal file
View File

@@ -0,0 +1,23 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/klauspost/cpuid/v2 v2.0.12 h1:p9dKCg8i4gmOxtv35DvrYoWqYzQrvEVdjQ762Y0OqZE=
github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
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/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=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
lukechampine.com/frand v1.5.1 h1:fg0eRtdmGFIxhP5zQJzM1lFDbD6CUfu/f+7WgAZd5/w=
lukechampine.com/frand v1.5.1/go.mod h1:4VstaWc2plN4Mjr10chUD46RAVGWhpkZ5Nja8+Azp0Q=

52
id/id.go Normal file
View File

@@ -0,0 +1,52 @@
package id
import (
"encoding/base64"
"github.com/zeebo/blake3"
"github.com/mleku/transit/chk"
"github.com/mleku/transit/errorf"
)
const Len = 32
var EncodedLen = base64.RawURLEncoding.EncodedLen(Len)
// I is a blake3 hash encoded in base64 URL format.
type I struct {
Hash []byte
}
// New creates a new id.I based on a provided message bytes.
func New[V []byte | string](msg V) (i *I) {
sum := blake3.Sum256([]byte(msg))
i = &I{Hash: sum[:]}
return
}
func (i *I) MarshalJSON() (b []byte, err error) {
if len(i.Hash) != Len {
err = errorf.E("id must be %d bytes long, got %d", Len, len(i.Hash))
return
}
b = make([]byte, EncodedLen+2)
b[0] = '"'
b[len(b)-1] = '"'
base64.RawURLEncoding.Encode(b[1:], i.Hash)
return
}
func (i *I) 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.Hash = make([]byte, Len)
var n int
if n, err = base64.RawURLEncoding.Decode(i.Hash, b[1:EncodedLen+1]); chk.E(err) {
err = errorf.E("error: decoding failed at %d: %s", n, err.Error())
return
}
return
}

29
id/id_test.go Normal file
View File

@@ -0,0 +1,29 @@
package id
import (
"bytes"
"encoding/json"
"testing"
"github.com/mleku/transit/chk"
"github.com/mleku/transit/log"
)
func TestNew(t *testing.T) {
msg := "this is a test"
i := New(msg)
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.Hash, b)
i2 := &I{}
if err = json.Unmarshal(b, i2); chk.E(err) {
t.Fatal(err)
}
log.I.F("decode %0x", i2.Hash)
if !bytes.Equal(i.Hash, i2.Hash) {
t.Fatal("failed to encode/decode")
}
}

25
kind/kind.go Normal file
View File

@@ -0,0 +1,25 @@
package kind
// K is a name for the application/intent of the event (eg post, reply, auth, etc)
type K struct {
Val []byte
}
// New creates a new kind.K based on input string or bytes of value.
func New[V []byte | string](ks V) (k *K) {
k = &K{Val: []byte(ks)}
return
}
func (k *K) MarshalJSON() (b []byte, err error) {
b = make([]byte, len(k.Val)+2)
copy(b[1:], k.Val)
b[0] = '"'
b[len(k.Val)+1] = '"'
return
}
func (k *K) UnmarshalJSON(b []byte) (err error) {
k.Val = b[1 : len(b)-1]
return
}

30
kind/kind_test.go Normal file
View File

@@ -0,0 +1,30 @@
package kind
import (
"bytes"
"encoding/json"
"testing"
"github.com/mleku/transit/chk"
"github.com/mleku/transit/log"
)
func TestNew(t *testing.T) {
kind := "reply"
log.I.F("%s", kind)
k := New(kind)
var err error
var b []byte
if b, err = json.Marshal(&k); chk.E(err) {
t.Fatal(err)
}
log.I.F("%s", b)
k2 := &K{}
if err = json.Unmarshal(b, k2); chk.E(err) {
t.Fatal(err)
}
log.I.F("%s", k2.Val)
if !bytes.Equal(k.Val, k2.Val) {
t.Fatal("failed to encode/decode")
}
}

12
log/log.go Normal file
View File

@@ -0,0 +1,12 @@
// Package log is a convenience shortcut to use shorter names to access the lol.Logger.
package log
import (
"github.com/mleku/transit/lol"
)
var F, E, W, I, D, T lol.LevelPrinter
func init() {
F, E, W, I, D, T = lol.Main.Log.F, lol.Main.Log.E, lol.Main.Log.W, lol.Main.Log.I, lol.Main.Log.D, lol.Main.Log.T
}

18
lol/README.md Normal file
View File

@@ -0,0 +1,18 @@
# lol
location of log
This is a very simple, but practical library for logging in applications. Its
main feature is printing source code locations to make debugging easier.
## terminals
Due to how so few terminals actually support source location hyperlinks, pretty much tilix and intellij terminal are
the only two that really provide adequate functionality, this logging library defaults to output format that works
best with intellij. As such, the terminal is aware of the CWD and the code locations printed are relative, as
required to get the hyperlinkization from this terminal. Handling support for Tilix requires more complications and
due to advances with IntelliJ's handling it is not practical to support any other for this purpose. Users of this
library can always fall back to manually interpreting and accessing the relative file path to find the source of a log.
In addition, due to this terminal's slow rendering of long lines, long log strings are automatically broken into 80
character lines, and if there is comma separators in the line, the line is broken at the comma instead of at column80. This works perfectly for this purpose.

368
lol/log.go Normal file
View File

@@ -0,0 +1,368 @@
// Package lol (log of location) is a simple logging library that prints a high precision unix
// timestamp and the source location of a log print to make tracing errors simpler. Includes a
// set of logging levels and the ability to filter out higher log levels for a more quiet
// output.
package lol
import (
"fmt"
"io"
"os"
"runtime"
"strings"
"sync/atomic"
"time"
"github.com/davecgh/go-spew/spew"
"github.com/fatih/color"
)
const (
Off = iota
Fatal
Error
Warn
Info
Debug
Trace
)
var LevelNames = []string{
"off",
"fatal",
"error",
"warn",
"info",
"debug",
"trace",
}
type (
// LevelPrinter defines a set of terminal printing primitives that output with extra data,
// time, log logLevelList, and code location
// Ln prints lists of interfaces with spaces in between
Ln func(a ...interface{})
// F prints like fmt.Println surrounded []byte log details
F func(format string, a ...interface{})
// S prints a spew.Sdump for an enveloper slice
S func(a ...interface{})
// C accepts a function so that the extra computation can be avoided if it is not being
// viewed
C func(closure func() string)
// Chk is a shortcut for printing if there is an error, or returning true
Chk func(e error) bool
// Err is a pass-through function that uses fmt.Errorf to construct an error and returns the
// error after printing it to the log
Err func(format string, a ...any) error
// LevelPrinter is the set of log printers on each log level.
LevelPrinter struct {
Ln
F
S
C
Chk
Err
}
// LevelSpec is the name, ID and Colorizer for a log level.
LevelSpec struct {
ID int
Name string
Colorizer func(a ...any) string
}
// Entry is a log entry to be printed as json to the log file
Entry struct {
Time time.Time
Level string
Package string
CodeLocation string
Text string
}
)
var (
// Writer can be swapped out for any io.*Writer* that you want to use instead of stdout.
Writer io.Writer = os.Stderr
// LevelSpecs specifies the id, string name and color-printing function
LevelSpecs = []LevelSpec{
{Off, "", NoSprint},
{Fatal, "FTL", color.New(color.BgRed, color.FgHiWhite).Sprint},
{Error, "ERR", color.New(color.FgHiRed).Sprint},
{Warn, "WRN", color.New(color.FgHiYellow).Sprint},
{Info, "INF", color.New(color.FgHiGreen).Sprint},
{Debug, "DBG", color.New(color.FgHiBlue).Sprint},
{Trace, "TRC", color.New(color.FgHiMagenta).Sprint},
}
NoTimeStamp atomic.Bool
ShortLoc atomic.Bool
)
// NoSprint is a noop for sprint (it returns nothing no matter what is given to it).
func NoSprint(a ...any) string { return "" }
// Log is a set of log printers for the various Level items.
type Log struct {
F, E, W, I, D, T LevelPrinter
}
// Check is the set of log levels for a Check operation (prints an error if the error is not
// nil).
type Check struct {
F, E, W, I, D, T Chk
}
// Errorf prints an error that is also returned as an error, so the error is logged at the site.
type Errorf struct {
F, E, W, I, D, T Err
}
// Logger is a collection of things that creates a logger, including levels.
type Logger struct {
*Log
*Check
*Errorf
}
// Level is the level that the logger is printing at.
var Level atomic.Int32
// Main is the main logger.
var Main = &Logger{}
func init() {
// Main = &Logger{}
Main.Log, Main.Check, Main.Errorf = New(os.Stderr, 2)
SetLoggers(Info)
}
// SetLoggers configures a log level.
func SetLoggers(level int) {
Main.Log.T.F("log level %s", LevelSpecs[level].Colorizer(LevelNames[level]))
Level.Store(int32(level))
}
// GetLogLevel returns the log level number of a string log level.
func GetLogLevel(level string) (i int) {
for i = range LevelNames {
if level == LevelNames[i] {
return i
}
}
return Info
}
// SetLogLevel sets the log level of the logger.
func SetLogLevel(level string) {
for i := range LevelNames {
if level == LevelNames[i] {
SetLoggers(i)
return
}
}
SetLoggers(Trace)
}
// JoinStrings joins together anything into a set of strings with space separating the items.
func JoinStrings(a ...any) (s string) {
for i := range a {
s += fmt.Sprint(a[i])
if i < len(a)-1 {
s += " "
}
}
return
}
var msgCol = color.New(color.FgBlue).Sprint
// GetPrinter returns a full logger that writes to the provided io.Writer.
func GetPrinter(l int32, writer io.Writer, skip int) LevelPrinter {
return LevelPrinter{
Ln: func(a ...interface{}) {
if Level.Load() < l {
return
}
fmt.Fprintf(writer,
"%s%s %s %s\n",
msgCol(TimeStamper()),
LevelSpecs[l].Colorizer(LevelSpecs[l].Name),
JoinStrings(a...),
msgCol(GetLoc(skip)),
)
},
F: func(format string, a ...interface{}) {
if Level.Load() < l {
return
}
fmt.Fprintf(writer,
"%s%s %s %s\n",
msgCol(TimeStamper()),
LevelSpecs[l].Colorizer(LevelSpecs[l].Name),
fmt.Sprintf(format, a...),
msgCol(GetLoc(skip)),
)
},
S: func(a ...interface{}) {
if Level.Load() < l {
return
}
fmt.Fprintf(writer,
"%s%s %s %s\n",
msgCol(TimeStamper()),
LevelSpecs[l].Colorizer(LevelSpecs[l].Name),
spew.Sdump(a...),
msgCol(GetLoc(skip)),
)
},
C: func(closure func() string) {
if Level.Load() < l {
return
}
fmt.Fprintf(writer,
"%s%s %s %s\n",
msgCol(TimeStamper()),
LevelSpecs[l].Colorizer(LevelSpecs[l].Name),
closure(),
msgCol(GetLoc(skip)),
)
},
Chk: func(e error) bool {
if Level.Load() < l {
return e != nil
}
if e != nil {
fmt.Fprintf(writer,
"%s%s %s %s\n",
msgCol(TimeStamper()),
LevelSpecs[l].Colorizer(LevelSpecs[l].Name),
e.Error(),
msgCol(GetLoc(skip)),
)
return true
}
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)),
)
}
return fmt.Errorf(format, a...)
},
}
}
// GetNullPrinter is a logger that doesn't log.
func GetNullPrinter() LevelPrinter {
return LevelPrinter{
Ln: func(a ...interface{}) {},
F: func(format string, a ...interface{}) {},
S: func(a ...interface{}) {},
C: func(closure func() string) {},
Chk: func(e error) bool { return e != nil },
Err: func(format string, a ...interface{}) error { return fmt.Errorf(format, a...) },
}
}
// New creates a new logger with all the levels and things.
func New(writer io.Writer, skip int) (l *Log, c *Check, errorf *Errorf) {
if writer == nil {
writer = Writer
}
l = &Log{
T: GetPrinter(Trace, writer, skip),
D: GetPrinter(Debug, writer, skip),
I: GetPrinter(Info, writer, skip),
W: GetPrinter(Warn, writer, skip),
E: GetPrinter(Error, writer, skip),
F: GetPrinter(Fatal, writer, skip),
}
c = &Check{
F: l.F.Chk,
E: l.E.Chk,
W: l.W.Chk,
I: l.I.Chk,
D: l.D.Chk,
T: l.T.Chk,
}
errorf = &Errorf{
F: l.F.Err,
E: l.E.Err,
W: l.W.Err,
I: l.I.Err,
D: l.D.Err,
T: l.T.Err,
}
return
}
// TimeStamper generates the timestamp for logs.
func TimeStamper() (s string) {
if NoTimeStamp.Load() {
return
}
return time.Now().Format("2006-01-02T15:04:05Z07:00.000 ")
}
// var wd, _ = os.Getwd()
// GetNLoc returns multiple levels of depth of code location from the current.
func GetNLoc(n int) (output string) {
for ; n > 1; n-- {
output += fmt.Sprintf("%s\n", GetLoc(n))
}
return
}
var prefix string
func init() {
// this enables us to remove the base of the path for a more compact code location string,
// this can be used with tilix custom hyperlinks feature
//
// create a script called `setcurrent` in your PATH ( eg ~/.local/bin/setcurrent )
//
// #!/usr/bin/bash
// echo $(pwd) > ~/.current
//
// set the following environment variable in your ~/.bashrc
//
// export PROMPT_COMMAND='setcurrent'
//
// using the following regular expressions, replacing the path as necessary, and setting
// perhaps a different program than ide (this is for goland, i use an alias to the binary)
//
// ^((([a-zA-Z@0-9-_.]+/)+([a-zA-Z@0-9-_.]+)):([0-9]+)) ide --line $5 $(cat /home/mleku/.current)/$2
// [ ]((([a-zA-Z@0-9-_./]+)+([a-zA-Z@0-9-_.]+)):([0-9]+)) ide --line $5 $(cat /home/mleku/.current)/$2
// ([/](([a-zA-Z@0-9-_.]+/)+([a-zA-Z@0-9-_.]+)):([0-9]+)) ide --line $5 /$2
//
// and so long as you use this with an app containing /lol/log.go as this one is, this finds
// that path and trims it off from the log line locations and in tilix you can click on the
// file locations that are relative to the CWD where you are running the relay from. if this
// is a remote machine, just go to the location where your source code is to make it work.
//
_, file, _, _ := runtime.Caller(0)
prefix = file[:len(file)-10]
}
// GetLoc returns the code location of the caller.
func GetLoc(skip int) (output string) {
_, file, line, _ := runtime.Caller(skip)
if strings.Contains(file, "pkg/mod/") || !ShortLoc.Load() {
} else {
var split []string
split = strings.Split(file, prefix)
file = split[1]
}
output = fmt.Sprintf("%s:%d", file, line)
return
}

25
mime/mime.go Normal file
View File

@@ -0,0 +1,25 @@
package mime
// Type is a structured mimetype using standard mimetype notation to denote the type of data contained in an event.E.
type Type struct {
Val []byte
}
// New creates a new mime.Type based on input string or bytes of value.
func New[V []byte | string](c V) (mt *Type) {
mt = &Type{Val: []byte(c)}
return
}
func (mt *Type) MarshalJSON() (b []byte, err error) {
b = make([]byte, len(mt.Val)+2)
copy(b[1:], mt.Val)
b[0] = '"'
b[len(mt.Val)+1] = '"'
return
}
func (mt *Type) UnmarshalJSON(b []byte) (err error) {
mt.Val = b[1 : len(b)-1]
return
}

30
mime/mine_test.go Normal file
View File

@@ -0,0 +1,30 @@
package mime
import (
"bytes"
"encoding/json"
"testing"
"github.com/mleku/transit/chk"
"github.com/mleku/transit/log"
)
func TestNew(t *testing.T) {
kind := "text/asciidoc"
log.I.F("%s", kind)
mt := New(kind)
var err error
var b []byte
if b, err = json.Marshal(&mt); chk.E(err) {
t.Fatal(err)
}
log.I.F("%s", b)
mt2 := &Type{}
if err = json.Unmarshal(b, mt2); chk.E(err) {
t.Fatal(err)
}
log.I.F("%s", mt2.Val)
if !bytes.Equal(mt.Val, mt2.Val) {
t.Fatal("failed to encode/decode")
}
}