├── .github └── workflows │ ├── ci.yml │ ├── docker.yml │ ├── issue-auto-label.yml │ ├── release.yml │ └── security-audit.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── context.go ├── docs ├── Context_and_Responses.md ├── Getting_Started.md ├── Logging.md ├── Middleware.md ├── README.md ├── Routing.md ├── Static_Files.md └── Templates.md ├── engine.go ├── examples ├── basic │ └── example_basic.go ├── dynamic_routes │ └── example_dynamic_route.go ├── html_template │ └── example_html_template.go ├── json_response │ └── example_json_response.go ├── middleware │ └── example_middleware.go ├── query_params │ └── example_query_params.go └── templates │ ├── hey.html │ └── index.gohtml ├── go.mod ├── goster.go ├── goster_test.go ├── logger.go ├── meta.go ├── meta_test.go ├── middleware.go ├── response.go ├── routes.go ├── routes_test.go ├── utilities ├── run_example.sh └── run_test.sh ├── utils.go └── utils_test.go /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: [ master ] 5 | pull_request: 6 | branches: [ master ] 7 | jobs: 8 | build-test-lint: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: read # minimal permissions for checking out code 12 | checks: write #allow write access to checks to allow the action to annotate code with lint errors in the PR 13 | steps: 14 | - name: Check out code 15 | uses: actions/checkout@v4 16 | 17 | - name: Ensure go.mod exists 18 | run: | 19 | if [ ! -f go.mod ]; then 20 | echo "go.mod not found, initializing module" 21 | go mod init github.com/dpouris/goster 22 | fi 23 | 24 | - name: Tidy modules 25 | run: go mod tidy 26 | 27 | - name: Set up Go 28 | uses: actions/setup-go@v5 29 | with: 30 | go-version: '1.22.x' 31 | cache: true 32 | 33 | - name: Create mod cache directory 34 | run: mkdir -p /home/runner/go/pkg/mod 35 | 36 | - name: Cache modules 37 | uses: actions/cache@v3 38 | with: 39 | path: /home/runner/go/pkg/mod 40 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 41 | restore-keys: | 42 | ${{ runner.os }}-go- 43 | 44 | - name: Build (compile) 45 | run: go build ./... 46 | 47 | - name: Run tests with coverage 48 | run: go test -v ./... -coverprofile=coverage.out 49 | 50 | - name: Upload coverage report 51 | uses: actions/upload-artifact@v4 52 | with: 53 | name: coverage-report 54 | path: coverage.out 55 | 56 | - name: Lint code 57 | id: lint-code 58 | uses: golangci/golangci-lint-action@v6 59 | with: 60 | version: latest 61 | problem-matchers: true 62 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Docker Image CI 2 | on: 3 | workflow_dispatch: 4 | # push: 5 | # tags: 6 | # - 'v*.*.*' 7 | jobs: 8 | docker-build: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: read # read access to repo (if needed for Dockerfile) 12 | packages: write # if pushing to GHCR, needed to push packages 13 | steps: 14 | - name: Check out code 15 | uses: actions/checkout@v4 16 | 17 | - name: Set up Docker Buildx 18 | uses: docker/setup-buildx-action@v2 19 | 20 | - name: Log in to Docker Hub 21 | uses: docker/login-action@v2 22 | with: 23 | registry: docker.io 24 | username: ${{ secrets.DOCKERHUB_USERNAME }} 25 | password: ${{ secrets.DOCKERHUB_TOKEN }} 26 | 27 | - name: Build and push Docker image 28 | uses: docker/build-push-action@v4 29 | with: 30 | context: ./ 31 | push: true 32 | tags: dpouris/goster:${{ github.ref_name }}, dpouris/goster:latest 33 | cache-from: type=gha 34 | cache-to: type=gha,mode=max 35 | -------------------------------------------------------------------------------- /.github/workflows/issue-auto-label.yml: -------------------------------------------------------------------------------- 1 | name: "Issue Triage" 2 | on: 3 | issues: 4 | types: [opened, reopened] 5 | jobs: 6 | label-and-notify: 7 | runs-on: ubuntu-latest 8 | permissions: 9 | issues: write # allow adding labels and comments on issues 10 | steps: 11 | - name: Checkout repo 12 | uses: actions/checkout@v2 13 | 14 | - name: Add git triage label 15 | run: gh issue edit "$ISSUE_NUMBER" --add-label "triage" 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | ISSUE_NUMBER: ${{ github.event.issue.number }} 19 | 20 | - name: Comment on issue 21 | run: gh issue comment "$ISSUE_NUMBER" --body "Thanks for opening an issue! Maintainers will review it soon" 22 | env: 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | ISSUE_NUMBER: ${{ github.event.issue.number }} 25 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: 5 | - 'v*.*.*' # triggers when a tag like v1.0.0 is pushed 6 | jobs: 7 | publish-release: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | contents: write # needed to create releases 11 | steps: 12 | - name: Check out code 13 | uses: actions/checkout@v4 14 | 15 | - name: Set up Go 16 | uses: actions/setup-go@v3 17 | with: 18 | go-version: '1.x' 19 | 20 | # Build binaries or run additional checks 21 | - name: Build binary 22 | run: GOOS=linux GOARCH=amd64 go build -o goster-linux-amd64 . 23 | 24 | - name: Create GitHub Release 25 | id: create_release 26 | uses: actions/create-release@v1 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GOSTER_TOKEN }} 29 | with: 30 | tag_name: ${{ github.ref_name }} 31 | release_name: "Release ${{ github.ref_name }}" 32 | body: "Changes in this release..." # Could be generated or manually edited 33 | draft: false 34 | prerelease: false 35 | 36 | # iupload built artifact to release 37 | - name: Upload Release Asset 38 | uses: actions/upload-release-asset@v1 39 | env: 40 | GITHUB_TOKEN: ${{ secrets.GOSTER_TOKEN }} 41 | with: 42 | upload_url: ${{ steps.create_release.outputs.upload_url }} 43 | asset_path: goster-linux-amd64 44 | asset_name: goster-linux-amd64 45 | asset_content_type: application/octet-stream 46 | -------------------------------------------------------------------------------- /.github/workflows/security-audit.yml: -------------------------------------------------------------------------------- 1 | name: "Security Audit" 2 | on: 3 | schedule: 4 | - cron: '0 0 * * 0' # runs every Sunday at midnight 5 | workflow_dispatch: # manual trigger 6 | jobs: 7 | dep-audit: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Check out code 11 | uses: actions/checkout@v4 12 | 13 | - name: Set up Go 14 | uses: actions/setup-go@v3 15 | with: 16 | go-version: '1.x' 17 | 18 | - name: Audit Go Modules for updates 19 | run: go list -m -u all > audit.txt 20 | 21 | - name: Display outdated modules 22 | run: cat audit.txt 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | nullbytes.txt 2 | *.png 3 | 4 | **/.DS_Store 5 | **/static/* 6 | **/templates/* 7 | 8 | tests_out/* 9 | examples_out/* 10 | 11 | *.ignoreme -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Goster 2 | 3 | Thank you for considering contributing to Goster! I appreciate your support and interest in making Goster better. Below are some guidelines to help you get started. 4 | 5 | ## How Can I Contribute? 6 | 7 | ### Reporting Bugs 8 | - **Search existing issues** to see if the bug has already been reported. 9 | - If not, create a new issue with detailed information: 10 | - A clear and descriptive title. 11 | - Steps to reproduce the issue. 12 | - Expected and actual behavior. 13 | - Screenshots or logs if applicable. 14 | 15 | ### Feature Requests 16 | - **Search existing feature requests** to avoid duplicates. 17 | - If your idea is new, create an issue with: 18 | - A clear and descriptive title. 19 | - Detailed description of the proposed feature. 20 | - Any relevant use cases or examples. 21 | 22 | ### Submitting Pull Requests 23 | - Fork the repository and create your branch from `master`. 24 | - Follow existing code style and conventions. 25 | - Include tests for your changes. 26 | - Ensure all tests pass before submitting. 27 | - Submit a pull request with a clear title and description of your changes. 28 | 29 | ### Improving Documentation 30 | Currently, there's no documentation as there's not a need for it. Goster is still a small package that is "self-documented" in a way but in the future I will most likely look into it. 31 | 32 | ## Development Setup 33 | 34 | 1. **Clone the repository:** 35 | ```sh 36 | git clone https://github.com/dpouris/goster 37 | cd goster 38 | ``` 39 | 40 | 2. **Install dependencies:** 41 | ```sh 42 | go mod tidy 43 | ``` 44 | 45 | ## Contact 46 | 47 | For any questions or discussions, you can send me an email at [jimpouris0\@gmail.com](mailto:jimpouris0@gmail.com?subject=Goster) 48 | 49 | I look forward to your contributions! -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 dpouris 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Goster 🚀 2 | [![GoDoc](https://godoc.org/github.com/gomarkdown/markdown?status.svg)](https://pkg.go.dev/github.com/dpouris/goster) 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/dpouris/goster)](https://goreportcard.com/report/github.com/dpouris/goster) 4 | [![License](https://img.shields.io/github/license/dpouris/goster)](https://github.com/dpouris/goster/blob/master/LICENSE) 5 | ![Go version](https://img.shields.io/github/go-mod/go-version/dpouris/goster) 6 | 7 | Welcome to **Goster**, a lightweight and efficient web framework for Go. Goster provides a minimal abstraction over Go’s built-in `net/http` package, allowing you to rapidly build microservices and APIs with little overhead. Its design emphasizes simplicity and performance, offering an intuitive API for routing, middleware, and more. 🌟 8 | 9 | ## Why Goster? 10 | 11 | - 🚀 **Fast and Lightweight:** Built with simplicity in mind, Goster adds only a thin layer on top of Go’s `net/http`, ensuring minimal performance overhead. Hello-world benchmarks show Goster’s throughput to be on par with the Go standard library and top Go frameworks (Gin handles ~100k req/s on similar hardware ([Fiber vs Gin: A Comparative Analysis for Golang - tillitsdone.com](https://tillitsdone.com/blogs/fiber-vs-gin--golang-framework-guide/#:~:text=Fiber%20leverages%20the%20blazing,110k%20on%20similar%20hardware)), and Goster achieves comparable results). 12 | - 📊 **Intuitive API:** Goster’s API is easy to learn and use, simplifying web development without sacrificing flexibility. Define routes with clear semantics and handle requests with a simple context object. 13 | - 🛠 **Extensible Middleware:** Add middleware functions globally or for specific routes to enhance functionality. This makes it easy to implement logging, authentication, or other cross-cutting concerns. 14 | - 🔍 **Dynamic Routing:** Effortlessly handle paths with parameters (e.g. `/users/:id`). Goster automatically parses URL parameters for you. 15 | - 🗂️ **Static Files & Templates:** Serve static assets (CSS, JS, images, etc.) directly from a directory, and render HTML templates with ease. 16 | - 🧪 **Logging:** Built-in logging captures all incoming requests and application messages. Goster stores logs internally for inspection and can print to stdout with different levels (Info, Warning, Error). 17 | 18 | ## Installation 19 | 20 | Install Goster using **Go modules**. Run the following command in your project: 21 | 22 | ```bash 23 | go get -u github.com/dpouris/goster 24 | ``` 25 | 26 | This will add Goster to your Go module dependencies. Goster requires **Go 1.18+** (Go 1.21 is recommended, as indicated in the module file). 27 | 28 | ## Quick Start 29 | 30 | Get your first Goster server running in just a few lines of code: 31 | 32 | ```go 33 | package main 34 | 35 | import "github.com/dpouris/goster" 36 | 37 | func main() { 38 | g := goster.NewServer() 39 | 40 | g.Get("/", func(ctx *goster.Ctx) error { 41 | ctx.Text("Hello, Goster!") 42 | return nil 43 | }) 44 | 45 | // Use the new Start function instead of ListenAndServe 46 | g.Start(":8080") 47 | } 48 | ``` 49 | 50 | You can also start a secure server with TLS: 51 | 52 | ```go 53 | package main 54 | 55 | import "github.com/dpouris/goster" 56 | 57 | func main() { 58 | g := goster.NewServer() 59 | 60 | // Replace with actual certificate and key paths 61 | g.StartTLS(":8443", "path/to/cert.pem", "path/to/key.pem") 62 | } 63 | ``` 64 | 65 | **Run the server:** Build and run your Go program, then navigate to `http://localhost:8080`. You should see **“Hello, Goster!”** in your browser, served by your new Goster server. 66 | 67 | This example creates a basic HTTP server that listens on port 8080 and responds with a text message for the root URL. For a more detailed tutorial, see the [Getting Started guide](docs/Getting_Started.md). 68 | 69 | ## Usage Examples 70 | 71 | Goster’s API lets you set up routes and middleware in a straightforward way. Below are some common usage patterns. For comprehensive documentation, refer to the [docs/](docs) directory. 72 | 73 | - **Defining Routes:** Use methods like `Get`, `Post`, `Put`, etc., on your server to register routes. For example: 74 | 75 | ```go 76 | g.Get("/hello", func(ctx *goster.Ctx) error { 77 | return ctx.Text("Hello World") 78 | }) 79 | ``` 80 | This registers a handler for `GET /hello`. You can similarly use `g.Post`, `g.Put`, `g.Patch`, `g.Delete`, etc., to handle other HTTP methods. 81 | 82 | - **Dynamic URL Parameters:** Define routes with parameters using the `:` prefix. For instance: 83 | 84 | ```go 85 | g.Get("/users/:id", func(ctx *goster.Ctx) error { 86 | userID, _ := ctx.Path.Get("id") // retrieve the :id parameter 87 | return ctx.Text("Requested user " + userID) 88 | }) 89 | ``` 90 | 91 | Goster will capture the segment after `/users/` as an `id` parameter. In the handler, `ctx.Path.Get("id")` provides the value. (See [Routing](docs/Routing.md) for more on dynamic routes.) 92 | 93 | - **Query Parameters:** Access query string values via `ctx.Query`. For example, for a URL like `/search?q=term`: 94 | 95 | ```go 96 | g.Get("/search", func(ctx *goster.Ctx) error { 97 | q, exists := ctx.Query.Get("q") 98 | if exists { 99 | return ctx.Text("Search query is: " + q) 100 | } 101 | return ctx.Text("No query provided") 102 | }) 103 | ``` 104 | 105 | Here `ctx.Query.Get("q")` checks if the `q` parameter was provided and returns its value. (See [Context and Responses](docs/Context_and_Responses.md) for details.) 106 | 107 | - **Middleware:** You can attach middleware functions that run before your route handlers. Use `UseGlobal` for middleware that should run on **all** routes, or `Use(path, ...)` for middleware on specific routes. For example: 108 | 109 | ```go 110 | // Global middleware (runs for every request) 111 | g.UseGlobal(func(ctx *goster.Ctx) error { 112 | // e.g., start time tracking or authentication check 113 | return nil 114 | }) 115 | 116 | // Path-specific middleware (runs only for /admin routes) 117 | g.Use("/admin", func(ctx *goster.Ctx) error { 118 | // e.g., verify admin privileges 119 | return nil 120 | }) 121 | ``` 122 | (See the [Middleware](docs/Middleware.md) documentation for more examples and use cases.) 123 | 124 | - **Serving Static Files:** Goster can serve static files (like images, CSS, JS) from a directory. Use `g.StaticDir("")` to register a static files directory. For example, `g.StaticDir("static")` will serve files in the **static/** folder at URLs matching the file paths. If you have *static/logo.png*, it becomes accessible at `http://localhost:8080/static/logo.png`. (See [Static Files](docs/Static_Files.md) for setup and details.) 125 | 126 | - **HTML Templates:** To serve HTML pages, place your template files (e.g. `.gohtml` or `.html`) in a directory and register it with `g.TemplateDir("")`. Then use `ctx.Template("", data)` in a handler to render the template. For example: 127 | 128 | ```go 129 | g.TemplateDir("templates") 130 | 131 | g.Get("/hello/:name", func(ctx *goster.Ctx) error { 132 | name, _ := ctx.Path.Get("name") 133 | return ctx.Template("hello.gohtml", name) 134 | }) 135 | ``` 136 | This will load **templates/hello.gohtml**, execute it (optionally with `name` data passed), and send the result as the response. (See [Templates](docs/Templates.md) for template guidelines.) 137 | 138 | - **JSON Responses:** Goster provides a convenient `ctx.JSON(obj)` method to respond with JSON. Simply pass any Go value (struct, map, etc.), and Goster will serialize it to JSON and set the appropriate content type. For example: 139 | 140 | ```go 141 | g.Get("/status", func(ctx *goster.Ctx) error { 142 | data := map[string]string{"status": "ok"} 143 | return ctx.JSON(data) 144 | }) 145 | ``` 146 | The client will receive a JSON object: `{"status":"ok"}`. (See [Context and Responses](docs/Context_and_Responses.md) for details on JSON serialization.) 147 | 148 | - **Logging:** Every Goster server has an embedded logger. You can use it to log custom events: 149 | 150 | ```go 151 | goster.LogInfo("Server started", g.Logger) 152 | goster.LogWarning("Deprecated endpoint called", g.Logger) 153 | goster.LogError("An error occurred", g.Logger) 154 | ``` 155 | 156 | These will print timestamped log entries to standard output. Goster also keeps an in-memory log of all requests and log messages. You can access `g.Logs` (a slice of log strings) for debugging or expose it via an endpoint. For instance, you might add a route to dump logs for inspection. (See [Logging](docs/Logging.md) for more.) 157 | 158 | The above examples only scratch the surface. Check out the [docs/](docs) directory for detailed documentation of each feature, and refer to the `examples/` directory in the repository for ready-to-run example programs. 159 | 160 | ## Documentation 161 | 162 | You can find **full documentation** for Goster in the [`docs/`](docs) directory of the repository. The documentation includes guides and reference for all major features: 163 | 164 | - [Getting Started](docs/Getting_Started.md) – High-level guide to building a simple service with Goster. 165 | - [Routing](docs/Routing.md) – Defining routes, path parameters, and handling requests. 166 | - [Middleware](docs/Middleware.md) – Using global and route-specific middleware for advanced functionality. 167 | - [Static Files](docs/Static_Files.md) – Serving static content (assets) through Goster. 168 | - [Templates](docs/Templates.md) – Configuring template directories and rendering HTML views. 169 | - [Context and Responses](docs/Context_and_Responses.md) – How the request context works, and responding with text/JSON. 170 | - [Logging](docs/Logging.md) – Utilizing Goster’s logging capabilities for your application. 171 | 172 | Feel free to explore the docs. Each section contains examples and best practices. If something isn’t clear, check the examples provided or raise an issue — we’re here to help! 173 | 174 | ## Benchmarks 175 | 176 | Performance is a key focus of Goster. We ran benchmarks comparing Goster to other Go web frameworks and the native `net/http` package: 177 | 178 | - **Hello World throughput:** In a simple "Hello World" HTTP benchmark, Goster achieved throughput comparable to using `net/http` directly, demonstrating negligible overhead. For example, popular Go frameworks like Gin (which also builds on `net/http`) handle on the order of 100k requests per second on standard hardware ([Fiber vs Gin: A Comparative Analysis for Golang - tillitsdone.com](https://tillitsdone.com/blogs/fiber-vs-gin--golang-framework-guide/#:~:text=Fiber%20leverages%20the%20blazing,110k%20on%20similar%20hardware)). Goster’s performance is in the same ballpark, thanks to its minimalistic design (essentially just a light routing layer on top of the standard library). 179 | 180 | - **Routing overhead:** Goster uses simple map lookups for routing, so route matching is fast even with dynamic parameters. In our tests, adding URL parameters had no significant impact on request latency. The latency remained in the microsecond range (per request) for routing logic, similar to other lightweight routers. 181 | 182 | - **Comparison with fasthttp frameworks:** Frameworks like Fiber use the fasthttp engine for even higher throughput. Fiber can edge out Gin by roughly 10-20% in some benchmarks ([Fiber vs Gin: A Comparative Analysis for Golang - tillitsdone.com](https://tillitsdone.com/blogs/fiber-vs-gin--golang-framework-guide/#:~:text=Fiber%20leverages%20the%20blazing,110k%20on%20similar%20hardware)). Goster, using Go’s standard HTTP server, is slightly below Fiber’s extreme throughput but still sufficiently fast for the vast majority of use cases. It delivers performance close to Go’s raw HTTP capabilities. 183 | 184 | **Conclusion:** You can expect Goster to perform on par with other minimal Go web frameworks. It’s suitable for high-throughput scenarios, and you likely won’t need to micro-optimize beyond what Goster provides out-of-the-box. (If you have specific performance requirements, we welcome community benchmarks and feedback!) 185 | 186 | ## Comparison with Similar Libraries 187 | 188 | Goster is part of a rich ecosystem of Go web frameworks. Here’s how it compares to a few popular choices: 189 | 190 | - **Go `net/http`:** The standard library provides the low-level HTTP server and mux. Goster **uses** `net/http` under the hood, so it feels familiar but saves you from writing repetitive boilerplate. Unlike using `net/http` alone, Goster handles common tasks (routing, parameters, etc.) for you. If you need absolute minimal dependency and are comfortable implementing everything from scratch, `net/http` is always an option – but Goster gives you the same performance with more convenience. 191 | 192 | - **Gorilla Mux:** Gorilla Mux was a widely-used router for Go. It offered powerful routing (with URL variables, regex, etc.), but the project is now archived (“discontinued”) ([Goster Alternatives and Reviews](https://www.libhunt.com/r/goster#:~:text=mux)). Goster provides similar routing capabilities (dynamic paths with variables) with a simpler API. If you’re looking for a replacement for Gorilla Mux, Goster’s routing can feel familiar, though it intentionally omits some of Gorilla’s more complex features to remain lightweight. 193 | 194 | - **Chi:** [Chi](https://github.com/go-chi/chi) is another minimal router for Go that focuses on idiomatic use of `context.Context`. Chi and Goster have similar philosophies – both aim to be lightweight and idiomatic. Chi has a rich ecosystem of middlewares and is a mature project. Goster differentiates itself by bundling a few extra conveniences (like built-in logging and static file serving) out-of-the-box, whereas Chi often relies on add-ons for such features. 195 | 196 | - **Gin:** Gin is a powerful framework with an API similar to Goster’s (context-based, routing with parameters, middleware support). Gin uses a radix tree for routing and is highly optimized. It’s a proven framework with a large community and many plugins. Goster, by contrast, is more minimal and young. If you need features like validation, serialization, or a large ecosystem of middleware, Gin might be a better choice. However, if you prefer something simpler than Gin with only the essentials, Goster is a good fit. Performance-wise, Goster and Gin are both very fast (both build on `net/http`), with Gin possibly having a slight edge in some routing scenarios due to its internal optimizations. 197 | 198 | - **Fiber:** Fiber is a framework built on top of the fasthttp library (bypassing `net/http` for performance). It has an API inspired by Express.js. Fiber can offer higher throughput in certain benchmarks ([Fiber vs Gin: A Comparative Analysis for Golang - tillitsdone.com](https://tillitsdone.com/blogs/fiber-vs-gin--golang-framework-guide/#:~:text=Fiber%20leverages%20the%20blazing,110k%20on%20similar%20hardware)), but using a custom HTTP engine means it’s less compatible with some `net/http` middleware and requires careful handling of certain aspects (like streaming, HTTP/2 support, etc.). Goster sticks to the standard `net/http` for maximum compatibility and simplicity. If you need extreme performance and are willing to trade some compatibility, Fiber is an alternative; otherwise, Goster’s performance is usually more than sufficient. 199 | 200 | In summary, **Goster’s niche** is for developers who want a very light, idiomatic Go web framework. It may not (yet) have all the bells and whistles of Gin or the ultra-performance of Fiber, but it covers the common needs for building APIs and microservices with minimal fuss. As the project grows, we aim to maintain this balance of simplicity and capability. 201 | 202 | ## FAQs 203 | 204 | **Q1: What Go version do I need to use Goster?** 205 | **A:** Go 1.18 or later is required. Goster is tested with the latest Go versions (the module file indicates Go 1.21). Using an up-to-date Go release is recommended to ensure compatibility with the `any` type and other modern Go features used by Goster. 206 | 207 | **Q2: How do I define routes with URL parameters (dynamic routes)?** 208 | **A:** Simply include parameters in the path prefixed with `:`. For example, `/users/:id/profile` defines a route with an `id` parameter. In your handler, use `ctx.Path.Get("id")` to retrieve the value. See the [Routing documentation](docs/Routing.md) for details and examples. 209 | 210 | **Q3: Can Goster handle query string parameters?** 211 | **A:** Yes. Use `ctx.Query.Get("")` to retrieve query parameters. This returns the value and a boolean indicating if it was present. For instance, for `/search?q=test`, `ctx.Query.Get("q")` would return `"test"`. If a parameter is missing, the returned boolean will be false (and the value empty). 212 | 213 | **Q4: How do I return JSON responses?** 214 | **A:** Use the `ctx.JSON(data interface{})` method. Pass any Go data (e.g. a struct or map), and Goster will serialize it to JSON and send it with `Content-Type: application/json`. Under the hood it uses Go’s `encoding/json`. Example: `ctx.JSON(map[string]string{"status": "ok"})` will return `{"status":"ok"}` to the client. (See [Context and Responses](docs/Context_and_Responses.md) for more.) 215 | 216 | **Q5: How can I serve static files (CSS, JavaScript, images)?** 217 | **A:** Call `g.StaticDir()` on your server. Suppose you have a folder `assets/` with static files – use `g.StaticDir("assets")`. All files in that directory will be served at paths prefixed with the directory name. For example, `assets/main.js` can be fetched from `http://yourserver/assets/main.js`. Goster will automatically serve the file with the correct content type. (See [Static Files docs](docs/Static_Files.md) for configuration tips.) 218 | 219 | **Q6: Does Goster support HTTPS (TLS)?** 220 | **A:** Goster itself doesn’t include a TLS server implementation, but you can use Go’s standard methods to run HTTPS. For example, you can call `http.ListenAndServeTLS(port, certFile, keyFile, g)`, passing your Goster server (`g`) as the handler. Since Goster’s `ListenAndServe` is a thin wrapper, using `net/http` directly for TLS is straightforward. Alternatively, you can put Goster behind a reverse proxy (like Nginx or Caddy) for TLS termination. 221 | 222 | **Q7: Can I use Goster’s middleware with standard `net/http` handlers or integrate external middleware?** 223 | **A:** Goster is compatible with the `net/http` ecosystem. You can wrap Goster’s `goster.Ctx` inside a standard `http.Handler` if needed, or use `g.Router` (or similar) to mount external handlers. Conversely, you can use `g.Use()` to add middleware that interacts with `ctx.Request` and `ctx.Response` which are standard `*http.Request` and `http.ResponseWriter` under the hood. Many external middlewares (for logging, tracing, etc.) can be adapted to Goster by accessing `ctx.Request`/`ctx.Response`. It may require a bit of glue code, but it’s doable thanks to Goster’s design around the standard library. 224 | 225 | **Q8: How do I render HTML templates with dynamic data?** 226 | **A:** First, set the templates directory with `g.TemplateDir("templates")` (replace "templates" with your folder name). Then, in a handler use `ctx.Template("file.gohtml", data)`. Ensure your template file (e.g. *file.gohtml*) is in the templates directory. The `data` can be any Go value or struct that your template expects. Goster will execute the template and write the output. (See [Templates](docs/Templates.md) for an example). If you see an error or nothing renders, make sure your template name and data are correct and that you called `TemplateDir` during setup. 227 | 228 | **Q9: Is Goster ready for production use?** 229 | **A:** Goster is MIT-licensed and open source. It is a young project (still below 1.0 version), but its core functionality is tested and quite stable. It’s suitable for small to medium projects and learning purposes. As of now, it might not have as large a community or ecosystem as more established frameworks. We recommend evaluating it against your requirements. For many microservices, Goster should work well. As always, you should conduct your own testing (including concurrency and load testing) to ensure it meets your production needs. We are actively improving Goster, and welcome issues or contributions to help make it production-ready for a wider range of use cases. 230 | 231 | **Q10: How can I contribute or report an issue?** 232 | **A:** Contributions are welcome! If you find a bug or have an idea for improvement, please open an issue on GitHub. If you’d like to contribute code, you can fork the repository and open a pull request. For major changes, it’s best to discuss via an issue first. Be sure to follow the existing code style and include tests for new features or fixes. See the [Contributing Guide](CONTRIBUTING.md) for more details on the contribution process and project standards. 233 | 234 | ## Contribution Guidelines 235 | 236 | I warmly welcome contributions from the community. Whether it’s bug fixes, new features, or improvements to documentation, your help is appreciated. To contribute: 237 | 238 | - **Report Issues:** If you encounter a bug or have a question/idea, open an issue on GitHub. Please provide details and steps to reproduce for bugs. For feature requests, describe the use case and potential solutions. 239 | - **Submit Pull Requests:** Fork the repository and create a branch for your changes. Try to follow the code style of the project and include tests for any new code. Once you’re ready, open a pull request with a clear description of your changes. The project maintainer will review your contribution. 240 | - **Join Discussions:** You can also participate in discussions by commenting on issues or proposing design suggestions. Input from users helps shape the project’s direction. 241 | - **Development Setup:** To work on Goster, clone the repo and run `go mod tidy` to install dependencies. Run `go test ./...` to ensure all tests pass. You can use the example programs under `examples/` for manual testing. There's also a folder called `utilities/` that contains QoL scripts that may come useful during development. 242 | 243 | For more detailed guidelines, please refer to the [CONTRIBUTING.md](CONTRIBUTING.md) file in the repository. 244 | 245 | ## License 246 | 247 | Goster is open source and available under the **MIT License**. This means you are free to use, modify, and distribute it in your own projects. See the [LICENSE](LICENSE) file for the full license text. 248 | 249 | Happy coding with Goster! 🚀 -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | package goster 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "html/template" 7 | "io" 8 | "net/http" 9 | "os" 10 | "path/filepath" 11 | "slices" 12 | ) 13 | 14 | type Ctx struct { 15 | Request *http.Request 16 | Response Response 17 | Meta 18 | } 19 | 20 | // Send an HTML template t file to the client. If template not in template dir then will return error. 21 | func (c *Ctx) Template(t string, data any) (err error) { 22 | templatePaths := engine.Config.TemplatePaths 23 | 24 | // iterate through all known templates 25 | for tmplId := range templatePaths { 26 | // if given template matches a known template get the template path, parse it and write it to response 27 | if tmplId == t { 28 | tmpl := template.Must(template.ParseFiles(templatePaths[tmplId])) 29 | err = tmpl.Execute(c.Response, data) 30 | 31 | if err != nil { 32 | return err 33 | } 34 | 35 | } 36 | } 37 | 38 | return 39 | } 40 | 41 | // Send an HTML template t file to the client. TemplateWithFuncs supports functions to be embedded in the html template for use. If template not in template dir then will return error. 42 | func (c *Ctx) TemplateWithFuncs(t string, data any, funcMap template.FuncMap) (err error) { 43 | templatePaths := engine.Config.TemplatePaths 44 | 45 | // iterate through all known templates 46 | for tmplId := range templatePaths { 47 | // if given template matches a known template get the template path, parse it and write it to response 48 | if tmplId == t { 49 | tmplFile := templatePaths[tmplId] 50 | baseFilename := filepath.Base(tmplId) 51 | tmpl := template.Must(template.New(baseFilename).Funcs(funcMap).ParseFiles(tmplFile)) 52 | err = tmpl.Execute(c.Response, data) 53 | 54 | if err != nil { 55 | return err 56 | } 57 | 58 | } 59 | } 60 | 61 | return 62 | } 63 | 64 | // Send an HTML f file to the client. If if file not in FilesDir dir then will return error. 65 | func (c *Ctx) HTML(t string) (err error) { 66 | templatePaths := engine.Config.TemplatePaths 67 | 68 | for tmplId := range templatePaths { 69 | if tmplId == t { 70 | // open template 71 | file, err := os.Open(templatePaths[tmplId]) 72 | if err != nil { 73 | fmt.Fprintln(os.Stderr, err) 74 | } 75 | defer file.Close() 76 | 77 | // read file 78 | fInfo, _ := file.Stat() 79 | buf := make([]byte, fInfo.Size()) 80 | _, err = io.ReadFull(file, buf) 81 | if err != nil { 82 | return err 83 | } 84 | 85 | t := string(buf) 86 | 87 | // set headers 88 | contentType := getContentType(file.Name()) 89 | c.Response.Header().Set("Content-Type", contentType) 90 | 91 | fmt.Fprint(c.Response.ResponseWriter, t) // write response 92 | } 93 | } 94 | return 95 | } 96 | 97 | // Send plain text to the client 98 | func (c *Ctx) Text(s string) { 99 | c.Response.Header().Set("Content-Length", fmt.Sprint(len(s))) 100 | c.Response.Header().Set("Content-Type", "text/plain") 101 | fmt.Fprint(c.Response.ResponseWriter, s) 102 | } 103 | 104 | // Send back a JSON response. Supply j with a value that's valid marsallable(?) to JSON -> error 105 | func (c *Ctx) JSON(j any) (err error) { 106 | if v, ok := j.([]byte); ok { 107 | v = slices.DeleteFunc(v, func(b byte) bool { 108 | return b == 0 109 | }) 110 | c.Response.Header().Set("Content-Type", "application/json") 111 | _, err = c.Response.Write(v) 112 | return 113 | } 114 | 115 | v, err := json.Marshal(j) 116 | 117 | if err != nil { 118 | fmt.Fprintln(os.Stderr, err.Error()) 119 | return err 120 | } 121 | 122 | c.Response.Header().Set("Content-Type", "application/json") 123 | _, err = c.Response.Write(v) 124 | return 125 | } 126 | -------------------------------------------------------------------------------- /docs/Context_and_Responses.md: -------------------------------------------------------------------------------- 1 | # Context and Response Handling in Goster 2 | 3 | Goster’s handlers revolve around the **context** (`Ctx`), which encapsulates the request and response. Understanding the `Ctx` structure and its methods will help you effectively read incoming data and write outgoing responses. 4 | 5 | ## The Ctx Structure 6 | 7 | When Goster receives a request, it creates a `Ctx` object and passes a pointer to it into your handler. The `Ctx` provides the following important fields and methods: 8 | 9 | - **Request Data:** 10 | - `ctx.Request` – the raw `*http.Request`. You can use this to read headers, the request body, etc., just as you would in any Go `net/http` handler. 11 | - `ctx.Query` – a helper to access query parameters. It behaves like a map of string to string for URL query strings. Use `ctx.Query.Get("key")` to retrieve a parameter value. 12 | - `ctx.Path` – a helper to access path parameters (from dynamic routes). Use `ctx.Path.Get("paramName")` to get the value of a URL parameter. 13 | 14 | - **Response Tools:** 15 | - `ctx.Response` – a wrapper around `http.ResponseWriter` that lets you write back to the client. In most cases, you won’t use `ctx.Response` directly; instead, you use methods like `ctx.Text`, `ctx.JSON`, `ctx.Template`, which under the hood write to `ctx.Response`. However, `ctx.Response` is available if you need advanced control (e.g., streaming data or setting headers manually). 16 | - `ctx.Text(content string) error` – sends a plain text response. It will set the `Content-Type` to `text/plain; charset=utf-8` and write the provided text. Example: `ctx.Text("Hello")` will send “Hello” to the client. 17 | - `ctx.JSON(data interface{}) error` – sends a JSON response. It will marshal the given data to JSON (using Go’s encoding/json by default) and set `Content-Type: application/json`. Example: `ctx.JSON(map[string]int{"status": 200})` sends `{"status":200}`. If JSON marshaling fails (e.g., due to an unsupported data type), this method will return an error. 18 | - `ctx.Template(name string, data interface{}) error` – renders an HTML template (previously loaded by `TemplateDir`) and sends it. It sets `Content-Type: text/html; charset=utf-8`. Example: `ctx.Template("home.gohtml", user)` will fill the `home.gohtml` template with `user` data and send the result. Errors can occur if the template is not found or fails to execute. 19 | 20 | - **Meta Information and Logs:** 21 | - `ctx.Meta` – holds internal metadata like the `Path` and `Query` maps. In most cases you won’t interact with `ctx.Meta` directly, but it’s where Goster stores parsed parameters. 22 | - `ctx.Logs` – (if accessible) references the server’s log storage. Actually, logs are stored in the `Goster` server (`g.Logs`), not directly in `ctx`. But every request automatically logs a basic entry like `"[GET] ON ROUTE /example"` to `g.Logs`. If needed, you could correlate `ctx` to the server to fetch logs, but typically you use `g.Logs` via an admin route as shown in examples. 23 | 24 | ## Reading Request Data 25 | 26 | **Query Parameters:** As mentioned, use `ctx.Query.Get("name")`. This returns two values: the value and a boolean `exists`. If `exists` is false, the parameter was not present in the URL. Example: 27 | 28 | ```go 29 | q, ok := ctx.Query.Get("q") 30 | if ok { 31 | // use q 32 | } else { 33 | // parameter not provided 34 | } 35 | ``` 36 | 37 | Internally, when the request comes in, Goster parses the raw query string into the `ctx.Meta.Query` map, so `ctx.Query.Get` is just a convenience accessor for that map. 38 | 39 | **Path Parameters:** Use `ctx.Path.Get("param")` similarly. Path params are captured from dynamic routes. If a route is not dynamic or the param name is wrong, `exists` will be false. Example: 40 | 41 | ```go 42 | id, ok := ctx.Path.Get("id") 43 | if ok { 44 | // id contains the path segment value 45 | } 46 | ``` 47 | 48 | Path params are parsed during routing, right before your handler is called. Goster populates `ctx.Meta.Path` with the values, which `ctx.Path.Get` accesses. 49 | 50 | **Headers:** Use `ctx.Request.Header.Get("Header-Name")` to retrieve header values. For example, `ctx.Request.Header.Get("Content-Type")` or custom headers like `Authorization`. Goster doesn’t wrap header access — you use the standard `http.Request` methods. 51 | 52 | **Body:** To read the request body (for POST/PUT, etc.), you can use `ctx.Request.Body`. For instance, if you expect JSON input, you might do: 53 | 54 | ```go 55 | var input SomeStruct 56 | err := json.NewDecoder(ctx.Request.Body).Decode(&input) 57 | if err != nil { 58 | // handle JSON parse error 59 | } 60 | ``` 61 | 62 | Remember to handle cases where the body might be empty or an error. Goster doesn’t automatically parse the body for you; you have full control via `ctx.Request`. 63 | 64 | **Form data:** If you’re dealing with form submissions (traditional HTML forms or `multipart/form-data` for file uploads), you can use `ctx.Request.ParseForm()` or `ctx.Request.ParseMultipartForm()` and then access `ctx.Request.Form` or `ctx.Request.MultipartForm`. Again, this is using Go’s standard library capabilities directly on the `http.Request`. 65 | 66 | ## Writing Responses 67 | 68 | The simplest way to respond is to use the helper methods: 69 | 70 | - `ctx.Text`: for plaintext. 71 | - `ctx.JSON`: for JSON. 72 | - `ctx.Template`: for HTML content. 73 | 74 | Each of these will automatically set a proper HTTP status code if not already set. By default, if you haven’t written any headers yet, writing through these methods will result in an implicit 200 OK status (unless an error occurs during JSON marshaling or template execution, in which case you should handle that by perhaps setting a 500). 75 | 76 | **Custom Status Codes:** If you need to set a status code (like 201 Created, 204 No Content, 400 Bad Request, etc.), you have two options: 77 | 1. Use `ctx.Response.WriteHeader(code)` before writing the body. For example: 78 | ```go 79 | ctx.Response.WriteHeader(http.StatusCreated) 80 | ctx.Text("resource created") 81 | ``` 82 | Or for JSON: 83 | ```go 84 | ctx.Response.WriteHeader(http.StatusBadRequest) 85 | ctx.JSON(map[string]string{"error": "bad input"}) 86 | ``` 87 | Make sure to call `WriteHeader` *before* the helper method, because methods like `ctx.JSON` will call `Write` on the response, which implicitly sends a 200 if no status has been set yet. 88 | 89 | 2. Use Goster’s `NewHeaders` utility if available via `ctx.Response`. The `Response.NewHeaders(h map[string]string, status int)` function can set multiple headers and a status at once. For instance: 90 | ```go 91 | ctx.Response.NewHeaders(map[string]string{ 92 | "Content-Type": "application/json", 93 | }, http.StatusAccepted) 94 | ctx.Response.Write([]byte(`{"status": "accepted"}`)) 95 | ``` 96 | However, using `ctx.JSON` after `NewHeaders` isn’t advisable because `ctx.JSON` would try to set `Content-Type` again. Typically, stick to one approach: either manual or using helpers. 97 | 98 | **No Response / Empty Response:** If your handler doesn’t need to send anything (for example, an endpoint that just consumes data and returns 204 No Content), you can do: 99 | ```go 100 | ctx.Response.WriteHeader(http.StatusNoContent) 101 | return nil 102 | ``` 103 | And not call any of the ctx helper methods to write a body. The client will get a 204 with an empty body. 104 | 105 | **Errors in Handlers:** If you return an error from a handler, Goster will log it (to stdout or `g.Logs`) but it will not automatically send an error to the client. It’s up to your application design how to handle errors. A common pattern is to have middleware catch errors and format a response, or simply always respond within the handler and ensure you don’t return error unless you’ve handled it. For instance, you might do: 106 | 107 | ```go 108 | g.Get("/data", func(ctx *goster.Ctx) error { 109 | data, err := loadData() 110 | if err != nil { 111 | ctx.Response.WriteHeader(http.StatusInternalServerError) 112 | ctx.Text("Internal Server Error") 113 | return nil // we handled the error by responding to client 114 | } 115 | return ctx.JSON(data) // happy path 116 | }) 117 | ``` 118 | 119 | Here we return `nil` after writing the error response so that upstream doesn’t attempt further handling. On the success path, we use `ctx.JSON`. This approach ensures the client always gets a response. 120 | 121 | ## Low-Level Access 122 | 123 | Because `ctx.Response` embeds `http.ResponseWriter`, you can use all standard methods on it: 124 | - `ctx.Response.Header().Set("X-Custom-Header", "value")` to set custom headers. 125 | - `ctx.Response.Write(bytes)` to write raw bytes (the helper methods ultimately call this). 126 | - `ctx.Response.WriteHeader(statusCode)` to set the HTTP status. 127 | 128 | And `ctx.Request` is a normal `http.Request`, so you can use: 129 | - `ctx.Request.URL.Path`, `ctx.Request.URL.Query()`, etc., if you prefer manual parsing. 130 | - `io.ReadAll(ctx.Request.Body)` or streaming reads from the body for large payloads. 131 | - `ctx.Request.Context()` if you need to observe cancellation (e.g., if the client disconnects, `ctx.Request.Context().Done()` will be signaled). 132 | 133 | Goster’s context does **not** use Go’s `context.Context` for request values; instead, it offers the `Query` and `Path` maps. This is a deliberate design for simplicity. If you have existing code expecting `context.Context` with values (like from `net/http`), you can still retrieve that via `ctx.Request.Context()`. 134 | 135 | ## Examples 136 | 137 | **Text response example:** 138 | 139 | ```go 140 | g.Get("/ping", func(ctx *goster.Ctx) error { 141 | return ctx.Text("pong") 142 | }) 143 | ``` 144 | Client gets "pong" (Content-Type text/plain, Status 200). 145 | 146 | **JSON response example:** 147 | 148 | ```go 149 | g.Get("/api/time", func(ctx *goster.Ctx) error { 150 | now := time.Now() 151 | data := map[string]string{"time": now.Format(time.RFC3339)} 152 | return ctx.JSON(data) 153 | }) 154 | ``` 155 | Client gets `{"time": "2025-03-07T14:00:00Z"}` with `Content-Type: application/json`. 156 | 157 | **HTML template example:** 158 | 159 | ```go 160 | g.Get("/welcome/:name", func(ctx *goster.Ctx) error { 161 | name, _ := ctx.Path.Get("name") 162 | // Template "welcome.gohtml" expects a struct with Name field 163 | return ctx.Template("welcome.gohtml", struct{ Name string }{Name: name}) 164 | }) 165 | ``` 166 | Client gets an HTML page with the name filled in. 167 | 168 | **Setting a status code example:** 169 | 170 | ```go 171 | g.Post("/items", func(ctx *goster.Ctx) error { 172 | // ... create item ... 173 | ctx.Response.WriteHeader(201) 174 | return ctx.Text("Created") 175 | }) 176 | ``` 177 | Client gets a "Created" message with HTTP 201 status. 178 | 179 | ## Conclusion 180 | 181 | The `Ctx` gives you access to everything you need for request/response lifecycle: 182 | - Use `ctx.Request` (and its Query and Path helpers) to **read input**. 183 | - Use `ctx.Response` (and Text/JSON/Template helpers) to **write output**. 184 | 185 | This design is similar to other Go frameworks (like Gin’s Context or Fiber’s Ctx) but stays close to the standard library, which makes it easy to integrate existing Go code. With this understanding, you can now handle virtually any kind of request in Goster – serving files, APIs, or web pages. Happy building! -------------------------------------------------------------------------------- /docs/Getting_Started.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Goster 2 | 3 | This guide will help you set up a basic web server using Goster. You’ll go from installation to having a running server that responds to HTTP requests. 4 | 5 | ## Installation 6 | 7 | Make sure you have **Go 1.18+** installed on your system. Initialize a Go module for your project if you haven’t already: 8 | 9 | ```bash 10 | go mod init myapp # replace 'myapp' with your module name 11 | ``` 12 | 13 | Then add Goster to your project: 14 | 15 | ```bash 16 | go get -u github.com/dpouris/goster 17 | ``` 18 | 19 | This will download the Goster module and update your `go.mod`. You’re now ready to use Goster in your code. 20 | 21 | ## Basic Setup 22 | 23 | Let’s create a simple web server with one route. Create a file `main.go` with the following: 24 | 25 | ```go 26 | package main 27 | 28 | import "github.com/dpouris/goster" 29 | 30 | func main() { 31 | // Initialize a new Goster server 32 | g := goster.NewServer() 33 | 34 | // Define a route for GET / 35 | g.Get("/", func(ctx *goster.Ctx) error { 36 | return ctx.Text("Welcome to Goster!") 37 | }) 38 | 39 | // Start the server on port 8080 40 | g.Start(":8080") 41 | } 42 | ``` 43 | 44 | **Run the server:** 45 | 46 | ```bash 47 | go run main.go 48 | ``` 49 | 50 | Open your browser (or use `curl`) and visit `http://localhost:8080`. You should see the response **“Welcome to Goster!”** displayed. Congratulations – you’ve just set up a Goster server! 51 | 52 | ## What’s Happening? 53 | 54 | - We created a Goster server with `goster.NewServer()`. This gives us an instance `g` that will handle HTTP requests. 55 | - We added a route using `g.Get("/")`. The first argument is the path (`"/"` for the root). The second argument is a **handler function** that Goster will call when a request comes in for that path. Our handler function uses `ctx.Text` to send a plain-text response. 56 | - Finally, `g.Start(":8080")` starts an HTTP server on port 8080 and begins listening for requests. Under the hood, this uses Go’s `http.ListenAndServe`, passing Goster’s router as the handler. 57 | 58 | When you visited the URL, Goster received the request, matched it to the `/` route, and executed your handler, which wrote “Welcome to Goster!” back to the client. 59 | 60 | ## Secure Server with TLS 61 | 62 | Goster also supports running an HTTPS server using TLS. For example, set up your certificate and key files and start the server with: 63 | 64 | ```go 65 | package main 66 | 67 | import "github.com/dpouris/goster" 68 | 69 | func main() { 70 | g := goster.NewServer() 71 | // Replace with the actual paths to your certificate and key 72 | g.StartTLS(":8443", "path/to/cert.pem", "path/to/key.pem") 73 | } 74 | ``` 75 | 76 | ## Next Steps 77 | 78 | Now that you have a basic server running, you can start adding more routes and functionality: 79 | 80 | - Define more routes (e.g., a `/hello` route) – see the [Routing](Routing.md) documentation. 81 | - Add middleware for logging or authentication – see [Middleware](Middleware.md). 82 | - Serve static files (like images or CSS) – see [Static Files](Static_Files.md). 83 | - Set up HTML templates for more complex responses – see [Templates](Templates.md). 84 | 85 | Goster is designed to scale from this simple example to a structure with multiple routes and middleware. Continue reading the documentation for guidance on each topic, or check out the `examples/` directory in the GitHub repository for additional sample programs. -------------------------------------------------------------------------------- /docs/Logging.md: -------------------------------------------------------------------------------- 1 | # Logging in Goster 2 | 3 | Logging is important for monitoring your application’s behavior and debugging issues. Goster includes a simple logging utility that captures events and also stores a history of requests. This document describes how logging works in Goster and how you can use it. 4 | 5 | ## How Goster Logs Requests 6 | 7 | By default, Goster logs each incoming HTTP request automatically. When any request is handled, Goster records a log entry of the form: 8 | ``` 9 | [] ON ROUTE 10 | ``` 11 | for example: 12 | ``` 13 | [GET] ON ROUTE /users/42 14 | ``` 15 | These entries are stored in the `Logs` slice on the Goster server (`g.Logs`). They are also printed to the console (stdout) if you run your app in a terminal, via the standard library `log.Logger`. 16 | 17 | This means you have two ways to access request logs: 18 | 1. **In memory** – `g.Logs` (a slice of strings) contains recent log entries. You could use this to build an admin endpoint to fetch logs or for testing. 19 | 2. **In console output** – by default, Goster uses `log.Print` behind the scenes for these entries, so they appear in your application’s standard output. 20 | 21 | ## Using the Logger for Custom Messages 22 | 23 | Goster’s logging utility is accessible through package functions that accept the server’s logger: 24 | - `goster.LogInfo(message, g.Logger)` 25 | - `goster.LogWarning(message, g.Logger)` 26 | - `goster.LogError(message, g.Logger)` 27 | 28 | These let you log custom messages at different levels (info, warning, error). All of them ultimately write to `g.Logger` (which is a `*log.Logger`). By default, `g.Logger` is configured to write to os.Stdout with a date/time prefix. 29 | 30 | Example: 31 | 32 | ```go 33 | g := goster.NewServer() 34 | goster.LogInfo("Server started successfully", g.Logger) 35 | 36 | g.Get("/compute", func(ctx *goster.Ctx) error { 37 | goster.LogInfo("Compute endpoint hit", g.Logger) 38 | // ... do work ... 39 | if somethingUnexpected { 40 | goster.LogWarning("Unexpected condition encountered", g.Logger) 41 | } 42 | return ctx.Text("done") 43 | }) 44 | ``` 45 | 46 | In the above: 47 | - When the server starts, we log an info message. 48 | - Each time `/compute` is called, we log an info. If an unusual condition occurs, we log a warning. 49 | 50 | The output in the console will look like (with timestamps prepended by Go’s log package): 51 | ``` 52 | 2025/03/07 16:45:10 INFO - Server started successfully 53 | 2025/03/07 16:47:05 INFO - Compute endpoint hit 54 | 2025/03/07 16:47:05 WARN - Unexpected condition encountered 55 | ``` 56 | 57 | Notice the format: by default, Goster prefixes the level (`INFO`, `WARN`, `ERROR`) in the log message. This is done by the `LogInfo/LogWarning/LogError` functions internally. 58 | 59 | These messages are also added to the `g.Logs` slice. So `g.Logs` would contain: 60 | ```json 61 | [ 62 | "[INFO] Server started successfully", 63 | "[INFO] Compute endpoint hit", 64 | "[WARN] Unexpected condition encountered", 65 | ... 66 | ] 67 | ``` 68 | (The exact format might be slightly different, but conceptually, the log level is included.) 69 | 70 | ## Accessing Logs Programmatically 71 | 72 | Because `g.Logs` holds the log entries, you can expose them via an endpoint for debugging. A simple example from the repository’s usage: 73 | 74 | ```go 75 | g.Get("/logs", func(ctx *goster.Ctx) error { 76 | logMap := make(map[int]string, len(g.Logs)) 77 | for i, entry := range g.Logs { 78 | logMap[i] = entry 79 | } 80 | return ctx.JSON(logMap) 81 | }) 82 | ``` 83 | 84 | This will return a JSON object where each key is an index and the value is the log entry. For instance: 85 | ```json 86 | { 87 | "0": "[GET] ON ROUTE /", 88 | "1": "[GET] ON ROUTE /logs" 89 | } 90 | ``` 91 | which shows that the root path `/` was accessed, and then the `/logs` path (to retrieve logs) was accessed. 92 | 93 | You can modify this approach as needed: 94 | - You might want to paginate or limit logs if the list grows large. 95 | - You could filter by log level (for example, only errors) by scanning `g.Logs` for entries containing `ERROR`. 96 | 97 | ## Customizing Logging 98 | 99 | As of now, Goster’s logging is relatively simple and **not fully configurable** (the README even marks “Configurable Logging” as a TODO feature). This means: 100 | - The format of the log messages is fixed in the `LogInfo/Warning/Error` functions. 101 | - The output destination of `g.Logger` is stdout by default. If you want to change it (say to log to a file), you could replace `g.Logger` with your own `log.Logger` instance after creating the server. For example: 102 | ```go 103 | g := goster.NewServer() 104 | file, _ := os.Create("app.log") 105 | g.Logger = log.New(file, "", log.LstdFlags) 106 | ``` 107 | This would make all Goster logs go to `app.log` instead of the console. Make sure to handle errors and close the file appropriately in a real application. 108 | 109 | - There is no built-in log rotation or level filtering. If you need more sophisticated logging (like only enabling debug logs in dev, etc.), you might integrate another logging library or simply conditionalize your calls to `LogInfo/Warning/Error`. 110 | 111 | ## Logging Middleware vs. Built-in Logging 112 | 113 | If you prefer, you can also use middleware to log requests. For example, a global middleware that logs `ctx.Request.Method` and `ctx.Request.URL.Path`. This gives you full control over format and where it’s logged (you could use a third-party logging library inside the middleware). If you go that route, you might want to disable or ignore Goster’s built-in request logging. While you can’t turn it off easily, you could just not use `g.Logs` or ignore its output. Alternatively, clearing `g.Logs` periodically will remove old entries if you solely rely on your own logging. 114 | 115 | For most basic uses, however, the built-in logging is sufficient to trace what endpoints are being hit and to sprinkle some custom logs for events. 116 | 117 | ## Example: Error Logging 118 | 119 | If an error occurs in your handler and you catch it, you can use `goster.LogError` to record it: 120 | 121 | ```go 122 | g.Post("/upload", func(ctx *goster.Ctx) error { 123 | err := handleUpload(ctx.Request) 124 | if err != nil { 125 | goster.LogError("Upload failed: "+err.Error(), g.Logger) 126 | ctx.Response.WriteHeader(http.StatusInternalServerError) 127 | return ctx.Text("Upload failed") 128 | } 129 | return ctx.Text("Upload successful") 130 | }) 131 | ``` 132 | 133 | This will log an ERROR with the error message. In your logs you might see: 134 | ``` 135 | 2025/03/07 17:00:00 ERROR - Upload failed: file too large 136 | ``` 137 | Meanwhile, the client gets a 500 response with the text "Upload failed". By logging the error, you have a record server-side to investigate later. 138 | 139 | ## Summary 140 | 141 | - Goster automatically logs all requests (method and route) to an internal list and stdout. 142 | - Use `goster.LogInfo`, `LogWarning`, `LogError` for your own log messages in handlers or elsewhere . 143 | - Retrieve logs via `g.Logs` for debugging or exposing through an API if needed. 144 | - The logging system is simple; for advanced needs, you can replace or augment it with custom middleware or a custom logger. 145 | 146 | Logging is essential for understanding your running application. Even though Goster’s logging is basic, it provides the hooks you need to implement a solid logging strategy for your service. -------------------------------------------------------------------------------- /docs/Middleware.md: -------------------------------------------------------------------------------- 1 | # Middleware in Goster 2 | 3 | Middleware allows you to execute code *before* or *after* your route handlers, enabling cross-cutting features like logging, authentication, error handling, etc. Goster provides a simple way to add middleware functions either globally (for all routes) or for specific routes. 4 | 5 | ## What is Middleware? 6 | 7 | In web frameworks, middleware is a function that sits in the request handling chain. It can inspect or modify the request/response, and decide whether to continue to the next handler or stop the chain (for example, to return an error or redirect). 8 | 9 | In Goster, a middleware is simply a function with the same signature as a handler: `func(ctx *goster.Ctx) error`. Middleware runs before the main handler for a route, in the order they were added. 10 | 11 | ## Global Middleware 12 | 13 | **Global middleware** runs on every request, regardless of the path or HTTP method. This is useful for things like logging every request or enforcing site-wide security checks. 14 | 15 | To add a global middleware, use the `UseGlobal` method: 16 | 17 | ```go 18 | g := goster.NewServer() 19 | 20 | g.UseGlobal(func(ctx *goster.Ctx) error { 21 | // Example: simple request logger 22 | println("Received request:", ctx.Request.Method, ctx.Request.URL.Path) 23 | return nil // continue to next handler (or next middleware/route) 24 | }) 25 | ``` 26 | 27 | You can call `UseGlobal` multiple times to add multiple middleware. They will execute in the order added. If a middleware returns a non-nil error, Goster will consider the request handling failed at that point (you might handle this by logging or sending an error response). 28 | 29 | Common use cases for global middleware: 30 | - Logging requests (as in the example). 31 | - Setting up common response headers (like security headers). 32 | - Authentication checks for all routes (if not many public routes). 33 | 34 | ## Route-Specific Middleware 35 | 36 | Sometimes you only want middleware on certain routes or groups of routes. Use `Use(path, middleware...)` to attach middleware to a specific path (or prefix). 37 | 38 | ```go 39 | // Middleware only for paths under /admin 40 | g.Use("/admin", func(ctx *goster.Ctx) error { 41 | if !isUserAdmin(ctx) { 42 | ctx.Response.WriteHeader(403) // Forbidden 43 | ctx.Text("Forbidden") 44 | return fmt.Errorf("unauthorized") // stop further handling 45 | } 46 | return nil 47 | }) 48 | ``` 49 | 50 | In this snippet, any request to a path that starts with `/admin` will go through the middleware. If the function returns an error (as in the case of a non-admin user), the main handler for the route will not run. If it returns nil, Goster will proceed to the next middleware (if any) or the final handler. 51 | 52 | **How specific does the path need to be?** 53 | 54 | The `Use` method uses simple prefix matching for the path you provide: 55 | - If you provide an exact path (e.g., `"/dashboard"`), it will apply to that path’s routes. 56 | - If you provide a prefix (e.g., `"/admin"` as above), it will apply to all routes that have that prefix (like `/admin`, `/admin/settings`, `/admin/users/123`). 57 | 58 | Internally, Goster stores middleware in a map keyed by the path or prefix you give. On each request, after the main handler is chosen, Goster runs: 59 | 1. All global middleware (in order added). 60 | 2. Any middleware whose path prefix matches the request path (in the order they were added, but note that only one middleware function list will match – exactly or by prefix). 61 | 62 | **Order and Matching Detail:** If you added both `Use("/admin", ...)` and `Use("/admin/settings", ...)`, a request to `/admin/settings` would trigger *only* the middleware associated with the exact `/admin/settings` match (because Goster’s implementation stores the middleware for exact path separately from prefix matches). In general, use either broad prefixes or exact paths to avoid confusion. Goster does not currently support middleware for multiple arbitrary patterns or regex. 63 | 64 | ## Middleware Execution Flow 65 | 66 | For a given request, the flow is: 67 | 68 | 1. **Global middleware** – all functions added via `UseGlobal` run, in the order added. 69 | 2. **Route-specific middleware** – if the request path matches a key used in `Use(path, ...)`, those middleware functions run (in order). 70 | 3. **Route handler** – finally, the main handler for the route executes. 71 | 72 | All middleware and the handler share the same `ctx` (context) for the request, so they can communicate via `ctx`. For example, a logging middleware could set a value in `ctx.Meta` or add to `ctx.Logs` that a later middleware or handler could use. 73 | 74 | If any middleware returns an error, Goster will still call the remaining middleware in that list but will skip the main route handler (see the use of `defer route.Handler(ctx)` in the internal implementation). It’s up to you how to handle errors: you might simply log them, or a middleware could send an early response (like `ctx.Text("Forbidden")` as shown above). 75 | 76 | ## Examples 77 | 78 | **1. Logging middleware (global):** 79 | 80 | ```go 81 | g.UseGlobal(func(ctx *goster.Ctx) error { 82 | start := time.Now() 83 | err := ctx.Next() // Note: Goster doesn't have ctx.Next(); this is a conceptual example 84 | duration := time.Since(start) 85 | fmt.Printf("%s %s completed in %v\n", ctx.Request.Method, ctx.Request.URL.Path, duration) 86 | return err 87 | }) 88 | ``` 89 | 90 | *(The above uses a conceptual `ctx.Next()` to illustrate timing around the handler, but Goster’s middleware does not currently provide a built-in next mechanism since it runs all middleware automatically.)* In practice, to measure time you could record `start` in the first middleware and in a **deferred** function, calculate the duration after the handler runs (since the handler runs after middleware). 91 | 92 | **2. Authentication middleware (specific path):** 93 | 94 | ```go 95 | g.Use("/api", func(ctx *goster.Ctx) error { 96 | token := ctx.Request.Header.Get("Authorization") 97 | if !validateToken(token) { 98 | ctx.Response.WriteHeader(401) 99 | ctx.Text("Unauthorized") 100 | return fmt.Errorf("auth failed") 101 | } 102 | return nil 103 | }) 104 | ``` 105 | 106 | This will protect all routes beginning with `/api`. If the token is missing or invalid, it returns 401 Unauthorized and stops the request from reaching the actual API handler. 107 | 108 | **3. Middleware stacking:** 109 | 110 | You can attach multiple middleware to the same path or globally. They will run in the order added: 111 | 112 | ```go 113 | g.UseGlobal(mw1) 114 | g.UseGlobal(mw2) 115 | // mw1 runs, then mw2, then the route handler. 116 | 117 | g.Use("/reports", mwA) 118 | g.Use("/reports", mwB) 119 | // For any /reports request, mwA runs then mwB then the handler. 120 | ``` 121 | 122 | Be mindful of the order, especially if one middleware’s behavior affects another. For example, if `mwA` modifies something that `mwB` relies on. 123 | 124 | ## Best Practices 125 | 126 | - **Keep middleware focused:** Each middleware should ideally do one thing (logging, auth check, etc.). This makes it easier to compose and reuse. 127 | - **Performance:** Remember that global middleware runs for every request. Don’t put extremely heavy processing in middleware (or guard it so it only runs when needed). 128 | - **Error Handling:** Decide how you want to handle errors in middleware. One pattern is to have middleware *not* return errors upward, but rather handle errors internally (for example, by writing a response directly). Another approach is to use a final middleware that catches errors from previous ones or the handler. Goster doesn’t have a special error-catching middleware mechanism, but you can structure your code to check for error returns after `ListenAndServe` (since Goster’s ListenAndServe currently doesn’t propagate handler errors, you might handle errors inside the middleware itself). 129 | 130 | - **ctx values:** You can use the `ctx.Meta` map if you need to pass information from middleware to handlers (for example, user info after authentication). Alternatively, since `ctx.Request` is available, you could use Go’s standard `Context` (in `ctx.Request.Context()`) to store values, but that’s typically not necessary for simple cases. 131 | 132 | Middleware can greatly enhance your application by separating concerns. With Goster’s `UseGlobal` and `Use` methods, you have the flexibility to apply middleware broadly or narrowly as needed. Continue to [Static Files](Static_Files.md) or other docs to explore more Goster features. -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Documentation Overview 2 | 3 | Welcome to the **Goster Documentation** directory. This README organizes and links to the various guides and reference materials available. 4 | 5 | ## Table of Contents 6 | 7 | - [Getting Started](Getting_Started.md) 8 | Setup instructions and a basic example to run your first Goster server. 9 | 10 | - [Routing](Routing.md) 11 | Learn how to define routes, dynamic segments, and path parameters. 12 | 13 | - [Templates](Templates.md) 14 | Configuring template directories and rendering views. 15 | 16 | - [Static Files](Static_Files.md) 17 | Serving assets through Goster. 18 | 19 | - [Middleware](Middleware.md) 20 | Applying global and route-specific middleware. 21 | 22 | - [Logging](Logging.md) 23 | How Goster logs events and request data. 24 | 25 | - [Context and Responses](Context_and_Responses.md) 26 | Handling incoming requests and constructing responses. 27 | -------------------------------------------------------------------------------- /docs/Routing.md: -------------------------------------------------------------------------------- 1 | # Routing in Goster 2 | 3 | Routing is at the core of Goster. It determines how incoming HTTP requests are matched to the code that handles them. This document explains how to define routes, handle path parameters, and understand Goster’s routing mechanisms. 4 | 5 | ## Defining Routes 6 | 7 | You register routes on a Goster server by calling methods named after HTTP verbs: 8 | 9 | - `g.Get(path, handler)` 10 | - `g.Post(path, handler)` 11 | - `g.Put(path, handler)` 12 | - `g.Patch(path, handler)` 13 | - `g.Delete(path, handler)` 14 | 15 | Each of these methods takes a URL path and a **handler function**. The handler should have the signature `func(ctx *goster.Ctx) error`. The handler is called when a request with the corresponding HTTP method and path is received. 16 | 17 | **Example – GET route:** 18 | 19 | ```go 20 | g := goster.NewServer() 21 | 22 | g.Get("/hello", func(ctx *goster.Ctx) error { 23 | return ctx.Text("Hello, world!") 24 | }) 25 | ``` 26 | 27 | In this example, any `GET` request to `/hello` will trigger the handler and respond with “Hello, world!”. 28 | 29 | Under the hood, Goster stores routes in a routing table (a map). When you call `g.Get` (or any method), Goster adds an entry associating the path to your handler. If you try to register the same path twice for the same method, Goster will return an error to prevent duplicates. 30 | 31 | ## Dynamic Routes and Path Parameters 32 | 33 | Often you need routes that capture parts of the URL (e.g., an ID in the path). Goster supports **dynamic routing** using path parameters. A path parameter is indicated by a `:` prefix in the route definition. 34 | 35 | **Example – dynamic route:** 36 | 37 | ```go 38 | g.Get("/users/:id", func(ctx *goster.Ctx) error { 39 | id, _ := ctx.Path.Get("id") 40 | return ctx.Text("User ID is " + id) 41 | }) 42 | ``` 43 | 44 | Here, `:id` in the path means that any value in that segment of the URL will match. For a request to `/users/42`, for instance, the handler will be called and `ctx.Path.Get("id")` will return `"42"`. 45 | 46 | You can use multiple parameters and static parts in one path. For example, `/users/:uid/books/:bid` would capture two parameters, `uid` and `bid`. In the handler, you’d retrieve both with `ctx.Path.Get("uid")` and `ctx.Path.Get("bid")`. The order and names should match what you put in the route pattern. 47 | 48 | **Note:** Path parameters only match up to the next `/` or the end of the path. For instance, in `/files/:name.txt`, the parameter would include “.txt” as part of the value (because the dot is not a separator). Generally, define parameters between slashes, like `/files/:name`. 49 | 50 | ## Route Handlers and the Context 51 | 52 | A route handler is a function with signature `func(ctx *goster.Ctx) error`. When a request comes in, Goster creates a new context (`ctx`) and passes it to your handler. This context contains: 53 | 54 | - `ctx.Request` – the original `*http.Request`. 55 | - `ctx.Response` – a response writer (through which you send output). 56 | - `ctx.Path` – a map of path parameters for this request. 57 | - `ctx.Query` – a map of query string parameters for this request. 58 | 59 | Typically, you won’t need to access `ctx.Request` or `ctx.Response` directly for basic tasks, because Goster provides helper methods on `ctx` (like `ctx.Text`, `ctx.JSON`, etc.). But they are available if you need lower-level control. 60 | 61 | **Writing responses:** A handler should return an `error`. If you encounter an error during processing, you can return it and handle it as needed (for example, you might use middleware to catch errors and return a JSON error response). If no error occurs, return `nil` (as in the examples above). 62 | 63 | Goster doesn’t automatically do anything with returned errors yet (beyond logging them), so you can also handle errors inside the handler (e.g., send an HTTP 500). This might be improved in the future. 64 | 65 | ## Order of Routes and Priority 66 | 67 | Goster’s routing is straightforward: each route is registered to an HTTP method and exact path (except dynamic segments which match variable text). When a request comes in, Goster checks the routing table for that HTTP method: 68 | 69 | 1. If an exact match is found for the path, it uses that handler. 70 | 2. If not, it tries to match dynamic routes. Goster will iterate through registered dynamic routes to find one that fits the request path. For each dynamic route pattern, it checks if the incoming path can be parsed to match that pattern. If a match is found, it stops searching further. 71 | 3. If no route matches, Goster will return a 404 Not Found (by default, simply an empty response with that status). 72 | 73 | Currently, dynamic route matching in Goster is simple and checks patterns in the order they were added. It’s a good practice to avoid overly ambiguous patterns. For example, if you have both `/users/:id` and `/users/profile`, Goster might interpret `/users/profile` as matching the `:id` route if added first. In such cases, check ordering or adjust your route design (maybe use `/users/:id/profile` for profiles to include an ID). 74 | 75 | ## Adding Routes for Other Methods 76 | 77 | We demonstrated `Get`. The same concept applies for other methods: 78 | 79 | ```go 80 | g.Post("/users", func(ctx *goster.Ctx) error { 81 | // Create a new user 82 | return ctx.Text("User created") 83 | }) 84 | 85 | g.Delete("/users/:id", func(ctx *goster.Ctx) error { 86 | // Delete user with given id 87 | return ctx.Text("User deleted") 88 | }) 89 | ``` 90 | 91 | Use the appropriate method name for the type of request you want to handle. If a client sends a request with a method that you haven’t defined, Goster will reply with a 405 Method Not Allowed for that path (assuming the path exists for a different method, otherwise 404). 92 | 93 | ## Wildcard Routing (Not supported) 94 | 95 | Some frameworks support wildcards or catch-all parameters (like `/files/*path` to match `/files/any/arbitrary/path`). **Goster does not currently support wildcard segments.** Every route segment is either static or a single parameter. If you need to serve arbitrary nested paths, you might consider using the static file serving feature (see [Static Files](Static_Files.md)) or handle it in your handler by parsing `ctx.Request.URL` yourself. 96 | 97 | ## Summary 98 | 99 | - Use `g.` to register routes for different HTTP methods. 100 | - Include `:param` in the path to capture dynamic path parameters. Retrieve them in the handler with `ctx.Path.Get`. 101 | - The `ctx` (context) passed to handlers provides request data and helper methods for responses. 102 | - Goster matches routes by method and then by path, supporting dynamic segments. If no match, it returns 404 by default. 103 | - Keep route patterns unambiguous to avoid confusion between static and dynamic routes. 104 | 105 | With routing set up, you can now move on to processing requests (e.g., reading request bodies, returning JSON) which is covered in [Context and Responses](Context_and_Responses.md), or adding [Middleware](Middleware.md) to extend functionality before/after handlers. -------------------------------------------------------------------------------- /docs/Static_Files.md: -------------------------------------------------------------------------------- 1 | # Serving Static Files with Goster 2 | 3 | Many web services need to serve static assets such as images, CSS files, JavaScript files, or downloadable content. Goster provides a convenient way to serve files from a directory on your filesystem through your web server. 4 | 5 | ## Setting a Static Directory 6 | 7 | To tell Goster about your static files, use the `StaticDir` method on your server. This method takes the path to a directory on your system that contains static files. 8 | 9 | ```go 10 | g := goster.NewServer() 11 | err := g.StaticDir("static") 12 | if err != nil { 13 | log.Fatal("Failed to set static dir:", err) 14 | } 15 | ``` 16 | 17 | In this example, Goster will serve files from the `static` directory (relative to where your program is running). Suppose the directory structure is: 18 | 19 | ``` 20 | static/ 21 | ├── css/ 22 | │ └── style.css 23 | ├── images/ 24 | │ └── logo.png 25 | └── index.html 26 | ``` 27 | 28 | After calling `g.StaticDir("static")`: 29 | 30 | - A request to `GET /static/index.html` will return the `static/index.html` file. 31 | - A request to `GET /static/css/style.css` will return the `static/css/style.css` file. 32 | - A request to `GET /static/images/logo.png` will return the `static/images/logo.png` file. 33 | 34 | In general, `g.StaticDir("")` makes the contents of that directory available under the URL path `//*`. 35 | 36 | **Behind the scenes:** When you call `StaticDir`, Goster scans the directory and automatically registers routes for each fil e. Each file gets a corresponding route (usually a GET route) that serves the file’s content. Goster also attempts to set the appropriate Content-Type based on file extension (using an internal utility function `getContentType`). 37 | 38 | If `StaticDir` returns an error, it likely means the directory doesn’t exist or couldn’t be read. Goster will print an error to stderr if, for example, the directory path is wrong or files can’t be opened. Ensure the path is correct and that your program has read access to the files. 39 | 40 | ## Accessing Static Files 41 | 42 | Once `StaticDir` is set up, clients can retrieve the files by making requests to the corresponding URL. Typically, you’ll use this to serve front-end assets. For example, if you have an `index.html` as a single-page app entry point, you might have: 43 | 44 | ```go 45 | g.StaticDir("static") 46 | g.Get("/", func(ctx *goster.Ctx) error { 47 | // Redirect root to the main index page 48 | return ctx.Template("index.html", nil) // or ctx.Text/ctx.File if you prefer 49 | }) 50 | ``` 51 | 52 | However, note that `ctx.Template` is for server-side templates, not static HTML files. If you just want to serve `index.html` as a static file, you could also place it in the static directory and let users access it via `/static/index.html`. Alternatively, use `ctx.Response` to manually serve files (but StaticDir handles this for you). 53 | 54 | ### Example 55 | 56 | If your app’s HTML, CSS, JS are in a folder named **web**: 57 | 58 | ```go 59 | g := goster.NewServer() 60 | g.StaticDir("web") 61 | g.ListenAndServe(":8080") 62 | ``` 63 | 64 | Now: 65 | - `http://localhost:8080/web/` will list the files or require a specific file (depending on the exact behavior, typically you’d request an explicit file). 66 | - `http://localhost:8080/web/app.js` serves the file `web/app.js` if it exists. 67 | - You might configure your front-end build to output to the **web** folder, so all static assets are served by Goster. 68 | 69 | ## Security Considerations 70 | 71 | Goster’s static file serving will expose all files in the directory you specify and its subdirectories. Be careful not to include sensitive files in that directory. For instance, do not point `StaticDir` at a directory that contains configuration files or private data. It’s best to keep a dedicated folder for public assets. 72 | 73 | Also, Goster’s static serving is intended for convenience. For high-throughput static file serving or serving very large files, a dedicated static file server or CDN might be more appropriate. But for many applications (especially APIs that just need to serve a few static files for a frontend), Goster’s approach is sufficient. 74 | 75 | ## Disabling Static Serving 76 | 77 | If you no longer want to serve static files, you could stop calling `StaticDir` or remove those routes. Currently, Goster doesn’t provide a method to *remove* a static directory once added. If needed, you would have to manage that logic (for example, by not calling `StaticDir` based on some config). Typically, though, you set it once at startup and leave it. 78 | 79 | ## Combining with Templates 80 | 81 | It’s common to use static file serving alongside template rendering. Static files are for assets, whereas templates are for dynamic HTML generation. You can use both in Goster: 82 | 83 | ```go 84 | g.StaticDir("static") // for CSS, JS, images 85 | g.TemplateDir("templates") // for dynamic HTML templates 86 | 87 | g.Get("/", func(ctx *goster.Ctx) error { 88 | return ctx.Template("home.gohtml", someData) 89 | }) 90 | ``` 91 | 92 | In this scenario: 93 | - `/static/*` URLs serve files. 94 | - The root `/` (and other non-static routes) can render templates that likely include links to those static assets (for example, `` in the HTML). 95 | 96 | ## Conclusion 97 | 98 | Serving static files in Goster is straightforward: just point `StaticDir` at your folder of assets. This allows you to build a simple web server that not only provides JSON APIs but also serves a web interface or documentation files, etc., without an additional server. For a more detailed understanding of how Goster registers static file routes, you can refer to the implementation in the source (Goster reads files and registers routes for each file). 99 | 100 | Continue to [Templates](Templates.md) to learn how to serve dynamic content using Goster’s template functionality. -------------------------------------------------------------------------------- /docs/Templates.md: -------------------------------------------------------------------------------- 1 | # Template Rendering in Goster 2 | 3 | Goster supports rendering HTML templates so you can serve dynamic content (e.g., HTML pages) in addition to raw text or JSON. It uses Go’s standard html/template or text/template packages under the hood. This document will guide you through setting up template rendering. 4 | 5 | ## Setting the Template Directory 6 | 7 | First, organize your template files (often with `.html` or `.gohtml` extensions) in a directory. Common practice is to have a folder like `templates/` in your project. 8 | 9 | Tell Goster where your templates live by using `TemplateDir`: 10 | 11 | ```go 12 | g := goster.NewServer() 13 | err := g.TemplateDir("templates") 14 | if err != nil { 15 | log.Fatal("Failed to load templates:", err) 16 | } 17 | ``` 18 | 19 | `TemplateDir` scans the specified directory (and subdirectories) for template files. It will record each template’s relative path in an internal map for quick access later. If the directory does not exist, Goster will create it (and log an info message) – this is convenient if you run the app and the template folder is empty, but you plan to add files later. 20 | 21 | **Note:** If a template file name appears more than once (e.g., two files with the same name in different subfolders), Goster may log a warning or error to avoid duplicates. Make sure template file names or relative paths are unique in the directory structure. 22 | 23 | ## Creating a Template 24 | 25 | Templates are text files with placeholders for dynamic data. For example, create a file `templates/hello.gohtml`: 26 | 27 | ```html 28 | 29 | 30 | 31 | Hello 32 | 33 | 34 |

