commit 2147470e1341ece25f39fa92921b24252b604535 Author: William Dillon Date: Wed May 7 20:06:38 2025 -0400 first commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..273db0d --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +# package apicontext +apicontext simply combines the context.Context with its corresponding context.CancelFunc, +making it simple to manage objects with context. It comes with some helper functiosn that +can replace infinite `for` loops (see `ContextIsNotDone`) \ No newline at end of file diff --git a/apicontext.go b/apicontext.go new file mode 100644 index 0000000..65c5fdf --- /dev/null +++ b/apicontext.go @@ -0,0 +1,76 @@ +package apicontext + +import ( + "context" + "time" +) + +type Context struct { + ctx context.Context + cancel context.CancelFunc +} + +func ContextIsDone(ctx context.Context) bool { + select { + case <-ctx.Done(): + return true + default: + return false + } +} + +func ContextIsNotDone(ctx context.Context) bool { return !ContextIsDone(ctx) } + +func Background() context.Context { return context.Background() } + +func (c *Context) Cancel() { + if c.cancel != nil { + c.cancel() + } +} + +func (c *Context) Deadline() (deadline time.Time, ok bool) { + return c.ctx.Deadline() +} + +func (c *Context) Done() <-chan struct{} { + return c.ctx.Done() +} + +func (c *Context) Err() error { + return c.ctx.Err() +} + +func (c *Context) Value(key any) any { + return c.ctx.Value(key) +} + +func (c *Context) IsDone() bool { + return ContextIsDone(c.ctx) +} + +func (c *Context) IsNotDone() bool { + return ContextIsNotDone(c.ctx) +} + +// constructors + +func WithCancel(parent context.Context) *Context { + ctx, cancel := context.WithCancel(parent) + return &Context{ctx: ctx, cancel: cancel} +} + +func WithTimeout(parent context.Context, timeout time.Duration) *Context { + ctx, cancel := context.WithTimeout(parent, timeout) + return &Context{ctx: ctx, cancel: cancel} +} + +func WithDeadline(parent context.Context, deadline time.Time) *Context { + ctx, cancel := context.WithDeadline(parent, deadline) + return &Context{ctx: ctx, cancel: cancel} +} + +func WithValue(parent context.Context, key, val any) *Context { + ctx := context.WithValue(parent, key, val) + return &Context{ctx: ctx} +} diff --git a/apicontext_test.go b/apicontext_test.go new file mode 100644 index 0000000..aad6b8b --- /dev/null +++ b/apicontext_test.go @@ -0,0 +1,88 @@ +package apicontext + +import ( + "context" + "errors" + "testing" + "time" +) + +func TestContextIsDone(t *testing.T) { + ctx := WithCancel(Background()) + defer ctx.Cancel() + if ContextIsDone(ctx) { + t.Fatalf("error: context was done before cancel()\n") + } else if err := ctx.Err(); err != nil { + t.Fatalf("error: context reporting error %v; expected nil\n", err) + } + ctx.Cancel() + if !ContextIsDone(ctx) { + t.Fatalf("error: context was not done after cancel()\n") + } else if want, got := context.Canceled, ctx.Err(); !errors.Is(got, want) { + t.Fatalf("error: wanted %v; got %v\n", want, got) + } +} + +func TestContextWithTimeout(t *testing.T) { + timeout := time.Second + controlCtx, cancelControlCtx := context.WithTimeout(Background(), timeout*2) + defer cancelControlCtx() + ctx := WithTimeout(Background(), timeout) + defer ctx.Cancel() + if want, got := true, ctx.IsNotDone(); want != got { + t.Fatalf("error: wanted %v; got %v\n", want, got) + } else if want, got := error(nil), ctx.Err(); want != got { + t.Fatalf("error: wanted err == %v; got %v\n", want, got) + } + select { + case <-ctx.Done(): + case <-controlCtx.Done(): + t.Fatalf("error: control timeout reached - apicontext.WithTimeout failed to fire\n") + } + if want, got := true, ctx.IsDone(); want != got { + t.Fatalf("error: wanted %v; got %v\n", want, got) + } else if want, got := context.DeadlineExceeded, ctx.Err(); !errors.Is(got, want) { + t.Fatalf("error: wanted %v; got %v\n", want, got) + } +} + +func TestContextWithDeadline(t *testing.T) { + deadline := time.Now().Add(time.Second) + controlCtx, cancelControlCtx := context.WithDeadline(Background(), deadline.Add(time.Second)) + defer cancelControlCtx() + ctx := WithDeadline(Background(), deadline) + defer ctx.Cancel() + if gotDeadline, ok := ctx.Deadline(); !ok { + t.Fatalf("error: deadline !ok\n") + } else if want, got := deadline, gotDeadline; want != got { + t.Fatalf("error: wanted deadline %s; got %s\n", want, got) + } else if err := ctx.Err(); err != nil { + t.Fatalf("error: wanted err == nil; got %v\n", err) + } + select { + case <-ctx.Done(): + case <-controlCtx.Done(): + t.Fatalf("error: control timeout reached - apicontext.WithDeadline failed to fire\n") + } + if want, got := context.DeadlineExceeded, ctx.Err(); !errors.Is(got, want) { + t.Fatalf("error: wanted err == %v; got %v\n", want, got) + } +} + +func TestContextWithValue(t *testing.T) { + type ValueKey string + key, value := ValueKey("TheTestKey"), "TheTestValue" + ctx := WithValue(Background(), key, value) + // expect no error from cancel + defer func() { + if err := recover(); err != nil { + t.Fatalf("error: expected no panic; got %s\n", err) + } + }() + ctx.Cancel() + if want, got := value, ctx.Value(key).(string); want != got { + t.Fatalf("error: wanted %s; got %s\n", want, got) + } else if err := ctx.Err(); err != nil { + t.Fatalf("error: got error from context...\n") + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..6eb9ddd --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module code.wmdillon.com/GoApi/apicontext + +go 1.24.3