From 7fd8bac2bc9c25b5f343e02d4ada27a35ea86ffe Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 18 Jul 2023 18:02:57 +0200 Subject: [PATCH] Added basic functionality --- .gitignore | 12 +---------- Context.go | 51 +++++++++++++++++++++++++++++++++++++++++++++++ LICENSE | 2 +- README.md | 1 + Server.go | 49 +++++++++++++++++++++++++++++++++++++++++++++ Server_test.go | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 7 +++++++ go.sum | 4 ++++ pool.go | 9 +++++++++ 9 files changed, 177 insertions(+), 12 deletions(-) create mode 100644 Context.go create mode 100644 Server.go create mode 100644 Server_test.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 pool.go diff --git a/.gitignore b/.gitignore index 5cbdfa9..7c62cc2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,3 @@ -# ---> Go.AllowList -# Allowlisting gitignore template for GO projects prevents us -# from adding various unwanted local files, such as generated -# files, developer configurations or IDE-specific files etc. -# -# Recommended: Go.AllowList.gitignore - # Ignore everything * @@ -18,8 +11,5 @@ !README.md !LICENSE -# !Makefile - # ...even if they are in subdirectories -!*/ - +!*/ \ No newline at end of file diff --git a/Context.go b/Context.go new file mode 100644 index 0000000..4d4e1a6 --- /dev/null +++ b/Context.go @@ -0,0 +1,51 @@ +package aero + +import ( + "net/http" +) + +// maxParams defines the maximum number of parameters per route. +const maxParams = 16 + +// Context represents the interface for a request & response context. +type Context interface { + Bytes([]byte) error + Error(int, error) error +} + +// context represents a request & response context. +type context struct { + request *http.Request + response http.ResponseWriter + paramNames [maxParams]string + paramValues [maxParams]string + paramCount int +} + +// newContext returns a new context from the pool. +func newContext(request *http.Request, response http.ResponseWriter) *context { + ctx := contextPool.Get().(*context) + ctx.request = request + ctx.response = response + ctx.paramCount = 0 + return ctx +} + +// Bytes responds with a raw byte slice. +func (ctx *context) Bytes(body []byte) error { + _, err := ctx.response.Write(body) + return err +} + +// Error is used for sending error messages to the client. +func (ctx *context) Error(status int, err error) error { + ctx.response.WriteHeader(status) + return err +} + +// addParameter adds a new parameter to the context. +func (ctx *context) addParameter(name string, value string) { + ctx.paramNames[ctx.paramCount] = name + ctx.paramValues[ctx.paramCount] = value + ctx.paramCount++ +} diff --git a/LICENSE b/LICENSE index 45695be..40d5bcd 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 go +Copyright (c) 2023 Eduard Urbach Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/README.md b/README.md index 0578fd6..25e3590 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,3 @@ # aero +High-performance web framework. diff --git a/Server.go b/Server.go new file mode 100644 index 0000000..d6299f8 --- /dev/null +++ b/Server.go @@ -0,0 +1,49 @@ +package aero + +import ( + "fmt" + "net/http" + + "git.akyoto.dev/go/router" +) + +// Handler is a function that deals with the given request/response context. +type Handler func(Context) error + +// Server represents a single web service. +type Server struct { + router *router.Router[Handler] +} + +// New creates a new server. +func New() *Server { + return &Server{ + router: router.New[Handler](), + } +} + +// Get registers your function to be called when the given GET path has been requested. +func (server *Server) Get(path string, handler Handler) { + server.router.Add("GET", path, handler) +} + +// ServeHTTP responds to the given request. +func (server *Server) ServeHTTP(response http.ResponseWriter, request *http.Request) { + ctx := newContext(request, response) + handler := server.router.LookupNoAlloc(request.Method, request.URL.Path, ctx.addParameter) + + if handler == nil { + response.WriteHeader(http.StatusNotFound) + fmt.Fprint(response, http.StatusText(http.StatusNotFound)) + contextPool.Put(ctx) + return + } + + err := handler(ctx) + + if err != nil { + fmt.Fprint(response, err.Error()) + } + + contextPool.Put(ctx) +} diff --git a/Server_test.go b/Server_test.go new file mode 100644 index 0000000..94ceeda --- /dev/null +++ b/Server_test.go @@ -0,0 +1,54 @@ +package aero_test + +import ( + "errors" + "io" + "net/http" + "net/http/httptest" + "testing" + + "git.akyoto.dev/go/aero" + "git.akyoto.dev/go/assert" +) + +func TestServer(t *testing.T) { + server := aero.New() + + server.Get("/", func(ctx aero.Context) error { + return ctx.Bytes([]byte("Hello")) + }) + + server.Get("/blog/:post", func(ctx aero.Context) error { + return ctx.Bytes([]byte("Hello")) + }) + + server.Get("/error", func(ctx aero.Context) error { + return ctx.Error(http.StatusUnauthorized, errors.New("Not logged in")) + }) + + tests := []struct { + URL string + Status int + Body string + }{ + {URL: "/", Status: http.StatusOK, Body: "Hello"}, + {URL: "/blog/post", Status: http.StatusOK, Body: "Hello"}, + {URL: "/error", Status: http.StatusUnauthorized, Body: "Not logged in"}, + {URL: "/not-found", Status: http.StatusNotFound, Body: http.StatusText(http.StatusNotFound)}, + } + + for _, test := range tests { + t.Run("example.com"+test.URL, func(t *testing.T) { + request := httptest.NewRequest(http.MethodGet, test.URL, nil) + response := httptest.NewRecorder() + server.ServeHTTP(response, request) + + result := response.Result() + assert.Equal(t, result.StatusCode, test.Status) + + body, err := io.ReadAll(result.Body) + assert.Nil(t, err) + assert.DeepEqual(t, string(body), test.Body) + }) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..a8709a7 --- /dev/null +++ b/go.mod @@ -0,0 +1,7 @@ +module git.akyoto.dev/go/aero + +go 1.20 + +require git.akyoto.dev/go/assert v0.1.2 + +require git.akyoto.dev/go/router v0.1.1 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d70dccc --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +git.akyoto.dev/go/assert v0.1.2 h1:3paz/5z/JcGK/2K9J+pVh5Jwt2gYfJQG+P5OE9/jB7Y= +git.akyoto.dev/go/assert v0.1.2/go.mod h1:Zr/UFuiqmqRmFFgpBGwF71jbzb6iYJfXFeePYHGtWsg= +git.akyoto.dev/go/router v0.1.1 h1:6fHjzv59MKMhO2DsM90mkI5hy5PrcjV4WeD1Vk1xXXs= +git.akyoto.dev/go/router v0.1.1/go.mod h1:IwwEUJU2ExmozpZKMbDOKdiVT516oAijnxGDg9kvBt4= diff --git a/pool.go b/pool.go new file mode 100644 index 0000000..6cb772b --- /dev/null +++ b/pool.go @@ -0,0 +1,9 @@ +package aero + +import "sync" + +var contextPool = sync.Pool{ + New: func() any { + return &context{} + }, +}