Hello, {{.}}!

35 | 36 | 37 | ``` 38 | 39 | This is a simple template that expects a single value (denoted by `{{.}}`) which it will insert into the HTML. 40 | 41 | ## Rendering a Template in a Handler 42 | 43 | Use `ctx.Template(name string, data interface{}) error` in your route handler to render a template. The `name` should match the file name (or relative path) of the template you want to render, and `data` is whatever you want to pass into the template (it becomes `.` inside the template). 44 | 45 | For example: 46 | 47 | ```go 48 | g.Get("/hello/:name", func(ctx *goster.Ctx) error { 49 | name, _ := ctx.Path.Get("name") 50 | return ctx.Template("hello.gohtml", name) 51 | }) 52 | ``` 53 | 54 | In this handler, when someone visits `/hello/John`, Goster will load the `hello.gohtml` template, execute it with the data `"John"`, and send the resulting HTML. The client will see an HTML page with “Hello, John!” as an `

`. 55 | 56 | Behind the scenes, when you call `ctx.Template("hello.gohtml", data)`, Goster will: 57 | 1. Find the compiled template associated with `"hello.gohtml"` (from the files loaded by `TemplateDir`). 58 | 2. Execute the template with the provided `data`. 59 | 3. Write the output to `ctx.Response` with `Content-Type: text/html`. 60 | 61 | If the template name isn’t found in the map (e.g., you gave the wrong name or forgot to call `TemplateDir`), Goster will likely return an error or write a message to stderr. Ensure that the template was recognized at startup. The `TemplateDir` function prints to stderr if a directory is invalid, so check your logs if nothing is rendering. 62 | 63 | ## Template Data and Functions 64 | 65 | The `data` you pass to `ctx.Template` can be any Go value: 66 | - It can be a string (as in the example above). 67 | - It can be a struct or map for more complex templates (e.g., passing a struct with multiple fields to use in the template). 68 | - It can even be a slice or any other type; how you use it depends on your template content. 69 | 70 | You can also define template functions, use template blocks, etc., by leveraging Go’s `html/template` capabilities. For instance, you might want to parse templates that include other files (partials) or define custom functions (like date formatting). Currently, Goster’s `TemplateDir` will load all files individually. If you need a more advanced setup (like combining templates or defining a base layout), you might manually use Go’s template package and incorporate that into Goster (for example, by storing a template instance in a global and writing to `ctx.Response`). However, for simple use cases, separate files per template as loaded by `TemplateDir` works fine. 71 | 72 | ## Example: Template with Struct Data 73 | 74 | Imagine you have a template `templates/profile.gohtml`: 75 | 76 | ```html 77 |

