From 6427f727d69e526336e21d6a207f4f52343d4cdc Mon Sep 17 00:00:00 2001 From: conneroisu Date: Wed, 6 Nov 2024 11:14:00 -0600 Subject: [PATCH 1/6] added ordermap package and cleaned up sandbox and tools --- extensions/e2b/model.go | 68 ------ extensions/e2b/options.go | 12 + extensions/e2b/sandbox.go | 237 +++++++++--------- extensions/e2b/sandbox_test.go | 7 +- extensions/e2b/tools.go | 6 +- extensions/e2b/unit_test.go | 13 +- extensions/jigsawstack/tts.mp3 | Bin 59328 -> 0 bytes go.mod | 6 +- go.sum | 5 - pkg/omap/doc.go | 2 + pkg/omap/json.go | 181 ++++++++++++++ pkg/omap/json_fuzz_test.go | 117 +++++++++ pkg/omap/json_test.go | 338 ++++++++++++++++++++++++++ pkg/omap/omap.go | 292 ++++++++++++++++++++++ pkg/omap/omap_test.go | 384 +++++++++++++++++++++++++++++ pkg/omap/utils_test.go | 76 ++++++ pkg/omap/wbuf.go | 276 +++++++++++++++++++++ pkg/omap/wbuf_test.go | 107 +++++++++ pkg/omap/writer.go | 428 +++++++++++++++++++++++++++++++++ pkg/schema/schema.go | 8 +- 20 files changed, 2342 insertions(+), 221 deletions(-) delete mode 100644 extensions/e2b/model.go create mode 100644 pkg/omap/doc.go create mode 100644 pkg/omap/json.go create mode 100644 pkg/omap/json_fuzz_test.go create mode 100644 pkg/omap/json_test.go create mode 100644 pkg/omap/omap.go create mode 100644 pkg/omap/omap_test.go create mode 100644 pkg/omap/utils_test.go create mode 100644 pkg/omap/wbuf.go create mode 100644 pkg/omap/wbuf_test.go create mode 100644 pkg/omap/writer.go diff --git a/extensions/e2b/model.go b/extensions/e2b/model.go deleted file mode 100644 index 80e26c7..0000000 --- a/extensions/e2b/model.go +++ /dev/null @@ -1,68 +0,0 @@ -package e2b - -import ( - "context" - "io" - "time" -) - -type ( - // Receiver is an interface for a constantly receiving instance that - // can closed. - // - // Implementations should be conccurent safe. - Receiver interface { - Read(ctx context.Context) error - io.Closer - } - // Identifier is an interface for a constantly running process to - // identify new request ids. - Identifier interface { - Identify(ctx context.Context) - } - // Sandboxer is an interface for a sandbox. - Sandboxer interface { - // KeepAlive keeps the underlying interface alive. - // - // If the context is cancelled before requesting the timeout, - // the error will be ctx.Err(). - KeepAlive( - ctx context.Context, - timeout time.Duration, - ) error - // NewProcess creates a new process. - NewProcess( - cmd string, - ) (*Processor, error) - - // Write writes a file to the filesystem. - Write( - ctx context.Context, - method Method, - params []any, - respCh chan<- []byte, - ) - // Read reads a file from the filesystem. - Read( - ctx context.Context, - path string, - ) (string, error) - } - // Processor is an interface for a process. - Processor interface { - Start( - ctx context.Context, - cmd string, - timeout time.Duration, - ) - SubscribeStdout() (events chan Event, err error) - SubscribeStderr() (events chan Event, err error) - } - // Watcher is an interface for a instance that can watch a filesystem. - Watcher interface { - Watch( - ctx context.Context, - path string, - ) (<-chan Event, error) - } -) diff --git a/extensions/e2b/options.go b/extensions/e2b/options.go index 03f7690..ebf9db6 100644 --- a/extensions/e2b/options.go +++ b/extensions/e2b/options.go @@ -38,8 +38,20 @@ func WithCwd(cwd string) Option { } // WithWsURL sets the websocket url resolving function for the e2b sandbox. +// +// This is useful for testing. func WithWsURL(wsURL func(s *Sandbox) string) Option { return func(s *Sandbox) { s.wsURL = wsURL } } // Process Options + +// ProcessWithEnv sets the environment variables for the process. +func ProcessWithEnv(env map[string]string) ProcessOption { + return func(p *Process) { p.Env = env } +} + +// ProcessWithCwd sets the current working directory for the process. +func ProcessWithCwd(cwd string) ProcessOption { + return func(p *Process) { p.Cwd = cwd } +} diff --git a/extensions/e2b/sandbox.go b/extensions/e2b/sandbox.go index 4e6d235..13a6d40 100644 --- a/extensions/e2b/sandbox.go +++ b/extensions/e2b/sandbox.go @@ -26,42 +26,42 @@ type ( // // The sandbox is like an isolated, but interactive system. Sandbox struct { - ID string `json:"sandboxID"` // ID of the sandbox. - Metadata map[string]string `json:"metadata"` // Metadata of the sandbox. - Template SandboxTemplate `json:"templateID"` // Template of the sandbox. - ClientID string `json:"clientID"` // ClientID of the sandbox. - Cwd string `json:"cwd"` // Cwd is the sandbox's current working directory. - - logger *slog.Logger `json:"-"` // logger is the sandbox's logger. - apiKey string `json:"-"` // apiKey is the sandbox's api key. - baseURL string `json:"-"` // baseAPIURL is the base api url of the sandbox. - client *http.Client `json:"-"` // client is the sandbox's http client. - header builders.Header `json:"-"` // header is the sandbox's request header builder. - ws *websocket.Conn `json:"-"` // ws is the sandbox's websocket connection. - wsURL func(s *Sandbox) string `json:"-"` // wsURL is the sandbox's websocket url. - Map sync.Map `json:"-"` // Map is the map of the sandbox. - idCh chan int `json:"-"` // idCh is the channel to generate ids for requests. - toolW ToolingWrapper `json:"-"` // toolW is the tooling wrapper for the sandbox. + ID string `json:"sandboxID"` // ID of the sandbox. + ClientID string `json:"clientID"` // ClientID of the sandbox. + Cwd string `json:"cwd"` // Cwd is the sandbox's current working directory. + apiKey string `json:"-"` // apiKey is the sandbox's api key. + Template SandboxTemplate `json:"templateID"` // Template of the sandbox. + baseURL string `json:"-"` // baseAPIURL is the base api url of the sandbox. + Metadata map[string]string `json:"metadata"` // Metadata of the sandbox. + logger *slog.Logger `json:"-"` // logger is the sandbox's logger. + client *http.Client `json:"-"` // client is the sandbox's http client. + header builders.Header `json:"-"` // header is the sandbox's request header builder. + ws *websocket.Conn `json:"-"` // ws is the sandbox's websocket connection. + wsURL func(s *Sandbox) string `json:"-"` // wsURL is the sandbox's websocket url. + Map *sync.Map `json:"-"` // Map is the map of the sandbox. + idCh chan int `json:"-"` // idCh is the channel to generate ids for requests. + toolW ToolingWrapper `json:"-"` // toolW is the tooling wrapper for the sandbox. } + // Option is an option for the sandbox. + Option func(*Sandbox) // Process is a process in the sandbox. Process struct { - sb *Sandbox // sb is the sandbox the process belongs to. - ctx context.Context // ctx is the context for the process. id string // ID is process id. cmd string // cmd is process's command. Cwd string // cwd is process's current working directory. + ctx context.Context // ctx is the context for the process. + sb *Sandbox // sb is the sandbox the process belongs to. Env map[string]string // env is process's environment variables. } - // Option is an option for the sandbox. - Option func(*Sandbox) + // ProcessOption is an option for the process. + ProcessOption func(*Process) // Event is a file system event. Event struct { - Path string `json:"path"` // Path is the path of the event. - Name string `json:"name"` // Name is the name of file or directory. - Timestamp int64 `json:"timestamp"` // Timestamp is the timestamp of the event. - Error string `json:"error"` // Error is the possible error of the event. - Params EventParams `json:"params"` // Params is the parameters of the event. - Operation OperationType `json:"operation"` // Operation is the operation type of the event. + Path string `json:"path"` // Path is the path of the event. + Name string `json:"name"` // Name is the name of file or directory. + Timestamp int64 `json:"timestamp"` // Timestamp is the timestamp of the event. + Error string `json:"error"` // Error is the possible error of the event. + Params EventParams `json:"params"` // Params is the parameters of the event. } // EventParams is the params for subscribing to a process event. EventParams struct { @@ -76,8 +76,6 @@ type ( IsDirectory bool `json:"isDirectory"` Error string `json:"error"` } - // OperationType is an operation type. - OperationType int // Request is a JSON-RPC request. Request struct { JSONRPC string `json:"jsonrpc"` // JSONRPC is the JSON-RPC version of the request. @@ -110,10 +108,6 @@ const ( OnStderr ProcessEvents = "onStderr" // OnStderr is the event for the stderr. OnExit ProcessEvents = "onExit" // OnExit is the event for the exit. - EventTypeCreate OperationType = iota // EventTypeCreate is an event for the creation of a file/dir. - EventTypeWrite // EventTypeWrite is an event for the write to a file. - EventTypeRemove // EventTypeRemove is an event for the removal of a file/dir. - rpc = "2.0" charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" defaultBaseURL = "https://api.e2b.dev" @@ -151,20 +145,23 @@ func NewSandbox( }, client: http.DefaultClient, logger: slog.New(slog.NewJSONHandler(io.Discard, nil)), - idCh: make(chan int), toolW: defaultToolWrapper, + idCh: make(chan int), + Map: new(sync.Map), wsURL: func(s *Sandbox) string { return fmt.Sprintf("wss://49982-%s-%s.e2b.dev/ws", s.ID, s.ClientID) }, + header: builders.Header{ + SetCommonHeaders: func(req *http.Request) { + req.Header.Set("X-API-Key", apiKey) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + }, + }, } for _, opt := range opts { opt(&sb) } - sb.header.SetCommonHeaders = func(req *http.Request) { - req.Header.Set("X-API-Key", sb.apiKey) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Accept", "application/json") - } req, err := builders.NewRequest( ctx, sb.header, http.MethodPost, fmt.Sprintf("%s%s", sb.baseURL, sandboxesRoute), @@ -185,7 +182,7 @@ func NewSandbox( go func() { err := sb.read(ctx) if err != nil { - fmt.Println(err) + sb.logger.Error("failed to read sandbox", "error", err) } }() return &sb, nil @@ -416,19 +413,24 @@ func (s *Sandbox) Watch( // NewProcess creates a new process startable in the sandbox. func (s *Sandbox) NewProcess( cmd string, - proc Process, + opts ...ProcessOption, ) (*Process, error) { - proc.cmd = cmd b := make([]byte, 12) for i := range b { b[i] = charset[rand.Intn(len(charset))] } - proc.id = string(b) - proc.sb = s + proc := &Process{ + id: string(b), + sb: s, + cmd: cmd, + } + for _, opt := range opts { + opt(proc) + } if proc.Cwd == "" { proc.Cwd = s.Cwd } - return &proc, nil + return proc, nil } // Start starts a process in the sandbox. @@ -478,18 +480,18 @@ func (p *Process) Done() <-chan struct{} { } // SubscribeStdout subscribes to the process's stdout. -func (p *Process) SubscribeStdout() (chan Event, chan error) { - return p.subscribe(p.ctx, OnStdout) +func (p *Process) SubscribeStdout(ctx context.Context) (chan Event, chan error) { + return p.subscribe(ctx, OnStdout) } // SubscribeStderr subscribes to the process's stderr. -func (p *Process) SubscribeStderr() (chan Event, chan error) { - return p.subscribe(p.ctx, OnStderr) +func (p *Process) SubscribeStderr(ctx context.Context) (chan Event, chan error) { + return p.subscribe(ctx, OnStderr) } // SubscribeExit subscribes to the process's exit. -func (p *Process) SubscribeExit() (chan Event, chan error) { - return p.subscribe(p.ctx, OnExit) +func (p *Process) SubscribeExit(ctx context.Context) (chan Event, chan error) { + return p.subscribe(ctx, OnExit) } // Subscribe subscribes to a process event. @@ -513,45 +515,37 @@ func (p *Process) subscribe( errCh <- err } p.sb.Map.Store(res.Result, respCh) + loop: for { select { case eventBd := <-respCh: - p.sb.logger.Debug("eventByCh", "event", string(eventBd)) var event Event _ = json.Unmarshal(eventBd, &event) if event.Error != "" { - p.sb.logger.Debug("failed to read event", "error", event.Error) - continue - } - if event.Params.Subscription != res.Result { - p.sb.logger.Debug("subscription id mismatch", "expected", res.Result, "got", event.Params.Subscription) + p.sb.logger.Error("failed to read event", "error", event.Error) continue } events <- event case <-ctx.Done(): - p.sb.Map.Delete(res.Result) - finishCtx, cancel := context.WithCancel(context.Background()) - defer cancel() - p.sb.logger.Debug("unsubscribing from process", "event", event, "id", res.Result) - _ = p.sb.writeRequest(finishCtx, processUnsubscribe, []any{res.Result}, respCh) - unsubRes, _ := decodeResponse[bool, string](<-respCh) - if unsubRes.Error != "" || !unsubRes.Result { - p.sb.logger.Debug("failed to unsubscribe from process", "error", unsubRes.Error) - } - return + break loop case <-p.Done(): - return + break loop } } + + p.sb.Map.Delete(res.Result) + finishCtx, cancel := context.WithCancel(context.Background()) + defer cancel() + p.sb.logger.Debug("unsubscribing from process", "event", event, "id", res.Result) + _ = p.sb.writeRequest(finishCtx, processUnsubscribe, []any{res.Result}, respCh) + unsubRes, _ := decodeResponse[bool, string](<-respCh) + if unsubRes.Error != "" || !unsubRes.Result { + p.sb.logger.Debug("failed to unsubscribe from process", "error", unsubRes.Error) + } }(errs) return events, errs } func (s *Sandbox) sendRequest(req *http.Request, v interface{}) error { - req.Header.Set("Accept", "application/json") - contentType := req.Header.Get("Content-Type") - if contentType == "" { - req.Header.Set("Content-Type", "application/json") - } res, err := s.client.Do(req) if err != nil { return err @@ -584,19 +578,9 @@ func decodeResponse[T any, Q any](body []byte) (*Response[T, Q], error) { } return decResp, nil } -func (s *Sandbox) identify(ctx context.Context) { - id := 1 - for { - select { - case <-ctx.Done(): - return - default: - s.idCh <- id - id++ - } - } -} -func (s *Sandbox) read(ctx context.Context) (err error) { +func (s *Sandbox) read(ctx context.Context) error { + var body []byte + var err error type decResp struct { Method string `json:"method"` ID int `json:"id"` @@ -604,9 +588,11 @@ func (s *Sandbox) read(ctx context.Context) (err error) { Subscription string `json:"subscription"` } } - var body []byte defer func() { - err = s.ws.Close() + err := s.ws.Close() + if err != nil { + s.logger.Error("failed to close sandbox", "error", err) + } }() msgCh := make(chan []byte, 10) for { @@ -617,50 +603,35 @@ func (s *Sandbox) read(ctx context.Context) (err error) { if err != nil { return err } - if decResp.Params.Subscription != "" { - toR, ok := s.Map.Load(decResp.Params.Subscription) - if !ok { - msgCh <- body - continue - } - toRCh, ok := toR.(chan []byte) - if !ok { - msgCh <- body - continue - } - s.logger.Debug("read", - "subscription", decResp.Params.Subscription, - "body", body, - "sandbox", s.ID, - ) - toRCh <- body - } + var key any + key = decResp.Params.Subscription if decResp.ID != 0 { - toR, ok := s.Map.Load(decResp.ID) - if !ok { - msgCh <- body - continue - } - toRCh, ok := toR.(chan []byte) - if !ok { - msgCh <- body - continue - } - s.logger.Debug("read", - "id", decResp.ID, - "body", body, - "sandbox", s.ID, - ) - toRCh <- body + key = decResp.ID + } + toR, ok := s.Map.Load(key) + if !ok { + msgCh <- body + continue } + toRCh, ok := toR.(chan []byte) + if !ok { + msgCh <- body + continue + } + s.logger.Debug("read", + "subscription", decResp.Params.Subscription, + "body", body, + "sandbox", s.ID, + ) + toRCh <- body case <-ctx.Done(): return ctx.Err() default: - _, body, err := s.ws.ReadMessage() + _, msg, err := s.ws.ReadMessage() if err != nil { return err } - msgCh <- body + msgCh <- msg } } } @@ -681,10 +652,10 @@ func (s *Sandbox) writeRequest( ID: id, } s.logger.Debug("request", - "method", req.Method, - "id", req.ID, - "params", req.Params, - "sandbox", s.ID, + "sandbox", id, + "method", method, + "id", id, + "params", params, ) s.Map.Store(req.ID, respCh) jsVal, err := json.Marshal(req) @@ -703,3 +674,15 @@ func (s *Sandbox) writeRequest( return nil } } +func (s *Sandbox) identify(ctx context.Context) { + id := 1 + for { + select { + case <-ctx.Done(): + return + default: + s.idCh <- id + id++ + } + } +} diff --git a/extensions/e2b/sandbox_test.go b/extensions/e2b/sandbox_test.go index 02806ab..401e18b 100644 --- a/extensions/e2b/sandbox_test.go +++ b/extensions/e2b/sandbox_test.go @@ -116,6 +116,9 @@ func decode(bod []byte) Request { } func TestNewSandbox(t *testing.T) { + if test.IsIntegrationTest() { + t.Skip() + } a := assert.New(t) ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -159,12 +162,12 @@ func TestNewSandbox(t *testing.T) { a.NoError(err) a.Equal("hello", readRes) - proc, err := sb.NewProcess("sleep 5 && echo 'hello world!'", Process{}) + proc, err := sb.NewProcess("sleep 5 && echo 'hello world!'") a.NoError(err) err = proc.Start(ctx) a.NoError(err) - e, errCh := proc.SubscribeStdout() + e, errCh := proc.SubscribeStdout(ctx) select { case <-errCh: t.Fatal("got error from SubscribeStdout") diff --git a/extensions/e2b/tools.go b/extensions/e2b/tools.go index 3148e2e..976a530 100644 --- a/extensions/e2b/tools.go +++ b/extensions/e2b/tools.go @@ -127,15 +127,15 @@ var ( s *Sandbox, params *Params, ) (groq.ChatCompletionMessage, error) { - proc, err := s.NewProcess(params.Cmd, Process{}) + proc, err := s.NewProcess(params.Cmd) if err != nil { return groq.ChatCompletionMessage{}, err } - e, errCh := proc.SubscribeStdout() + e, errCh := proc.SubscribeStdout(ctx) if err != nil { return groq.ChatCompletionMessage{}, err } - e2, errCh := proc.SubscribeStderr() + e2, errCh := proc.SubscribeStderr(ctx) if err != nil { return groq.ChatCompletionMessage{}, err } diff --git a/extensions/e2b/unit_test.go b/extensions/e2b/unit_test.go index 0346a24..cfb48e3 100644 --- a/extensions/e2b/unit_test.go +++ b/extensions/e2b/unit_test.go @@ -90,20 +90,17 @@ func TestCreateProcess(t *testing.T) { e2b.WithLogger(test.DefaultLogger), ) a.NoError(err, "NewSandbox error") - proc, err := sb.NewProcess("echo 'Hello World!'", - e2b.Process{ - Env: map[string]string{ - "FOO": "bar", - }, - }) + proc, err := sb.NewProcess("echo 'Hello World!'", e2b.ProcessWithEnv(map[string]string{ + "FOO": "bar", + })) a.NoError(err, "could not create process") err = proc.Start(ctx) a.NoError(err) - proc, err = sb.NewProcess("sleep 2 && echo 'Hello World!'", e2b.Process{}) + proc, err = sb.NewProcess("sleep 2 && echo 'Hello World!'") a.NoError(err, "could not create process") err = proc.Start(ctx) a.NoError(err) - stdOutEvents, errCh := proc.SubscribeStdout() + stdOutEvents, errCh := proc.SubscribeStdout(ctx) a.NoError(err) select { case <-errCh: diff --git a/extensions/jigsawstack/tts.mp3 b/extensions/jigsawstack/tts.mp3 index 4e380231540d79849449655ccb15980b0a1942e5..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644 GIT binary patch literal 0 HcmV?d00001 literal 59328 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 c(<3l9fBos9``3YU@n 0 { + r, size := utf8.DecodeRune(remaining) + if r == utf8.RuneError && size <= 1 { + return "", fmt.Errorf("not a valid UTF-8 string (at position %d): %s", offset, string(input)) + } + + runes = append(runes, r) + remaining = remaining[size:] + offset += size + } + + return string(runes), nil +} diff --git a/pkg/omap/json_fuzz_test.go b/pkg/omap/json_fuzz_test.go new file mode 100644 index 0000000..f1abb42 --- /dev/null +++ b/pkg/omap/json_fuzz_test.go @@ -0,0 +1,117 @@ +package omap + +// Adapted from https://github.com/dvyukov/go-fuzz-corpus/blob/c42c1b2/json/json.go + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func FuzzRoundTripJSON(f *testing.F) { + f.Fuzz(func(t *testing.T, data []byte) { + for _, testCase := range []struct { + name string + constructor func() any + // should be a function that asserts that 2 objects of the type returned by constructor are equal + equalityAssertion func(*testing.T, any, any) bool + }{ + { + name: "with a string -> string map", + constructor: func() any { return &OrderedMap[string, string]{} }, + equalityAssertion: assertOrderedMapsEqual[string, string], + }, + { + name: "with a string -> int map", + constructor: func() any { return &OrderedMap[string, int]{} }, + equalityAssertion: assertOrderedMapsEqual[string, int], + }, + { + name: "with a string -> any map", + constructor: func() any { return &OrderedMap[string, any]{} }, + equalityAssertion: assertOrderedMapsEqual[string, any], + }, + { + name: "with a struct with map fields", + constructor: func() any { return new(testFuzzStruct) }, + equalityAssertion: assertTestFuzzStructEqual, + }, + } { + t.Run(testCase.name, func(t *testing.T) { + v1 := testCase.constructor() + if json.Unmarshal(data, v1) != nil { + return + } + + jsonData, err := json.Marshal(v1) + require.NoError(t, err) + + v2 := testCase.constructor() + require.NoError(t, json.Unmarshal(jsonData, v2)) + + if !assert.True(t, testCase.equalityAssertion(t, v1, v2), "failed with input data %q", string(data)) { + // look at that what the standard lib does with regular map, to help with debugging + + var m1 map[string]any + require.NoError(t, json.Unmarshal(data, &m1)) + + mapJsonData, err := json.Marshal(m1) + require.NoError(t, err) + + var m2 map[string]any + require.NoError(t, json.Unmarshal(mapJsonData, &m2)) + + t.Logf("initial data = %s", string(data)) + t.Logf("unmarshalled map = %v", m1) + t.Logf("re-marshalled from map = %s", string(mapJsonData)) + t.Logf("re-marshalled from test obj = %s", string(jsonData)) + t.Logf("re-unmarshalled map = %s", m2) + } + }) + } + }) +} + +// only works for fairly basic maps, that's why it's just in this file +func assertOrderedMapsEqual[K comparable, V any](t *testing.T, v1, v2 any) bool { + om1, ok1 := v1.(*OrderedMap[K, V]) + om2, ok2 := v2.(*OrderedMap[K, V]) + + if !assert.True(t, ok1, "v1 not an orderedmap") || + !assert.True(t, ok2, "v2 not an orderedmap") { + return false + } + + success := assert.Equal(t, om1.Len(), om2.Len(), "om1 and om2 have different lengths: %d vs %d", om1.Len(), om2.Len()) + + for i, pair1, pair2 := 0, om1.Oldest(), om2.Oldest(); pair1 != nil && pair2 != nil; i, pair1, pair2 = i+1, pair1.Next(), pair2.Next() { + success = assert.Equal(t, pair1.Key, pair2.Key, "different keys at position %d: %v vs %v", i, pair1.Key, pair2.Key) && success + success = assert.Equal(t, pair1.Value, pair2.Value, "different values at position %d: %v vs %v", i, pair1.Value, pair2.Value) && success + } + + return success +} + +type testFuzzStruct struct { + M1 *OrderedMap[int, any] + M2 *OrderedMap[int, string] + M3 *OrderedMap[string, string] +} + +func assertTestFuzzStructEqual(t *testing.T, v1, v2 any) bool { + s1, ok := v1.(*testFuzzStruct) + s2, ok := v2.(*testFuzzStruct) + + if !assert.True(t, ok, "v1 not an testFuzzStruct") || + !assert.True(t, ok, "v2 not an testFuzzStruct") { + return false + } + + success := assertOrderedMapsEqual[int, any](t, s1.M1, s2.M1) + success = assertOrderedMapsEqual[int, string](t, s1.M2, s2.M2) && success + success = assertOrderedMapsEqual[string, string](t, s1.M3, s2.M3) && success + + return success +} diff --git a/pkg/omap/json_test.go b/pkg/omap/json_test.go new file mode 100644 index 0000000..43b1ec6 --- /dev/null +++ b/pkg/omap/json_test.go @@ -0,0 +1,338 @@ +package omap + +import ( + "encoding/json" + "errors" + "fmt" + "strconv" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// to test marshalling TextMarshalers and unmarshalling TextUnmarshalers +type marshallable int + +func (m marshallable) MarshalText() ([]byte, error) { + return []byte(fmt.Sprintf("#%d#", m)), nil +} + +func (m *marshallable) UnmarshalText(text []byte) error { + if len(text) < 3 { + return errors.New("too short") + } + if text[0] != '#' || text[len(text)-1] != '#' { + return errors.New("missing prefix or suffix") + } + + value, err := strconv.Atoi(string(text[1 : len(text)-1])) + if err != nil { + return err + } + + *m = marshallable(value) + return nil +} + +func TestMarshalJSON(t *testing.T) { + t.Run("int key", func(t *testing.T) { + om := New[int, any]() + om.Set(1, "bar") + om.Set(7, "baz") + om.Set(2, 28) + om.Set(3, 100) + om.Set(4, "baz") + om.Set(5, "28") + om.Set(6, "100") + om.Set(8, "baz") + om.Set(8, "baz") + om.Set(9, "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque auctor augue accumsan mi maximus, quis viverra massa pretium. Phasellus imperdiet sapien a interdum sollicitudin. Duis at commodo lectus, a lacinia sem.") + + b, err := json.Marshal(om) + assert.NoError(t, err) + assert.Equal(t, `{"1":"bar","7":"baz","2":28,"3":100,"4":"baz","5":"28","6":"100","8":"baz","9":"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque auctor augue accumsan mi maximus, quis viverra massa pretium. Phasellus imperdiet sapien a interdum sollicitudin. Duis at commodo lectus, a lacinia sem."}`, string(b)) + }) + + t.Run("string key", func(t *testing.T) { + om := New[string, any]() + om.Set("test", "bar") + om.Set("abc", true) + + b, err := json.Marshal(om) + assert.NoError(t, err) + assert.Equal(t, `{"test":"bar","abc":true}`, string(b)) + }) + + t.Run("typed string key", func(t *testing.T) { + type myString string + om := New[myString, any]() + om.Set("test", "bar") + om.Set("abc", true) + + b, err := json.Marshal(om) + assert.NoError(t, err) + assert.Equal(t, `{"test":"bar","abc":true}`, string(b)) + }) + + t.Run("typed int key", func(t *testing.T) { + type myInt uint32 + om := New[myInt, any]() + om.Set(1, "bar") + om.Set(7, "baz") + om.Set(2, 28) + om.Set(3, 100) + om.Set(4, "baz") + + b, err := json.Marshal(om) + assert.NoError(t, err) + assert.Equal(t, `{"1":"bar","7":"baz","2":28,"3":100,"4":"baz"}`, string(b)) + }) + + t.Run("TextMarshaller key", func(t *testing.T) { + om := New[marshallable, any]() + om.Set(marshallable(1), "bar") + om.Set(marshallable(28), true) + + b, err := json.Marshal(om) + assert.NoError(t, err) + assert.Equal(t, `{"#1#":"bar","#28#":true}`, string(b)) + }) + + t.Run("empty map", func(t *testing.T) { + om := New[string, any]() + + b, err := json.Marshal(om) + assert.NoError(t, err) + assert.Equal(t, `{}`, string(b)) + }) +} + +func TestUnmarshallJSON(t *testing.T) { + t.Run("int key", func(t *testing.T) { + data := `{"1":"bar","7":"baz","2":28,"3":100,"4":"baz","5":"28","6":"100","8":"baz"}` + + om := New[int, any]() + require.NoError(t, json.Unmarshal([]byte(data), &om)) + + assertOrderedPairsEqual(t, om, + []int{1, 7, 2, 3, 4, 5, 6, 8}, + []any{"bar", "baz", float64(28), float64(100), "baz", "28", "100", "baz"}) + }) + + t.Run("string key", func(t *testing.T) { + data := `{"test":"bar","abc":true}` + + om := New[string, any]() + require.NoError(t, json.Unmarshal([]byte(data), &om)) + + assertOrderedPairsEqual(t, om, + []string{"test", "abc"}, + []any{"bar", true}) + }) + + t.Run("typed string key", func(t *testing.T) { + data := `{"test":"bar","abc":true}` + + type myString string + om := New[myString, any]() + require.NoError(t, json.Unmarshal([]byte(data), &om)) + + assertOrderedPairsEqual(t, om, + []myString{"test", "abc"}, + []any{"bar", true}) + }) + + t.Run("typed int key", func(t *testing.T) { + data := `{"1":"bar","7":"baz","2":28,"3":100,"4":"baz","5":"28","6":"100","8":"baz"}` + + type myInt uint32 + om := New[myInt, any]() + require.NoError(t, json.Unmarshal([]byte(data), &om)) + + assertOrderedPairsEqual(t, om, + []myInt{1, 7, 2, 3, 4, 5, 6, 8}, + []any{"bar", "baz", float64(28), float64(100), "baz", "28", "100", "baz"}) + }) + + t.Run("TextUnmarshaler key", func(t *testing.T) { + data := `{"#1#":"bar","#28#":true}` + + om := New[marshallable, any]() + require.NoError(t, json.Unmarshal([]byte(data), &om)) + + assertOrderedPairsEqual(t, om, + []marshallable{1, 28}, + []any{"bar", true}) + }) + + t.Run("when fed with an input that's not an object", func(t *testing.T) { + for _, data := range []string{"true", `["foo"]`, "42", `"foo"`} { + om := New[int, any]() + require.Error(t, json.Unmarshal([]byte(data), &om)) + } + }) + + t.Run("empty map", func(t *testing.T) { + data := `{}` + + om := New[int, any]() + require.NoError(t, json.Unmarshal([]byte(data), &om)) + + assertLenEqual(t, om, 0) + }) +} + +// const specialCharacters = "\\\\/\"\b\f\n\r\t\x00\uffff\ufffd世界\u007f\u00ff\U0010FFFF" +const specialCharacters = "\uffff\ufffd世界\u007f\u00ff\U0010FFFF" + +func TestJSONSpecialCharacters(t *testing.T) { + baselineMap := map[string]any{specialCharacters: specialCharacters} + baselineData, err := json.Marshal(baselineMap) + require.NoError(t, err) // baseline proves this key is supported by official json library + t.Logf("specialCharacters: %#v as []rune:%v", specialCharacters, []rune(specialCharacters)) + t.Logf("baseline json data: %s", baselineData) + + t.Run("marshal special characters", func(t *testing.T) { + om := New[string, any]() + om.Set(specialCharacters, specialCharacters) + b, err := json.Marshal(om) + require.NoError(t, err) + require.Equal(t, baselineData, b) + + type myString string + om2 := New[myString, myString]() + om2.Set(specialCharacters, specialCharacters) + b, err = json.Marshal(om2) + require.NoError(t, err) + require.Equal(t, baselineData, b) + }) + + t.Run("unmarshall special characters", func(t *testing.T) { + om := New[string, any]() + require.NoError(t, json.Unmarshal(baselineData, &om)) + assertOrderedPairsEqual(t, om, + []string{specialCharacters}, + []any{specialCharacters}) + + type myString string + om2 := New[myString, myString]() + require.NoError(t, json.Unmarshal(baselineData, &om2)) + assertOrderedPairsEqual(t, om2, + []myString{specialCharacters}, + []myString{specialCharacters}) + }) +} + +// to test structs that have nested map fields +type nestedMaps struct { + X int `json:"x" yaml:"x"` + M *OrderedMap[string, []*OrderedMap[int, *OrderedMap[string, any]]] `json:"m" yaml:"m"` +} + +func TestJSONRoundTrip(t *testing.T) { + for _, testCase := range []struct { + name string + input string + targetFactory func() any + isPrettyPrinted bool + }{ + { + name: "", + input: `{ + "x": 28, + "m": { + "foo": [ + { + "12": { + "i": 12, + "b": true, + "n": null, + "m": { + "a": "b", + "c": 28 + } + }, + "28": { + "a": false, + "b": [ + 1, + 2, + 3 + ] + } + }, + { + "3": { + "c": null, + "d": 87 + }, + "4": { + "e": true + }, + "5": { + "f": 4, + "g": 5, + "h": 6 + } + } + ], + "bar": [ + { + "5": { + "foo": "bar" + } + } + ] + } +}`, + targetFactory: func() any { return &nestedMaps{} }, + isPrettyPrinted: true, + }, + { + name: "with UTF-8 special chars in key", + input: `{"�":0}`, + targetFactory: func() any { return &OrderedMap[string, int]{} }, + }, + } { + t.Run(testCase.name, func(t *testing.T) { + target := testCase.targetFactory() + + require.NoError(t, json.Unmarshal([]byte(testCase.input), target)) + + var ( + out []byte + err error + ) + if testCase.isPrettyPrinted { + out, err = json.MarshalIndent(target, "", " ") + } else { + out, err = json.Marshal(target) + } + + if assert.NoError(t, err) { + assert.Equal(t, strings.TrimSpace(testCase.input), string(out)) + } + }) + } +} + +func BenchmarkMarshalJSON(b *testing.B) { + om := New[int, any]() + om.Set(1, "bar") + om.Set(7, "baz") + om.Set(2, 28) + om.Set(3, 100) + om.Set(4, "baz") + om.Set(5, "28") + om.Set(6, "100") + om.Set(8, "baz") + om.Set(8, "baz") + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + _, _ = json.Marshal(om) + } +} diff --git a/pkg/omap/omap.go b/pkg/omap/omap.go new file mode 100644 index 0000000..a17c9eb --- /dev/null +++ b/pkg/omap/omap.go @@ -0,0 +1,292 @@ +package omap + +import ( + "fmt" + + list "github.com/bahlo/generic-list-go" +) + +// Pair is a generic pair. +type Pair[K comparable, V any] struct { + Key K + Value V + + element *list.Element[*Pair[K, V]] +} + +// OrderedMap is a generic ordered map. +type OrderedMap[K comparable, V any] struct { + pairs map[K]*Pair[K, V] + list *list.List[*Pair[K, V]] +} + +type initConfig[K comparable, V any] struct { + capacity int + initialData []Pair[K, V] +} + +// InitOption is an option for initializing an OrderedMap. +type InitOption[K comparable, V any] func(config *initConfig[K, V]) + +// WithCapacity allows giving a capacity hint for the map, akin to the standard make(map[K]V, capacity). +func WithCapacity[K comparable, V any](capacity int) InitOption[K, V] { + return func(c *initConfig[K, V]) { + c.capacity = capacity + } +} + +// WithInitialData allows passing in initial data for the map. +func WithInitialData[K comparable, V any](initialData ...Pair[K, V]) InitOption[K, V] { + return func(c *initConfig[K, V]) { + c.initialData = initialData + if c.capacity < len(initialData) { + c.capacity = len(initialData) + } + } +} + +// New creates a new OrderedMap. +// options can either be one or several InitOption[K, V], or a single integer, +// which is then interpreted as a capacity hint, à la make(map[K]V, capacity). +func New[K comparable, V any](options ...any) *OrderedMap[K, V] { //nolint:varnamelen + orderedMap := &OrderedMap[K, V]{} + + var config initConfig[K, V] + for _, untypedOption := range options { + switch option := untypedOption.(type) { + case int: + if len(options) != 1 { + invalidOption() + } + config.capacity = option + + case InitOption[K, V]: + option(&config) + + default: + invalidOption() + } + } + + orderedMap.initialize(config.capacity) + orderedMap.AddPairs(config.initialData...) + + return orderedMap +} + +const invalidOptionMessage = `when using orderedmap.New[K,V]() with options, either provide one or several InitOption[K, V]; or a single integer which is then interpreted as a capacity hint, à la make(map[K]V, capacity).` //nolint:lll + +func invalidOption() { panic(invalidOptionMessage) } + +func (om *OrderedMap[K, V]) initialize(capacity int) { + om.pairs = make(map[K]*Pair[K, V], capacity) + om.list = list.New[*Pair[K, V]]() +} + +// Get looks for the given key, and returns the value associated with it, +// or V's nil value if not found. The boolean it returns says whether the key is present in the map. +func (om *OrderedMap[K, V]) Get(key K) (val V, present bool) { + if pair, present := om.pairs[key]; present { + return pair.Value, true + } + + return +} + +// Load is an alias for Get, mostly to present an API similar to `sync.Map`'s. +func (om *OrderedMap[K, V]) Load(key K) (V, bool) { + return om.Get(key) +} + +// Value returns the value associated with the given key or the zero value. +func (om *OrderedMap[K, V]) Value(key K) (val V) { + if pair, present := om.pairs[key]; present { + val = pair.Value + } + return +} + +// GetPair looks for the given key, and returns the pair associated with it, +// or nil if not found. The Pair struct can then be used to iterate over the ordered map +// from that point, either forward or backward. +func (om *OrderedMap[K, V]) GetPair(key K) *Pair[K, V] { + return om.pairs[key] +} + +// Set sets the key-value pair, and returns what `Get` would have returned +// on that key prior to the call to `Set`. +func (om *OrderedMap[K, V]) Set(key K, value V) (val V, present bool) { + if pair, present := om.pairs[key]; present { + oldValue := pair.Value + pair.Value = value + return oldValue, true + } + + pair := &Pair[K, V]{ + Key: key, + Value: value, + } + pair.element = om.list.PushBack(pair) + om.pairs[key] = pair + + return +} + +// AddPairs allows setting multiple pairs at a time. It's equivalent to calling +// Set on each pair sequentially. +func (om *OrderedMap[K, V]) AddPairs(pairs ...Pair[K, V]) { + for _, pair := range pairs { + om.Set(pair.Key, pair.Value) + } +} + +// Store is an alias for Set, mostly to present an API similar to `sync.Map`'s. +func (om *OrderedMap[K, V]) Store(key K, value V) (V, bool) { + return om.Set(key, value) +} + +// Delete removes the key-value pair, and returns what `Get` would have returned +// on that key prior to the call to `Delete`. +func (om *OrderedMap[K, V]) Delete(key K) (val V, present bool) { + if pair, present := om.pairs[key]; present { + om.list.Remove(pair.element) + delete(om.pairs, key) + return pair.Value, true + } + return +} + +// Len returns the length of the ordered map. +func (om *OrderedMap[K, V]) Len() int { + if om == nil || om.pairs == nil { + return 0 + } + return len(om.pairs) +} + +// Oldest returns a pointer to the oldest pair. It's meant to be used to iterate on the ordered map's +// pairs from the oldest to the newest, e.g.: +// for pair := orderedMap.Oldest(); pair != nil; pair = pair.Next() { fmt.Printf("%v => %v\n", pair.Key, pair.Value) } +func (om *OrderedMap[K, V]) Oldest() *Pair[K, V] { + if om == nil || om.list == nil { + return nil + } + return listElementToPair(om.list.Front()) +} + +// Newest returns a pointer to the newest pair. It's meant to be used to iterate on the ordered map's +// pairs from the newest to the oldest, e.g.: +// for pair := orderedMap.Oldest(); pair != nil; pair = pair.Next() { fmt.Printf("%v => %v\n", pair.Key, pair.Value) } +func (om *OrderedMap[K, V]) Newest() *Pair[K, V] { + if om == nil || om.list == nil { + return nil + } + return listElementToPair(om.list.Back()) +} + +// Next returns a pointer to the next pair. +func (p *Pair[K, V]) Next() *Pair[K, V] { + return listElementToPair(p.element.Next()) +} + +// Prev returns a pointer to the previous pair. +func (p *Pair[K, V]) Prev() *Pair[K, V] { + return listElementToPair(p.element.Prev()) +} + +func listElementToPair[K comparable, V any](element *list.Element[*Pair[K, V]]) *Pair[K, V] { + if element == nil { + return nil + } + return element.Value +} + +// KeyNotFoundError may be returned by functions in this package when they're called with keys that are not present +// in the map. +type KeyNotFoundError[K comparable] struct { + MissingKey K +} + +func (e *KeyNotFoundError[K]) Error() string { + return fmt.Sprintf("missing key: %v", e.MissingKey) +} + +// MoveAfter moves the value associated with key to its new position after the one associated with markKey. +// Returns an error iff key or markKey are not present in the map. If an error is returned, +// it will be a KeyNotFoundError. +func (om *OrderedMap[K, V]) MoveAfter(key, markKey K) error { + elements, err := om.getElements(key, markKey) + if err != nil { + return err + } + om.list.MoveAfter(elements[0], elements[1]) + return nil +} + +// MoveBefore moves the value associated with key to its new position before the one associated with markKey. +// Returns an error iff key or markKey are not present in the map. If an error is returned, +// it will be a KeyNotFoundError. +func (om *OrderedMap[K, V]) MoveBefore(key, markKey K) error { + elements, err := om.getElements(key, markKey) + if err != nil { + return err + } + om.list.MoveBefore(elements[0], elements[1]) + return nil +} + +func (om *OrderedMap[K, V]) getElements(keys ...K) ([]*list.Element[*Pair[K, V]], error) { + elements := make([]*list.Element[*Pair[K, V]], len(keys)) + for i, k := range keys { + pair, present := om.pairs[k] + if !present { + return nil, &KeyNotFoundError[K]{k} + } + elements[i] = pair.element + } + return elements, nil +} + +// MoveToBack moves the value associated with key to the back of the ordered map, +// i.e. makes it the newest pair in the map. +// Returns an error iff key is not present in the map. If an error is returned, +// it will be a KeyNotFoundError. +func (om *OrderedMap[K, V]) MoveToBack(key K) error { + _, err := om.GetAndMoveToBack(key) + return err +} + +// MoveToFront moves the value associated with key to the front of the ordered map, +// i.e. makes it the oldest pair in the map. +// Returns an error iff key is not present in the map. If an error is returned, +// it will be a KeyNotFoundError. +func (om *OrderedMap[K, V]) MoveToFront(key K) error { + _, err := om.GetAndMoveToFront(key) + return err +} + +// GetAndMoveToBack combines Get and MoveToBack in the same call. If an error is returned, +// it will be a KeyNotFoundError. +func (om *OrderedMap[K, V]) GetAndMoveToBack(key K) (val V, err error) { + if pair, present := om.pairs[key]; present { + val = pair.Value + om.list.MoveToBack(pair.element) + } else { + err = &KeyNotFoundError[K]{key} + } + + return +} + +// GetAndMoveToFront combines Get and MoveToFront in the same call. If an error is returned, +// it will be a KeyNotFoundError. +func (om *OrderedMap[K, V]) GetAndMoveToFront(key K) (val V, err error) { + if pair, present := om.pairs[key]; present { + val = pair.Value + om.list.MoveToFront(pair.element) + } else { + err = &KeyNotFoundError[K]{key} + } + + return +} diff --git a/pkg/omap/omap_test.go b/pkg/omap/omap_test.go new file mode 100644 index 0000000..2c33bea --- /dev/null +++ b/pkg/omap/omap_test.go @@ -0,0 +1,384 @@ +package omap + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestBasicFeatures(t *testing.T) { + n := 100 + om := New[int, int]() + + // set(i, 2 * i) + for i := 0; i < n; i++ { + assertLenEqual(t, om, i) + oldValue, present := om.Set(i, 2*i) + assertLenEqual(t, om, i+1) + + assert.Equal(t, 0, oldValue) + assert.False(t, present) + } + + // get what we just set + for i := 0; i < n; i++ { + value, present := om.Get(i) + + assert.Equal(t, 2*i, value) + assert.Equal(t, value, om.Value(i)) + assert.True(t, present) + } + + // get pairs of what we just set + for i := 0; i < n; i++ { + pair := om.GetPair(i) + + assert.NotNil(t, pair) + assert.Equal(t, 2*i, pair.Value) + } + + // forward iteration + i := 0 + for pair := om.Oldest(); pair != nil; pair = pair.Next() { + assert.Equal(t, i, pair.Key) + assert.Equal(t, 2*i, pair.Value) + i++ + } + // backward iteration + i = n - 1 + for pair := om.Newest(); pair != nil; pair = pair.Prev() { + assert.Equal(t, i, pair.Key) + assert.Equal(t, 2*i, pair.Value) + i-- + } + + // forward iteration starting from known key + i = 42 + for pair := om.GetPair(i); pair != nil; pair = pair.Next() { + assert.Equal(t, i, pair.Key) + assert.Equal(t, 2*i, pair.Value) + i++ + } + + // double values for pairs with even keys + for j := 0; j < n/2; j++ { + i = 2 * j + oldValue, present := om.Set(i, 4*i) + + assert.Equal(t, 2*i, oldValue) + assert.True(t, present) + } + // and delete pairs with odd keys + for j := 0; j < n/2; j++ { + i = 2*j + 1 + assertLenEqual(t, om, n-j) + value, present := om.Delete(i) + assertLenEqual(t, om, n-j-1) + + assert.Equal(t, 2*i, value) + assert.True(t, present) + + // deleting again shouldn't change anything + value, present = om.Delete(i) + assertLenEqual(t, om, n-j-1) + assert.Equal(t, 0, value) + assert.False(t, present) + } + + // get the whole range + for j := 0; j < n/2; j++ { + i = 2 * j + value, present := om.Get(i) + assert.Equal(t, 4*i, value) + assert.Equal(t, value, om.Value(i)) + assert.True(t, present) + + i = 2*j + 1 + value, present = om.Get(i) + assert.Equal(t, 0, value) + assert.Equal(t, value, om.Value(i)) + assert.False(t, present) + } + + // check iterations again + i = 0 + for pair := om.Oldest(); pair != nil; pair = pair.Next() { + assert.Equal(t, i, pair.Key) + assert.Equal(t, 4*i, pair.Value) + i += 2 + } + i = 2 * ((n - 1) / 2) + for pair := om.Newest(); pair != nil; pair = pair.Prev() { + assert.Equal(t, i, pair.Key) + assert.Equal(t, 4*i, pair.Value) + i -= 2 + } +} + +func TestUpdatingDoesntChangePairsOrder(t *testing.T) { + om := New[string, any]() + om.Set("foo", "bar") + om.Set("wk", 28) + om.Set("po", 100) + om.Set("bar", "baz") + + oldValue, present := om.Set("po", 102) + assert.Equal(t, 100, oldValue) + assert.True(t, present) + + assertOrderedPairsEqual(t, om, + []string{"foo", "wk", "po", "bar"}, + []any{"bar", 28, 102, "baz"}) +} + +func TestDeletingAndReinsertingChangesPairsOrder(t *testing.T) { + om := New[string, any]() + om.Set("foo", "bar") + om.Set("wk", 28) + om.Set("po", 100) + om.Set("bar", "baz") + + // delete a pair + oldValue, present := om.Delete("po") + assert.Equal(t, 100, oldValue) + assert.True(t, present) + + // re-insert the same pair + oldValue, present = om.Set("po", 100) + assert.Nil(t, oldValue) + assert.False(t, present) + + assertOrderedPairsEqual(t, om, + []string{"foo", "wk", "bar", "po"}, + []any{"bar", 28, "baz", 100}) +} + +func TestEmptyMapOperations(t *testing.T) { + om := New[string, any]() + + oldValue, present := om.Get("foo") + assert.Nil(t, oldValue) + assert.Nil(t, om.Value("foo")) + assert.False(t, present) + + oldValue, present = om.Delete("bar") + assert.Nil(t, oldValue) + assert.False(t, present) + + assertLenEqual(t, om, 0) + + assert.Nil(t, om.Oldest()) + assert.Nil(t, om.Newest()) +} + +type dummyTestStruct struct { + value string +} + +func TestPackUnpackStructs(t *testing.T) { + om := New[string, dummyTestStruct]() + om.Set("foo", dummyTestStruct{"foo!"}) + om.Set("bar", dummyTestStruct{"bar!"}) + + value, present := om.Get("foo") + assert.True(t, present) + assert.Equal(t, value, om.Value("foo")) + if assert.NotNil(t, value) { + assert.Equal(t, "foo!", value.value) + } + + value, present = om.Set("bar", dummyTestStruct{"baz!"}) + assert.True(t, present) + if assert.NotNil(t, value) { + assert.Equal(t, "bar!", value.value) + } + + value, present = om.Get("bar") + assert.Equal(t, value, om.Value("bar")) + assert.True(t, present) + if assert.NotNil(t, value) { + assert.Equal(t, "baz!", value.value) + } +} + +// shamelessly stolen from https://github.com/python/cpython/blob/e19a91e45fd54a56e39c2d12e6aaf4757030507f/Lib/test/test_ordered_dict.py#L55-L61 +func TestShuffle(t *testing.T) { + ranLen := 100 + + for _, n := range []int{0, 10, 20, 100, 1000, 10000} { + t.Run(fmt.Sprintf("shuffle test with %d items", n), func(t *testing.T) { + om := New[string, string]() + + keys := make([]string, n) + values := make([]string, n) + + for i := 0; i < n; i++ { + // we prefix with the number to ensure that we don't get any duplicates + keys[i] = fmt.Sprintf("%d_%s", i, randomHexString(t, ranLen)) + values[i] = randomHexString(t, ranLen) + + value, present := om.Set(keys[i], values[i]) + assert.Equal(t, "", value) + assert.False(t, present) + } + + assertOrderedPairsEqual(t, om, keys, values) + }) + } +} + +func TestMove(t *testing.T) { + om := New[int, any]() + om.Set(1, "bar") + om.Set(2, 28) + om.Set(3, 100) + om.Set(4, "baz") + om.Set(5, "28") + om.Set(6, "100") + om.Set(7, "baz") + om.Set(8, "baz") + + err := om.MoveAfter(2, 3) + assert.Nil(t, err) + assertOrderedPairsEqual(t, om, + []int{1, 3, 2, 4, 5, 6, 7, 8}, + []any{"bar", 100, 28, "baz", "28", "100", "baz", "baz"}) + + err = om.MoveBefore(6, 4) + assert.Nil(t, err) + assertOrderedPairsEqual(t, om, + []int{1, 3, 2, 6, 4, 5, 7, 8}, + []any{"bar", 100, 28, "100", "baz", "28", "baz", "baz"}) + + err = om.MoveToBack(3) + assert.Nil(t, err) + assertOrderedPairsEqual(t, om, + []int{1, 2, 6, 4, 5, 7, 8, 3}, + []any{"bar", 28, "100", "baz", "28", "baz", "baz", 100}) + + err = om.MoveToFront(5) + assert.Nil(t, err) + assertOrderedPairsEqual(t, om, + []int{5, 1, 2, 6, 4, 7, 8, 3}, + []any{"28", "bar", 28, "100", "baz", "baz", "baz", 100}) + + err = om.MoveToFront(100) + assert.Equal(t, &KeyNotFoundError[int]{100}, err) +} + +func TestGetAndMove(t *testing.T) { + om := New[int, any]() + om.Set(1, "bar") + om.Set(2, 28) + om.Set(3, 100) + om.Set(4, "baz") + om.Set(5, "28") + om.Set(6, "100") + om.Set(7, "baz") + om.Set(8, "baz") + + value, err := om.GetAndMoveToBack(3) + assert.Nil(t, err) + assert.Equal(t, 100, value) + assertOrderedPairsEqual(t, om, + []int{1, 2, 4, 5, 6, 7, 8, 3}, + []any{"bar", 28, "baz", "28", "100", "baz", "baz", 100}) + + value, err = om.GetAndMoveToFront(5) + assert.Nil(t, err) + assert.Equal(t, "28", value) + assertOrderedPairsEqual(t, om, + []int{5, 1, 2, 4, 6, 7, 8, 3}, + []any{"28", "bar", 28, "baz", "100", "baz", "baz", 100}) + + value, err = om.GetAndMoveToBack(100) + assert.Equal(t, &KeyNotFoundError[int]{100}, err) +} + +func TestAddPairs(t *testing.T) { + om := New[int, any]() + om.AddPairs( + Pair[int, any]{ + Key: 28, + Value: "foo", + }, + Pair[int, any]{ + Key: 12, + Value: "bar", + }, + Pair[int, any]{ + Key: 28, + Value: "baz", + }, + ) + + assertOrderedPairsEqual(t, om, + []int{28, 12}, + []any{"baz", "bar"}) +} + +// sadly, we can't test the "actual" capacity here, see https://github.com/golang/go/issues/52157 +func TestNewWithCapacity(t *testing.T) { + zero := New[int, string](0) + assert.Empty(t, zero.Len()) + + assert.PanicsWithValue(t, invalidOptionMessage, func() { + _ = New[int, string](1, 2) + }) + assert.PanicsWithValue(t, invalidOptionMessage, func() { + _ = New[int, string](1, 2, 3) + }) + + om := New[int, string](-1) + om.Set(1337, "quarante-deux") + assert.Equal(t, 1, om.Len()) +} + +func TestNewWithOptions(t *testing.T) { + t.Run("wih capacity", func(t *testing.T) { + om := New[string, any](WithCapacity[string, any](98)) + assert.Equal(t, 0, om.Len()) + }) + + t.Run("with initial data", func(t *testing.T) { + om := New[string, int](WithInitialData( + Pair[string, int]{ + Key: "a", + Value: 1, + }, + Pair[string, int]{ + Key: "b", + Value: 2, + }, + Pair[string, int]{ + Key: "c", + Value: 3, + }, + )) + + assertOrderedPairsEqual(t, om, + []string{"a", "b", "c"}, + []int{1, 2, 3}) + }) + + t.Run("with an invalid option type", func(t *testing.T) { + assert.PanicsWithValue(t, invalidOptionMessage, func() { + _ = New[int, string]("foo") + }) + }) +} + +func TestNilMap(t *testing.T) { + // we want certain behaviors of a nil ordered map to be the same as they are for standard nil maps + var om *OrderedMap[int, any] + + t.Run("len", func(t *testing.T) { + assert.Equal(t, 0, om.Len()) + }) + + t.Run("iterating - akin to range", func(t *testing.T) { + assert.Nil(t, om.Oldest()) + assert.Nil(t, om.Newest()) + }) +} diff --git a/pkg/omap/utils_test.go b/pkg/omap/utils_test.go new file mode 100644 index 0000000..bf15175 --- /dev/null +++ b/pkg/omap/utils_test.go @@ -0,0 +1,76 @@ +package omap + +import ( + "crypto/rand" + "encoding/hex" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +// assertOrderedPairsEqual asserts that the map contains the given keys and values +// from oldest to newest. +func assertOrderedPairsEqual[K comparable, V any]( + t *testing.T, orderedMap *OrderedMap[K, V], expectedKeys []K, expectedValues []V, +) { + t.Helper() + + assertOrderedPairsEqualFromNewest(t, orderedMap, expectedKeys, expectedValues) + assertOrderedPairsEqualFromOldest(t, orderedMap, expectedKeys, expectedValues) +} + +func assertOrderedPairsEqualFromNewest[K comparable, V any]( + t *testing.T, orderedMap *OrderedMap[K, V], expectedKeys []K, expectedValues []V, +) { + t.Helper() + + if assert.Equal(t, len(expectedKeys), len(expectedValues)) && assert.Equal(t, len(expectedKeys), orderedMap.Len()) { + i := orderedMap.Len() - 1 + for pair := orderedMap.Newest(); pair != nil; pair = pair.Prev() { + assert.Equal(t, expectedKeys[i], pair.Key, "from newest index=%d on key", i) + assert.Equal(t, expectedValues[i], pair.Value, "from newest index=%d on value", i) + i-- + } + } +} + +func assertOrderedPairsEqualFromOldest[K comparable, V any]( + t *testing.T, orderedMap *OrderedMap[K, V], expectedKeys []K, expectedValues []V, +) { + t.Helper() + + if assert.Equal(t, len(expectedKeys), len(expectedValues)) && assert.Equal(t, len(expectedKeys), orderedMap.Len()) { + i := 0 + for pair := orderedMap.Oldest(); pair != nil; pair = pair.Next() { + assert.Equal(t, expectedKeys[i], pair.Key, "from oldest index=%d on key", i) + assert.Equal(t, expectedValues[i], pair.Value, "from oldest index=%d on value", i) + i++ + } + } +} + +func assertLenEqual[K comparable, V any](t *testing.T, orderedMap *OrderedMap[K, V], expectedLen int) { + t.Helper() + + assert.Equal(t, expectedLen, orderedMap.Len()) + + // also check the list length, for good measure + assert.Equal(t, expectedLen, orderedMap.list.Len()) +} + +func randomHexString(t *testing.T, length int) string { + t.Helper() + + b := length / 2 //nolint:gomnd + randBytes := make([]byte, b) + + if n, err := rand.Read(randBytes); err != nil || n != b { + if err == nil { + err = fmt.Errorf("only got %v random bytes, expected %v", n, b) + } + t.Fatal(err) + } + + return hex.EncodeToString(randBytes) +} diff --git a/pkg/omap/wbuf.go b/pkg/omap/wbuf.go new file mode 100644 index 0000000..090ae49 --- /dev/null +++ b/pkg/omap/wbuf.go @@ -0,0 +1,276 @@ +package omap + +import ( + "io" + "net" + "sync" +) + +// PoolConfig contains configuration for the allocation and reuse strategy. +type PoolConfig struct { + StartSize int // Minimum chunk size that is allocated. + PooledSize int // Minimum chunk size that is reused, reusing chunks too small will result in overhead. + MaxSize int // Maximum chunk size that will be allocated. +} + +var config = PoolConfig{ + StartSize: 128, + PooledSize: 512, + MaxSize: 32768, +} + +// Reuse pool: chunk size -> pool. +var buffers = map[int]*sync.Pool{} + +func initBuffers() { + for l := config.PooledSize; l <= config.MaxSize; l *= 2 { + buffers[l] = new(sync.Pool) + } +} + +func init() { + initBuffers() +} + +// Init sets up a non-default pooling and allocation strategy. Should be run before serialization is done. +func Init(cfg PoolConfig) { + config = cfg + initBuffers() +} + +// putBuf puts a chunk to reuse pool if it can be reused. +func putBuf(buf []byte) { + size := cap(buf) + if size < config.PooledSize { + return + } + if c := buffers[size]; c != nil { + c.Put(buf[:0]) + } +} + +// getBuf gets a chunk from reuse pool or creates a new one if reuse failed. +func getBuf(size int) []byte { + if size >= config.PooledSize { + if c := buffers[size]; c != nil { + v := c.Get() + if v != nil { + return v.([]byte) + } + } + } + return make([]byte, 0, size) +} + +// Buffer is a buffer optimized for serialization without extra copying. +type Buffer struct { + + // Buf is the current chunk that can be used for serialization. + Buf []byte + + toPool []byte + bufs [][]byte +} + +// EnsureSpace makes sure that the current chunk contains at least s free bytes, +// possibly creating a new chunk. +func (b *Buffer) EnsureSpace(s int) { + if cap(b.Buf)-len(b.Buf) < s { + b.ensureSpaceSlow(s) + } +} + +func (b *Buffer) ensureSpaceSlow(p int) { + l := len(b.Buf) + if l > 0 { + if cap(b.toPool) != cap(b.Buf) { + // Chunk was reallocated, toPool can be pooled. + putBuf(b.toPool) + } + if cap(b.bufs) == 0 { + b.bufs = make([][]byte, 0, 8) + } + b.bufs = append(b.bufs, b.Buf) + l = cap(b.toPool) * 2 + } else { + l = config.StartSize + } + + if l > config.MaxSize { + l = config.MaxSize + } + b.Buf = getBuf(l) + b.toPool = b.Buf +} + +// AppendByte appends a single byte to buffer. +func (b *Buffer) AppendByte(data byte) { + b.EnsureSpace(1) + b.Buf = append(b.Buf, data) +} + +// AppendBytes appends a byte slice to buffer. +func (b *Buffer) AppendBytes(data []byte) { + if len(data) <= cap(b.Buf)-len(b.Buf) { + b.Buf = append(b.Buf, data...) // fast path + } else { + b.appendBytesSlow(data) + } +} + +func (b *Buffer) appendBytesSlow(data []byte) { + for len(data) > 0 { + b.EnsureSpace(1) + + sz := cap(b.Buf) - len(b.Buf) + if sz > len(data) { + sz = len(data) + } + + b.Buf = append(b.Buf, data[:sz]...) + data = data[sz:] + } +} + +// AppendString appends a string to buffer. +func (b *Buffer) AppendString(data string) { + if len(data) <= cap(b.Buf)-len(b.Buf) { + b.Buf = append(b.Buf, data...) // fast path + } else { + b.appendStringSlow(data) + } +} + +func (b *Buffer) appendStringSlow(data string) { + for len(data) > 0 { + b.EnsureSpace(1) + + sz := cap(b.Buf) - len(b.Buf) + if sz > len(data) { + sz = len(data) + } + + b.Buf = append(b.Buf, data[:sz]...) + data = data[sz:] + } +} + +// Size computes the size of a buffer by adding sizes of every chunk. +func (b *Buffer) Size() int { + size := len(b.Buf) + for _, buf := range b.bufs { + size += len(buf) + } + return size +} + +// DumpTo outputs the contents of a buffer to a writer and resets the buffer. +func (b *Buffer) DumpTo(w io.Writer) (written int, err error) { + bufs := net.Buffers(b.bufs) + if len(b.Buf) > 0 { + bufs = append(bufs, b.Buf) + } + n, err := bufs.WriteTo(w) + + for _, buf := range b.bufs { + putBuf(buf) + } + putBuf(b.toPool) + + b.bufs = nil + b.Buf = nil + b.toPool = nil + + return int(n), err +} + +// BuildBytes creates a single byte slice with all the contents of the buffer. Data is +// copied if it does not fit in a single chunk. You can optionally provide one byte +// slice as argument that it will try to reuse. +func (b *Buffer) BuildBytes(reuse ...[]byte) []byte { + if len(b.bufs) == 0 { + ret := b.Buf + b.toPool = nil + b.Buf = nil + return ret + } + + var ret []byte + size := b.Size() + + // If we got a buffer as argument and it is big enough, reuse it. + if len(reuse) == 1 && cap(reuse[0]) >= size { + ret = reuse[0][:0] + } else { + ret = make([]byte, 0, size) + } + for _, buf := range b.bufs { + ret = append(ret, buf...) + putBuf(buf) + } + + ret = append(ret, b.Buf...) + putBuf(b.toPool) + + b.bufs = nil + b.toPool = nil + b.Buf = nil + + return ret +} + +type readCloser struct { + offset int + bufs [][]byte +} + +func (r *readCloser) Read(p []byte) (n int, err error) { + for _, buf := range r.bufs { + // Copy as much as we can. + x := copy(p[n:], buf[r.offset:]) + n += x // Increment how much we filled. + + // Did we empty the whole buffer? + if r.offset+x == len(buf) { + // On to the next buffer. + r.offset = 0 + r.bufs = r.bufs[1:] + + // We can release this buffer. + putBuf(buf) + } else { + r.offset += x + } + + if n == len(p) { + break + } + } + // No buffers left or nothing read? + if len(r.bufs) == 0 { + err = io.EOF + } + return +} + +func (r *readCloser) Close() error { + // Release all remaining buffers. + for _, buf := range r.bufs { + putBuf(buf) + } + // In case Close gets called multiple times. + r.bufs = nil + + return nil +} + +// ReadCloser creates an io.ReadCloser with all the contents of the buffer. +func (b *Buffer) ReadCloser() io.ReadCloser { + ret := &readCloser{0, append(b.bufs, b.Buf)} + + b.bufs = nil + b.toPool = nil + b.Buf = nil + + return ret +} diff --git a/pkg/omap/wbuf_test.go b/pkg/omap/wbuf_test.go new file mode 100644 index 0000000..3fc1ce9 --- /dev/null +++ b/pkg/omap/wbuf_test.go @@ -0,0 +1,107 @@ +package omap + +import ( + "bytes" + "testing" +) + +func TestAppendByte(t *testing.T) { + var b Buffer + var want []byte + + for i := 0; i < 1000; i++ { + b.AppendByte(1) + b.AppendByte(2) + want = append(want, 1, 2) + } + + got := b.BuildBytes() + if !bytes.Equal(got, want) { + t.Errorf("BuildBytes() = %v; want %v", got, want) + } +} + +func TestAppendBytes(t *testing.T) { + var b Buffer + var want []byte + + for i := 0; i < 1000; i++ { + b.AppendBytes([]byte{1, 2}) + want = append(want, 1, 2) + } + + got := b.BuildBytes() + if !bytes.Equal(got, want) { + t.Errorf("BuildBytes() = %v; want %v", got, want) + } +} + +func TestAppendString(t *testing.T) { + var b Buffer + var want []byte + + s := "test" + for i := 0; i < 1000; i++ { + b.AppendString(s) + want = append(want, s...) + } + + got := b.BuildBytes() + if !bytes.Equal(got, want) { + t.Errorf("BuildBytes() = %v; want %v", got, want) + } +} + +func TestDumpTo(t *testing.T) { + var b Buffer + var want []byte + + s := "test" + for i := 0; i < 1000; i++ { + b.AppendBytes([]byte(s)) + want = append(want, s...) + } + + out := &bytes.Buffer{} + n, err := b.DumpTo(out) + if err != nil { + t.Errorf("DumpTo() error: %v", err) + } + + got := out.Bytes() + if !bytes.Equal(got, want) { + t.Errorf("DumpTo(): got %v; want %v", got, want) + } + + if n != len(want) { + t.Errorf("DumpTo() = %v; want %v", n, len(want)) + } +} + +func TestReadCloser(t *testing.T) { + var b Buffer + var want []byte + + s := "test" + for i := 0; i < 1000; i++ { + b.AppendBytes([]byte(s)) + want = append(want, s...) + } + + out := &bytes.Buffer{} + rc := b.ReadCloser() + n, err := out.ReadFrom(rc) + if err != nil { + t.Errorf("ReadCloser() error: %v", err) + } + rc.Close() // Will always return nil + + got := out.Bytes() + if !bytes.Equal(got, want) { + t.Errorf("DumpTo(): got %v; want %v", got, want) + } + + if n != int64(len(want)) { + t.Errorf("DumpTo() = %v; want %v", n, len(want)) + } +} diff --git a/pkg/omap/writer.go b/pkg/omap/writer.go new file mode 100644 index 0000000..97a2334 --- /dev/null +++ b/pkg/omap/writer.go @@ -0,0 +1,428 @@ +package omap + +import ( + "io" + "strconv" + "unicode/utf8" +) + +// Flags describe various encoding options. The behavior may be actually implemented in the encoder, but +// Flags field in Writer is used to set and pass them around. +type Flags int + +const ( + NilMapAsEmpty Flags = 1 << iota // Encode nil map as '{}' rather than 'null'. + NilSliceAsEmpty // Encode nil slice as '[]' rather than 'null'. +) + +// Writer is a JSON writer. +type Writer struct { + Flags Flags + + Error error + Buffer Buffer + NoEscapeHTML bool +} + +// Size returns the size of the data that was written out. +func (w *Writer) Size() int { + return w.Buffer.Size() +} + +// DumpTo outputs the data to given io.Writer, resetting the buffer. +func (w *Writer) DumpTo(out io.Writer) (written int, err error) { + return w.Buffer.DumpTo(out) +} + +// BuildBytes returns writer data as a single byte slice. You can optionally provide one byte slice +// as argument that it will try to reuse. +func (w *Writer) BuildBytes(reuse ...[]byte) ([]byte, error) { + if w.Error != nil { + return nil, w.Error + } + + return w.Buffer.BuildBytes(reuse...), nil +} + +// ReadCloser returns an io.ReadCloser that can be used to read the data. +// ReadCloser also resets the buffer. +func (w *Writer) ReadCloser() (io.ReadCloser, error) { + if w.Error != nil { + return nil, w.Error + } + + return w.Buffer.ReadCloser(), nil +} + +// RawByte appends raw binary data to the buffer. +func (w *Writer) RawByte(c byte) { + w.Buffer.AppendByte(c) +} + +// RawString appends raw binary data to the buffer. +func (w *Writer) RawString(s string) { + w.Buffer.AppendString(s) +} + +// Raw appends raw binary data to the buffer or sets the error if it is given. Useful for +// calling with results of MarshalJSON-like functions. +func (w *Writer) Raw(data []byte, err error) { + switch { + case w.Error != nil: + return + case err != nil: + w.Error = err + case len(data) > 0: + w.Buffer.AppendBytes(data) + default: + w.RawString("null") + } +} + +// RawText encloses raw binary data in quotes and appends in to the buffer. +// Useful for calling with results of MarshalText-like functions. +func (w *Writer) RawText(data []byte, err error) { + switch { + case w.Error != nil: + return + case err != nil: + w.Error = err + case len(data) > 0: + w.String(string(data)) + default: + w.RawString("null") + } +} + +// Base64Bytes appends data to the buffer after base64 encoding it +func (w *Writer) Base64Bytes(data []byte) { + if data == nil { + w.Buffer.AppendString("null") + return + } + w.Buffer.AppendByte('"') + w.base64(data) + w.Buffer.AppendByte('"') +} + +// Uint8 appends an uint8 to the buffer. +func (w *Writer) Uint8(n uint8) { + w.Buffer.EnsureSpace(3) + w.Buffer.Buf = strconv.AppendUint(w.Buffer.Buf, uint64(n), 10) +} + +// Uint16 appends an uint16 to the buffer. +func (w *Writer) Uint16(n uint16) { + w.Buffer.EnsureSpace(5) + w.Buffer.Buf = strconv.AppendUint(w.Buffer.Buf, uint64(n), 10) +} + +// Uint32 appends an uint32 to the buffer. +func (w *Writer) Uint32(n uint32) { + w.Buffer.EnsureSpace(10) + w.Buffer.Buf = strconv.AppendUint(w.Buffer.Buf, uint64(n), 10) +} + +// Uint appends an uint to the buffer. +func (w *Writer) Uint(n uint) { + w.Buffer.EnsureSpace(20) + w.Buffer.Buf = strconv.AppendUint(w.Buffer.Buf, uint64(n), 10) +} + +// Uint64 appends an uint64 to the buffer. +func (w *Writer) Uint64(n uint64) { + w.Buffer.EnsureSpace(20) + w.Buffer.Buf = strconv.AppendUint(w.Buffer.Buf, n, 10) +} + +// Int8 appends an int8 to the buffer. +func (w *Writer) Int8(n int8) { + w.Buffer.EnsureSpace(4) + w.Buffer.Buf = strconv.AppendInt(w.Buffer.Buf, int64(n), 10) +} + +// Int16 appends an int16 to the buffer. +func (w *Writer) Int16(n int16) { + w.Buffer.EnsureSpace(6) + w.Buffer.Buf = strconv.AppendInt(w.Buffer.Buf, int64(n), 10) +} + +// Int32 appends an int32 to the buffer. +func (w *Writer) Int32(n int32) { + w.Buffer.EnsureSpace(11) + w.Buffer.Buf = strconv.AppendInt(w.Buffer.Buf, int64(n), 10) +} + +// Int appends an int to the buffer. +func (w *Writer) Int(n int) { + w.Buffer.EnsureSpace(21) + w.Buffer.Buf = strconv.AppendInt(w.Buffer.Buf, int64(n), 10) +} + +// Int64 appends an int64 to the buffer. +func (w *Writer) Int64(n int64) { + w.Buffer.EnsureSpace(21) + w.Buffer.Buf = strconv.AppendInt(w.Buffer.Buf, n, 10) +} + +// Uint8Str appends an uint8 to the buffer as a quoted string. +func (w *Writer) Uint8Str(n uint8) { + w.Buffer.EnsureSpace(3) + w.Buffer.Buf = append(w.Buffer.Buf, '"') + w.Buffer.Buf = strconv.AppendUint(w.Buffer.Buf, uint64(n), 10) + w.Buffer.Buf = append(w.Buffer.Buf, '"') +} + +// Uint16Str appends an uint16 to the buffer as a quoted string. +func (w *Writer) Uint16Str(n uint16) { + w.Buffer.EnsureSpace(5) + w.Buffer.Buf = append(w.Buffer.Buf, '"') + w.Buffer.Buf = strconv.AppendUint(w.Buffer.Buf, uint64(n), 10) + w.Buffer.Buf = append(w.Buffer.Buf, '"') +} + +// Uint32Str appends an uint32 to the buffer as a quoted string. +func (w *Writer) Uint32Str(n uint32) { + w.Buffer.EnsureSpace(10) + w.Buffer.Buf = append(w.Buffer.Buf, '"') + w.Buffer.Buf = strconv.AppendUint(w.Buffer.Buf, uint64(n), 10) + w.Buffer.Buf = append(w.Buffer.Buf, '"') +} + +// UintStr appends an uint to the buffer as a quoted string. +func (w *Writer) UintStr(n uint) { + w.Buffer.EnsureSpace(20) + w.Buffer.Buf = append(w.Buffer.Buf, '"') + w.Buffer.Buf = strconv.AppendUint(w.Buffer.Buf, uint64(n), 10) + w.Buffer.Buf = append(w.Buffer.Buf, '"') +} + +// Uint64Str appends an uint64 to the buffer as a quoted string. +func (w *Writer) Uint64Str(n uint64) { + w.Buffer.EnsureSpace(20) + w.Buffer.Buf = append(w.Buffer.Buf, '"') + w.Buffer.Buf = strconv.AppendUint(w.Buffer.Buf, n, 10) + w.Buffer.Buf = append(w.Buffer.Buf, '"') +} + +// UintptrStr appends an uintptr to the buffer as a quoted string. +func (w *Writer) UintptrStr(n uintptr) { + w.Buffer.EnsureSpace(20) + w.Buffer.Buf = append(w.Buffer.Buf, '"') + w.Buffer.Buf = strconv.AppendUint(w.Buffer.Buf, uint64(n), 10) + w.Buffer.Buf = append(w.Buffer.Buf, '"') +} + +// Int8Str appends an int8 to the buffer as a quoted string. +func (w *Writer) Int8Str(n int8) { + w.Buffer.EnsureSpace(4) + w.Buffer.Buf = append(w.Buffer.Buf, '"') + w.Buffer.Buf = strconv.AppendInt(w.Buffer.Buf, int64(n), 10) + w.Buffer.Buf = append(w.Buffer.Buf, '"') +} + +// Int16Str appends an int16 to the buffer as a quoted string. +func (w *Writer) Int16Str(n int16) { + w.Buffer.EnsureSpace(6) + w.Buffer.Buf = append(w.Buffer.Buf, '"') + w.Buffer.Buf = strconv.AppendInt(w.Buffer.Buf, int64(n), 10) + w.Buffer.Buf = append(w.Buffer.Buf, '"') +} + +// Int32Str appends an int32 to the buffer as a quoted string. +func (w *Writer) Int32Str(n int32) { + w.Buffer.EnsureSpace(11) + w.Buffer.Buf = append(w.Buffer.Buf, '"') + w.Buffer.Buf = strconv.AppendInt(w.Buffer.Buf, int64(n), 10) + w.Buffer.Buf = append(w.Buffer.Buf, '"') +} + +// IntStr appends an int to the buffer as a quoted string. +func (w *Writer) IntStr(n int) { + w.Buffer.EnsureSpace(21) + w.Buffer.Buf = append(w.Buffer.Buf, '"') + w.Buffer.Buf = strconv.AppendInt(w.Buffer.Buf, int64(n), 10) + w.Buffer.Buf = append(w.Buffer.Buf, '"') +} + +// Int64Str appends an int64 to the buffer as a quoted string. +func (w *Writer) Int64Str(n int64) { + w.Buffer.EnsureSpace(21) + w.Buffer.Buf = append(w.Buffer.Buf, '"') + w.Buffer.Buf = strconv.AppendInt(w.Buffer.Buf, n, 10) + w.Buffer.Buf = append(w.Buffer.Buf, '"') +} + +// Float32 appends a float32 to the buffer. +func (w *Writer) Float32(n float32) { + w.Buffer.EnsureSpace(20) + w.Buffer.Buf = strconv.AppendFloat(w.Buffer.Buf, float64(n), 'g', -1, 32) +} + +// Float32Str appends a float32 to the buffer as a quoted string. +func (w *Writer) Float32Str(n float32) { + w.Buffer.EnsureSpace(20) + w.Buffer.Buf = append(w.Buffer.Buf, '"') + w.Buffer.Buf = strconv.AppendFloat(w.Buffer.Buf, float64(n), 'g', -1, 32) + w.Buffer.Buf = append(w.Buffer.Buf, '"') +} + +// Float64 appends a float64 to the buffer. +func (w *Writer) Float64(n float64) { + w.Buffer.EnsureSpace(20) + w.Buffer.Buf = strconv.AppendFloat(w.Buffer.Buf, n, 'g', -1, 64) +} + +// Float64Str appends a float64 to the buffer as a quoted string. +func (w *Writer) Float64Str(n float64) { + w.Buffer.EnsureSpace(20) + w.Buffer.Buf = append(w.Buffer.Buf, '"') + w.Buffer.Buf = strconv.AppendFloat(w.Buffer.Buf, float64(n), 'g', -1, 64) + w.Buffer.Buf = append(w.Buffer.Buf, '"') +} + +// Bool appends a bool to the buffer. +func (w *Writer) Bool(v bool) { + w.Buffer.EnsureSpace(5) + if v { + w.Buffer.Buf = append(w.Buffer.Buf, "true"...) + } else { + w.Buffer.Buf = append(w.Buffer.Buf, "false"...) + } +} + +const chars = "0123456789abcdef" + +func getTable(falseValues ...int) [128]bool { + table := [128]bool{} + + for i := 0; i < 128; i++ { + table[i] = true + } + + for _, v := range falseValues { + table[v] = false + } + + return table +} + +var ( + htmlEscapeTable = getTable(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, '"', '&', '<', '>', '\\') + htmlNoEscapeTable = getTable(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, '"', '\\') +) + +func (w *Writer) String(s string) { + w.Buffer.AppendByte('"') + + // Portions of the string that contain no escapes are appended as + // byte slices. + + p := 0 // last non-escape symbol + + escapeTable := &htmlEscapeTable + if w.NoEscapeHTML { + escapeTable = &htmlNoEscapeTable + } + + for i := 0; i < len(s); { + c := s[i] + + if c < utf8.RuneSelf { + if escapeTable[c] { + // single-width character, no escaping is required + i++ + continue + } + + w.Buffer.AppendString(s[p:i]) + switch c { + case '\t': + w.Buffer.AppendString(`\t`) + case '\r': + w.Buffer.AppendString(`\r`) + case '\n': + w.Buffer.AppendString(`\n`) + case '\\': + w.Buffer.AppendString(`\\`) + case '"': + w.Buffer.AppendString(`\"`) + default: + w.Buffer.AppendString(`\u00`) + w.Buffer.AppendByte(chars[c>>4]) + w.Buffer.AppendByte(chars[c&0xf]) + } + + i++ + p = i + continue + } + + // broken utf + runeValue, runeWidth := utf8.DecodeRuneInString(s[i:]) + if runeValue == utf8.RuneError && runeWidth == 1 { + w.Buffer.AppendString(s[p:i]) + w.Buffer.AppendString(`\ufffd`) + i++ + p = i + continue + } + + // jsonp stuff - tab separator and line separator + if runeValue == '\u2028' || runeValue == '\u2029' { + w.Buffer.AppendString(s[p:i]) + w.Buffer.AppendString(`\u202`) + w.Buffer.AppendByte(chars[runeValue&0xf]) + i += runeWidth + p = i + continue + } + i += runeWidth + } + w.Buffer.AppendString(s[p:]) + w.Buffer.AppendByte('"') +} + +const encode = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" +const padChar = '=' + +func (w *Writer) base64(in []byte) { + + if len(in) == 0 { + return + } + + w.Buffer.EnsureSpace(((len(in)-1)/3 + 1) * 4) + + si := 0 + n := (len(in) / 3) * 3 + + for si < n { + // Convert 3x 8bit source bytes into 4 bytes + val := uint(in[si+0])<<16 | uint(in[si+1])<<8 | uint(in[si+2]) + + w.Buffer.Buf = append(w.Buffer.Buf, encode[val>>18&0x3F], encode[val>>12&0x3F], encode[val>>6&0x3F], encode[val&0x3F]) + + si += 3 + } + + remain := len(in) - si + if remain == 0 { + return + } + + // Add the remaining small block + val := uint(in[si+0]) << 16 + if remain == 2 { + val |= uint(in[si+1]) << 8 + } + + w.Buffer.Buf = append(w.Buffer.Buf, encode[val>>18&0x3F], encode[val>>12&0x3F]) + + switch remain { + case 2: + w.Buffer.Buf = append(w.Buffer.Buf, encode[val>>6&0x3F], byte(padChar)) + case 1: + w.Buffer.Buf = append(w.Buffer.Buf, byte(padChar), byte(padChar)) + } +} diff --git a/pkg/schema/schema.go b/pkg/schema/schema.go index 94ed0f7..545f6e6 100644 --- a/pkg/schema/schema.go +++ b/pkg/schema/schema.go @@ -12,7 +12,7 @@ import ( "strings" "time" - orderedmap "github.com/wk8/go-ordered-map/v2" + "github.com/conneroisu/groq-go/pkg/omap" ) const ( @@ -328,7 +328,7 @@ type ( // // Omitting this field has the same assertion behavior as an empty // object. - Properties *orderedmap.OrderedMap[string, *Schema] `json:"properties,omitempty"` + Properties *omap.OrderedMap[string, *Schema] `json:"properties,omitempty"` // PatternProperties are the pattern properties of the schema as specified in section 10.3.2.2 of RFC // draft-bhutton-json-schema-00. // @@ -1425,8 +1425,8 @@ func ToSnakeCase(str string) string { // newProperties is a helper method to instantiate a new properties ordered // map. -func newProperties() *orderedmap.OrderedMap[string, *Schema] { - return orderedmap.New[string, *Schema]() +func newProperties() *omap.OrderedMap[string, *Schema] { + return omap.New[string, *Schema]() } // Validate is used to check if the ID looks like a proper schema. From 37e7a121e47cfc7f3ec6d131d5632408257f7810 Mon Sep 17 00:00:00 2001 From: conneroisu Date: Wed, 6 Nov 2024 14:14:09 -0600 Subject: [PATCH 2/6] reorder struct for performance --- extensions/toolhouse/toolhouse.go | 4 ++-- pkg/omap/json_fuzz_test.go | 24 +++++++++--------------- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/extensions/toolhouse/toolhouse.go b/extensions/toolhouse/toolhouse.go index d38070e..318ad1b 100644 --- a/extensions/toolhouse/toolhouse.go +++ b/extensions/toolhouse/toolhouse.go @@ -23,10 +23,10 @@ type ( Toolhouse struct { apiKey string baseURL string - client *http.Client provider string - metadata map[string]any bundle string + client *http.Client + metadata map[string]any logger *slog.Logger header builders.Header } diff --git a/pkg/omap/json_fuzz_test.go b/pkg/omap/json_fuzz_test.go index f1abb42..cb81d64 100644 --- a/pkg/omap/json_fuzz_test.go +++ b/pkg/omap/json_fuzz_test.go @@ -1,7 +1,5 @@ package omap -// Adapted from https://github.com/dvyukov/go-fuzz-corpus/blob/c42c1b2/json/json.go - import ( "encoding/json" "testing" @@ -13,9 +11,8 @@ import ( func FuzzRoundTripJSON(f *testing.F) { f.Fuzz(func(t *testing.T, data []byte) { for _, testCase := range []struct { - name string - constructor func() any - // should be a function that asserts that 2 objects of the type returned by constructor are equal + name string + constructor func() any equalityAssertion func(*testing.T, any, any) bool }{ { @@ -52,20 +49,18 @@ func FuzzRoundTripJSON(f *testing.F) { require.NoError(t, json.Unmarshal(jsonData, v2)) if !assert.True(t, testCase.equalityAssertion(t, v1, v2), "failed with input data %q", string(data)) { - // look at that what the standard lib does with regular map, to help with debugging - var m1 map[string]any require.NoError(t, json.Unmarshal(data, &m1)) - mapJsonData, err := json.Marshal(m1) + mapJSONData, err := json.Marshal(m1) require.NoError(t, err) var m2 map[string]any - require.NoError(t, json.Unmarshal(mapJsonData, &m2)) + require.NoError(t, json.Unmarshal(mapJSONData, &m2)) t.Logf("initial data = %s", string(data)) t.Logf("unmarshalled map = %v", m1) - t.Logf("re-marshalled from map = %s", string(mapJsonData)) + t.Logf("re-marshalled from map = %s", string(mapJSONData)) t.Logf("re-marshalled from test obj = %s", string(jsonData)) t.Logf("re-unmarshalled map = %s", m2) } @@ -74,7 +69,6 @@ func FuzzRoundTripJSON(f *testing.F) { }) } -// only works for fairly basic maps, that's why it's just in this file func assertOrderedMapsEqual[K comparable, V any](t *testing.T, v1, v2 any) bool { om1, ok1 := v1.(*OrderedMap[K, V]) om2, ok2 := v2.(*OrderedMap[K, V]) @@ -101,11 +95,11 @@ type testFuzzStruct struct { } func assertTestFuzzStructEqual(t *testing.T, v1, v2 any) bool { - s1, ok := v1.(*testFuzzStruct) - s2, ok := v2.(*testFuzzStruct) + s1, ok1 := v1.(*testFuzzStruct) + s2, ok2 := v2.(*testFuzzStruct) - if !assert.True(t, ok, "v1 not an testFuzzStruct") || - !assert.True(t, ok, "v2 not an testFuzzStruct") { + if !assert.True(t, ok1, "v1 not an testFuzzStruct") || + !assert.True(t, ok2, "v2 not an testFuzzStruct") { return false } From 63fd74b1d568104ce9e91d865041008d67954771 Mon Sep 17 00:00:00 2001 From: conneroisu Date: Wed, 6 Nov 2024 14:20:06 -0600 Subject: [PATCH 3/6] removed doubly linked list dependency with own implementation --- go.mod | 1 - go.sum | 2 - pkg/list/doc.go | 2 + pkg/list/element.go | 35 +++++ pkg/list/list.go | 192 +++++++++++++++++++++++++++ pkg/list/list_test.go | 292 ++++++++++++++++++++++++++++++++++++++++++ pkg/omap/omap.go | 2 +- 7 files changed, 522 insertions(+), 4 deletions(-) create mode 100644 pkg/list/doc.go create mode 100644 pkg/list/element.go create mode 100644 pkg/list/list.go create mode 100644 pkg/list/list_test.go diff --git a/go.mod b/go.mod index 4a9194b..6270f73 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module github.com/conneroisu/groq-go go 1.23.2 require ( - github.com/bahlo/generic-list-go v0.2.0 github.com/buger/jsonparser v1.1.1 github.com/gorilla/websocket v1.5.3 github.com/stretchr/testify v1.9.0 diff --git a/go.sum b/go.sum index 6e460a3..c6c3137 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= -github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= diff --git a/pkg/list/doc.go b/pkg/list/doc.go new file mode 100644 index 0000000..15e3852 --- /dev/null +++ b/pkg/list/doc.go @@ -0,0 +1,2 @@ +// Package list containes the implementation a doubly linked list. +package list diff --git a/pkg/list/element.go b/pkg/list/element.go new file mode 100644 index 0000000..cdd6738 --- /dev/null +++ b/pkg/list/element.go @@ -0,0 +1,35 @@ +package list + +// Element is an element of a linked list. +type Element[T any] struct { + // Next and previous pointers in the doubly-linked list of elements. + // To simplify the implementation, internally a list l is implemented + // as a ring, such that &l.root is both the next element of the last + // list element (l.Back()) and the previous element of the first list + // element (l.Front()). + next, prev *Element[T] + + // The list to which this element belongs. + list *List[T] + + // The value stored with this element. + Value T +} + +// Next returns the next list element or nil. +func (e *Element[T]) Next() *Element[T] { + p := e.next + if e.list != nil && p != &e.list.root { + return p + } + return nil +} + +// Prev returns the previous list element or nil. +func (e *Element[T]) Prev() *Element[T] { + p := e.prev + if e.list != nil && p != &e.list.root { + return p + } + return nil +} diff --git a/pkg/list/list.go b/pkg/list/list.go new file mode 100644 index 0000000..b78a5ca --- /dev/null +++ b/pkg/list/list.go @@ -0,0 +1,192 @@ +package list + +// List represents a doubly linked list. +// The zero value for List is an empty list ready to use. +type List[T any] struct { + root Element[T] // sentinel list element, only &root, root.prev, and root.next are used + len int // current list length excluding (this) sentinel element +} + +// Init initializes or clears list l. +func (l *List[T]) Init() *List[T] { + l.root.next = &l.root + l.root.prev = &l.root + l.len = 0 + return l +} + +// New returns an initialized list. +func New[T any]() *List[T] { return new(List[T]).Init() } + +// Len returns the number of elements of list l. +// The complexity is O(1). +func (l *List[T]) Len() int { return l.len } + +// Front returns the first element of list l or nil if the list is empty. +func (l *List[T]) Front() *Element[T] { + if l.len == 0 { + return nil + } + return l.root.next +} + +// Back returns the last element of list l or nil if the list is empty. +func (l *List[T]) Back() *Element[T] { + if l.len == 0 { + return nil + } + return l.root.prev +} + +// lazyInit lazily initializes a zero List value. +func (l *List[T]) lazyInit() { + if l.root.next == nil { + l.Init() + } +} + +// insert inserts e after at, increments l.len, and returns e. +func (l *List[T]) insert(e, at *Element[T]) *Element[T] { + e.prev = at + e.next = at.next + e.prev.next = e + e.next.prev = e + e.list = l + l.len++ + return e +} + +// insertValue is a convenience wrapper for insert(&Element{Value: v}, at). +func (l *List[T]) insertValue(v T, at *Element[T]) *Element[T] { + return l.insert(&Element[T]{Value: v}, at) +} + +// remove removes e from its list, decrements l.len +func (l *List[T]) remove(e *Element[T]) { + e.prev.next = e.next + e.next.prev = e.prev + e.next = nil // avoid memory leaks + e.prev = nil // avoid memory leaks + e.list = nil + l.len-- +} + +// move moves e to next to at. +func (l *List[T]) move(e, at *Element[T]) { + if e == at { + return + } + e.prev.next = e.next + e.next.prev = e.prev + + e.prev = at + e.next = at.next + e.prev.next = e + e.next.prev = e +} + +// Remove removes e from l if e is an element of list l. +// It returns the element value e.Value. +// The element must not be nil. +func (l *List[T]) Remove(e *Element[T]) T { + if e.list == l { + // if e.list == l, l must have been initialized when e was inserted + // in l or l == nil (e is a zero Element) and l.remove will crash + l.remove(e) + } + return e.Value +} + +// PushFront inserts a new element e with value v at the front of list l and returns e. +func (l *List[T]) PushFront(v T) *Element[T] { + l.lazyInit() + return l.insertValue(v, &l.root) +} + +// PushBack inserts a new element e with value v at the back of list l and returns e. +func (l *List[T]) PushBack(v T) *Element[T] { + l.lazyInit() + return l.insertValue(v, l.root.prev) +} + +// InsertBefore inserts a new element e with value v immediately before mark and returns e. +// If mark is not an element of l, the list is not modified. +// The mark must not be nil. +func (l *List[T]) InsertBefore(v T, mark *Element[T]) *Element[T] { + if mark.list != l { + return nil + } + // see comment in List.Remove about initialization of l + return l.insertValue(v, mark.prev) +} + +// InsertAfter inserts a new element e with value v immediately after mark and returns e. +// If mark is not an element of l, the list is not modified. +// The mark must not be nil. +func (l *List[T]) InsertAfter(v T, mark *Element[T]) *Element[T] { + if mark.list != l { + return nil + } + // see comment in List.Remove about initialization of l + return l.insertValue(v, mark) +} + +// MoveToFront moves element e to the front of list l. +// If e is not an element of l, the list is not modified. +// The element must not be nil. +func (l *List[T]) MoveToFront(e *Element[T]) { + if e.list != l || l.root.next == e { + return + } + // see comment in List.Remove about initialization of l + l.move(e, &l.root) +} + +// MoveToBack moves element e to the back of list l. +// If e is not an element of l, the list is not modified. +// The element must not be nil. +func (l *List[T]) MoveToBack(e *Element[T]) { + if e.list != l || l.root.prev == e { + return + } + // see comment in List.Remove about initialization of l + l.move(e, l.root.prev) +} + +// MoveBefore moves element e to its new position before mark. +// If e or mark is not an element of l, or e == mark, the list is not modified. +// The element and mark must not be nil. +func (l *List[T]) MoveBefore(e, mark *Element[T]) { + if e.list != l || e == mark || mark.list != l { + return + } + l.move(e, mark.prev) +} + +// MoveAfter moves element e to its new position after mark. +// If e or mark is not an element of l, or e == mark, the list is not modified. +// The element and mark must not be nil. +func (l *List[T]) MoveAfter(e, mark *Element[T]) { + if e.list != l || e == mark || mark.list != l { + return + } + l.move(e, mark) +} + +// PushBackList inserts a copy of another list at the back of list l. +// The lists l and other may be the same. They must not be nil. +func (l *List[T]) PushBackList(other *List[T]) { + l.lazyInit() + for i, e := other.Len(), other.Front(); i > 0; i, e = i-1, e.Next() { + l.insertValue(e.Value, l.root.prev) + } +} + +// PushFrontList inserts a copy of another list at the front of list l. +// The lists l and other may be the same. They must not be nil. +func (l *List[T]) PushFrontList(other *List[T]) { + l.lazyInit() + for i, e := other.Len(), other.Back(); i > 0; i, e = i-1, e.Prev() { + l.insertValue(e.Value, &l.root) + } +} diff --git a/pkg/list/list_test.go b/pkg/list/list_test.go new file mode 100644 index 0000000..cd8ee3b --- /dev/null +++ b/pkg/list/list_test.go @@ -0,0 +1,292 @@ +package list + +import "testing" + +func checkListLen[T any](t *testing.T, l *List[T], len int) bool { + if n := l.Len(); n != len { + t.Errorf("l.Len() = %d, want %d", n, len) + return false + } + return true +} +func checkListPointers[T any](t *testing.T, l *List[T], es []*Element[T]) { + root := &l.root + if !checkListLen(t, l, len(es)) { + return + } + // zero length lists must be the zero value or properly initialized (sentinel circle) + if len(es) == 0 { + if l.root.next != nil && l.root.next != root || l.root.prev != nil && l.root.prev != root { + t.Errorf("l.root.next = %p, l.root.prev = %p; both should both be nil or %p", l.root.next, l.root.prev, root) + } + return + } + // len(es) > 0 + // check internal and external prev/next connections + for i, e := range es { + prev := root + Prev := (*Element[T])(nil) + if i > 0 { + prev = es[i-1] + Prev = prev + } + if p := e.prev; p != prev { + t.Errorf("elt[%d](%p).prev = %p, want %p", i, e, p, prev) + } + if p := e.Prev(); p != Prev { + t.Errorf("elt[%d](%p).Prev() = %p, want %p", i, e, p, Prev) + } + next := root + Next := (*Element[T])(nil) + if i < len(es)-1 { + next = es[i+1] + Next = next + } + if n := e.next; n != next { + t.Errorf("elt[%d](%p).next = %p, want %p", i, e, n, next) + } + if n := e.Next(); n != Next { + t.Errorf("elt[%d](%p).Next() = %p, want %p", i, e, n, Next) + } + } +} +func TestList(t *testing.T) { + // Single element list + { + l := New[string]() + checkListPointers(t, l, []*Element[string]{}) + e := l.PushFront("a") + checkListPointers(t, l, []*Element[string]{e}) + l.MoveToFront(e) + checkListPointers(t, l, []*Element[string]{e}) + l.MoveToBack(e) + checkListPointers(t, l, []*Element[string]{e}) + l.Remove(e) + checkListPointers(t, l, []*Element[string]{}) + } + // Bigger list + l := New[int]() + checkListPointers(t, l, []*Element[int]{}) + e2 := l.PushFront(2) + e1 := l.PushFront(1) + e3 := l.PushBack(3) + e4 := l.PushBack(4) + checkListPointers(t, l, []*Element[int]{e1, e2, e3, e4}) + l.Remove(e2) + checkListPointers(t, l, []*Element[int]{e1, e3, e4}) + l.MoveToFront(e3) // move from middle + checkListPointers(t, l, []*Element[int]{e3, e1, e4}) + l.MoveToFront(e1) + l.MoveToBack(e3) // move from middle + checkListPointers(t, l, []*Element[int]{e1, e4, e3}) + l.MoveToFront(e3) // move from back + checkListPointers(t, l, []*Element[int]{e3, e1, e4}) + l.MoveToFront(e3) // should be no-op + checkListPointers(t, l, []*Element[int]{e3, e1, e4}) + l.MoveToBack(e3) // move from front + checkListPointers(t, l, []*Element[int]{e1, e4, e3}) + l.MoveToBack(e3) // should be no-op + checkListPointers(t, l, []*Element[int]{e1, e4, e3}) + e2 = l.InsertBefore(2, e1) // insert before front + checkListPointers(t, l, []*Element[int]{e2, e1, e4, e3}) + l.Remove(e2) + e2 = l.InsertBefore(2, e4) // insert before middle + checkListPointers(t, l, []*Element[int]{e1, e2, e4, e3}) + l.Remove(e2) + e2 = l.InsertBefore(2, e3) // insert before back + checkListPointers(t, l, []*Element[int]{e1, e4, e2, e3}) + l.Remove(e2) + e2 = l.InsertAfter(2, e1) // insert after front + checkListPointers(t, l, []*Element[int]{e1, e2, e4, e3}) + l.Remove(e2) + e2 = l.InsertAfter(2, e4) // insert after middle + checkListPointers(t, l, []*Element[int]{e1, e4, e2, e3}) + l.Remove(e2) + e2 = l.InsertAfter(2, e3) // insert after back + checkListPointers(t, l, []*Element[int]{e1, e4, e3, e2}) + l.Remove(e2) + // Check standard iteration. + sum := 0 + for e := l.Front(); e != nil; e = e.Next() { + sum += e.Value + } + if sum != 8 { + t.Errorf("sum over l = %d, want 8", sum) + } + // Clear all elements by iterating + var next *Element[int] + for e := l.Front(); e != nil; e = next { + next = e.Next() + l.Remove(e) + } + checkListPointers(t, l, []*Element[int]{}) +} +func checkList[T int](t *testing.T, l *List[T], es []T) { + if !checkListLen(t, l, len(es)) { + return + } + i := 0 + for e := l.Front(); e != nil; e = e.Next() { + le := e.Value + if le != es[i] { + t.Errorf("elt[%d].Value = %v, want %v", i, le, es[i]) + } + i++ + } +} +func TestExtending(t *testing.T) { + l1 := New[int]() + l2 := New[int]() + l1.PushBack(1) + l1.PushBack(2) + l1.PushBack(3) + l2.PushBack(4) + l2.PushBack(5) + l3 := New[int]() + l3.PushBackList(l1) + checkList(t, l3, []int{1, 2, 3}) + l3.PushBackList(l2) + checkList(t, l3, []int{1, 2, 3, 4, 5}) + l3 = New[int]() + l3.PushFrontList(l2) + checkList(t, l3, []int{4, 5}) + l3.PushFrontList(l1) + checkList(t, l3, []int{1, 2, 3, 4, 5}) + checkList(t, l1, []int{1, 2, 3}) + checkList(t, l2, []int{4, 5}) + l3 = New[int]() + l3.PushBackList(l1) + checkList(t, l3, []int{1, 2, 3}) + l3.PushBackList(l3) + checkList(t, l3, []int{1, 2, 3, 1, 2, 3}) + l3 = New[int]() + l3.PushFrontList(l1) + checkList(t, l3, []int{1, 2, 3}) + l3.PushFrontList(l3) + checkList(t, l3, []int{1, 2, 3, 1, 2, 3}) + l3 = New[int]() + l1.PushBackList(l3) + checkList(t, l1, []int{1, 2, 3}) + l1.PushFrontList(l3) + checkList(t, l1, []int{1, 2, 3}) +} +func TestRemove(t *testing.T) { + l := New[int]() + e1 := l.PushBack(1) + e2 := l.PushBack(2) + checkListPointers(t, l, []*Element[int]{e1, e2}) + e := l.Front() + l.Remove(e) + checkListPointers(t, l, []*Element[int]{e2}) + l.Remove(e) + checkListPointers(t, l, []*Element[int]{e2}) +} +func TestIssue4103(t *testing.T) { + l1 := New[int]() + l1.PushBack(1) + l1.PushBack(2) + l2 := New[int]() + l2.PushBack(3) + l2.PushBack(4) + e := l1.Front() + l2.Remove(e) // l2 should not change because e is not an element of l2 + if n := l2.Len(); n != 2 { + t.Errorf("l2.Len() = %d, want 2", n) + } + l1.InsertBefore(8, e) + if n := l1.Len(); n != 3 { + t.Errorf("l1.Len() = %d, want 3", n) + } +} +func TestIssue6349(t *testing.T) { + l := New[int]() + l.PushBack(1) + l.PushBack(2) + e := l.Front() + l.Remove(e) + if e.Value != 1 { + t.Errorf("e.value = %d, want 1", e.Value) + } + if e.Next() != nil { + t.Errorf("e.Next() != nil") + } + if e.Prev() != nil { + t.Errorf("e.Prev() != nil") + } +} +func TestMove(t *testing.T) { + l := New[int]() + e1 := l.PushBack(1) + e2 := l.PushBack(2) + e3 := l.PushBack(3) + e4 := l.PushBack(4) + l.MoveAfter(e3, e3) + checkListPointers(t, l, []*Element[int]{e1, e2, e3, e4}) + l.MoveBefore(e2, e2) + checkListPointers(t, l, []*Element[int]{e1, e2, e3, e4}) + l.MoveAfter(e3, e2) + checkListPointers(t, l, []*Element[int]{e1, e2, e3, e4}) + l.MoveBefore(e2, e3) + checkListPointers(t, l, []*Element[int]{e1, e2, e3, e4}) + l.MoveBefore(e2, e4) + checkListPointers(t, l, []*Element[int]{e1, e3, e2, e4}) + e2, e3 = e3, e2 + l.MoveBefore(e4, e1) + checkListPointers(t, l, []*Element[int]{e4, e1, e2, e3}) + e1, e2, e3, e4 = e4, e1, e2, e3 + l.MoveAfter(e4, e1) + checkListPointers(t, l, []*Element[int]{e1, e4, e2, e3}) + e2, e3, e4 = e4, e2, e3 + l.MoveAfter(e2, e3) + checkListPointers(t, l, []*Element[int]{e1, e3, e2, e4}) +} + +// Test PushFront, PushBack, PushFrontList, PushBackList with uninitialized List +func TestZeroList(t *testing.T) { + var l1 = new(List[int]) + l1.PushFront(1) + checkList(t, l1, []int{1}) + var l2 = new(List[int]) + l2.PushBack(1) + checkList(t, l2, []int{1}) + var l3 = new(List[int]) + l3.PushFrontList(l1) + checkList(t, l3, []int{1}) + var l4 = new(List[int]) + l4.PushBackList(l2) + checkList(t, l4, []int{1}) +} + +// Test that a list l is not modified when calling InsertBefore with a mark that is not an element of l. +func TestInsertBeforeUnknownMark(t *testing.T) { + var l List[int] + l.PushBack(1) + l.PushBack(2) + l.PushBack(3) + l.InsertBefore(1, new(Element[int])) + checkList(t, &l, []int{1, 2, 3}) +} + +// Test that a list l is not modified when calling InsertAfter with a mark that is not an element of l. +func TestInsertAfterUnknownMark(t *testing.T) { + var l List[int] + l.PushBack(1) + l.PushBack(2) + l.PushBack(3) + l.InsertAfter(1, new(Element[int])) + checkList(t, &l, []int{1, 2, 3}) +} + +// Test that a list l is not modified when calling MoveAfter or MoveBefore with a mark that is not an element of l. +func TestMoveUnknownMark(t *testing.T) { + var l1 List[int] + e1 := l1.PushBack(1) + var l2 List[int] + e2 := l2.PushBack(2) + l1.MoveAfter(e1, e2) + checkList(t, &l1, []int{1}) + checkList(t, &l2, []int{2}) + l1.MoveBefore(e1, e2) + checkList(t, &l1, []int{1}) + checkList(t, &l2, []int{2}) +} diff --git a/pkg/omap/omap.go b/pkg/omap/omap.go index a17c9eb..307e193 100644 --- a/pkg/omap/omap.go +++ b/pkg/omap/omap.go @@ -3,7 +3,7 @@ package omap import ( "fmt" - list "github.com/bahlo/generic-list-go" + "github.com/conneroisu/groq-go/pkg/list" ) // Pair is a generic pair. From 1713ff9bbf89ad54943f0cda288023bb25a45459 Mon Sep 17 00:00:00 2001 From: conneroisu Date: Wed, 6 Nov 2024 18:10:42 -0600 Subject: [PATCH 4/6] fix linting errors --- pkg/list/list_test.go | 6 +++--- pkg/omap/omap_test.go | 1 + pkg/omap/wbuf.go | 7 ++----- pkg/omap/writer.go | 6 ++++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/pkg/list/list_test.go b/pkg/list/list_test.go index cd8ee3b..cd071c4 100644 --- a/pkg/list/list_test.go +++ b/pkg/list/list_test.go @@ -2,9 +2,9 @@ package list import "testing" -func checkListLen[T any](t *testing.T, l *List[T], len int) bool { - if n := l.Len(); n != len { - t.Errorf("l.Len() = %d, want %d", n, len) +func checkListLen[T any](t *testing.T, l *List[T], length int) bool { + if n := l.Len(); n != length { + t.Errorf("l.Len() = %d, want %d", n, length) return false } return true diff --git a/pkg/omap/omap_test.go b/pkg/omap/omap_test.go index 2c33bea..4bebc02 100644 --- a/pkg/omap/omap_test.go +++ b/pkg/omap/omap_test.go @@ -294,6 +294,7 @@ func TestGetAndMove(t *testing.T) { value, err = om.GetAndMoveToBack(100) assert.Equal(t, &KeyNotFoundError[int]{100}, err) + assert.Nil(t, value) } func TestAddPairs(t *testing.T) { diff --git a/pkg/omap/wbuf.go b/pkg/omap/wbuf.go index 090ae49..d962405 100644 --- a/pkg/omap/wbuf.go +++ b/pkg/omap/wbuf.go @@ -44,9 +44,6 @@ func putBuf(buf []byte) { if size < config.PooledSize { return } - if c := buffers[size]; c != nil { - c.Put(buf[:0]) - } } // getBuf gets a chunk from reuse pool or creates a new one if reuse failed. @@ -76,11 +73,11 @@ type Buffer struct { // possibly creating a new chunk. func (b *Buffer) EnsureSpace(s int) { if cap(b.Buf)-len(b.Buf) < s { - b.ensureSpaceSlow(s) + b.ensureSpaceSlow() } } -func (b *Buffer) ensureSpaceSlow(p int) { +func (b *Buffer) ensureSpaceSlow() { l := len(b.Buf) if l > 0 { if cap(b.toPool) != cap(b.Buf) { diff --git a/pkg/omap/writer.go b/pkg/omap/writer.go index 97a2334..9240c4a 100644 --- a/pkg/omap/writer.go +++ b/pkg/omap/writer.go @@ -11,8 +11,10 @@ import ( type Flags int const ( - NilMapAsEmpty Flags = 1 << iota // Encode nil map as '{}' rather than 'null'. - NilSliceAsEmpty // Encode nil slice as '[]' rather than 'null'. + // NilMapAsEmpty encodes nil map as '{}' rather than 'null'. + NilMapAsEmpty Flags = 1 << iota + // NilSliceAsEmpty encodes nil slice as '[]' rather than 'null'. + NilSliceAsEmpty ) // Writer is a JSON writer. From 7bb52d174923e69135cd30a7b7d464d4e4e41bd2 Mon Sep 17 00:00:00 2001 From: conneroisu Date: Wed, 6 Nov 2024 18:12:35 -0600 Subject: [PATCH 5/6] sync workspace --- scripts/generate-jigsaw-accents/go.mod | 2 ++ scripts/generate-jigsaw-accents/go.sum | 1 + 2 files changed, 3 insertions(+) create mode 100644 scripts/generate-jigsaw-accents/go.sum diff --git a/scripts/generate-jigsaw-accents/go.mod b/scripts/generate-jigsaw-accents/go.mod index d9a6860..ce42a30 100644 --- a/scripts/generate-jigsaw-accents/go.mod +++ b/scripts/generate-jigsaw-accents/go.mod @@ -1,3 +1,5 @@ module github.com/conneroisu/groq-go/generate-jigsaw-accents go 1.23.2 + +require golang.org/x/text v0.18.0 diff --git a/scripts/generate-jigsaw-accents/go.sum b/scripts/generate-jigsaw-accents/go.sum new file mode 100644 index 0000000..94d17ef --- /dev/null +++ b/scripts/generate-jigsaw-accents/go.sum @@ -0,0 +1 @@ +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= From 3c8352d08c059f0c11cc050d249f255a725ab987 Mon Sep 17 00:00:00 2001 From: conneroisu Date: Wed, 6 Nov 2024 18:14:05 -0600 Subject: [PATCH 6/6] remove agents for now --- agents.go | 63 ------------------------------------------------------- 1 file changed, 63 deletions(-) delete mode 100644 agents.go diff --git a/agents.go b/agents.go deleted file mode 100644 index df5693e..0000000 --- a/agents.go +++ /dev/null @@ -1,63 +0,0 @@ -package groq - -import ( - "context" - "log/slog" - - "github.com/conneroisu/groq-go/pkg/tools" -) - -type ( - // Agenter is an interface for an agent. - Agenter interface { - ToolManager - } - // ToolManager is an interface for a tool manager. - ToolManager interface { - ToolGetter - ToolRunner - } - // ToolGetter is an interface for a tool getter. - ToolGetter interface { - Get( - ctx context.Context, - params ToolGetParams, - ) ([]tools.Tool, error) - } - // ToolRunner is an interface for a tool runner. - ToolRunner interface { - Run( - ctx context.Context, - response ChatCompletionResponse, - ) ([]ChatCompletionMessage, error) - } - // ToolGetParams are the parameters for getting tools. - ToolGetParams struct { - } - // Router is an agent router. - // - // It is used to route messages to the appropriate model. - Router struct { - // Agents is the agents of the router. - Agents []Agent - // Logger is the logger of the router. - Logger *slog.Logger - } -) - -// Agent is an agent. -type Agent struct { - client *Client - logger *slog.Logger -} - -// NewAgent creates a new agent. -func NewAgent( - client *Client, - logger *slog.Logger, -) *Agent { - return &Agent{ - client: client, - logger: logger, - } -}