├── .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 | [](https://pkg.go.dev/github.com/dpouris/goster)
3 | [](https://goreportcard.com/report/github.com/dpouris/goster)
4 | [](https://github.com/dpouris/goster/blob/master/LICENSE)
5 | 
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 |
--------------------------------------------------------------------------------