User Profile

78 |

Name: {{.Name}}

79 |

Age: {{.Age}}

80 | ``` 81 | 82 | And a Go struct: 83 | 84 | ```go 85 | type User struct { 86 | Name string 87 | Age int 88 | } 89 | ``` 90 | 91 | Your handler could be: 92 | 93 | ```go 94 | g.Get("/users/:id/profile", func(ctx *goster.Ctx) error { 95 | // Fetch user data (this is just a stub example) 96 | user := User{Name: "Alice", Age: 30} 97 | return ctx.Template("profile.gohtml", user) 98 | }) 99 | ``` 100 | 101 | This will render the profile template, replacing `{{.Name}}` with "Alice" and `{{.Age}}` with 30. 102 | 103 | ## Template Reloading 104 | 105 | Goster loads templates once at startup (when you call `TemplateDir`). If you edit template files while the server is running, Goster will not automatically reload them. You would need to restart the server to pick up changes. In development, this is fine (just restart on template edits). In production, you typically don’t change templates on the fly. If hot-reloading of templates is required, you’d have to implement a custom solution (which might involve calling `TemplateDir` again or managing your own template cache). 106 | 107 | ## Error Handling 108 | 109 | If there’s an error during template execution (for example, a template syntax error, or the data doesn’t match what the template expects), `ctx.Template` will return an error. You should handle that error (perhaps by returning it from the handler, which could be picked up by middleware or result in a 500). When developing, check your console output for any errors that Goster’s template functions might log. 110 | 111 | ## Comparison to JSON/Text responses 112 | 113 | Using `ctx.Template` is analogous to `ctx.JSON` or `ctx.Text`, except it’s for HTML content. Use it when you need to send structured HTML pages. For simple APIs that only send JSON or plain text, you won’t need templates. But if your microservice also serves a web UI or emails (in HTML format), templates can be very useful. 114 | 115 | ## Recap 116 | 117 | - Use `g.TemplateDir("path/to/templates")` to load templates from disk. 118 | - In handlers, call `ctx.Template("filename", data)` to render a template and send it to the client. 119 | - Organize your templates and data so that they match (template placeholders correspond to fields in the data you pass). 120 | - Remember to restart the server if template files change (in development). 121 | 122 | With templates covered, you have a full spectrum of response options: plain text, JSON, and HTML. Next, you might want to read about [Logging](Logging.md) to see how to monitor your application’s behavior. 123 | -------------------------------------------------------------------------------- /engine.go: -------------------------------------------------------------------------------- 1 | package goster 2 | 3 | import ( 4 | "fmt" 5 | "io/fs" 6 | "log" 7 | "os" 8 | "path/filepath" 9 | "slices" 10 | "sync" 11 | ) 12 | 13 | type Engine struct { 14 | Goster *Goster 15 | startUp sync.Once 16 | Config *Config 17 | } 18 | 19 | type Config struct { 20 | BaseTemplateDir string 21 | StaticDir string 22 | TemplatePaths map[string]string 23 | StaticFilePaths map[string]string 24 | } 25 | 26 | var engine = Engine{} 27 | 28 | // init will run only once and set all the necessary fields for our one and only Goster instance 29 | func (e *Engine) init() *Goster { 30 | initial := func() { 31 | logger := log.New(os.Stdout, "[SERVER] - ", log.LstdFlags) 32 | methods := make(map[string]map[string]Route) 33 | methods["GET"] = make(map[string]Route) 34 | methods["POST"] = make(map[string]Route) 35 | methods["PUT"] = make(map[string]Route) 36 | methods["PATCH"] = make(map[string]Route) 37 | methods["DELETE"] = make(map[string]Route) 38 | e.Goster = &Goster{Routes: methods, Middleware: make(map[string][]RequestHandler), Logger: logger} 39 | } 40 | 41 | // should set up config in here 42 | e.DefaultConfig() 43 | 44 | e.startUp.Do(initial) 45 | 46 | return e.Goster 47 | } 48 | 49 | // Set the default config settings for the engine 50 | func (e *Engine) DefaultConfig() { 51 | e.Config = &Config{ 52 | StaticDir: "", 53 | BaseTemplateDir: "", 54 | TemplatePaths: make(map[string]string, 0), 55 | StaticFilePaths: make(map[string]string, 0), 56 | } 57 | } 58 | 59 | // Sets the template directory to `d` relative to the path of the executable. 60 | func (e *Engine) SetTemplateDir(path string) (err error) { 61 | templateDir, err := resolveAppPath(path) 62 | if err != nil { 63 | return 64 | } 65 | 66 | // if the given directory doesn't exist, create it and report it 67 | if exists := pathExists(templateDir); !exists { 68 | err = os.Mkdir(templateDir, 0o711) // rwx--x--x (o+rwx) (g+x) (u+x) 69 | 70 | if err != nil { 71 | err = fmt.Errorf("template dir couldn't be created: %s", err) 72 | return 73 | } 74 | 75 | fmt.Printf("[ENGINE INFO] - given template path `%s` doesn't exist\n", path) 76 | fmt.Printf("[ENGINE INFO] - creating `%s`...\n", path) 77 | } 78 | e.Config.BaseTemplateDir = templateDir 79 | templatesMap := make(map[string]string) 80 | err = filepath.WalkDir(templateDir, func(path string, d fs.DirEntry, err error) error { 81 | if err != nil { 82 | return fmt.Errorf("cannot walk %s dir", templateDir) 83 | } 84 | if !d.IsDir() { 85 | relativePath, err := filepath.Rel(templateDir, path) 86 | if err != nil { 87 | return err 88 | } 89 | templateExts := []string{".html", ".gohtml"} 90 | fileExt := filepath.Ext(d.Name()) 91 | if slices.Contains(templateExts, fileExt) { 92 | templatesMap[relativePath] = path 93 | } 94 | } 95 | return nil 96 | }) 97 | 98 | if err != nil { 99 | fmt.Fprintf(os.Stderr, "%s is not a valid directory\n", path) 100 | return 101 | } 102 | 103 | for templ := range templatesMap { 104 | fmt.Printf("[ENGINE INFO] - Recorded template `%s` -> %s\n", templ, templatesMap[templ]) 105 | if !e.Config.AddTemplatePath(templ, templatesMap[templ]) { 106 | return fmt.Errorf("template `%s` already exists in `%s`", templ, e.Config.BaseTemplateDir) 107 | } 108 | } 109 | 110 | return 111 | } 112 | 113 | func (e *Engine) SetStaticDir(path string) (err error) { 114 | staticPath, err := resolveAppPath(path) 115 | if err != nil { 116 | return err 117 | } 118 | 119 | // if the given directory doesn't exist, create it and report it 120 | if exists := pathExists(staticPath); !exists { 121 | err = os.Mkdir(staticPath, 0o711) // rwx--x--x (o+rwx) (g+x) (u+x) 122 | 123 | if err != nil { 124 | err = fmt.Errorf("static dir couldn't be created: %s", err) 125 | return 126 | } 127 | 128 | fmt.Printf("[ENGINE INFO] - given static path `%s` doesn't exist\n", path) 129 | fmt.Printf("[ENGINE INFO] - creating static path `%s`...\n", path) 130 | } 131 | 132 | e.Config.StaticDir = staticPath 133 | staticFileMap := make(map[string]string) 134 | // walk the directory and register a GET route for each file found 135 | err = filepath.WalkDir(staticPath, func(filePath string, d fs.DirEntry, err error) error { 136 | if err != nil { 137 | return err 138 | } 139 | 140 | // process only files (skip directories) 141 | if !d.IsDir() { 142 | // compute the route path relative to the static directory 143 | relPath, _ := filepath.Rel(staticPath, filePath) 144 | cleanPath(&relPath) 145 | 146 | staticFileMap[relPath] = filePath 147 | } 148 | 149 | return nil 150 | }) 151 | 152 | for relPath := range staticFileMap { 153 | fmt.Printf("[ENGINE INFO] - Recorded static file `%s` -> %s\n", relPath, staticFileMap[relPath]) 154 | if !e.Config.AddStaticFilePath(relPath, staticFileMap[relPath]) { 155 | return fmt.Errorf("static file `%s` already exists in `%s`", relPath, e.Config.StaticDir) 156 | } 157 | } 158 | 159 | return 160 | } 161 | func (c *Config) AddTemplatePath(relPath string, fullPath string) (added bool) { 162 | // check if path exists 163 | _, exists := c.TemplatePaths[relPath] 164 | 165 | if !exists { 166 | // add if it doesn't 167 | c.TemplatePaths[relPath] = fullPath 168 | added = true 169 | return 170 | } 171 | 172 | added = false 173 | return 174 | } 175 | 176 | func (c *Config) AddStaticFilePath(relPath string, fullPath string) (added bool) { 177 | // check if path exists 178 | _, exists := c.StaticFilePaths[relPath] 179 | 180 | if !exists { 181 | // add if it doesn't 182 | c.StaticFilePaths[relPath] = fullPath 183 | added = true 184 | return 185 | } 186 | 187 | added = false 188 | return 189 | } 190 | -------------------------------------------------------------------------------- /examples/basic/example_basic.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | Goster "github.com/dpouris/goster" 5 | ) 6 | 7 | func main() { 8 | g := Goster.NewServer() 9 | err := g.StaticDir("/static") 10 | if err != nil { 11 | Goster.LogError("could not set static dir", g.Logger) 12 | } 13 | 14 | err = g.TemplateDir("/templates") 15 | if err != nil { 16 | Goster.LogError("could not set templates dir", g.Logger) 17 | } 18 | 19 | g.Get("/", func(ctx *Goster.Ctx) error { 20 | ctx.Text("Welcome to Goster!") 21 | return nil 22 | }) 23 | 24 | g.Start(":8080") 25 | } 26 | -------------------------------------------------------------------------------- /examples/dynamic_routes/example_dynamic_route.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | 7 | Goster "github.com/dpouris/goster" 8 | ) 9 | 10 | func main() { 11 | g := Goster.NewServer() 12 | 13 | g.Get("/greet", func(ctx *Goster.Ctx) error { 14 | ctx.Text("Hey there stranger!\nGo to `/greet/:yourName` to see a message just for you!") 15 | return nil 16 | }) 17 | 18 | g.Get("/greet/:name", func(ctx *Goster.Ctx) error { 19 | name, _ := ctx.Path.Get("name") 20 | ctx.Text(fmt.Sprintf("Hello there %s!\nGo to `/greet/:yourName/:yourAge` to see another message just for you!", name)) 21 | return nil 22 | }) 23 | 24 | g.Get("/greet/:name/:age", func(ctx *Goster.Ctx) error { 25 | name, _ := ctx.Path.Get("name") 26 | ageStr, _ := ctx.Path.Get("age") 27 | 28 | age, err := strconv.Atoi(ageStr) 29 | 30 | if err != nil { 31 | ctx.Text(fmt.Sprintf("%s the value `%s` is not a valid age value :C", name, ageStr)) 32 | } 33 | 34 | ctx.Text(fmt.Sprintf("Hello %s who is %d years old!", name, age)) 35 | return nil 36 | }) 37 | 38 | g.Start(":8080") 39 | } 40 | -------------------------------------------------------------------------------- /examples/html_template/example_html_template.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | 7 | Goster "github.com/dpouris/goster" 8 | ) 9 | 10 | func main() { 11 | g := Goster.NewServer() 12 | err := g.TemplateDir("/templates") 13 | if err != nil { 14 | Goster.LogError("could not set templates dir", g.Logger) 15 | } 16 | 17 | g.Get("/greet/:name", func(ctx *Goster.Ctx) error { 18 | name, _ := ctx.Path.Get("name") 19 | age, exists := ctx.Query.Get("age") 20 | if !exists { 21 | ctx.Response.WriteHeader(http.StatusUnprocessableEntity) 22 | ctx.JSON(map[string]string{ 23 | "status": strconv.Itoa(http.StatusUnprocessableEntity), 24 | "context": "`age` query parameter not specified", 25 | }) 26 | return nil 27 | } 28 | 29 | ctx.Template("index.gohtml", map[string]string{ 30 | "name": name, 31 | "age": age, 32 | }) 33 | return nil 34 | }) 35 | 36 | g.Start(":8080") 37 | } 38 | -------------------------------------------------------------------------------- /examples/json_response/example_json_response.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | Goster "github.com/dpouris/goster" 5 | ) 6 | 7 | func main() { 8 | g := Goster.NewServer() 9 | 10 | g.Get("/json", func(ctx *Goster.Ctx) error { 11 | response := struct { 12 | Message string `json:"message"` 13 | }{ 14 | Message: "Hello, JSON!", 15 | } 16 | ctx.JSON(response) 17 | return nil 18 | }) 19 | 20 | g.Start(":8080") 21 | } 22 | -------------------------------------------------------------------------------- /examples/middleware/example_middleware.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | Goster "github.com/dpouris/goster" 7 | ) 8 | 9 | func main() { 10 | g := Goster.NewServer() 11 | 12 | // Middleware to log requests 13 | g.UseGlobal(func(ctx *Goster.Ctx) error { 14 | log.Printf("Received request for %s", ctx.Request.URL.Path) 15 | return nil 16 | }) 17 | 18 | g.Get("/", func(ctx *Goster.Ctx) error { 19 | ctx.Text("Middleware example") 20 | return nil 21 | }) 22 | 23 | g.Start(":8080") 24 | } 25 | -------------------------------------------------------------------------------- /examples/query_params/example_query_params.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | Goster "github.com/dpouris/goster" 5 | ) 6 | 7 | func main() { 8 | g := Goster.NewServer() 9 | 10 | g.Get("/", func(ctx *Goster.Ctx) error { 11 | q, exists := ctx.Query.Get("q") 12 | if exists { 13 | ctx.Text("Query parameter 'q' is: " + q) 14 | } else { 15 | ctx.Text("Query parameter 'q' not found") 16 | } 17 | return nil 18 | }) 19 | 20 | g.Start(":8080") 21 | } 22 | -------------------------------------------------------------------------------- /examples/templates/hey.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | WORKS 😎 8 | 9 | 10 |

HEY IT WORKS

11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/templates/index.gohtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | Hello there {{.}}! :) 11 | 12 | 13 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/dpouris/goster 2 | 3 | go 1.21 4 | -------------------------------------------------------------------------------- /goster.go: -------------------------------------------------------------------------------- 1 | package goster 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | ) 8 | 9 | // Goster is the main structure of the package. It handles the addition of new routes and middleware, and manages logging. 10 | type Goster struct { 11 | Routes Routes // Routes is a map of HTTP methods to their respective route handlers. 12 | Middleware map[string][]RequestHandler // Middleware is a map of routes to their respective middleware handlers. 13 | Logger *log.Logger // Logger is used for logging information and errors. 14 | Logs []string // Logs stores logs for future reference. 15 | } 16 | 17 | // Route represents an HTTP route with a type and a handler function. 18 | type Route struct { 19 | Type string // Type specifies the type of the route (e.g., "static", "dynamic"). 20 | Handler RequestHandler // Handler is the function that handles the route. 21 | } 22 | 23 | // RequestHandler is a type for functions that handle HTTP requests within a given context. 24 | type RequestHandler func(ctx *Ctx) error 25 | 26 | // ------------------------------------------Public Methods--------------------------------------------------- // 27 | 28 | // NewServer creates a new Goster instance. 29 | func NewServer() *Goster { 30 | g := engine.init() 31 | return g 32 | } 33 | 34 | // UseGlobal adds middleware handlers that will be applied to every single request. 35 | func (g *Goster) UseGlobal(m ...RequestHandler) { 36 | g.Middleware["*"] = append(g.Middleware["*"], m...) 37 | } 38 | 39 | // Use adds middleware handlers that will be applied to specific routes/paths. 40 | func (g *Goster) Use(path string, m ...RequestHandler) { 41 | cleanPath(&path) 42 | g.Middleware[path] = m 43 | } 44 | 45 | // TemplateDir extends the engine's file paths with the specified directory `d`, 46 | // which is joined to Engine.Config.BaseStaticDir (default is the execution path of the program). 47 | // 48 | // This instructs the engine where to look for template files like .html, .gohtml. 49 | // If the directory doesn't exist, it will return an appropriate error. 50 | func (g *Goster) TemplateDir(d string) (err error) { 51 | err = engine.SetTemplateDir(d) 52 | 53 | if err != nil { 54 | LogError(err.Error(), g.Logger) 55 | } 56 | 57 | return 58 | } 59 | 60 | // StaticDir sets the directory from which static files like .css, .js, etc are served. 61 | // 62 | // It integrates the specified directory into the server's static file handling 63 | // by invoking AddStaticDir on the Routes collection. 64 | // 65 | // If an error occurs during this process, the error is printed to the standard error output. 66 | // The function returns the error encountered, if any. 67 | func (g *Goster) StaticDir(dir string) (err error) { 68 | err = engine.SetStaticDir(dir) 69 | if err != nil { 70 | LogError(err.Error(), g.Logger) 71 | } 72 | 73 | err = g.Routes.prepareStaticRoutes(dir) 74 | if err != nil { 75 | return fmt.Errorf("could not prepare routes for static files: %s", err) 76 | } 77 | 78 | return 79 | } 80 | 81 | // Start starts listening for incoming requests on the specified port (e.g., ":8080"). 82 | func (g *Goster) Start(p string) { 83 | g.cleanUp() 84 | LogInfo("LISTENING ON http://127.0.0.1"+p, g.Logger) 85 | log.Fatal(http.ListenAndServe(p, g)) 86 | } 87 | 88 | func (g *Goster) StartTLS(addr string, certFile string, keyFile string) { 89 | g.cleanUp() 90 | LogInfo("LISTENING ON https://127.0.0.1"+addr, g.Logger) 91 | log.Fatal(http.ListenAndServeTLS(addr, certFile, keyFile, g)) 92 | } 93 | 94 | // ServeHTTP is the handler for incoming HTTP requests to the server. 95 | // It parses the request, manages routing, and is required to implement the http.Handler interface. 96 | func (g *Goster) ServeHTTP(w http.ResponseWriter, r *http.Request) { 97 | ctx := Ctx{ 98 | Request: r, 99 | Response: Response{w}, 100 | Meta: Meta{ 101 | Query: make(map[string]string), 102 | Path: make(map[string]string), 103 | }, 104 | } 105 | // Parse the URL and extract query parameters into the Meta struct 106 | urlPath := ctx.Request.URL.EscapedPath() 107 | method := ctx.Request.Method 108 | DefaultHeader(&ctx) 109 | 110 | // Validate the route based on the HTTP method and URL 111 | status := g.validateRoute(method, urlPath) 112 | if status != http.StatusOK { 113 | ctx.Response.WriteHeader(status) 114 | return 115 | } 116 | 117 | // Construct a normal route from URL path if it matches a specific dynamic route 118 | for routePath, route := range g.Routes[method] { 119 | if route.Type != "dynamic" { 120 | continue 121 | } 122 | 123 | if matchesDynamicRoute(urlPath, routePath) { 124 | ctx.Meta.ParseDynamicPath(urlPath, routePath) 125 | err := g.Routes.New(method, urlPath, route.Handler) 126 | if err != nil { 127 | _ = fmt.Errorf("route %s is duplicate", urlPath) // TODO: it is duplicate, handle 128 | } 129 | break 130 | } 131 | } 132 | 133 | // Parses query params if any and adds them to query map 134 | ctx.Meta.ParseQueryParams(r.URL.String()) 135 | 136 | // Execute global middleware handlers 137 | for _, middleware := range g.Middleware["*"] { 138 | err := middleware(&ctx) 139 | if err != nil { 140 | LogError(fmt.Sprintf("error occured while running global middleware: %s", err.Error()), g.Logger) 141 | } 142 | } 143 | logRequest(&ctx, g, nil) // TODO: streamline builtin middleware 144 | 145 | g.launchHandler(&ctx, method, urlPath) 146 | } 147 | 148 | // ------------------------------------------Private Methods--------------------------------------------------- // 149 | 150 | // launchHandler launches the necessary handler for the incoming request based on the route. 151 | func (g *Goster) launchHandler(ctx *Ctx, method, urlPath string) { 152 | cleanPath(&urlPath) 153 | route := g.Routes[method][urlPath] 154 | defer func() { 155 | err := route.Handler(ctx) 156 | // TODO: figure out what to do with handler error 157 | if err != nil { 158 | LogError(err.Error(), g.Logger) 159 | } 160 | }() 161 | // Run all route-specific middleware defined by the user 162 | for _, rh := range g.Middleware[urlPath] { 163 | err := rh(ctx) 164 | if err != nil { 165 | LogError(fmt.Sprintf("error occured while running middleware: %s", err.Error()), g.Logger) 166 | } 167 | } 168 | } 169 | 170 | // validateRoute checks if the route "reqURL" exists inside the `g.Routes` collection 171 | // and under the method "reqMethod" so that the following expression evaluates to true: 172 | // 173 | // if _, exists := g.Routes[m][u]; exists { 174 | // // some code 175 | // } 176 | // 177 | // If "reqURL" exists but not under the method "reqMethod", then the status `http.StatusMethodNotAllowed` is returned 178 | // 179 | // If "reqURL" doesn't exist then the status `http.StatusNotFound` is returned 180 | func (g *Goster) validateRoute(method, urlPath string) int { 181 | cleanPath(&urlPath) 182 | for m := range g.Routes { 183 | _, exists := g.Routes[m][urlPath] 184 | if exists && m == method { 185 | return http.StatusOK 186 | } else if exists && m != method { 187 | return http.StatusMethodNotAllowed 188 | } 189 | } 190 | 191 | return http.StatusNotFound 192 | } 193 | 194 | func (g *Goster) cleanUp() { 195 | if engine.Config.BaseTemplateDir == "" { 196 | LogInfo("No specified template directory. Defaulting to `templates/`...", g.Logger) 197 | err := engine.SetTemplateDir("templates") 198 | if err != nil { 199 | LogError(err.Error(), g.Logger) 200 | } 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /goster_test.go: -------------------------------------------------------------------------------- 1 | package goster 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | type IsDynamicRouteCase struct { 9 | name string 10 | url string 11 | dynamicPath string 12 | expectedResult bool 13 | } 14 | 15 | func TestIsDynamicRoute(t *testing.T) { 16 | testCases := []IsDynamicRouteCase{ 17 | { 18 | name: "Depth 1", 19 | dynamicPath: "path/another_path/:var", 20 | url: "path/another_path/something", 21 | expectedResult: true, 22 | }, 23 | { 24 | name: "Depth 1 (with '/' suffix on URL)", 25 | dynamicPath: "path/another_path/:var", 26 | url: "path/another_path/something/", 27 | expectedResult: true, 28 | }, 29 | { 30 | name: "Depth 1 (with '/' suffix on dynPath)", 31 | dynamicPath: "path/another_path/:var/", 32 | url: "path/another_path/something", 33 | expectedResult: true, 34 | }, 35 | { 36 | name: "Depth 1 (with Depth 2 URL)", 37 | dynamicPath: "path/another_path/:var", 38 | url: "path/another_path/something/something2", 39 | expectedResult: false, 40 | }, 41 | { 42 | name: "Depth 2", 43 | dynamicPath: "path/another_path/:var/:var2", 44 | url: "path/another_path/something/something2", 45 | expectedResult: true, 46 | }, 47 | { 48 | name: "Depth 2 (with Depth 1 URL)", 49 | dynamicPath: "path/another_path/:var/:var2", 50 | url: "path/another_path/something", 51 | expectedResult: false, 52 | }, 53 | { 54 | name: "Depth 2 (with '/' suffix on dynPath)", 55 | dynamicPath: "path/another_path/:var/:var2/", 56 | url: "path/another_path/something/something2", 57 | expectedResult: true, 58 | }, 59 | { 60 | name: "Depth 2 (with '/' suffix on URL)", 61 | dynamicPath: "path/another_path/:var/:var2", 62 | url: "path/another_path/something/something2/", 63 | expectedResult: true, 64 | }, 65 | { 66 | name: "Depth 2 with wrong static path (with '/' suffix on URL)", 67 | dynamicPath: "path/another_path/:var/:var2", 68 | url: "path/wrong_path/something/something2/", 69 | expectedResult: false, 70 | }, 71 | } 72 | 73 | failedCases := make(map[int]IsDynamicRouteCase, 0) 74 | for i, c := range testCases { 75 | if matchesDynamicRoute(c.url, c.dynamicPath) != c.expectedResult { 76 | failedCases[i] = c 77 | } else { 78 | t.Logf("PASSED [%d] - %s\n", i, c.name) 79 | } 80 | } 81 | 82 | // Space 83 | t.Log("") 84 | 85 | for i, c := range failedCases { 86 | t.Errorf("FAILED [%d] - %s\n", i, c.name) 87 | t.Errorf("Expected %t for '%s' and '%s'", c.expectedResult, c.url, c.dynamicPath) 88 | } 89 | 90 | t.Logf("TOTAL CASES: %d\n", len(testCases)) 91 | t.Logf("FAILED CASES: %d\n", len(failedCases)) 92 | } 93 | 94 | type TemplateDirMatch struct { 95 | name string 96 | givenPath string 97 | exectedPath string 98 | } 99 | 100 | func TestTemplateDir(t *testing.T) { 101 | g := NewServer() 102 | 103 | testCases := []TemplateDirMatch{ 104 | { 105 | name: "Test 1", 106 | givenPath: "/templates", 107 | exectedPath: "", 108 | }, 109 | // { 110 | // name: "Test 2", 111 | // givenPath: "/static/templates/", 112 | // exectedPath: "", 113 | // }, 114 | } 115 | 116 | failedCases := make(map[int]TemplateDirMatch, 0) 117 | for _, tmpl := range testCases { 118 | err := g.TemplateDir(tmpl.givenPath) 119 | if err != nil { 120 | t.Error(err) 121 | } 122 | } 123 | 124 | t.Logf("TOTAL CASES: %d\n", len(testCases)) 125 | t.Logf("FAILED CASES: %d\n", len(failedCases)) 126 | } 127 | 128 | func TestStaticDir(t *testing.T) { 129 | g := NewServer() 130 | 131 | testCases := []TemplateDirMatch{ 132 | { 133 | name: "Test 1", 134 | givenPath: "/static", 135 | exectedPath: "", 136 | }, 137 | } 138 | 139 | failedCases := make(map[int]TemplateDirMatch, 0) 140 | for _, tmpl := range testCases { 141 | err := g.StaticDir(tmpl.givenPath) 142 | if err != nil { 143 | t.Error("could not set templates dir") 144 | } 145 | 146 | for route := range g.Routes["GET"] { 147 | fmt.Printf("Route %s\n", route) 148 | } 149 | } 150 | 151 | t.Logf("TOTAL CASES: %d\n", len(testCases)) 152 | t.Logf("FAILED CASES: %d\n", len(failedCases)) 153 | } 154 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package goster 2 | 3 | import ( 4 | "log" 5 | ) 6 | 7 | // Supply msg with a string value to be logged to the console and a logger 8 | func LogInfo(msg string, logger *log.Logger) { 9 | logger.Printf("INFO - %s\n", msg) 10 | } 11 | 12 | // Supply msg with a string value to be logged to the console and a logger 13 | func LogWarning(msg string, logger *log.Logger) { 14 | logger.Printf("WARN - %s\n", msg) 15 | } 16 | 17 | // Supply msg with a string value to be logged to the console and a logger 18 | func LogError(msg string, logger *log.Logger) { 19 | logger.Printf("ERROR - %s\n", msg) 20 | } 21 | -------------------------------------------------------------------------------- /meta.go: -------------------------------------------------------------------------------- 1 | package goster 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | ) 7 | 8 | type Meta struct { 9 | Query Params 10 | Path Path 11 | } 12 | 13 | type DynamicPath struct { 14 | path string 15 | value string 16 | } 17 | 18 | type Params map[string]string 19 | 20 | type Path map[string]string 21 | 22 | // Get tries to find if `id` is in the URL's Query Params 23 | // 24 | // If the specified `id` isn't found `exists` will be false 25 | func (p *Params) Get(id string) (value string, exists bool) { 26 | value, exists = (*p)[id] 27 | return 28 | } 29 | 30 | // Get tries to find if `id` is in the URL's as a Dynamic Path Identifier 31 | // 32 | // If the specified `id` isn't found `exists` will be false 33 | func (p *Path) Get(id string) (value string, exists bool) { 34 | value, exists = (*p)[id] 35 | return 36 | } 37 | 38 | // Pass in a `url` and see if there're parameters in it 39 | // 40 | // If there're, ParseQueryParams will construct a Params struct and populate Meta.Query.Params 41 | // 42 | // If there aren't any, ParseQueryParams will return 43 | // 44 | // The `url` string reference that is passed in will have the parameters stripped in either case 45 | func (m *Meta) ParseQueryParams(url string) { 46 | paramValues := make(map[string]string, 0) 47 | paramPattern := regexp.MustCompile(`\?.+(\/)?`) 48 | 49 | defer func() { 50 | m.Query = paramValues 51 | }() 52 | 53 | params := paramPattern.FindString(url) 54 | params = strings.Trim(params, "/?") 55 | 56 | if len(params) == 0 { 57 | return 58 | } 59 | 60 | for _, v := range strings.Split(params, "&") { 61 | query := strings.Split(v, "=") 62 | 63 | if len(query) == 1 { 64 | paramValues[query[0]] = "" 65 | continue 66 | } 67 | 68 | paramValues[query[0]] = query[1] 69 | } 70 | } 71 | 72 | func (m *Meta) ParseDynamicPath(url, urlPath string) { 73 | cleanPath(&url) 74 | cleanPath(&urlPath) 75 | dynamicPaths, isDynamic := matchDynamicPath(url, urlPath) 76 | 77 | if !isDynamic { 78 | return 79 | } 80 | 81 | for _, dynamicPath := range dynamicPaths { 82 | m.Path[dynamicPath.path] = dynamicPath.value 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /meta_test.go: -------------------------------------------------------------------------------- 1 | package goster 2 | 3 | import ( 4 | "maps" 5 | "testing" 6 | ) 7 | 8 | type ParseUrlCase struct { 9 | name string 10 | url string 11 | expectedQueryParams map[string]string 12 | shouldFail bool 13 | } 14 | 15 | func TestParseUrl(t *testing.T) { 16 | testCases := []ParseUrlCase{ 17 | { 18 | name: "1", 19 | url: "/var/home?name=dimitris&age=24", 20 | expectedQueryParams: map[string]string{ 21 | "name": "dimitris", 22 | "age": "24", 23 | }, 24 | shouldFail: false, 25 | }, 26 | { 27 | name: "2", 28 | url: "/var/home?name=dimitris&age=23", 29 | expectedQueryParams: map[string]string{ 30 | "name": "dimitris", 31 | "age": "24", 32 | }, 33 | shouldFail: true, 34 | }, 35 | { 36 | name: "3", 37 | url: "/var/home?name=dimitris&name=gearge&age=24", 38 | expectedQueryParams: map[string]string{ 39 | "name": "dimitris", 40 | "age": "24", 41 | }, 42 | shouldFail: true, 43 | }, 44 | { 45 | name: "4", 46 | url: "/var/home?name=dimitris&name=gearge&age=24&isAdmin", 47 | expectedQueryParams: map[string]string{ 48 | "name": "gearge", 49 | "age": "24", 50 | "isAdmin": "", 51 | }, 52 | shouldFail: false, 53 | }, 54 | } 55 | 56 | failedCases := make(map[int]struct { 57 | Meta 58 | ParseUrlCase 59 | }, 0) 60 | for i, c := range testCases { 61 | meta := Meta{ 62 | Query: make(map[string]string), 63 | } 64 | meta.ParseQueryParams(c.url) 65 | if (!maps.Equal(meta.Query, c.expectedQueryParams)) == !c.shouldFail { 66 | failedCases[i] = struct { 67 | Meta 68 | ParseUrlCase 69 | }{meta, c} 70 | } else { 71 | t.Logf("PASSED [%d] - %s\n", i, c.name) 72 | } 73 | } 74 | 75 | // Space 76 | t.Log("") 77 | 78 | for i, c := range failedCases { 79 | t.Errorf("FAILED [%d] - %s\n", i, c.ParseUrlCase.name) 80 | t.Errorf("Expected '%v' path, but got '%v'", c.expectedQueryParams, c.Query) 81 | } 82 | 83 | t.Logf("TOTAL CASES: %d\n", len(testCases)) 84 | t.Logf("FAILED CASES: %d\n", len(failedCases)) 85 | } 86 | -------------------------------------------------------------------------------- /middleware.go: -------------------------------------------------------------------------------- 1 | package goster 2 | 3 | func logRequest(c *Ctx, g *Goster, err error) { 4 | m := c.Request.Method 5 | u := c.Request.URL.String() 6 | 7 | if err != nil { 8 | l := err.Error() 9 | g.Logs = append(g.Logs, l) 10 | LogError(l, g.Logger) 11 | return 12 | } 13 | l := "[" + m + "]" + " ON ROUTE " + u 14 | g.Logs = append(g.Logs, l) 15 | LogInfo(l, g.Logger) 16 | } 17 | 18 | // TODO: should be auth middleware 19 | -------------------------------------------------------------------------------- /response.go: -------------------------------------------------------------------------------- 1 | package goster 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | type Response struct { 8 | http.ResponseWriter 9 | } 10 | 11 | // Supply h with a map[string]string for the headers and s with an int representing the response status code or use the http.Status(...). They keys and values will be translated to the header of the response and the header will be locked afterwards not allowing changes to be made. 12 | func (r *Response) NewHeaders(h map[string]string, s int) { 13 | for k, v := range h { 14 | r.Header().Set(k, v) 15 | } 16 | 17 | r.WriteHeader(s) 18 | } 19 | -------------------------------------------------------------------------------- /routes.go: -------------------------------------------------------------------------------- 1 | package goster 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | ) 10 | 11 | type Routes map[string]map[string]Route 12 | 13 | func (rs *Routes) prepareStaticRoutes(dir string) (err error) { 14 | staticPaths := engine.Config.StaticFilePaths 15 | for relPath := range staticPaths { 16 | staticPath := staticPaths[relPath] 17 | file, err := os.Open(staticPath) 18 | if err != nil { 19 | fmt.Fprintf(os.Stderr, "cannot open static file `%s`\n", file.Name()) 20 | return err 21 | } 22 | 23 | // register a GET route that serves the static file 24 | routePath := filepath.Join(dir, relPath) 25 | cleanPath(&routePath) 26 | err = rs.New("GET", routePath, func(ctx *Ctx) error { 27 | return staticFileHandler(ctx, file) 28 | }) 29 | if err != nil { 30 | fmt.Fprintf(os.Stderr, "couldn't add route `%s`. Most likely there's a duplicate entry\n", routePath) 31 | } 32 | } 33 | 34 | return 35 | } 36 | 37 | // New creates a new Route for the specified method and url using the provided handler. If the Route already exists an error is returned. 38 | func (rs *Routes) New(method string, url string, handler RequestHandler) (err error) { 39 | for name := range (*rs)[method] { 40 | if name == url { 41 | err = fmt.Errorf("[%s] -> [%s] route already exists", method, url) 42 | return 43 | } 44 | } 45 | 46 | routeType := "normal" 47 | if strings.Contains(url, ":") { 48 | routeType = "dynamic" 49 | } 50 | 51 | cleanPath(&url) 52 | 53 | (*rs)[method][url] = Route{Type: routeType, Handler: handler} 54 | 55 | return 56 | } 57 | 58 | // Get creates a new Route under the GET method for `path`. If the Route aleady exists an error is returned. 59 | func (g *Goster) Get(url string, handler RequestHandler) error { 60 | return g.Routes.New("GET", url, handler) 61 | } 62 | 63 | // Post creates a new Route under the POST method for `path`. If the Route aleady exists an error is returned. 64 | func (g *Goster) Post(path string, handler RequestHandler) error { 65 | return g.Routes.New("POST", path, handler) 66 | } 67 | 68 | // Patch creates a new Route under the PATCH method for `path`. If the Route aleady exists an error is returned. 69 | func (g *Goster) Patch(path string, handler RequestHandler) error { 70 | return g.Routes.New("PATCH", path, handler) 71 | } 72 | 73 | // Put creates a new Route under the PUT method for `path`. If the Route aleady exists an error is returned. 74 | func (g *Goster) Put(path string, handler RequestHandler) error { 75 | return g.Routes.New("PUT", path, handler) 76 | } 77 | 78 | // Delete creates a new Route under the DELETE method for `path`. If the Route aleady exists an error is returned. 79 | func (g *Goster) Delete(path string, handler RequestHandler) error { 80 | return g.Routes.New("DELETE", path, handler) 81 | } 82 | 83 | func staticFileHandler(ctx *Ctx, file *os.File) (err error) { 84 | // read the file contents 85 | _, err = file.Seek(0, 0) 86 | if err != nil { 87 | return 88 | } 89 | fInfo, _ := file.Stat() 90 | fSize := fInfo.Size() 91 | buffer := make([]byte, fSize) 92 | _, _ = io.ReadFull(file, buffer) 93 | 94 | // prepare and write response 95 | contentType := getContentType(file.Name()) 96 | ctx.Response.Header().Set("Content-Type", contentType) 97 | _, err = ctx.Response.Write(buffer) 98 | return 99 | } 100 | -------------------------------------------------------------------------------- /routes_test.go: -------------------------------------------------------------------------------- 1 | package goster 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | ) 7 | 8 | type MethodNewCase struct { 9 | name string 10 | method string 11 | url string 12 | expectedPath string 13 | handler RequestHandler 14 | } 15 | 16 | func TestMethodNew(t *testing.T) { 17 | g := NewServer() 18 | testCases := []MethodNewCase{ 19 | { 20 | name: "1", 21 | method: "GET", 22 | url: "/home/var", 23 | expectedPath: "/home/var", 24 | handler: func(ctx *Ctx) error { return errors.New("test error") }, 25 | }, 26 | { 27 | name: "2", 28 | method: "GET", 29 | url: "/home/var////", 30 | expectedPath: "/home/var///", 31 | handler: func(ctx *Ctx) error { return errors.New("test error") }, 32 | }, 33 | } 34 | 35 | failedCases := make(map[int]MethodNewCase, 0) 36 | for i, c := range testCases { 37 | err := g.Routes.New(c.method, c.url, c.handler) 38 | if err != nil { 39 | t.Errorf("route `%s` already exists", c.url) 40 | } 41 | if _, exists := g.Routes[c.method][c.expectedPath]; exists { 42 | t.Logf("PASSED [%d] - %s\n", i, c.name) 43 | } else { 44 | failedCases[i] = c 45 | } 46 | } 47 | 48 | // Space 49 | t.Log("") 50 | 51 | for i, c := range failedCases { 52 | t.Errorf("FAILED [%d] - %s\n", i, c.name) 53 | } 54 | 55 | // Space 56 | t.Log("") 57 | t.Logf("Routes: ") 58 | t.Log(g.Routes) 59 | 60 | t.Logf("TOTAL CASES: %d\n", len(testCases)) 61 | t.Logf("FAILED CASES: %d\n", len(failedCases)) 62 | } 63 | 64 | // TestAddStaticDir verifies that AddStaticDir properly walks the given directory, 65 | // registers a route for each file (here we create one test file), and that the route's key 66 | // matches the file's path (normalized to use forward slashes). 67 | func TestAddStaticDir(t *testing.T) { 68 | // Get the directory of the current executable. 69 | // exPath, err := os.Executable() 70 | // if err != nil { 71 | // t.Fatalf("os.Executable() error: %v", err) 72 | // } 73 | 74 | testDir := "../static/" 75 | // baseDir := filepath.Dir(exPath) 76 | 77 | // Initialize Routes. Ensure that the "GET" method map is created. 78 | r := Routes{ 79 | "GET": make(map[string]Route), 80 | } 81 | 82 | // Call AddStaticDir with the relative directory. 83 | if err := r.prepareStaticRoutes(testDir); err != nil { 84 | t.Fatalf("AddStaticDir returned error: %v", err) 85 | } 86 | 87 | for route := range r["GET"] { 88 | t.Logf("ROute: %s\n", route) 89 | } 90 | 91 | // // Check that the route for the file was added. 92 | // if route, exists := r["GET"][expectedKey]; !exists { 93 | // t.Errorf("expected route %q not found in GET routes", expectedKey) 94 | // } else { 95 | // if route.Handler == nil { 96 | // t.Errorf("route handler for %q is nil", expectedKey) 97 | // } else { 98 | // t.Logf("PASSED - Route for %q added successfully", expectedKey) 99 | // } 100 | // } 101 | 102 | t.Logf("TOTAL ROUTES in GET: %d", len(r["GET"])) 103 | } 104 | -------------------------------------------------------------------------------- /utilities/run_example.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #ccheck if number of arguments provided is correct 4 | if [ "$#" -ne 1 ]; then 5 | echo "Usage: $0 " 6 | exit 1 7 | fi 8 | 9 | # path to the example file 10 | example_file=$1 11 | 12 | # check if file exits 13 | if [ ! -f "$example_file" ]; then 14 | echo "file \`$example_file\` not found" 15 | exit 1 16 | fi 17 | 18 | # make output directory if it doesnt exist 19 | output_dir="./examples_out" 20 | mkdir -p "$output_dir" 21 | 22 | # get the name of the example file without extension .go 23 | base_name=$(basename "$example_file" .go) 24 | 25 | # dir name of the example file 26 | dir_name=$(basename $(dirname "$example_file")) 27 | 28 | # make dir for the compiled file 29 | compiled_dir="$output_dir/$dir_name" 30 | mkdir -p "$compiled_dir" 31 | 32 | # compile 33 | go build -o "$compiled_dir/$base_name" "$example_file" 34 | 35 | # check if compilation was successful 36 | if [ $? -ne 0 ]; then 37 | echo "failed to compile $example_file" 38 | exit 1 39 | fi 40 | 41 | # run 42 | "$compiled_dir/$base_name" 43 | -------------------------------------------------------------------------------- /utilities/run_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # check if number of arguments provided is correct 4 | if [ "$#" -ne 1 ]; then 5 | echo "Usage: $0 " 6 | exit 1 7 | fi 8 | 9 | # path to the test file 10 | test_file=$1 11 | 12 | # check file exists 13 | if [ ! -f "$test_file" ]; then 14 | echo "file \`$test_file\`not found" 15 | exit 1 16 | fi 17 | 18 | # make output dir if not exist 19 | output_dir="./tests_out" 20 | mkdir -p "$output_dir" 21 | 22 | # name of the test file without the extension 23 | base_name=$(basename "$test_file" .go) 24 | 25 | # dir of the test file 26 | test_dir=$(dirname "$test_file") 27 | 28 | # make dir for the compiled test file 29 | compiled_dir="$output_dir/$base_name" 30 | mkdir -p "$compiled_dir" 31 | 32 | # compile test 33 | go test -c -v -o "$compiled_dir/$base_name" "$test_dir" 34 | 35 | # check if compilation was succesful 36 | if [ $? -ne 0 ]; then 37 | echo "failed to compile \`$test_file\`" 38 | exit 1 39 | fi 40 | 41 | # run 42 | "$compiled_dir/$base_name" 43 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package goster 2 | 3 | import ( 4 | "fmt" 5 | "mime" 6 | "os" 7 | "path" 8 | "path/filepath" 9 | "strings" 10 | ) 11 | 12 | // Adds basic headers 13 | func DefaultHeader(c *Ctx) { 14 | c.Response.Header().Set("Access-Control-Allow-Origin", "*") 15 | c.Response.Header().Set("Connection", "Keep-Alive") 16 | c.Response.Header().Set("Keep-Alive", "timeout=5, max=997") 17 | } 18 | 19 | // cleanPath sanatizes a URL path. It removes suffix '/' if any and adds prefix '/' if missing. If the URL contains Query Parameters or Anchors, 20 | // they will be removed as well. 21 | func cleanPath(path *string) { 22 | if len(*path) == 0 { 23 | return 24 | } 25 | 26 | if (*path)[0] != '/' { 27 | *path = "/" + *path 28 | } 29 | 30 | *path = strings.TrimSuffix(*path, "/") 31 | } 32 | 33 | // matchesDynamicRoute checks URL path `reqURL` matches a Dynamic Route path 34 | // `dynamicPath`. A Dynamic Route is a path string that has the following format: "path/anotherPath/:variablePathname" where `:variablePathname` 35 | // is a catch-all identifier that matches any route with the same structure up to that point. 36 | // 37 | // Ex: 38 | // 39 | // var ctx = ... 40 | // var url = "path/anotherPath/andYetAnotherPath" 41 | // var dynamicPath = "path/anotherPath/:identifier" 42 | // if !matchesDynamicRoute(&ctx, url, dynamicPath) { 43 | // panic(...) 44 | // } 45 | // 46 | // The above code will not panic as the matchesDynamicRoute will evaluate to `true` 47 | func matchesDynamicRoute(urlPath string, routePath string) (isDynamic bool) { 48 | cleanPath(&urlPath) 49 | cleanPath(&routePath) 50 | 51 | _, isDynamic = matchDynamicPath(urlPath, routePath) 52 | return 53 | } 54 | 55 | func matchDynamicPath(urlPath, routePath string) (dp []DynamicPath, isDynamic bool) { 56 | routePathSlice := strings.Split(routePath, "/") 57 | urlSlice := strings.Split(urlPath, "/") 58 | 59 | if len(routePathSlice) != len(urlSlice) { 60 | return nil, false 61 | } 62 | 63 | hasDynamic := false 64 | dp = []DynamicPath{} 65 | for i, seg := range routePathSlice { 66 | if strings.HasPrefix(seg, ":") { 67 | hasDynamic = true 68 | dynamicValue := strings.Split(urlSlice[i], "?")[0] 69 | dp = append(dp, DynamicPath{ 70 | path: strings.TrimPrefix(seg, ":"), 71 | value: dynamicValue, 72 | }) 73 | } else if seg != urlSlice[i] { 74 | // static segment doesn't match 75 | return nil, false 76 | } 77 | } 78 | 79 | return dp, hasDynamic 80 | } 81 | 82 | func getContentType(filename string) string { 83 | ext := filepath.Ext(filename) 84 | contentType := mime.TypeByExtension(ext) 85 | if contentType == "" { 86 | contentType = "application/octet-stream" 87 | } 88 | return contentType 89 | } 90 | 91 | func resolveAppPath(dir string) (string, error) { 92 | fileDir, err := filepath.Abs(filepath.Dir(os.Args[0])) 93 | if err != nil { 94 | fmt.Fprintf(os.Stderr, "cannot determine working directory for static dir %s\n", dir) 95 | return dir, err 96 | } 97 | 98 | // construct the full path to the static directory 99 | return path.Join(fileDir, dir), nil 100 | } 101 | 102 | func pathExists(path string) (exists bool) { 103 | _, err := os.Stat(path) 104 | return err == nil 105 | } 106 | 107 | // func cleanEmptyBytes(b *[]byte) { 108 | // cleaned := []byte{} 109 | 110 | // for _, v := range *b { 111 | // if v == 0 { 112 | // break 113 | // } 114 | // cleaned = append(cleaned, v) 115 | // } 116 | // *b = cleaned 117 | // } 118 | -------------------------------------------------------------------------------- /utils_test.go: -------------------------------------------------------------------------------- 1 | package goster 2 | 3 | import "testing" 4 | 5 | type CleanPathCase struct { 6 | name string 7 | path string 8 | expectedPath string 9 | } 10 | 11 | func TestCleanPath(t *testing.T) { 12 | testCases := []CleanPathCase{ 13 | { 14 | name: "Same path", 15 | path: "home/var", 16 | expectedPath: "/home/var", 17 | }, 18 | { 19 | name: "No path", 20 | path: "", 21 | expectedPath: "", 22 | }, 23 | { 24 | name: "Single slash", 25 | path: "/", 26 | expectedPath: "", 27 | }, 28 | { 29 | name: "A lot of slashes", 30 | path: "/////", 31 | expectedPath: "////", 32 | }, 33 | { 34 | name: "Trailing slash", 35 | path: "home/var/", 36 | expectedPath: "/home/var", 37 | }, 38 | { 39 | name: "A lot of trailing slashes", 40 | path: "home/var///////", 41 | expectedPath: "/home/var//////", 42 | }, 43 | } 44 | 45 | failedCases := make(map[int]CleanPathCase, 0) 46 | for i, c := range testCases { 47 | cleanPath(&c.path) 48 | if c.path != c.expectedPath { 49 | failedCases[i] = c 50 | } else { 51 | t.Logf("PASSED [%d] - %s\n", i, c.name) 52 | } 53 | } 54 | 55 | // Space 56 | t.Log("") 57 | 58 | for i, c := range failedCases { 59 | t.Errorf("FAILED [%d] - %s\n", i, c.name) 60 | } 61 | 62 | t.Logf("TOTAL CASES: %d\n", len(testCases)) 63 | t.Logf("FAILED CASES: %d\n", len(failedCases)) 64 | } 65 | --------------------------------------------------------------------------------