[
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: [bnkamalesh] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]\npatreon: # Replace with a single Patreon username\nopen_collective: # Replace with a single Open Collective username\nko_fi: # Replace with a single Ko-fi username\ntidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel\ncommunity_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry\nliberapay: # Replace with a single Liberapay username\nissuehunt: # Replace with a single IssueHunt username\nlfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry\npolar: # Replace with a single Polar username\nbuy_me_a_coffee: # Replace with a single Buy Me a Coffee username\nthanks_dev: # Replace with a single thanks.dev username\ncustom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To Reproduce**\nSteps to reproduce the behavior:\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Screenshots**\nIf applicable, add screenshots to help explain your problem.\n\n**Desktop (please complete the following information):**\n - OS: [e.g. iOS]\n - Browser [e.g. chrome, safari]\n - Version [e.g. 22]\n\n**Smartphone (please complete the following information):**\n - Device: [e.g. iPhone6]\n - OS: [e.g. iOS8.1]\n - Browser [e.g. stock browser, safari]\n - Version [e.g. 22]\n\n**Additional context**\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Is your feature request related to a problem? Please describe.**\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".github/workflows/go.yml",
    "content": "# This workflow will build a golang project\n# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go\n\nname: Go\n\non:\n  push:\n    branches: [\"master\"]\n  pull_request:\n    branches: [\"master\"]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Set up Go\n        uses: actions/setup-go@v4\n        with:\n          go-version: \"1.23\"\n\n      - name: Build\n        run: go build -v ./...\n\n      - name: Tests\n        run: |\n          go install github.com/mattn/goveralls@latest\n          go test -covermode atomic -coverprofile=covprofile $(go list ./... | grep -v /cmd | grep -v /extensions/)\n\n      - name: Send coverage\n        uses: shogo82148/actions-goveralls@v1\n        with:\n          path-to-profile: covprofile\n\n      - name: golangci-lint\n        uses: golangci/golangci-lint-action@v6\n        with:\n          version: v1.60\n"
  },
  {
    "path": ".gitignore",
    "content": "# Created by https://www.gitignore.io/api/go,osx,linux,windows\n\n### Go ###\n# Binaries for programs and plugins\n*.exe\n*.dll\n*.so\n*.dylib\n\n# Test binary, build with `go test -c`\n*.test\n\n# Output of the go coverage tool, specifically when used with LiteIDE\n*.out\n\n# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736\n.glide/\n\n# Golang project vendor packages which should be ignored\nvendor/\n\n### Linux ###\n*~\n\n# temporary files which can be created if a process still has a handle open of a deleted file\n.fuse_hidden*\n\n# KDE directory preferences\n.directory\n\n# Linux trash folder which might appear on any partition or disk\n.Trash-*\n\n# .nfs files are created when an open file is removed but is still being accessed\n.nfs*\n\n### OSX ###\n*.DS_Store\n.AppleDouble\n.LSOverride\n\n# Icon must end with two \\r\nIcon\n\n# Thumbnails\n._*\n\n# Files that might appear in the root of a volume\n.DocumentRevisions-V100\n.fseventsd\n.Spotlight-V100\n.TemporaryItems\n.Trashes\n.VolumeIcon.icns\n.com.apple.timemachine.donotpresent\n\n# Directories potentially created on remote AFP share\n.AppleDB\n.AppleDesktop\nNetwork Trash Folder\nTemporary Items\n.apdisk\n\n### Windows ###\n# Windows thumbnail cache files\nThumbs.db\nehthumbs.db\nehthumbs_vista.db\n\n# Folder config file\nDesktop.ini\n\n# Recycle Bin used on file shares\n$RECYCLE.BIN/\n\n# Windows Installer files\n*.cab\n*.msi\n*.msm\n*.msp\n\n# Windows shortcuts\n*.lnk\n\n# End of https://www.gitignore.io/api/go,osx,linux,windows\n\n.vscode"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.\n\n## Our Standards\n\nExamples of behavior that contributes to creating a positive environment include:\n\n* Using welcoming and inclusive language\n* Being respectful of differing viewpoints and experiences\n* Gracefully accepting constructive criticism\n* Focusing on what is best for the community\n* Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n* The use of sexualized language or imagery and unwelcome sexual attention or advances\n* Trolling, insulting/derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or electronic address, without explicit permission\n* Other conduct which could reasonably be considered inappropriate in a professional setting\n\n## Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.\n\nProject maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.\n\n## Scope\n\nThis Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at bn_kamalesh@yahoo.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]\n\n[homepage]: http://contributor-covenant.org\n[version]: http://contributor-covenant.org/version/1/4/\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "Contributions are welcome from everyone. Please adhere to the [code of conduct](https://github.com/naughtygopher/webgo/blob/master/CODE_OF_CONDUCT.md) of the project, and be respectful to all.\n\nPlease follow the guidelines provided for contribution\n\n1. Updates to the project are only accepted via Pull Requests (PR)\n2. Pull requests will be reviewed & tested\n3. Every PR should be accompanied by its test wherever applicable\n4. While creating an issue\n   1. Mention the steps to reproduce the issue\n   2. Mention the environment in which it was run\n   3. Include your 1st level of troubleshooting results\n5. Provide meaningful commit messages\n\n### Versioning & PR messages\n\nWebGo tries to use [semantic versioning](https://semver.org/) and starting recently, have decided to adhere to the following syntax in PR description. List down the changes as bulleted list, as follows:\n\n```markdown\n[major] any backward incompatible or breaking change\n[minor] any new feature\n[patch] enhancements of existing features, refactor, bug fix etc.\n[-] for changes which does not require a version number update\n```\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2024 Naughty Gopher\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\"><img src=\"https://user-images.githubusercontent.com/1092882/60883564-20142380-a268-11e9-988a-d98fb639adc6.png\" alt=\"webgo gopher\" width=\"256px\"/></p>\n\n[![](https://github.com/naughtygopher/webgo/actions/workflows/go.yml/badge.svg?branch=master)](https://github.com/naughtygopher/webgo/actions)\n[![Go Reference](https://pkg.go.dev/badge/github.com/naughtygopher/webgo.svg)](https://pkg.go.dev/github.com/naughtygopher/webgo)\n[![Go Report Card](https://goreportcard.com/badge/github.com/naughtygopher/webgo)](https://goreportcard.com/report/github.com/naughtygopher/webgo)\n[![Coverage Status](https://coveralls.io/repos/github/naughtygopher/webgo/badge.svg?branch=master)](https://coveralls.io/github/naughtygopher/webgo?branch=master)\n[![](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go#web-frameworks)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/creativecreature/sturdyc/blob/master/LICENSE)\n\n# WebGo v7.0.4\n\nWebGo is a minimalistic router for [Go](https://golang.org) to build web applications (server side) with no 3rd party dependencies. WebGo will always be Go standard library compliant; with the HTTP handlers having the same signature as [http.HandlerFunc](https://golang.org/pkg/net/http/#HandlerFunc).\n\n### Contents\n\n1. [Router](https://github.com/naughtygopher/webgo#router)\n2. [Handler chaining](https://github.com/naughtygopher/webgo#handler-chaining)\n3. [Middleware](https://github.com/naughtygopher/webgo#middleware)\n4. [Error handling](https://github.com/naughtygopher/webgo#error-handling)\n5. [Helper functions](https://github.com/naughtygopher/webgo#helper-functions)\n6. [HTTPS ready](https://github.com/naughtygopher/webgo#https-ready)\n7. [Graceful shutdown](https://github.com/naughtygopher/webgo#graceful-shutdown)\n8. [Logging](https://github.com/naughtygopher/webgo#logging)\n9. [Server-Sent Events](https://github.com/naughtygopher/webgo#server-sent-events)\n10. [Usage](https://github.com/naughtygopher/webgo#usage)\n\n## Router\n\nWebgo has a simplistic, linear path matching router and supports defining [URI](https://developer.mozilla.org/en-US/docs/Glossary/URI)s with the following patterns\n\n1. `/api/users` - URI with no dynamic values\n2. `/api/users/:userID`\n   - URI with a named parameter, `userID`\n   - If TrailingSlash is set to true, it will accept the URI ending with a '/', refer to [sample](https://github.com/naughtygopher/webgo#sample)\n3. `/api/users/:misc*`\n   - Named URI parameter `misc`, with a wildcard suffix '\\*'\n   - This matches everything after `/api/users`. e.g. `/api/users/a/b/c/d`\n\nWhen there are multiple handlers matching the same URI, only the first occurring handler will handle the request.\nRefer to the [sample](https://github.com/naughtygopher/webgo#sample) to see how routes are configured. You can access named parameters of the URI using the `Context` function.\n\nNote: webgo Context is **not** available inside the special handlers (not found & method not implemented)\n\n```golang\nfunc helloWorld(w http.ResponseWriter, r *http.Request) {\n\t// WebGo context\n\twctx := webgo.Context(r)\n\t// URI paramaters, map[string]string\n\tparams := wctx.Params()\n\t// route, the webgo.Route which is executing this request\n\troute := wctx.Route\n\twebgo.R200(\n\t\tw,\n\t\tfmt.Sprintf(\n\t\t\t\"Route name: '%s', params: '%s'\",\n\t\t\troute.Name,\n\t\t\tparams,\n\t\t),\n\t)\n}\n```\n\n## Handler chaining\n\nHandler chaining lets you execute multiple handlers for a given route. Execution of a chain can be configured to run even after a handler has written a response to the HTTP request, if you set `FallThroughPostResponse` to `true` (refer [sample](https://github.com/naughtygopher/webgo/blob/master/cmd/main.go#L70)).\n\n## Middleware\n\nWebGo [middlware](https://godoc.org/github.com/naughtygopher/webgo#Middleware) lets you wrap all the routes with a middleware unlike handler chaining. The router exposes a method [Use](https://godoc.org/github.com/naughtygopher/webgo#Router.Use) && [UseOnSpecialHandlers](https://godoc.org/github.com/naughtygopher/webgo#Router.UseOnSpecialHandlers) to add a Middleware to the router.\n\nNotFound && NotImplemented are considered `Special` handlers. `webgo.Context(r)` within special handlers will return `nil`.\n\nAny number of middleware can be added to the router, the order of execution of middleware would be [LIFO](<https://en.wikipedia.org/wiki/Stack_(abstract_data_type)>) (Last In First Out). i.e. in case of the following code\n\n```golang\nfunc main() {\n\trouter.Use(accesslog.AccessLog, cors.CORS(nil))\n\trouter.Use(<more middleware>)\n}\n```\n\n**_CorsWrap_** would be executed first, followed by **_AccessLog_**.\n\n## Error handling\n\nWebgo context has 2 methods to [set](https://github.com/naughtygopher/webgo/blob/master/webgo.go#L60) & [get](https://github.com/naughtygopher/webgo/blob/master/webgo.go#L66) erro within a request context. It enables Webgo to implement a single middleware where you can handle error returned within an HTTP handler. [set error](https://github.com/naughtygopher/webgo/blob/master/cmd/main.go#L45), [get error](https://github.com/naughtygopher/webgo/blob/master/cmd/main.go#L51).\n\n## Helper functions\n\nWebGo provides a few helper functions. When using `Send` or `SendResponse` (other Rxxx responder functions), the response is wrapped in WebGo's [response struct](https://github.com/naughtygopher/webgo/blob/master/responses.go#L17) and is serialized as JSON.\n\n```json\n{\n  \"data\": \"<any valid JSON payload>\",\n  \"status\": \"<HTTP status code, of type integer>\"\n}\n```\n\nWhen using `SendError`, the response is wrapped in WebGo's [error response struct](https://github.com/naughtygopher/webgo/blob/master/responses.go#L23) and is serialzied as JSON.\n\n```json\n{\n  \"errors\": \"<any valid JSON payload>\",\n  \"status\": \"<HTTP status code, of type integer>\"\n}\n```\n\n## HTTPS ready\n\nHTTPS server can be started easily, by providing the key & cert file. You can also have both HTTP & HTTPS servers running side by side.\n\nStart HTTPS server\n\n```golang\ncfg := &webgo.Config{\n\tPort: \"80\",\n\tHTTPSPort: \"443\",\n\tCertFile: \"/path/to/certfile\",\n\tKeyFile: \"/path/to/keyfile\",\n}\nrouter := webgo.NewRouter(cfg, routes()...)\nrouter.StartHTTPS()\n```\n\nStarting both HTTP & HTTPS server\n\n```golang\ncfg := &webgo.Config{\n\tPort: \"80\",\n\tHTTPSPort: \"443\",\n\tCertFile: \"/path/to/certfile\",\n\tKeyFile: \"/path/to/keyfile\",\n}\n\nrouter := webgo.NewRouter(cfg, routes()...)\ngo router.StartHTTPS()\nrouter.Start()\n```\n\n## Graceful shutdown\n\nGraceful shutdown lets you shutdown the server without affecting any live connections/clients connected to the server. Any new connection request after initiating a shutdown would be ignored.\n\nSample code to show how to use shutdown\n\n```golang\nfunc main() {\n\tosSig := make(chan os.Signal, 5)\n\n\tcfg := &webgo.Config{\n\t\tHost:            \"\",\n\t\tPort:            \"8080\",\n\t\tReadTimeout:     15 * time.Second,\n\t\tWriteTimeout:    60 * time.Second,\n\t\tShutdownTimeout: 15 * time.Second,\n\t}\n\trouter := webgo.NewRouter(cfg, routes()...)\n\n\tgo func() {\n\t\t<-osSig\n\t\t// Initiate HTTP server shutdown\n\t\terr := router.Shutdown()\n\t\tif err != nil {\n\t\t\tfmt.Println(err)\n\t\t\tos.Exit(1)\n\t\t} else {\n\t\t\tfmt.Println(\"shutdown complete\")\n\t\t\tos.Exit(0)\n\t\t}\n\n\t\t// If you have HTTPS server running, you can use the following code\n\t\t// err := router.ShutdownHTTPS()\n\t\t// if err != nil {\n\t\t// \tfmt.Println(err)\n\t\t// \tos.Exit(1)\n\t\t// } else {\n\t\t// \tfmt.Println(\"shutdown complete\")\n\t\t// \tos.Exit(0)\n\t\t// }\n\t}()\n\n\tgo func(){\n\t\ttime.Sleep(time.Second*15)\n\t\tsignal.Notify(osSig, os.Interrupt, syscall.SIGTERM)\n\t}()\n\n\trouter.Start()\n}\n```\n\n## Logging\n\nWebGo exposes a singleton & global scoped logger variable [LOGHANDLER](https://godoc.org/github.com/naughtygopher/webgo#Logger) with which you can plug in your custom logger by implementing the [Logger](https://godoc.org/github.com/naughtygopher/webgo#Logger) interface.\n\n### Configuring the default Logger\n\nThe default logger uses Go standard library's `log.Logger` with `os.Stdout` (for debug and info logs) & `os.Stderr` (for warning, error, fatal) as default io.Writers. You can set the io.Writer as well as disable specific types of logs using the `GlobalLoggerConfig(stdout, stderr, cfgs...)` function.\n\n## Server-Sent Events\n\n[MDN has a very good documentation of what SSE (Server-Sent Events)](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) are. The sample app provided shows how to use the SSE extension of webgo.\n\n## Usage\n\nA fully functional sample is provided [here](https://github.com/naughtygopher/webgo/blob/master/cmd/main.go).\n\n### Benchmark\n\n1. [the-benchmarker](https://github.com/the-benchmarker/web-frameworks)\n2. [go-web-framework-benchmark](https://github.com/smallnest/go-web-framework-benchmark)\n\n### Contributing\n\nRefer [here](https://github.com/naughtygopher/webgo/blob/master/CONTRIBUTING.md) to find out details about making a contribution\n\n### Credits\n\nThanks to all the [contributors](https://github.com/naughtygopher/webgo/graphs/contributors)\n\n## The gopher\n\nThe gopher used here was created using [Gopherize.me](https://gopherize.me/). WebGo stays out of developers' way, so sitback and enjoy a cup of coffee.\n"
  },
  {
    "path": "_config.yml",
    "content": "theme: jekyll-theme-cayman"
  },
  {
    "path": "cmd/README.md",
    "content": "# Webgo Sample\n\n### Server Sent Events\n\n![sse-demo](https://user-images.githubusercontent.com/1092882/158047065-447eb868-1efd-4a8d-b748-7caee2b3fcfd.png)\n\nThis picture shows the sample SSE implementation provided with this application. In the sample app, the server is\nsending timestamp every second, to all the clients.\n\n**Important**: _[SSE](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events)\nis a live connection between server & client. So a short WriteTimeout duration in webgo.Config will\nkeep dropping the connection. If you have any middleware which is setting deadlines or timeouts on the\nrequest.Context, will also effect these connections._\n\n## How to run\n\nIf you have Go installed on your computer, open the terminal and:\n\n```bash\n$ cd $GOPATH/src\n$ git clone https://github.com/naughtygopher/webgo.git\n$ cd webgo/cmd\n$ go run *.go\n\nInfo 2023/02/05 08:51:26 HTTP server, listening on :8080\nInfo 2023/02/05 08:51:26 HTTPS server, listening on :9595\n```\n\nOr if you have [Docker](https://www.docker.com/), open the terminal and:\n\n```bash\n$ git clone https://github.com/naughtygopher/webgo.git\n$ cd webgo\n$ docker run \\\n-p 8080:8080 \\\n-p 9595:9595 \\\n-v ${PWD}:/go/src/github.com/naughtygopher/webgo/ \\\n-w /go/src/github.com/naughtygopher/webgo/cmd \\\n--rm -ti golang:latest go run *.go\n\nInfo 2023/02/05 08:51:26 HTTP server, listening on :8080\nInfo 2023/02/05 08:51:26 HTTPS server, listening on :9595\n```\n\nYou can try the following API calls with the sample app. It also uses all the features provided by webgo\n\n1. `http://localhost:8080/`\n   - Loads an HTML page\n2. `http://localhost:8080/matchall/`\n   - Route with wildcard parameter configured\n   - All URIs which begin with `/matchall` will be matched because it has a wildcard variable\n   - e.g.\n     - http://localhost:8080/matchall/hello\n     - http://localhost:8080/matchall/hello/world\n     - http://localhost:8080/matchall/hello/world/user\n3. `http://localhost:8080/api/<param>`\n   - Route with a named 'param' configured\n   - It will match all requests which match `/api/<single parameter>`\n   - e.g.\n     - http://localhost:8080/api/hello\n     - http://localhost:8080/api/world\n4. `http://localhost:8080/error-setter`\n   - Route which sets an error and sets response status 500\n5. `http://localhost:8080/v7.0.0/api/<param>`\n   - Route with a named 'param' configured\n   - It will match all requests which match `/v7.0.0/api/<single parameter>`\n   - e.g.\n     - http://localhost:8080/v7.0.0/api/hello\n     - http://localhost:8080/v7.0.0/api/world\n"
  },
  {
    "path": "cmd/certs/CA.key",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nProc-Type: 4,ENCRYPTED\nDEK-Info: DES-EDE3-CBC,3172865781AB630D\n\nb65fTCSGMadtvR5GnVfTsssu9qrQgm5XNCXRJ+PLpuXuWdywORTOEM4/FtRA62AN\nf41Dl/OO7nUNTTF4J+0fNiLMmnkiYvGSa5lcjRqBikk6waZjYkSUoo4TdGh502F/\njFzbGzqXlX0qt+pj0HY/fhKxpr45bOkBr9S3ucMbgyuREX1HShxF759Mc+sJFx7w\nGhs+byTL3cZABHRn/0xpC9S/PhEnsW3dPPJItCkEUsLumwTFCuaGskpP3chuVhZh\n36p74An+Tg9wlgcaCaUSUHWs22rnLBHAjv9JzVuoLJWdUwIYrcQTJz2+8mVlun24\nQns7dhRafOHlIeOCxI/fmKlXE/S9S7tuXaMsHtqGK/22MAXJx/AOymjfgXfTLnF1\nXmLK0FTyp9BB1pfW9P+D6JEp7bj1fRHZeCOkesJOjyDEb+v+oYMaha6IVfQCMKQf\nP1+okMwBPGQIAr5d4Ov82mwpDyO1+rHAFn3b3zuro4rfHiHPDTo9Oa7EtMBIWZgj\nLn0KLeaRkPht6wSUubCSl8Ypg27xbHwbQbWcQsr/OwLgWMJ40/1QKaqYIDV4LytZ\nmSzwwo4kQASKI1jwFWff+4yVqd3SuyW5uGcPNnkbneyKZzFHd/rAWnsT9cIIOX0v\nUS5LEn9r+qjYk+ZVCSgqmrwUHyPfbx/BeARJTDBzo2jJC+ZiR7UkzD54r5pzs7R4\n9N3hVVmocS9BNPj45ioLw/Fhbu62N34NkRdDAisPhlQ+yM7klKBciQsEsKj1c6iw\nQwJNScvgW6x0+47J+tp21KLuxdfVP+Bq9wSb2B4kR3l93/ZAXR1S0cg0cuez1frd\n8g1kpQMzxhWVMzB6NHG7wAIo7p947mueP3Ggh1rgA6WLTltvH11ywadglG23HB7R\nzuxcQqoPm7Tzcift316DE5Q+qipHDA2UmiWZ83ZVQCshiJxILsPkGbh7k+mRwRyh\ne08TLEJMtCWiqvCmxHbebx7y8+oX035QIUVIOxHGier2CyZtgpaZyeEa4DHZCwuH\nZrTMfigGDSXnonCrVtsC9zSYGQav/tRVxahKsM/TA+O1gOWNTncEe2ESKWaBzJpQ\nwUo7u/e/hbyPxMd2ezmeYVWwRSy9J/uZOsH0DsUlGsUzTWj+qrbmrdDYGVI2sgA1\nxfTfv7vdLFpESDRV4eRWscblusHffjCFA9oC9Y9qj2M8X+HLa/EELMb5CCWGZm3E\nHMZKwg6cah5rc82FFk0nDTzNpw57rTorPOGDe3oUif2NR+A4gxyepVrNDBFxDzbd\nPT2YMPr2IsHgxnRwFyoAPGG3rKCO+rFNJKcfrxvdeTj7mZ0Yzo93f9ycgNhqDKnR\n+n8vCWtWnRuDu8hk3d5YjhaZux/SmOuF9G6VX0jnJA534aLhEo0mzddGKM0bnOGN\nJ1aMc65s4rPqUOQiase/3w7fNDu6szF/tcUMEWPRCDymZpZ9yznK122ajQVrKyOq\nHDcftg1rrKli8I/AljJMgC9ACZ9YfOfZ3qymcY7X7ZJyMucKi822ajIggi630aJQ\nsEqwxl5coM/N+Rhcp2/NiYyq3MXQBibKhq00OBLq76QQeNsJbRhUDw==\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": "cmd/certs/CA.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDijCCAnICCQCImGKiTiq7ITANBgkqhkiG9w0BAQsFADCBhjELMAkGA1UEBhMC\nSU4xCzAJBgNVBAgMAktBMRIwEAYDVQQHDAlCZW5nYWx1cnUxFTATBgNVBAoMDERl\nbGl2ZXJ5SGVybzETMBEGA1UECwwKUS1Db21tZXJjZTESMBAGA1UEAwwJbG9jYWxo\nb3N0MRYwFAYJKoZIhvcNAQkBFgdrQGIuY29tMB4XDTIyMTIwOTE2MDQxMFoXDTMy\nMTIwNjE2MDQxMFowgYYxCzAJBgNVBAYTAklOMQswCQYDVQQIDAJLQTESMBAGA1UE\nBwwJQmVuZ2FsdXJ1MRUwEwYDVQQKDAxEZWxpdmVyeUhlcm8xEzARBgNVBAsMClEt\nQ29tbWVyY2UxEjAQBgNVBAMMCWxvY2FsaG9zdDEWMBQGCSqGSIb3DQEJARYHa0Bi\nLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM83cerEFzgRxu/l\nNrFOGOCn1bwexXoP1xawzVaj/vmTL4+bN114e4KnJrmon9k9NHUxhJJGfNxM7cSG\n7lYlIwIDdMt1FNm1iWjPycvcxWZppEnjbwlOGYn0miekr+SSh18AbCZ+kcZFx6Cp\nO4wEb29kXCxInGEdgj5M23EpQdi5qXCmfmrIl/ueiNnJPQezFir8UizRKG5xHnZK\nBaGcT0E4lOJLLKGqpvN5v0cO1Vwu2nxFmXlcV5dWsPOjxvPPSCYuu0EHhJ2jucQW\nMNtc6cucG70rOJwkQi6JMbS5XU9pboux2O/H0WUwFSYjI40opWjKpXE5eo6GIgl/\neHCiNJ8CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAH9a3WOMgGvDn80O8hvhZzXO8\n6XPBDqjW7Zk/l1GiZYpNuDvAuqiBIDxKQZdRtnbRBTCLGO6yyHt974WStJud1/sN\nLoam78+GMEMJq0tUNUNXuOVNLo/Zz/4tN2cDosnB8k+Atm+c3m5TSHaOayOy+PJL\nOiDi7RP5IPpiEYtdGvE2eoYfqjSnY00kIV5ea57PIc3gkFO9FXP+M6UXzkxo2xC7\nfvVhYEVjQ7uvdWLGKYMvF/PRV2OKRnAdFasga3k8PyC/ToxjN/87ypeUy1VZzEm7\n3zsbislIKL36CCOYmGaUgTPfxXWN/MvUgb87lWfrqPbSM7ooSQGHFe1eP5Wd2g==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "cmd/certs/CA.srl",
    "content": "FDC7B23B140FA79F\n"
  },
  {
    "path": "cmd/certs/localhost.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIEgjCCA2qgAwIBAgIJAP3HsjsUD6efMA0GCSqGSIb3DQEBCwUAMIGGMQswCQYD\nVQQGEwJJTjELMAkGA1UECAwCS0ExEjAQBgNVBAcMCUJlbmdhbHVydTEVMBMGA1UE\nCgwMRGVsaXZlcnlIZXJvMRMwEQYDVQQLDApRLUNvbW1lcmNlMRIwEAYDVQQDDAls\nb2NhbGhvc3QxFjAUBgkqhkiG9w0BCQEWB2tAYi5jb20wHhcNMjIxMjA5MTYwOTQ2\nWhcNMzIxMjA2MTYwOTQ2WjCBlzELMAkGA1UEBhMCREUxDzANBgNVBAgMBkJlcmxp\nbjEWMBQGA1UEBwwNUmVpbmlja2VuZG9yZjEWMBQGA1UECgwNRGVsaXZlcnkgSGVy\nbzETMBEGA1UECwwKUS1Db21tZXJjZTEZMBcGA1UEAwwQZGVsaXZlcnloZXJvLmNv\nbTEXMBUGCSqGSIb3DQEJARYIa0Bibi5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IB\nDwAwggEKAoIBAQDRT6l39GZ3vSAi1eLt8oauseH4uNwijzcaqHot45f3087eqSej\nhfqmTfhga+MSDtbKIo73O4wq12klCbTtil4UpRT4dVJwKQLXLriFCiq40Wzcyhqy\nE0qGoZG1TCoy3PLUCwkxXlixAdEimhuZPIVPDQIY0fs1c8GxdFfhxMQ88WEqs0Rp\nrygYp+hD18Hk0VYhPmqZXb0m3BG7/eTYHrYDVAdk9f2OYMR925idwk94iHvTjOqC\nbOpVOzF4FM+jkT7r4hfa2UuCF4sYNhAP2DEZWHnoYjb7cxRDKYshMqCH5WhlbB+v\nrAllLs+4GEu1yOs0VYqt2TzXNr/6KK1G77SRAgMBAAGjgd8wgdwwgaUGA1UdIwSB\nnTCBmqGBjKSBiTCBhjELMAkGA1UEBhMCSU4xCzAJBgNVBAgMAktBMRIwEAYDVQQH\nDAlCZW5nYWx1cnUxFTATBgNVBAoMDERlbGl2ZXJ5SGVybzETMBEGA1UECwwKUS1D\nb21tZXJjZTESMBAGA1UEAwwJbG9jYWxob3N0MRYwFAYJKoZIhvcNAQkBFgdrQGIu\nY29tggkAiJhiok4quyEwCQYDVR0TBAIwADALBgNVHQ8EBAMCBPAwGgYDVR0RBBMw\nEYIJbG9jYWxob3N0hwR/AAABMA0GCSqGSIb3DQEBCwUAA4IBAQA2P0I8ujEPS2GB\n7cMUAE3cB4fxEwI2t89JGcmeK0BUyVBlvdwZGEIVs2Tn0FFbs0VpcgeY3YkV2ogs\nekrUHQzmnRX9EtTMucGM6gX4JeDJWWthehVIB6Jp1iDLtAAbyCGph5nrdArkA0tR\nANkyrXKTcMAx3giBzSZrpxguF+fnASZ+p99c57FnRXjMv5NkQnSCgRQkmaHtUIKJ\ndDAlEIyPrpfe2bbw7BtUt2UiW9KPz/CG9TDrpWzh5jRyoJRzXUhlOPgFPCBvH1AC\nDGl2ciAGfScEY+HZp+YPdTzln5TSc4w/REuWDqBIydJwytUQX+EcTA0936TQq3ec\nlYKaXexW\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "cmd/certs/localhost.csr",
    "content": "-----BEGIN CERTIFICATE REQUEST-----\nMIIC3TCCAcUCAQAwgZcxCzAJBgNVBAYTAkRFMQ8wDQYDVQQIDAZCZXJsaW4xFjAU\nBgNVBAcMDVJlaW5pY2tlbmRvcmYxFjAUBgNVBAoMDURlbGl2ZXJ5IEhlcm8xEzAR\nBgNVBAsMClEtQ29tbWVyY2UxGTAXBgNVBAMMEGRlbGl2ZXJ5aGVyby5jb20xFzAV\nBgkqhkiG9w0BCQEWCGtAYm4uY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\nCgKCAQEA0U+pd/Rmd70gItXi7fKGrrHh+LjcIo83Gqh6LeOX99PO3qkno4X6pk34\nYGvjEg7WyiKO9zuMKtdpJQm07YpeFKUU+HVScCkC1y64hQoquNFs3MoashNKhqGR\ntUwqMtzy1AsJMV5YsQHRIpobmTyFTw0CGNH7NXPBsXRX4cTEPPFhKrNEaa8oGKfo\nQ9fB5NFWIT5qmV29JtwRu/3k2B62A1QHZPX9jmDEfduYncJPeIh704zqgmzqVTsx\neBTPo5E+6+IX2tlLgheLGDYQD9gxGVh56GI2+3MUQymLITKgh+VoZWwfr6wJZS7P\nuBhLtcjrNFWKrdk81za/+iitRu+0kQIDAQABoAAwDQYJKoZIhvcNAQELBQADggEB\nADGo51Y/2/uJhndzitmBLM9yvwXmsD7JQvT3y+8xSne0p+jniwHhzqLww6SLLtIQ\nLRUzbXQlnJnPzj2fhdPYM9238Cxyd/w59/cg/RXnkjMnoaiH/9FZpqwIwnMFugkp\n+BcqszZat70OjdhZPkI/WzImNHdtSzUFhI3OACXqhdSM2wGkzHQCWxMzRmKsE8XF\niMmuvFVBExXLoG/PqoRS5W3Op1SZdYvKybhmrgM+XeHlvTv8VOFgUBmBEolhZtvU\n9eT3ommzMbyZSbf6eXlTj+OrBTlN7n8et42TyDaf03kZhhiSpiq+lNz7+eHcF64Z\n8u4AOXi22aPkwy2fffYKr4E=\n-----END CERTIFICATE REQUEST-----\n"
  },
  {
    "path": "cmd/certs/localhost.decrypted.key",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA0U+pd/Rmd70gItXi7fKGrrHh+LjcIo83Gqh6LeOX99PO3qkn\no4X6pk34YGvjEg7WyiKO9zuMKtdpJQm07YpeFKUU+HVScCkC1y64hQoquNFs3Moa\nshNKhqGRtUwqMtzy1AsJMV5YsQHRIpobmTyFTw0CGNH7NXPBsXRX4cTEPPFhKrNE\naa8oGKfoQ9fB5NFWIT5qmV29JtwRu/3k2B62A1QHZPX9jmDEfduYncJPeIh704zq\ngmzqVTsxeBTPo5E+6+IX2tlLgheLGDYQD9gxGVh56GI2+3MUQymLITKgh+VoZWwf\nr6wJZS7PuBhLtcjrNFWKrdk81za/+iitRu+0kQIDAQABAoIBAQCwHW5Df0HkiB6N\nERiTC9ilDwlKxQh8j7JW3OGI0RJiNTbABOZUYfwHiF1vi/eQjynNBIz0m4cR2RQg\nVO2GXUFR76EYeWb29prsQeSCFI7j2VrW37rckPzJERNPz5lGGMC9B9ghUPghX5z/\nl1mXcuPcIt7b0XqkfBTC4li7n1taxluvKpxtFoN7XuV7QyhpQtR74M9CAmmtxAjY\n3HSY3fC7kSbT+8mii29stm3/zJSdnuSdlfrHRv+cXsrWfN5isjpvDb7HCcvm7OHB\nQyUC89zy/WM8xReKEStkRX+/+zW2//y7aXMzI/YmiPdX+XoIY+MdxqvMjzbATZF+\nhaBeM5ttAoGBAP5GhhmuvKSURJTfoEroIxtXnfpvE4T/+TQmR1Wc9m/Ecc6iVuHF\nRgHpkKhOFl5P457Y1oRpYkmECxZ4KuvHwIqe9rRofGByxbxilvA69p8RkV8oI/gc\nqP5+VN2Sx77tTTKVX7S6SFrROSmB9YAE7/83Vo1oD9absl47SeR9+EBbAoGBANK7\nEhIYE12q5nQBDhjjAt9Xa6N+ROjonjW1cWq/v6FWgoBXiQtsh1MSK2KZPi7F05kP\n9oYM//4oMy/mFsKbS8HTy+Y7HDYvh7Gz3gAxUWrabIFrJlJMXMj9VIgYDZsnQiBD\n3j2sUOpr+5B4laHD5oM8rYjR10F1AN7oAuTdDzKDAoGAfmkHH9t70wIW+kAWi0bO\ntTggxLDV7mfnNyLUkd5fsX7i6UxRjxoozKiWDuYLPsXOrli0hM1zXIL1lC0XgXIj\n6YZPta7ALp7AaQBGc5WMp9XvBHSLNTziUurxO9pNzUBiAYS7OLjnYabkGRuPth4+\nRg33zILwZMuwqCIngR2S/kMCgYB9ssCQsnO6x5o3T/nMtnycJFU8bLFGDJtyhgxl\nFIOGBUhKrew9ODtwPcJLSgVhePdCsdbnFxIL1IbT53dkFaYWs/NIHbIyUB+szBF8\nI+7gwfE/MV7mcE5YRWQK2e4jwkMbY+BJAWQysL6Z6pO2rlftqGAK4MB5dwVR8Sro\nwUOzaQKBgQDZ3ohj61drnkkdRRRX5115ne3AMctx1rn7Ss6AByDn21wYUrOP0qAy\ni4kmah0kibqz8+N6PM7G7gA5uXHk0bwFfgU/TPQdz6DfzSbTmO7lEEJ7ejI9ml0C\n1Y16Rya8Ny3fLjCFWKYReFhXUWNzuWPHsQezUiwnMeHRka0c9QFVSA==\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": "cmd/certs/localhost.ext",
    "content": "authorityKeyIdentifier = keyid,issuer\nbasicConstraints = CA:FALSE\nkeyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment\nsubjectAltName = @alt_names\n\n[alt_names]\nDNS.1 = localhost\nIP.1 = 127.0.0.1"
  },
  {
    "path": "cmd/certs/localhost.key",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nProc-Type: 4,ENCRYPTED\nDEK-Info: DES-EDE3-CBC,9D6364828FCEE2E0\n\n2RhK7oE6kVWBNDFGjRUuxPO5XI3qarcJTswXNk7YFqmyQpgt9cFXUdRI36Ppbpcs\nSw6h2gmBaqsS7sbL3wWZyZL+PcVVCy7TRTBI/mU1Cvj1iRe9ms3+b7uwnuSGh0DF\nG40X+VACjwmP+hAKQivcPtog+c5laAx/QQ456kNQMl9yYCdbJCTCBNKVS8ycIB54\n0moAxCv8lgO0mllh4vvMl5RbfOqPVr0Ky8EWJJqs9CWCy7eKLe8BIzy7nDGcVhx+\nBZ24r8h59OIUWCFmJ/FXhDTLXrPr2xpF0COTKuPrTtU7LMaCdYljyilfkCo4l+Pk\nctawRwKLUd4/IE26P90WaoV85y9tne1145pcKQVQa0NdfI6rRA2jRqzv6iFrVYBM\nAUeybrX+HnaHABwJYNunXBCk3YUWk5NuMPsTNGlPqMPQyVC/D196YSU16sDx2zlT\nZ886GqLfr5SqwInvlb7zQn72+1beoh2Du7NQ6oXuVZsc5w1sGiv7B7axOEznJ8KT\nQxTst9pCLj2zpm45fRW8cKscC9g/fXMYQrRwcSeeuWvXtI9csljpWKpolvepKFmx\nbUrYUIqFyfCZsLBHaXf3vsbPF/h3q0ZGDadVTWioiA9jpi+y5rcb8mFDl9qiRBsC\nRDafd0aoJPXiWotO58Fo4VXRLWqEUhyHF9oVN5SD5CWSWos/YfkRXAVmElEK+dbM\nTpumlQvt6gbXculag7c/OtHJtVfLuhqdW4yGXvwY1z07U+EdYLrMomKffYaxjTi5\n2c1bNA7t2cF4mQk++DH9jvj1Xfuxdh5p84g1tvSYrVvJWt4JHzbzknylteGMSJ5w\ndkuDXdBS9qrwjvc6nLNAn+qXduzAyzf2GR5obe/mFwZSvm33o6W/DFWkhQDdHjSC\no7QuyNvHYYMCau7CKcpgoBycBrkktQ6gFAGR1HsT4Xdsk60XkLi8ctUiLPdFL6zc\ntRcmZt+ddzJA2qaNCxNopj2J8Qkav208zQ9f7P6DZNe3pWI09SrUisnAb0cuYASm\nXfAcBP9vBfKx9pvxfpVgp6DYFaVllgXDM+Z1lmKzZMOAb0RFcyWsYtILIgLnTDeG\ntMeHygAevAAT1N1uKlQMvXvOtmehOGVfBAaJVt1U4xTWjCxBGfWed+bs9NkL2zSU\n0/kEacWEbq0c23Wsiv8IHKxqZLPyeiyHvZgDWLt4v1Y+t9l+q+HZIQxXmFhb96qt\n+tOv7pY6dig2fLRAApp9Q2kKfimQBtO7sxafK3qBmT75Y86gBdmSgjREviec9Bb2\nVeZeqf/TPhiBVbRpYAe4RixF5cal3pxC9N9GmEMk+Qfr/MEonkUgX8w4tlP3hkXx\nHS+5GpltrIag5GEaKbFgX/FYcHbDQNMXmIqV6ieavN+04MbHuC4Cm1joHVTEQJUn\nII0mfCAgxFOYKqr2Sex86zxhury5O1oYS0ETCTCLVgab1dIin4+cElrGW42Sw3Bm\ntGChrevGYi2hBQkBOHfnw2lFLtJmQ0R0pke+CICVWLriTV5N183f5zosdjJyYpxa\nJL7p/ePLjLRCvMpY185/mbvc8h1AtIsm6N67r8xK250oZBid9LZXa6G449RnPku7\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": "cmd/handlers.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/naughtygopher/webgo/v7\"\n\t\"github.com/naughtygopher/webgo/v7/extensions/sse\"\n)\n\n// StaticFilesHandler is used to serve static files\nfunc StaticFilesHandler(rw http.ResponseWriter, r *http.Request) {\n\twctx := webgo.Context(r)\n\t// '..' is replaced to prevent directory traversal which could go out of static directory\n\tpath := strings.ReplaceAll(wctx.Params()[\"w\"], \"..\", \"-\")\n\tpath = strings.ReplaceAll(path, \"~\", \"-\")\n\n\trw.Header().Set(\"Last-Modified\", lastModified)\n\thttp.ServeFile(rw, r, fmt.Sprintf(\"./static/%s\", path))\n}\n\nfunc OriginalResponseWriterHandler(w http.ResponseWriter, r *http.Request) {\n\trw := webgo.OriginalResponseWriter(w)\n\tif rw == nil {\n\t\twebgo.Send(w, \"text/html\", \"got nil\", http.StatusPreconditionFailed)\n\t\treturn\n\t}\n\n\twebgo.Send(w, \"text/html\", \"success\", http.StatusOK)\n}\n\nfunc HomeHandler(w http.ResponseWriter, r *http.Request) {\n\tfs, err := os.OpenFile(\"./static/index.html\", os.O_RDONLY, 0600)\n\tif err != nil {\n\t\twebgo.SendError(w, err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\tinfo, err := fs.Stat()\n\tif err != nil {\n\t\twebgo.SendError(w, err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tout := make([]byte, info.Size())\n\t_, err = fs.Read(out)\n\tif err != nil {\n\t\twebgo.SendError(w, err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\tpushHomepage(r, w)\n\n\t_, _ = w.Write(out)\n\n}\n\nfunc pushCSS(pusher http.Pusher, r *http.Request, path string) {\n\tcssOpts := &http.PushOptions{\n\t\tHeader: http.Header{\n\t\t\t\"Accept-Encoding\": r.Header[\"Accept-Encoding\"],\n\t\t\t\"Content-Type\":    []string{\"text/css; charset=UTF-8\"},\n\t\t},\n\t}\n\terr := pusher.Push(path, cssOpts)\n\tif err != nil {\n\t\twebgo.LOGHANDLER.Error(err)\n\t}\n}\n\nfunc pushJS(pusher http.Pusher, r *http.Request, path string) {\n\tcssOpts := &http.PushOptions{\n\t\tHeader: http.Header{\n\t\t\t\"Accept-Encoding\": r.Header[\"Accept-Encoding\"],\n\t\t\t\"Content-Type\":    []string{\"application/javascript\"},\n\t\t},\n\t}\n\terr := pusher.Push(path, cssOpts)\n\tif err != nil {\n\t\twebgo.LOGHANDLER.Error(err)\n\t}\n}\n\nfunc pushHomepage(r *http.Request, w http.ResponseWriter) {\n\tpusher, ok := w.(http.Pusher)\n\tif !ok {\n\t\treturn\n\t}\n\n\tcp, _ := r.Cookie(\"pusher\")\n\tif cp != nil {\n\t\treturn\n\t}\n\n\tcookie := &http.Cookie{\n\t\tName:   \"pusher\",\n\t\tValue:  \"css,js\",\n\t\tMaxAge: 300,\n\t}\n\thttp.SetCookie(w, cookie)\n\tpushCSS(pusher, r, \"/static/css/main.css\")\n\tpushCSS(pusher, r, \"/static/css/normalize.css\")\n\tpushJS(pusher, r, \"/static/js/main.js\")\n\tpushJS(pusher, r, \"/static/js/sse.js\")\n}\n\nfunc SSEHandler(sse *sse.SSE) http.HandlerFunc {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\tparams := webgo.Context(r).Params()\n\t\tr.Header.Set(sse.ClientIDHeader, params[\"clientID\"])\n\n\t\terr := sse.Handler(w, r)\n\t\tif err != nil && !errors.Is(err, context.Canceled) {\n\t\t\tlog.Println(\"errorLogger:\", err.Error())\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc ErrorSetterHandler(w http.ResponseWriter, r *http.Request) {\n\terr := errors.New(\"oh no, server error\")\n\twebgo.SetError(r, err)\n\n\twebgo.R500(w, err.Error())\n}\n\nfunc ParamHandler(w http.ResponseWriter, r *http.Request) {\n\t// WebGo context\n\twctx := webgo.Context(r)\n\t// URI parameters, map[string]string\n\tparams := wctx.Params()\n\t// route, the webgo.Route which is executing this request\n\troute := wctx.Route\n\twebgo.R200(\n\t\tw,\n\t\tmap[string]interface{}{\n\t\t\t\"route_name\":    route.Name,\n\t\t\t\"route_pattern\": route.Pattern,\n\t\t\t\"params\":        params,\n\t\t\t\"chained\":       r.Header.Get(\"chained\"),\n\t\t},\n\t)\n}\n\nfunc InvalidJSONHandler(w http.ResponseWriter, r *http.Request) {\n\twebgo.R200(w, make(chan int))\n}\n"
  },
  {
    "path": "cmd/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/naughtygopher/webgo/v7\"\n\t\"github.com/naughtygopher/webgo/v7/extensions/sse\"\n\t\"github.com/naughtygopher/webgo/v7/middleware/accesslog\"\n\t\"github.com/naughtygopher/webgo/v7/middleware/cors\"\n)\n\nvar (\n\tlastModified = time.Now().Format(http.TimeFormat)\n)\n\nfunc chain(w http.ResponseWriter, r *http.Request) {\n\tr.Header.Set(\"chained\", \"true\")\n}\n\n// errLogger is a middleware which will log all errors returned/set by a handler\nfunc errLogger(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {\n\tnext(w, r)\n\n\terr := webgo.GetError(r)\n\tif err != nil {\n\t\t// log only server errors\n\t\tif webgo.ResponseStatus(w) > 499 {\n\t\t\tlog.Println(\"errorLogger:\", err.Error())\n\t\t}\n\t}\n}\n\nfunc routegroupMiddleware(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {\n\tw.Header().Add(\"routegroup\", \"true\")\n\tnext(w, r)\n}\n\nfunc getRoutes(sse *sse.SSE) []*webgo.Route {\n\treturn []*webgo.Route{\n\t\t{\n\t\t\tName:          \"root\",\n\t\t\tMethod:        http.MethodGet,\n\t\t\tPattern:       \"/\",\n\t\t\tHandlers:      []http.HandlerFunc{HomeHandler},\n\t\t\tTrailingSlash: true,\n\t\t},\n\t\t{\n\t\t\tName:          \"matchall\",\n\t\t\tMethod:        http.MethodGet,\n\t\t\tPattern:       \"/matchall/:wildcard*\",\n\t\t\tHandlers:      []http.HandlerFunc{ParamHandler},\n\t\t\tTrailingSlash: true,\n\t\t},\n\t\t{\n\t\t\tName:                    \"api\",\n\t\t\tMethod:                  http.MethodGet,\n\t\t\tPattern:                 \"/api/:param\",\n\t\t\tHandlers:                []http.HandlerFunc{chain, ParamHandler},\n\t\t\tTrailingSlash:           true,\n\t\t\tFallThroughPostResponse: true,\n\t\t},\n\t\t{\n\t\t\tName:          \"invalidjson\",\n\t\t\tMethod:        http.MethodGet,\n\t\t\tPattern:       \"/invalidjson\",\n\t\t\tHandlers:      []http.HandlerFunc{InvalidJSONHandler},\n\t\t\tTrailingSlash: true,\n\t\t},\n\t\t{\n\t\t\tName:          \"error-setter\",\n\t\t\tMethod:        http.MethodGet,\n\t\t\tPattern:       \"/error-setter\",\n\t\t\tHandlers:      []http.HandlerFunc{ErrorSetterHandler},\n\t\t\tTrailingSlash: true,\n\t\t},\n\t\t{\n\t\t\tName:          \"original-responsewriter\",\n\t\t\tMethod:        http.MethodGet,\n\t\t\tPattern:       \"/original-responsewriter\",\n\t\t\tHandlers:      []http.HandlerFunc{OriginalResponseWriterHandler},\n\t\t\tTrailingSlash: true,\n\t\t},\n\t\t{\n\t\t\tName:          \"static\",\n\t\t\tMethod:        http.MethodGet,\n\t\t\tPattern:       \"/static/:w*\",\n\t\t\tHandlers:      []http.HandlerFunc{StaticFilesHandler},\n\t\t\tTrailingSlash: true,\n\t\t},\n\t\t{\n\t\t\tName:          \"sse\",\n\t\t\tMethod:        http.MethodGet,\n\t\t\tPattern:       \"/sse/:clientID\",\n\t\t\tHandlers:      []http.HandlerFunc{SSEHandler(sse)},\n\t\t\tTrailingSlash: true,\n\t\t},\n\t}\n}\n\nfunc setup() (*webgo.Router, *sse.SSE) {\n\tport := strings.TrimSpace(os.Getenv(\"HTTP_PORT\"))\n\tif port == \"\" {\n\t\tport = \"8080\"\n\t}\n\tcfg := &webgo.Config{\n\t\tHost:         \"\",\n\t\tPort:         port,\n\t\tHTTPSPort:    \"9595\",\n\t\tReadTimeout:  15 * time.Second,\n\t\tWriteTimeout: 1 * time.Hour,\n\t\tCertFile:     \"./certs/localhost.crt\",\n\t\tKeyFile:      \"./certs/localhost.decrypted.key\",\n\t}\n\n\twebgo.GlobalLoggerConfig(\n\t\tnil, nil,\n\t\twebgo.LogCfgDisableDebug,\n\t)\n\n\trouteGroup := webgo.NewRouteGroup(\"/v7.0.0\", false)\n\trouteGroup.Add(webgo.Route{\n\t\tName:     \"router-group-prefix-v7.0.0_api\",\n\t\tMethod:   http.MethodGet,\n\t\tPattern:  \"/api/:param\",\n\t\tHandlers: []http.HandlerFunc{chain, ParamHandler},\n\t})\n\trouteGroup.Use(routegroupMiddleware)\n\n\tsseService := sse.New()\n\tsseService.OnRemoveClient = func(ctx context.Context, clientID string, count int) {\n\t\tlog.Printf(\"\\nClient %q removed, active client(s): %d\\n\", clientID, count)\n\t}\n\tsseService.OnCreateClient = func(ctx context.Context, client *sse.Client, count int) {\n\t\tlog.Printf(\"\\nClient %q added, active client(s): %d\\n\", client.ID, count)\n\t}\n\n\troutes := getRoutes(sseService)\n\troutes = append(routes, routeGroup.Routes()...)\n\n\trouter := webgo.NewRouter(cfg, routes...)\n\trouter.UseOnSpecialHandlers(accesslog.AccessLog)\n\trouter.Use(\n\t\terrLogger,\n\t\tcors.CORS(nil),\n\t\taccesslog.AccessLog,\n\t)\n\n\treturn router, sseService\n}\n\nfunc main() {\n\trouter, sseService := setup()\n\tclients := []*sse.Client{}\n\tsseService.OnCreateClient = func(ctx context.Context, client *sse.Client, count int) {\n\t\tclients = append(clients, client)\n\t}\n\t// broadcast server time to all SSE listeners\n\tgo func() {\n\t\tretry := time.Millisecond * 500\n\t\tfor {\n\t\t\tnow := time.Now().Format(time.RFC1123Z)\n\t\t\tsseService.Broadcast(sse.Message{\n\t\t\t\tData:  now + fmt.Sprintf(\" (%d)\", sseService.ActiveClients()),\n\t\t\t\tRetry: retry,\n\t\t\t})\n\t\t\ttime.Sleep(time.Second)\n\t\t}\n\t}()\n\n\tgo router.StartHTTPS()\n\trouter.Start()\n}\n"
  },
  {
    "path": "cmd/static/css/main.css",
    "content": "* {\n  transition: color 0.25s, margin 0.25s, padding 0.25s, width 0.25s, height 0.25s, background-color 0.25s;\n}\nhtml, body {\n  font-family: sans-serif;\n  font-size: 16px;\n  line-height: 1.5em;\n  font-weight: 400;\n  background: #efefef;\n  color: #444;\n}\np {\n  margin: 0 0 1em;\n}\n\na {\n  color: #999;\n}\na:hover {\n  color: #222;\n}\n\nsection.main {\n  background: #fff;\n  width: 90%;\n  max-width: 370px;\n  margin: 10vw auto;\n  padding: 0 2em;\n  border-radius: 4px;\n  overflow: hidden;\n}\n\ntable {\n  width: 100%;\n  font-size: 12px;\n  line-height: 1.5em;\n  border: 1px solid #eee;\n  border-collapse: collapse;\n}\ntr, td {\n  border: 1px solid #eee;\n}\ntd {\n  padding: 0.25rem;\n  text-align: right;\n}\ntd:nth-child(1) {\n  text-align: left;\n  background-color: rgba(0,0,0,0.02);\n}"
  },
  {
    "path": "cmd/static/css/normalize.css",
    "content": "/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */\n\n/* Document\n   ========================================================================== */\n\n/**\n * 1. Correct the line height in all browsers.\n * 2. Prevent adjustments of font size after orientation changes in iOS.\n */\n\nhtml {\n  line-height: 1.15; /* 1 */\n  -webkit-text-size-adjust: 100%; /* 2 */\n}\n\n/* Sections\n   ========================================================================== */\n\n/**\n * Remove the margin in all browsers.\n */\n\nbody {\n  margin: 0;\n}\n\n/**\n * Render the `main` element consistently in IE.\n */\n\nmain {\n  display: block;\n}\n\n/**\n * Correct the font size and margin on `h1` elements within `section` and\n * `article` contexts in Chrome, Firefox, and Safari.\n */\n\nh1 {\n  font-size: 2em;\n  margin: 0.67em 0;\n}\n\n/* Grouping content\n   ========================================================================== */\n\n/**\n * 1. Add the correct box sizing in Firefox.\n * 2. Show the overflow in Edge and IE.\n */\n\nhr {\n  box-sizing: content-box; /* 1 */\n  height: 0; /* 1 */\n  overflow: visible; /* 2 */\n}\n\n/**\n * 1. Correct the inheritance and scaling of font size in all browsers.\n * 2. Correct the odd `em` font sizing in all browsers.\n */\n\npre {\n  font-family: monospace, monospace; /* 1 */\n  font-size: 1em; /* 2 */\n}\n\n/* Text-level semantics\n   ========================================================================== */\n\n/**\n * Remove the gray background on active links in IE 10.\n */\n\na {\n  background-color: transparent;\n}\n\n/**\n * 1. Remove the bottom border in Chrome 57-\n * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.\n */\n\nabbr[title] {\n  border-bottom: none; /* 1 */\n  text-decoration: underline; /* 2 */\n  text-decoration: underline dotted; /* 2 */\n}\n\n/**\n * Add the correct font weight in Chrome, Edge, and Safari.\n */\n\nb,\nstrong {\n  font-weight: bolder;\n}\n\n/**\n * 1. Correct the inheritance and scaling of font size in all browsers.\n * 2. Correct the odd `em` font sizing in all browsers.\n */\n\ncode,\nkbd,\nsamp {\n  font-family: monospace, monospace; /* 1 */\n  font-size: 1em; /* 2 */\n}\n\n/**\n * Add the correct font size in all browsers.\n */\n\nsmall {\n  font-size: 80%;\n}\n\n/**\n * Prevent `sub` and `sup` elements from affecting the line height in\n * all browsers.\n */\n\nsub,\nsup {\n  font-size: 75%;\n  line-height: 0;\n  position: relative;\n  vertical-align: baseline;\n}\n\nsub {\n  bottom: -0.25em;\n}\n\nsup {\n  top: -0.5em;\n}\n\n/* Embedded content\n   ========================================================================== */\n\n/**\n * Remove the border on images inside links in IE 10.\n */\n\nimg {\n  border-style: none;\n}\n\n/* Forms\n   ========================================================================== */\n\n/**\n * 1. Change the font styles in all browsers.\n * 2. Remove the margin in Firefox and Safari.\n */\n\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n  font-family: inherit; /* 1 */\n  font-size: 100%; /* 1 */\n  line-height: 1.15; /* 1 */\n  margin: 0; /* 2 */\n}\n\n/**\n * Show the overflow in IE.\n * 1. Show the overflow in Edge.\n */\n\nbutton,\ninput { /* 1 */\n  overflow: visible;\n}\n\n/**\n * Remove the inheritance of text transform in Edge, Firefox, and IE.\n * 1. Remove the inheritance of text transform in Firefox.\n */\n\nbutton,\nselect { /* 1 */\n  text-transform: none;\n}\n\n/**\n * Correct the inability to style clickable types in iOS and Safari.\n */\n\nbutton,\n[type=\"button\"],\n[type=\"reset\"],\n[type=\"submit\"] {\n  -webkit-appearance: button;\n}\n\n/**\n * Remove the inner border and padding in Firefox.\n */\n\nbutton::-moz-focus-inner,\n[type=\"button\"]::-moz-focus-inner,\n[type=\"reset\"]::-moz-focus-inner,\n[type=\"submit\"]::-moz-focus-inner {\n  border-style: none;\n  padding: 0;\n}\n\n/**\n * Restore the focus styles unset by the previous rule.\n */\n\nbutton:-moz-focusring,\n[type=\"button\"]:-moz-focusring,\n[type=\"reset\"]:-moz-focusring,\n[type=\"submit\"]:-moz-focusring {\n  outline: 1px dotted ButtonText;\n}\n\n/**\n * Correct the padding in Firefox.\n */\n\nfieldset {\n  padding: 0.35em 0.75em 0.625em;\n}\n\n/**\n * 1. Correct the text wrapping in Edge and IE.\n * 2. Correct the color inheritance from `fieldset` elements in IE.\n * 3. Remove the padding so developers are not caught out when they zero out\n *    `fieldset` elements in all browsers.\n */\n\nlegend {\n  box-sizing: border-box; /* 1 */\n  color: inherit; /* 2 */\n  display: table; /* 1 */\n  max-width: 100%; /* 1 */\n  padding: 0; /* 3 */\n  white-space: normal; /* 1 */\n}\n\n/**\n * Add the correct vertical alignment in Chrome, Firefox, and Opera.\n */\n\nprogress {\n  vertical-align: baseline;\n}\n\n/**\n * Remove the default vertical scrollbar in IE 10+.\n */\n\ntextarea {\n  overflow: auto;\n}\n\n/**\n * 1. Add the correct box sizing in IE 10.\n * 2. Remove the padding in IE 10.\n */\n\n[type=\"checkbox\"],\n[type=\"radio\"] {\n  box-sizing: border-box; /* 1 */\n  padding: 0; /* 2 */\n}\n\n/**\n * Correct the cursor style of increment and decrement buttons in Chrome.\n */\n\n[type=\"number\"]::-webkit-inner-spin-button,\n[type=\"number\"]::-webkit-outer-spin-button {\n  height: auto;\n}\n\n/**\n * 1. Correct the odd appearance in Chrome and Safari.\n * 2. Correct the outline style in Safari.\n */\n\n[type=\"search\"] {\n  -webkit-appearance: textfield; /* 1 */\n  outline-offset: -2px; /* 2 */\n}\n\n/**\n * Remove the inner padding in Chrome and Safari on macOS.\n */\n\n[type=\"search\"]::-webkit-search-decoration {\n  -webkit-appearance: none;\n}\n\n/**\n * 1. Correct the inability to style clickable types in iOS and Safari.\n * 2. Change font properties to `inherit` in Safari.\n */\n\n::-webkit-file-upload-button {\n  -webkit-appearance: button; /* 1 */\n  font: inherit; /* 2 */\n}\n\n/* Interactive\n   ========================================================================== */\n\n/*\n * Add the correct display in Edge, IE 10+, and Firefox.\n */\n\ndetails {\n  display: block;\n}\n\n/*\n * Add the correct display in all browsers.\n */\n\nsummary {\n  display: list-item;\n}\n\n/* Misc\n   ========================================================================== */\n\n/**\n * Add the correct display in IE 10+.\n */\n\ntemplate {\n  display: none;\n}\n\n/**\n * Add the correct display in IE 10.\n */\n\n[hidden] {\n  display: none;\n}\n"
  },
  {
    "path": "cmd/static/index.html",
    "content": "<!doctype html>\n<html class=\"no-js\" lang=\"\">\n\n<head>\n    <meta charset=\"utf-8\">\n    <title>Webgo - Sample</title>\n    <meta name=\"description\" content=\"This is a sample app using Webgo\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n\n    <meta property=\"og:title\" content=\"Webgo Sample\">\n    <meta property=\"og:type\" content=\"website\">\n\n    <link rel=\"stylesheet\" href=\"/static/css/normalize.css\">\n    <link rel=\"stylesheet\" href=\"/static/css/main.css\">\n\n    <meta name=\"theme-color\" content=\"#fafafa\">\n</head>\n\n<body>\n    <section class=\"main\">\n        <p align=\"center\"><img\n                src=\"https://user-images.githubusercontent.com/1092882/60883564-20142380-a268-11e9-988a-d98fb639adc6.png\"\n                alt=\"webgo gopher\" style=\"width: 100%; max-width: 256px;\" /></p>\n\n        <p class=\"tags\"  align=\"justify\">\n            <a href=\"https://codecov.io/gh/bnkamalesh/webgo\"><img\n                    src=\"https://img.shields.io/codecov/c/github/bnkamalesh/webgo.svg\" alt=\"coverage\"></a>\n            <a href=\"https://goreportcard.com/report/github.com/naughtygopher/webgo\"><img\n                    src=\"https://goreportcard.com/badge/github.com/naughtygopher/webgo\" alt=\"\"></a>\n            <a href=\"https://codeclimate.com/github/bnkamalesh/webgo/maintainability\"><img\n                    src=\"https://api.codeclimate.com/v1/badges/85b3a55c3fa6b4c5338d/maintainability\" alt=\"\"></a>\n            <a href=\"http://godoc.org/github.com/naughtygopher/webgo\"><img\n                    src=\"https://godoc.org/github.com/nathany/looper?status.svg\" alt=\"\"></a>\n            <a href=\"https://github.com/avelino/awesome-go#web-frameworks\"><img\n                    src=\"https://awesome.re/mentioned-badge.svg\" alt=\"\"></a>\n        </p>\n        <h1 align=\"center\">WebGo</h1>\n        <p class=\"sse-container\">\n        <table>\n            <tr>\n                <td><a\n                    href=\"https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events\">SSE</a> Client ID</td>\n                <td><span id=\"sse-client-id\"></span></td>\n            </tr>\n            <tr>\n                <td><a\n                        href=\"https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events\">SSE</a> data\n                </td>\n                <td><span id=\"sse\"></span></td>\n            </tr>\n            <tr>\n                <td>Active clients</td>\n                <td><span id=\"sse-clients\"></span></td>\n            </tr>\n        </table>\n\n        </p>\n    </section>\n    <script src=\"/static/js/main.js\"></script>\n</body>\n\n</html>"
  },
  {
    "path": "cmd/static/js/main.js",
    "content": "const webgo = async () => {\n  const clientID = Math.random()\n    .toString(36)\n    .replace(/[^a-z]+/g, \"\")\n    .substring(0, 16);\n  const sseDOM = document.getElementById(\"sse\");\n  const sseClientsDOM = document.getElementById(\"sse-clients\");\n  const sseClientIDDOM = document.getElementById(\"sse-client-id\");\n\n  const formatBackoff = (backoff, precision = 2) => {\n    let boff = `${backoff}ms`;\n    if (backoff >= 1000) {\n      boff = `${parseFloat(backoff / 1000).toFixed(precision)}s`;\n    }\n    return boff;\n  };\n\n  const config = {\n    url: `/sse/${clientID}`,\n    onMessage: (data) => {\n      const parts = data?.split?.(\"(\");\n      if (!parts || !parts.length) {\n        return;\n      }\n\n      const date = new Date(parts[0]);\n      const activeClients = parts[1].replace(\")\", \"\");\n      sseDOM.innerText = date.toLocaleString();\n      sseClientsDOM.innerText = activeClients;\n      sseClientIDDOM.innerText = clientID;\n    },\n    onError: (err, { backoff }) => {\n      sseClientsDOM.innerText = \"N/A\";\n\n      let interval = null;\n      interval = window.setInterval(() => {\n        sseDOM.innerHTML = `SSE failed, attempting reconnect in <strong>${formatBackoff(\n          backoff,\n          0\n        )}</strong>`;\n        backoff -= 1000;\n        if (backoff < 0) {\n          sseDOM.innerHTML = `SSE failed, attempting reconnect in <strong>0s</strong>`;\n          window.clearInterval(interval);\n        }\n      }, 1000);\n\n      console.log(err);\n    },\n    initialBackoff: 1000,\n    backoffStep: 1000,\n  };\n\n  const sseworker = new Worker(\"/static/js/sse.js\");\n  sseworker.onerror = (e) => {\n    sseworker.terminate();\n  };\n\n  sseworker.onmessage = (e) => {\n    if (e?.data?.error) {\n      config.onError(\"SSE failed\", e?.data);\n    } else {\n      config.onMessage(e?.data);\n    }\n  };\n\n  sseworker.postMessage({\n    url: config.url,\n    initialBackoff: config.initialBackoff,\n    backoffStep: config.backoffStep,\n  });\n};\nwebgo();\n"
  },
  {
    "path": "cmd/static/js/sse.js",
    "content": "const sse = (url, config = {}) => {\n  const {\n    onMessage,\n    onError,\n    initialBackoff = 10, // milliseconds\n    maxBackoff = 15 * 1000, // 15 seconds\n    backoffStep = 50, // milliseconds\n  } = config;\n\n  let backoff = initialBackoff,\n    sseRetryTimeout = null;\n\n  const start = () => {\n    const source = new EventSource(url);\n    const configState = { initialBackoff, maxBackoff, backoffStep, backoff };\n\n    source.onopen = () => {\n      clearTimeout(sseRetryTimeout);\n      // reset backoff to initial, so further failures will again start with initial backoff\n      // instead of previous duration\n      backoff = initialBackoff;\n      configState.backoff = backoff;\n    };\n\n    source.onmessage = (event) => {\n      onMessage && onMessage(event, configState);\n    };\n\n    source.onerror = (err) => {\n      source.close();\n      if (!backoffStep) {\n        onError && onError(err, configState);\n        return;\n      }\n\n      clearTimeout(sseRetryTimeout);\n      // reattempt connecting with *linear* backoff\n      sseRetryTimeout = self.setTimeout(() => {\n        start(url, onMessage);\n        if (backoff < maxBackoff) {\n          backoff += backoffStep;\n          if (backoff > maxBackoff) {\n            backoff = maxBackoff;\n          }\n        }\n      }, backoff);\n      onError && onError(err, configState);\n    };\n  };\n  return start;\n};\n\nonmessage = (e) => {\n  sse(e?.data?.url, {\n    onMessage: (event) => {\n      postMessage(event?.data);\n    },\n    onError: (err, attrs) => {\n      postMessage({ error: \"SSE failed\", ...attrs });\n    },\n  })();\n};\n"
  },
  {
    "path": "config.go",
    "content": "package webgo\n\nimport (\n\t\"encoding/json\"\n\t\"os\"\n\t\"strconv\"\n\t\"time\"\n)\n\n// Config is used for reading app's configuration from json file\ntype Config struct {\n\t// Host is the host on which the server is listening\n\tHost string `json:\"host,omitempty\"`\n\t// Port is the port number where the server has to listen for the HTTP requests\n\tPort string `json:\"port,omitempty\"`\n\n\t// CertFile is the TLS/SSL certificate file path, required for HTTPS\n\tCertFile string `json:\"certFile,omitempty\"`\n\t// KeyFile is the filepath of private key of the certificate\n\tKeyFile string `json:\"keyFile,omitempty\"`\n\t// HTTPSPort is the port number where the server has to listen for the HTTP requests\n\tHTTPSPort string `json:\"httpsPort,omitempty\"`\n\n\t// ReadTimeout is the maximum duration for which the server would read a request\n\tReadTimeout time.Duration `json:\"readTimeout,omitempty\"`\n\t// WriteTimeout is the maximum duration for which the server would try to respond\n\tWriteTimeout time.Duration `json:\"writeTimeout,omitempty\"`\n\n\t// InsecureSkipVerify is the HTTP certificate verification\n\tInsecureSkipVerify bool `json:\"insecureSkipVerify,omitempty\"`\n\n\t// ShutdownTimeout is the duration in which graceful shutdown is completed\n\tShutdownTimeout time.Duration\n\n\t// ReverseMiddleware if true, will reverse the order of execution middleware\n\t// from the order of it was added. e.g. router.Use(m1,m2), m2 will execute first\n\t// if ReverseMiddleware is true\n\tReverseMiddleware bool\n}\n\n// Load config file from the provided filepath and validate\nfunc (cfg *Config) Load(filepath string) {\n\tfile, err := os.ReadFile(filepath)\n\tif err != nil {\n\t\tLOGHANDLER.Fatal(err)\n\t}\n\n\terr = json.Unmarshal(file, cfg)\n\tif err != nil {\n\t\tLOGHANDLER.Fatal(err)\n\t}\n\n\terr = cfg.Validate()\n\tif err != nil {\n\t\tLOGHANDLER.Fatal(ErrInvalidPort)\n\t}\n}\n\n// Validate the config parsed into the Config struct\nfunc (cfg *Config) Validate() error {\n\ti, err := strconv.Atoi(cfg.Port)\n\tif err != nil {\n\t\treturn ErrInvalidPort\n\t}\n\n\tif i <= 0 || i > 65535 {\n\t\treturn ErrInvalidPort\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "config_test.go",
    "content": "package webgo\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestConfig_LoadInvalid(t *testing.T) {\n\tt.Parallel()\n\ttl := &testLogger{\n\t\tout: bytes.Buffer{},\n\t}\n\tLOGHANDLER = tl\n\n\tcfg := &Config{}\n\tcfg.Load(\"\")\n\tstr := tl.out.String()\n\twant := \"open : no such file or directoryunexpected end of JSON inputport number not provided or is invalid (should be between 0 - 65535)\"\n\tgot := str\n\tif got != want {\n\t\tt.Errorf(\n\t\t\t\"Expected '%s', got '%s'\",\n\t\t\twant,\n\t\t\tgot,\n\t\t)\n\t}\n\ttl.out.Reset()\n}\n\nfunc TestConfig_LoadValid(t *testing.T) {\n\tt.Parallel()\n\tcfg := Config{}\n\tcfg.Load(\"tests/config.json\")\n\n\tcfg.Port = \"a\"\n\tif cfg.Validate() != ErrInvalidPort {\n\t\tt.Error(\"Port validation failed\")\n\t}\n\tcfg.Port = \"65536\"\n\tif cfg.Validate() != ErrInvalidPort {\n\t\tt.Error(\"Port validation failed\")\n\t}\n}\n\nfunc TestConfig_Validate(t *testing.T) {\n\tt.Parallel()\n\ttype fields struct {\n\t\tHost               string\n\t\tPort               string\n\t\tCertFile           string\n\t\tKeyFile            string\n\t\tHTTPSPort          string\n\t\tReadTimeout        time.Duration\n\t\tWriteTimeout       time.Duration\n\t\tInsecureSkipVerify bool\n\t\tShutdownTimeout    time.Duration\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname: \"invalid port\",\n\t\t\tfields: fields{\n\t\t\t\tPort: \"-12\",\n\t\t\t},\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"valid port\",\n\t\t\tfields: fields{\n\t\t\t\tPort: \"9000\",\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tcfg := &Config{\n\t\t\t\tHost:               tt.fields.Host,\n\t\t\t\tPort:               tt.fields.Port,\n\t\t\t\tCertFile:           tt.fields.CertFile,\n\t\t\t\tKeyFile:            tt.fields.KeyFile,\n\t\t\t\tHTTPSPort:          tt.fields.HTTPSPort,\n\t\t\t\tReadTimeout:        tt.fields.ReadTimeout,\n\t\t\t\tWriteTimeout:       tt.fields.WriteTimeout,\n\t\t\t\tInsecureSkipVerify: tt.fields.InsecureSkipVerify,\n\t\t\t\tShutdownTimeout:    tt.fields.ShutdownTimeout,\n\t\t\t}\n\t\t\tif err := cfg.Validate(); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Config.Validate() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n\n}\n"
  },
  {
    "path": "errors.go",
    "content": "package webgo\n\nimport (\n\t\"errors\"\n\t\"io\"\n\t\"log\"\n\t\"os\"\n)\n\nvar (\n\t// ErrInvalidPort is the error returned when the port number provided in the config file is invalid\n\tErrInvalidPort = errors.New(\"port number not provided or is invalid (should be between 0 - 65535)\")\n\n\tlh *logHandler\n)\n\ntype logCfg string\n\nconst (\n\t// LogCfgDisableDebug is used to disable debug logs\n\tLogCfgDisableDebug = logCfg(\"disable-debug\")\n\t// LogCfgDisableInfo is used to disable info logs\n\tLogCfgDisableInfo = logCfg(\"disable-info\")\n\t// LogCfgDisableWarn is used to disable warning logs\n\tLogCfgDisableWarn = logCfg(\"disable-warn\")\n\t// LogCfgDisableError is used to disable error logs\n\tLogCfgDisableError = logCfg(\"disable-err\")\n\t// LogCfgDisableFatal is used to disable fatal logs\n\tLogCfgDisableFatal = logCfg(\"disable-fatal\")\n)\n\n// Logger defines all the logging methods to be implemented\ntype Logger interface {\n\tDebug(data ...interface{})\n\tInfo(data ...interface{})\n\tWarn(data ...interface{})\n\tError(data ...interface{})\n\tFatal(data ...interface{})\n}\n\n// logHandler has all the log writer handlers\ntype logHandler struct {\n\tdebug *log.Logger\n\tinfo  *log.Logger\n\twarn  *log.Logger\n\terr   *log.Logger\n\tfatal *log.Logger\n}\n\n// Debug prints log of severity 5\nfunc (lh *logHandler) Debug(data ...interface{}) {\n\tif lh.debug == nil {\n\t\treturn\n\t}\n\tlh.debug.Println(data...)\n}\n\n// Info prints logs of severity 4\nfunc (lh *logHandler) Info(data ...interface{}) {\n\tif lh.info == nil {\n\t\treturn\n\t}\n\tlh.info.Println(data...)\n}\n\n// Warn prints log of severity 3\nfunc (lh *logHandler) Warn(data ...interface{}) {\n\tif lh.warn == nil {\n\t\treturn\n\t}\n\tlh.warn.Println(data...)\n}\n\n// Error prints log of severity 2\nfunc (lh *logHandler) Error(data ...interface{}) {\n\tif lh.err == nil {\n\t\treturn\n\t}\n\tlh.err.Println(data...)\n}\n\n// Fatal prints log of severity 1\nfunc (lh *logHandler) Fatal(data ...interface{}) {\n\tif lh.fatal == nil {\n\t\treturn\n\t}\n\tlh.fatal.Fatalln(data...)\n}\n\n// LOGHANDLER is a global variable which webgo uses to log messages\nvar LOGHANDLER Logger\n\nfunc init() {\n\tGlobalLoggerConfig(nil, nil)\n}\n\nfunc loggerWithCfg(stdout io.Writer, stderr io.Writer, cfgs ...logCfg) *logHandler {\n\tlh = &logHandler{\n\t\tdebug: log.New(stdout, \"Debug \", log.LstdFlags),\n\t\tinfo:  log.New(stdout, \"Info \", log.LstdFlags),\n\t\twarn:  log.New(stderr, \"Warning \", log.LstdFlags),\n\t\terr:   log.New(stderr, \"Error \", log.LstdFlags),\n\t\tfatal: log.New(stderr, \"Fatal \", log.LstdFlags|log.Llongfile),\n\t}\n\n\tfor _, c := range cfgs {\n\t\tswitch c {\n\t\tcase LogCfgDisableDebug:\n\t\t\t{\n\t\t\t\tlh.debug = nil\n\t\t\t}\n\t\tcase LogCfgDisableInfo:\n\t\t\t{\n\t\t\t\tlh.info = nil\n\t\t\t}\n\t\tcase LogCfgDisableWarn:\n\t\t\t{\n\t\t\t\tlh.warn = nil\n\t\t\t}\n\t\tcase LogCfgDisableError:\n\t\t\t{\n\t\t\t\tlh.err = nil\n\t\t\t}\n\t\tcase LogCfgDisableFatal:\n\t\t\t{\n\t\t\t\tlh.fatal = nil\n\t\t\t}\n\t\t}\n\t}\n\treturn lh\n}\n\n// GlobalLoggerConfig is used to configure the global/default logger of webgo\n// IMPORTANT: This is not concurrent safe\nfunc GlobalLoggerConfig(stdout io.Writer, stderr io.Writer, cfgs ...logCfg) {\n\tif stdout == nil {\n\t\tstdout = os.Stdout\n\t}\n\n\tif stderr == nil {\n\t\tstderr = os.Stderr\n\t}\n\n\tLOGHANDLER = loggerWithCfg(stdout, stderr, cfgs...)\n}\n"
  },
  {
    "path": "errors_test.go",
    "content": "package webgo\n\nimport (\n\t\"testing\"\n)\n\nfunc Test_loggerWithCfg(t *testing.T) {\n\tt.Parallel()\n\tcfgs := []logCfg{\n\t\tLogCfgDisableDebug,\n\t\tLogCfgDisableInfo,\n\t\tLogCfgDisableWarn,\n\t\tLogCfgDisableError,\n\t\tLogCfgDisableFatal,\n\t}\n\tl := loggerWithCfg(nil, nil, cfgs...)\n\tif l.debug != nil {\n\t\tt.Errorf(\"expected debug to be nil, got %v\", l.debug)\n\t}\n\tif l.err != nil {\n\t\tt.Errorf(\"expected err to be nil, got %v\", l.err)\n\t}\n\tif l.fatal != nil {\n\t\tt.Errorf(\"expected fatal to be nil, got %v\", l.fatal)\n\t}\n\tif l.info != nil {\n\t\tt.Errorf(\"expected info to be nil, got %v\", l.info)\n\t}\n\tif l.warn != nil {\n\t\tt.Errorf(\"expected warn to be nil, got %v\", l.warn)\n\t}\n}\n"
  },
  {
    "path": "extensions/sse/README.md",
    "content": "# Server-Sent Events\n\nThis extension provides support for [Server-Sent](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) Events for any net/http compliant http server.\nIt provides the following hooks for customizing the workflows:\n\n1. `OnCreateClient func(ctx context.Context, client *Client, count int)`\n2. `OnRemoveClient func(ctx context.Context, clientID string, count int)`\n3. `OnSend func(ctx context.Context, client *Client, err error)`\n4. `BeforeSend func(ctx context.Context, client *Client)`\n\n```golang\nimport (\n    \"github.com/naughtygopher/webgo/extensions/sse\"\n)\nfunc main() {\n    sseService := sse.New()\n    // broadcast to all active clients\n    sseService.Broadcast(Message{\n        Data:  \"Hello world\",\n        Retry: time.MilliSecond,\n\t})\n\n\t// You can replace the ClientManager with your custom implementation, and override the default one\n\t// sseService.Clients = <your custom client manager>\n\n    // send message to an individual client\n    clientID := \"cli123\"\n    cli := sseService.Client(clientID)\n    if cli != nil {\n        cli.Message <- &Message{Data: fmt.Sprintf(\"Hello %s\",clientID), Retry: time.MilliSecond }\n    }\n}\n```\n\n## Client Manager\n\nClient manager is an interface which is required for SSE to function, since this is an interface it's easier for you to replace if required. The default implementation is a simple one using mutex. If you have a custom implementation which is faster/better, you can easily swap out the default one.\n\n```golang\ntype ClientManager interface {\n\t// New should return a new client, and the total number of active clients after adding this new one\n\tNew(ctx context.Context, w http.ResponseWriter, clientID string) (*Client, int)\n\t// Range should iterate through all the active clients\n\tRange(func(*Client))\n\t// Remove should remove the active client given a clientID, and close the connection\n\tRemove(clientID string) int\n\t// Active returns the number of active clients\n\tActive() int\n\t// Clients returns a list of all active clients\n\tClients() []*Client\n\t// Client returns *Client if clientID is active\n\tClient(clientID string) *Client\n}\n```\n"
  },
  {
    "path": "extensions/sse/client.go",
    "content": "package sse\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype ClientManager interface {\n\t// New should return a new client, and the total number of active clients after adding this new one\n\tNew(ctx context.Context, w http.ResponseWriter, clientID string) (*Client, int)\n\t// Range should iterate through all the active clients\n\tRange(func(*Client))\n\t// Remove should remove the active client given a clientID, and close the connection\n\tRemove(clientID string) int\n\t// Active returns the number of active clients\n\tActive() int\n\t// Clients returns a list of all active clients\n\tClients() []*Client\n\t// Client returns *Client if clientID is active\n\tClient(clientID string) *Client\n}\n\ntype Client struct {\n\tID             string\n\tMsg            chan *Message\n\tResponseWriter http.ResponseWriter\n\tCtx            context.Context\n}\n\ntype eventType int\n\nconst (\n\teTypeNewClient eventType = iota\n\teTypeClientList\n\teTypeRemoveClient\n\teTypeActiveClientCount\n\teTypeClient\n)\n\nfunc (et eventType) String() string {\n\tswitch et {\n\tcase eTypeNewClient:\n\t\treturn \"new_client\"\n\tcase eTypeClientList:\n\t\treturn \"client_list\"\n\tcase eTypeRemoveClient:\n\t\treturn \"remove_client\"\n\tcase eTypeActiveClientCount:\n\t\treturn \"active_client_count\"\n\t}\n\treturn \"unknown\"\n}\n\ntype event struct {\n\tType     eventType\n\tClientID string\n\tClient   *Client\n\tResponse chan *eventResponse\n}\ntype eventResponse struct {\n\tClients          []*Client\n\tRemainingClients int\n\tClient           *Client\n}\ntype Clients struct {\n\tclients   map[string]*Client\n\tMsgBuffer int\n\tevents    chan<- event\n}\n\nfunc (cs *Clients) listener(events <-chan event) {\n\tfor ev := range events {\n\t\tswitch ev.Type {\n\t\tcase eTypeNewClient:\n\t\t\tcs.clients[ev.Client.ID] = ev.Client\n\n\t\tcase eTypeClientList:\n\t\t\tcopied := make([]*Client, 0, len(cs.clients))\n\t\t\tfor clientID := range cs.clients {\n\t\t\t\tcopied = append(copied, cs.clients[clientID])\n\t\t\t}\n\t\t\tev.Response <- &eventResponse{\n\t\t\t\tClients: copied,\n\t\t\t}\n\n\t\tcase eTypeRemoveClient:\n\t\t\tcli := cs.clients[ev.ClientID]\n\t\t\tif cli == nil {\n\t\t\t\tev.Response <- nil\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// Ctx.Done() is needed to close its streaming handler\n\t\t\tcli.Ctx.Done()\n\t\t\tdelete(cs.clients, ev.ClientID)\n\t\t\tev.Response <- nil\n\n\t\tcase eTypeClient:\n\t\t\tev.Response <- &eventResponse{\n\t\t\t\tClient: cs.clients[ev.ClientID],\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (cs *Clients) New(ctx context.Context, w http.ResponseWriter, clientID string) (*Client, int) {\n\tmchan := make(chan *Message, cs.MsgBuffer)\n\tcli := &Client{\n\t\tID:             clientID,\n\t\tMsg:            mchan,\n\t\tResponseWriter: w,\n\t\tCtx:            ctx,\n\t}\n\n\tcs.events <- event{\n\t\tType:   eTypeNewClient,\n\t\tClient: cli,\n\t}\n\n\treturn cli, len(cs.clients)\n}\n\nfunc (cs *Clients) Range(f func(cli *Client)) {\n\trch := make(chan *eventResponse)\n\tcs.events <- event{\n\t\tType:     eTypeClientList,\n\t\tResponse: rch,\n\t}\n\n\tresponse := <-rch\n\tfor i := range response.Clients {\n\t\tf(response.Clients[i])\n\t}\n\n}\n\nfunc (cs *Clients) Remove(clientID string) int {\n\trch := make(chan *eventResponse)\n\tcs.events <- event{\n\t\tType:     eTypeRemoveClient,\n\t\tClientID: clientID,\n\t\tResponse: rch,\n\t}\n\n\t<-rch\n\n\treturn len(cs.clients)\n}\n\nfunc (cs *Clients) Active() int {\n\treturn len(cs.clients)\n\n}\n\n// MessageChannels returns a slice of message channels of all clients\n// which you can then use to send message concurrently\nfunc (cs *Clients) Clients() []*Client {\n\trch := make(chan *eventResponse)\n\tcs.events <- event{\n\t\tType:     eTypeClientList,\n\t\tResponse: rch,\n\t}\n\n\tresponse := <-rch\n\treturn response.Clients\n}\n\nfunc (cs *Clients) Client(clientID string) *Client {\n\trch := make(chan *eventResponse)\n\tcs.events <- event{\n\t\tType:     eTypeClientList,\n\t\tResponse: rch,\n\t}\n\tcli := <-rch\n\treturn cli.Client\n}\n\nfunc NewClientManager() ClientManager {\n\tconst buffer = 10\n\tevents := make(chan event, buffer)\n\tcli := &Clients{\n\t\tclients:   make(map[string]*Client),\n\t\tevents:    events,\n\t\tMsgBuffer: buffer,\n\t}\n\tgo cli.listener(events)\n\treturn cli\n}\n"
  },
  {
    "path": "extensions/sse/message.go",
    "content": "package sse\n\nimport (\n\t\"bytes\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"time\"\n)\n\n// Message represents a valid SSE message\n// ref: https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events\ntype Message struct {\n\t// Event is a string identifying the type of event described. If this is specified, an event will be dispatched on the browser to the listener for the specified event name; the website source code should use addEventListener() to listen for named events. The onmessage handler is called if no event name is specified for a message.\n\tEvent string\n\n\t// Data field for the message. When the EventSource receives multiple consecutive lines that begin with data:, it concatenates them, inserting a newline character between each one. Trailing newlines are removed.\n\tData string\n\n\t// ID to set the EventSource object's last event ID value.\n\tID string\n\n\t// Retry is the reconnection time. If the connection to the server is lost, the browser will wait for the specified time before attempting to reconnect. This must be an integer, specifying the reconnection time in milliseconds. If a non-integer value is specified, the field is ignored.\n\tRetry time.Duration\n}\n\nfunc (m *Message) Bytes() []byte {\n\t// The event stream is a simple stream of text data which must be encoded using UTF-8.\n\t// Messages in the event stream are separated by a pair of newline characters.\n\t// A colon as the first character of a line is in essence a comment, and is ignored.\n\n\tbuff := bytes.NewBufferString(\"\")\n\tif m.Event != \"\" {\n\t\tbuff.WriteString(\"event:\" + m.Event + \"\\n\")\n\t}\n\tif m.ID != \"\" {\n\t\tbuff.WriteString(\"id:\" + m.ID + \"\\n\")\n\t}\n\tif m.Data != \"\" {\n\t\tbuff.WriteString(\"data:\" + m.Data + \"\\n\")\n\t}\n\tif m.Retry != 0 {\n\t\tbuff.WriteString(\"retry:\" + strconv.Itoa(int(m.Retry.Milliseconds())) + \"\\n\")\n\t}\n\tbuff.WriteString(\"\\n\")\n\treturn buff.Bytes()\n}\n\nfunc DefaultUnsupportedMessageHandler(w http.ResponseWriter, r *http.Request) error {\n\tw.WriteHeader(http.StatusNotImplemented)\n\t_, err := w.Write([]byte(\"Streaming not supported\"))\n\treturn err\n}\n"
  },
  {
    "path": "extensions/sse/sse.go",
    "content": "// Package sse implements Server-Sent Events(SSE)\n// This extension is compliant with any net/http implementation, and is not limited to WebGo.\npackage sse\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\nvar closeMessage = (&Message{\n\tData: `{\"error\":\"close\"}`,\n}).Bytes()\n\ntype SSE struct {\n\t// ClientIDHeader is the HTTP request header in which the client ID is set. Default is `sse-clientid`\n\tClientIDHeader string\n\t// UnsupportedMessage is used to send the error response to client if the\n\t// server doesn't support SSE\n\tUnsupportedMessage func(http.ResponseWriter, *http.Request) error\n\n\t// OnCreateClient is a hook, for when a client is added to the active clients. count is the number\n\t// of active clients after adding the latest client\n\tOnCreateClient func(ctx context.Context, client *Client, count int)\n\n\t// OnRemoveClient is a hook, for when a client is removed from the active clients. count is the number\n\t// of active clients after removing a client\n\tOnRemoveClient func(ctx context.Context, clientID string, count int)\n\n\t// OnSend is a hook, which is called *after* a message is sent to a client\n\tOnSend func(ctx context.Context, client *Client, err error)\n\t// BeforeSend is a hook, which is called before starting to listen for messages to send\n\tBeforeSend func(ctx context.Context, client *Client)\n\n\tClients ClientManager\n}\n\n// Handler returns an error rather than being directly used as an http.HandlerFunc,\n// to let the user handle error. e.g. if the error has to be logged\nfunc (sse *SSE) Handler(w http.ResponseWriter, r *http.Request) error {\n\tflusher, hasFlusher := w.(http.Flusher)\n\tif !hasFlusher {\n\t\treturn sse.UnsupportedMessage(w, r)\n\t}\n\theader := w.Header()\n\theader.Set(\"Content-Type\", \"text/event-stream\")\n\theader.Set(\"Connection\", \"keep-alive\")\n\theader.Set(\"X-Accel-Buffering\", \"no\")\n\tw.WriteHeader(http.StatusOK)\n\n\tctx := r.Context()\n\tclientID := r.Header.Get(sse.ClientIDHeader)\n\tclient := sse.NewClient(ctx, w, clientID)\n\tdefer func() {\n\t\tw.WriteHeader(http.StatusNoContent)\n\t\tsse.RemoveClient(ctx, clientID)\n\t}()\n\n\tsse.BeforeSend(ctx, client)\n\tfor {\n\t\tselect {\n\n\t\tcase payload, ok := <-client.Msg:\n\t\t\tif !ok {\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\t_, err := w.Write(payload.Bytes())\n\t\t\tsse.OnSend(ctx, client, err)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\tcase <-ctx.Done():\n\t\t\t{\n\t\t\t\t_, err := w.Write(closeMessage)\n\t\t\t\tsse.OnSend(ctx, client, err)\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\tflusher.Flush()\n\t}\n}\n\n// HandlerFunc is a convenience function which can be directly used with net/http implementations.\n// Important: You cannot handle any error returned by the Handler\nfunc (sse *SSE) HandlerFunc(w http.ResponseWriter, r *http.Request) {\n\t_ = sse.Handler(w, r)\n}\n\n// Broadcast sends the message to all active clients\nfunc (sse *SSE) Broadcast(msg Message) {\n\tsse.Clients.Range(func(cli *Client) {\n\t\tcli.Msg <- &msg\n\t})\n}\n\nfunc (sse *SSE) NewClient(ctx context.Context, w http.ResponseWriter, clientID string) *Client {\n\tcli, count := sse.Clients.New(ctx, w, clientID)\n\tsse.OnCreateClient(ctx, cli, count)\n\treturn cli\n}\n\nfunc (sse *SSE) ActiveClients() int {\n\treturn sse.Clients.Active()\n}\n\nfunc (sse *SSE) RemoveClient(ctx context.Context, clientID string) {\n\tsse.OnRemoveClient(\n\t\tctx,\n\t\tclientID,\n\t\tsse.Clients.Remove(clientID),\n\t)\n}\n\nfunc (sse *SSE) Client(id string) *Client {\n\treturn sse.Clients.Client(id)\n}\n\nfunc DefaultCreateHook(ctx context.Context, client *Client, count int)  {}\nfunc DefaultRemoveHook(ctx context.Context, clientID string, count int) {}\nfunc DefaultOnSend(ctx context.Context, client *Client, err error)      {}\nfunc DefaultBeforeSend(ctx context.Context, client *Client)             {}\n\nfunc New() *SSE {\n\ts := &SSE{\n\t\tClientIDHeader: \"sse-clientid\",\n\t\tClients:        NewClientManager(),\n\n\t\tUnsupportedMessage: DefaultUnsupportedMessageHandler,\n\n\t\tOnRemoveClient: DefaultRemoveHook,\n\t\tOnCreateClient: DefaultCreateHook,\n\t\tOnSend:         DefaultOnSend,\n\t\tBeforeSend:     DefaultBeforeSend,\n\t}\n\n\treturn s\n}\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/naughtygopher/webgo/v7\n\ngo 1.22\n"
  },
  {
    "path": "go.sum",
    "content": ""
  },
  {
    "path": "middleware/accesslog/accesslog.go",
    "content": "/*\nPackage accesslogs provides a simple straight forward access log middleware. The logs are of the\nfollowing format:\n<timestamp> <HTTP request method> <full URL including query string parameters> <duration of execution> <HTTP response status code>\n*/\npackage accesslog\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/naughtygopher/webgo/v7\"\n)\n\n// AccessLog is a middleware which prints access log to stdout\nfunc AccessLog(rw http.ResponseWriter, req *http.Request, next http.HandlerFunc) {\n\tstart := time.Now()\n\tnext(rw, req)\n\tend := time.Now()\n\n\twebgo.LOGHANDLER.Info(\n\t\tfmt.Sprintf(\n\t\t\t\"%s %s %s %d\",\n\t\t\treq.Method,\n\t\t\treq.URL.String(),\n\t\t\tend.Sub(start).String(),\n\t\t\twebgo.ResponseStatus(rw),\n\t\t),\n\t)\n}\n"
  },
  {
    "path": "middleware/accesslog/accesslog_test.go",
    "content": "/*\nPackage accesslogs provides a simple straight forward access log middleware. The logs are of the\nfollowing format:\n<timestamp> <HTTP request method> <full URL including query string parameters> <duration of execution> <HTTP response status code>\n*/\npackage accesslog\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/naughtygopher/webgo/v7\"\n)\n\nfunc TestAccessLog(t *testing.T) {\n\tstdout := bytes.NewBuffer([]byte(``))\n\tstderr := bytes.NewBuffer([]byte(``))\n\twebgo.GlobalLoggerConfig(stdout, stderr)\n\tport := \"9696\"\n\trouter, err := setup(port)\n\tif err != nil {\n\t\tt.Error(err.Error())\n\t\treturn\n\t}\n\trouter.Use(AccessLog)\n\trouter.SetupMiddleware()\n\n\turl := fmt.Sprintf(\"http://localhost:%s/hello\", port)\n\tw := httptest.NewRecorder()\n\n\treq := httptest.NewRequest(\n\t\thttp.MethodGet,\n\t\turl,\n\t\tnil,\n\t)\n\n\trouter.ServeHTTP(w, req)\n\n\tparts := strings.Split(stdout.String(), \" \")\n\tif len(parts) != 7 {\n\t\tt.Errorf(\n\t\t\t\"Expected log to have %d parts, got %d\",\n\t\t\t7,\n\t\t\tlen(parts),\n\t\t)\n\t\treturn\n\t}\n\n\tif parts[0] != \"Info\" {\n\t\tt.Errorf(\"expected log type 'Info', got '%s'\", parts[0])\n\t}\n\n\tif parts[3] != http.MethodGet {\n\t\tt.Errorf(\"expected HTTP method %s, got %s\", http.MethodGet, parts[3])\n\t}\n\n\tif parts[4] != url {\n\t\tt.Errorf(\"expected HTTP full URL '%s', got '%s'\", url, parts[4])\n\t}\n\n\tif parts[6][0:3] != \"200\" {\n\t\tt.Errorf(\"expected HTTP status code '%d', got '%s'\", http.StatusOK, parts[6][0:3])\n\t}\n}\n\nfunc handler(w http.ResponseWriter, r *http.Request) {\n\t_, _ = w.Write([]byte(`hello`))\n}\n\nfunc setup(port string) (*webgo.Router, error) {\n\tcfg := &webgo.Config{\n\t\tPort:            \"9696\",\n\t\tReadTimeout:     time.Second * 1,\n\t\tWriteTimeout:    time.Second * 1,\n\t\tShutdownTimeout: time.Second * 10,\n\t\tCertFile:        \"tests/ssl/server.crt\",\n\t\tKeyFile:         \"tests/ssl/server.key\",\n\t}\n\trouter := webgo.NewRouter(cfg, &webgo.Route{\n\t\tName:     \"hello\",\n\t\tPattern:  \"/hello\",\n\t\tMethod:   http.MethodGet,\n\t\tHandlers: []http.HandlerFunc{handler},\n\t})\n\treturn router, nil\n}\n"
  },
  {
    "path": "middleware/cors/cors.go",
    "content": "/*\nPackage cors sets the appropriate CORS(https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)\nresponse headers, and lets you customize. Following customizations are allowed:\n  - provide a list of allowed domains\n  - provide a list of headers\n  - set the max-age of CORS headers\n\nThe list of allowed methods are\n*/\npackage cors\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/naughtygopher/webgo/v7\"\n)\n\nconst (\n\theaderOrigin           = \"Access-Control-Allow-Origin\"\n\theaderMethods          = \"Access-Control-Allow-Methods\"\n\theaderCreds            = \"Access-Control-Allow-Credentials\"\n\theaderAllowHeaders     = \"Access-Control-Allow-Headers\"\n\theaderReqHeaders       = \"Access-Control-Request-Headers\"\n\theaderAccessControlAge = \"Access-Control-Max-Age\"\n\tallowHeaders           = \"Accept,Content-Type,Content-Length,Accept-Encoding,Access-Control-Request-Headers,\"\n)\n\nvar (\n\tdefaultAllowMethods = \"HEAD,GET,POST,PUT,PATCH,DELETE,OPTIONS\"\n)\n\nfunc allowedDomains() []string {\n\t// The domains mentioned here are default\n\tdomains := []string{\"*\"}\n\treturn domains\n}\n\nfunc getReqOrigin(r *http.Request) string {\n\treturn r.Header.Get(\"Origin\")\n}\n\nfunc allowedOriginsRegex(allowedOrigins ...string) []regexp.Regexp {\n\tif len(allowedOrigins) == 0 {\n\t\tallowedOrigins = []string{\"*\"}\n\t} else {\n\t\t// If \"*\" is one of the allowed domains, i.e. all domains, then rest of the values are ignored\n\t\tfor _, val := range allowedOrigins {\n\t\t\tval = strings.TrimSpace(val)\n\n\t\t\tif val == \"*\" {\n\t\t\t\tallowedOrigins = []string{\"*\"}\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\tallowedOriginRegex := make([]regexp.Regexp, 0, len(allowedOrigins))\n\tfor _, ao := range allowedOrigins {\n\t\tparts := strings.Split(ao, \":\")\n\t\tstr := strings.TrimSpace(parts[0])\n\t\tif str == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tif str == \"*\" {\n\t\t\tallowedOriginRegex = append(\n\t\t\t\tallowedOriginRegex,\n\t\t\t\t*(regexp.MustCompile(\".+\")),\n\t\t\t)\n\t\t\tbreak\n\t\t}\n\n\t\tregStr := fmt.Sprintf(`^(http)?(https)?(:\\/\\/)?(.+\\.)?%s(:[0-9]+)?$`, str)\n\n\t\tallowedOriginRegex = append(\n\t\t\tallowedOriginRegex,\n\t\t\t// Allow any port number of the specified domain\n\t\t\t*(regexp.MustCompile(regStr)),\n\t\t)\n\t}\n\n\treturn allowedOriginRegex\n}\n\nfunc allowedMethods(routes []*webgo.Route) string {\n\tif len(routes) == 0 {\n\t\treturn defaultAllowMethods\n\t}\n\n\tmethods := make([]string, 0, len(routes))\n\tfor _, r := range routes {\n\t\tfound := false\n\t\tfor _, m := range methods {\n\t\t\tif m == r.Method {\n\t\t\t\tfound = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif found {\n\t\t\tcontinue\n\t\t}\n\t\tmethods = append(methods, r.Method)\n\t}\n\tsort.Strings(methods)\n\treturn strings.Join(methods, \",\")\n}\n\n// Config holds all the configurations which is available for customizing this middleware\ntype Config struct {\n\tTimeoutSecs    int\n\tRoutes         []*webgo.Route\n\tAllowedOrigins []string\n\tAllowedHeaders []string\n}\n\nfunc allowedHeaders(headers []string) string {\n\tif len(headers) == 0 {\n\t\treturn allowHeaders\n\t}\n\n\tallowedHeaders := strings.Join(headers, \",\")\n\tif allowedHeaders[len(allowedHeaders)-1] != ',' {\n\t\tallowedHeaders += \",\"\n\t}\n\treturn allowedHeaders\n}\n\nfunc allowOrigin(reqOrigin string, allowedOriginRegex []regexp.Regexp) bool {\n\n\tfor _, o := range allowedOriginRegex {\n\t\t// Set appropriate response headers required for CORS\n\t\tif o.MatchString(reqOrigin) || reqOrigin == \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// Middleware can be used as well, it lets the user use this middleware without webgo\nfunc Middleware(allowedOriginRegex []regexp.Regexp, corsTimeout, allowedMethods, allowedHeaders string) webgo.Middleware {\n\treturn func(rw http.ResponseWriter, req *http.Request, next http.HandlerFunc) {\n\t\treqOrigin := getReqOrigin(req)\n\t\tallowed := allowOrigin(reqOrigin, allowedOriginRegex)\n\n\t\tif !allowed {\n\t\t\t// If CORS failed, no respective headers are set. But the execution is allowed to continue\n\t\t\t// Earlier this middleware blocked access altogether, which was considered an added\n\t\t\t// security measure despite it being outside the scope of this middelware. Though, such\n\t\t\t// restrictions create unnecessary complexities during inter-app communication.\n\t\t\tnext(rw, req)\n\t\t\treturn\n\t\t}\n\n\t\t// Set appropriate response headers required for CORS\n\t\trw.Header().Set(headerOrigin, reqOrigin)\n\t\trw.Header().Set(headerAccessControlAge, corsTimeout)\n\t\trw.Header().Set(headerCreds, \"true\")\n\t\trw.Header().Set(headerMethods, allowedMethods)\n\t\trw.Header().Set(headerAllowHeaders, allowedHeaders+req.Header.Get(headerReqHeaders))\n\n\t\tif req.Method == http.MethodOptions {\n\t\t\twebgo.SendHeader(rw, http.StatusOK)\n\t\t\treturn\n\t\t}\n\n\t\tnext(rw, req)\n\t}\n}\n\n// AddOptionsHandlers appends OPTIONS handler for all the routes\n// The response body would be empty for all the new handlers added\nfunc AddOptionsHandlers(routes []*webgo.Route) []*webgo.Route {\n\tdummyHandler := func(w http.ResponseWriter, r *http.Request) {}\n\tif len(routes) == 0 {\n\t\treturn []*webgo.Route{\n\t\t\t{\n\t\t\t\tName:          \"cors\",\n\t\t\t\tPattern:       \"/:w*\",\n\t\t\t\tMethod:        http.MethodOptions,\n\t\t\t\tTrailingSlash: true,\n\t\t\t\tHandlers:      []http.HandlerFunc{dummyHandler},\n\t\t\t},\n\t\t}\n\t}\n\n\tlist := make([]*webgo.Route, 0, len(routes))\n\tlist = append(list, routes...)\n\n\tfor _, r := range routes {\n\t\tlist = append(list, &webgo.Route{\n\t\t\tName:          fmt.Sprintf(\"%s-CORS\", r.Name),\n\t\t\tMethod:        http.MethodOptions,\n\t\t\tPattern:       r.Pattern,\n\t\t\tTrailingSlash: true,\n\t\t\tHandlers:      []http.HandlerFunc{dummyHandler},\n\t\t})\n\t}\n\n\treturn list\n}\n\n// CORS is a single CORS middleware which can be applied to the whole app at once\nfunc CORS(cfg *Config) webgo.Middleware {\n\tif cfg == nil {\n\t\tcfg = new(Config)\n\t\t// 30 minutes\n\t\tcfg.TimeoutSecs = 30 * 60\n\t}\n\n\tallowedOrigins := cfg.AllowedOrigins\n\tif len(allowedOrigins) == 0 {\n\t\tallowedOrigins = allowedDomains()\n\t}\n\n\tallowedOriginRegex := allowedOriginsRegex(allowedOrigins...)\n\tallowedmethods := allowedMethods(cfg.Routes)\n\tallowedHeaders := allowedHeaders(cfg.AllowedHeaders)\n\tcorsTimeout := fmt.Sprintf(\"%d\", cfg.TimeoutSecs)\n\n\treturn Middleware(\n\t\tallowedOriginRegex,\n\t\tcorsTimeout,\n\t\tallowedmethods,\n\t\tallowedHeaders,\n\t)\n}\n"
  },
  {
    "path": "middleware/cors/cors_test.go",
    "content": "/*\nPackage cors sets the appropriate CORS(https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)\nresponse headers, and lets you customize. Following customizations are allowed:\n  - provide a list of allowed domains\n  - provide a list of headers\n  - set the max-age of CORS headers\n\nThe list of allowed methods are\n*/\npackage cors\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/naughtygopher/webgo/v7\"\n)\n\nfunc TestCORSEmptyconfig(t *testing.T) {\n\tport := \"9696\"\n\troutes := getRoutes()\n\troutes = append(routes, AddOptionsHandlers(nil)...)\n\trouter, err := setup(port, routes)\n\tif err != nil {\n\t\tt.Error(err.Error())\n\t\treturn\n\t}\n\trouter.Use(CORS(&Config{TimeoutSecs: 50}))\n\trouter.SetupMiddleware()\n\n\turl := fmt.Sprintf(\"http://localhost:%s/hello\", port)\n\tw := httptest.NewRecorder()\n\n\treq := httptest.NewRequest(\n\t\thttp.MethodGet,\n\t\turl,\n\t\tnil,\n\t)\n\n\trouter.ServeHTTP(w, req)\n\tbody, _ := io.ReadAll(w.Body)\n\tstr := string(body)\n\tif str != \"hello\" {\n\t\tt.Errorf(\n\t\t\t\"Expected body '%s', got '%s'\",\n\t\t\t\"hello\",\n\t\t\tstr,\n\t\t)\n\t}\n\n\tif w.Header().Get(headerMethods) != defaultAllowMethods {\n\t\tt.Errorf(\n\t\t\t\"Expected header %s to be '%s', got '%s'\",\n\t\t\theaderMethods,\n\t\t\tdefaultAllowMethods,\n\t\t\tw.Header().Get(headerMethods),\n\t\t)\n\t}\n\tif w.Header().Get(headerCreds) != \"true\" {\n\t\tt.Errorf(\n\t\t\t\"Expected header %s to be 'true', got '%s'\",\n\t\t\theaderCreds,\n\t\t\tw.Header().Get(headerCreds),\n\t\t)\n\t}\n\tif w.Header().Get(headerAccessControlAge) != \"50\" {\n\t\tt.Errorf(\n\t\t\t\"Expected '%s' to be '50', got '%s'\",\n\t\t\theaderAccessControlAge,\n\t\t\tw.Header().Get(headerAccessControlAge),\n\t\t)\n\t}\n\n\tif w.Header().Get(headerAllowHeaders) != allowHeaders {\n\t\tt.Errorf(\n\t\t\t\"Expected '%s' to be '%s', got '%s'\",\n\t\t\theaderAllowHeaders,\n\t\t\tallowHeaders,\n\t\t\tw.Header().Get(headerAllowHeaders),\n\t\t)\n\t}\n\n\t// check OPTIONS method\n\tw = httptest.NewRecorder()\n\treq = httptest.NewRequest(\n\t\thttp.MethodOptions,\n\t\turl,\n\t\tnil,\n\t)\n\trouter.ServeHTTP(w, req)\n\tbody, _ = io.ReadAll(w.Body)\n\tstr = string(body)\n\tif str != \"\" {\n\t\tt.Errorf(\n\t\t\t\"Expected empty body, got '%s'\",\n\t\t\tstr,\n\t\t)\n\t}\n\n}\n\nfunc TestCORSWithConfig(t *testing.T) {\n\tport := \"9696\"\n\troutes := AddOptionsHandlers(getRoutes())\n\trouter, err := setup(port, routes)\n\tif err != nil {\n\t\tt.Error(err.Error())\n\t\treturn\n\t}\n\n\tcfg := &Config{\n\t\tRoutes:         routes,\n\t\tAllowedOrigins: []string{\"example.com\", fmt.Sprintf(\"localhost:%s\", port)},\n\t\tAllowedHeaders: []string{\"x-custom\"},\n\t}\n\trouter.Use(CORS(cfg))\n\n\tbaseAPI := fmt.Sprintf(\"http://localhost:%s\", port)\n\turl := fmt.Sprintf(\"%s/hello\", baseAPI)\n\tw := httptest.NewRecorder()\n\n\treq := httptest.NewRequest(\n\t\thttp.MethodGet,\n\t\turl,\n\t\tnil,\n\t)\n\n\trouter.SetupMiddleware()\n\trouter.ServeHTTP(w, req)\n\n\tif w.Header().Get(headerMethods) != \"GET,OPTIONS\" {\n\t\tt.Errorf(\n\t\t\t\"Expected value for %s header is 'GET', got '%s'\",\n\t\t\theaderMethods,\n\t\t\tw.Header().Get(headerMethods),\n\t\t)\n\t}\n\n\twant := strings.Join(cfg.AllowedHeaders, \",\") + \",\"\n\tif w.Header().Get(headerAllowHeaders) != want {\n\t\tt.Errorf(\n\t\t\t\"Expected value for %s header is '%s', got '%s'\",\n\t\t\theaderAllowHeaders,\n\t\t\twant,\n\t\t\tw.Header().Get(headerAllowHeaders),\n\t\t)\n\t}\n\n\t// test OPTIONS request\n\tw = httptest.NewRecorder()\n\treq = httptest.NewRequest(\n\t\thttp.MethodOptions,\n\t\turl,\n\t\tnil,\n\t)\n\n\treq.Header.Set(\"Origin\", \"helloworld.com\")\n\trouter.ServeHTTP(w, req)\n\tbody, _ := io.ReadAll(w.Body)\n\tstr := string(body)\n\tif str != \"\" {\n\t\tt.Errorf(\n\t\t\t\"Expected empty body, got '%s'\",\n\t\t\tstr,\n\t\t)\n\t}\n\t// since origin is set as \"helloworld.com\",  which is not in the allowed list of origins\n\t// CORS headers should NOT be set\n\tif w.Header().Get(headerOrigin) != \"\" {\n\t\tt.Errorf(\n\t\t\t\"Expected empty value for header '%s', got '%s'\",\n\t\t\theaderOrigin,\n\t\t\tw.Header().Get(headerOrigin),\n\t\t)\n\t}\n\tif w.Header().Get(headerAccessControlAge) != \"\" {\n\t\tt.Errorf(\n\t\t\t\"Expected empty value for header '%s', got '%s'\",\n\t\t\theaderAccessControlAge,\n\t\t\tw.Header().Get(headerAccessControlAge),\n\t\t)\n\t}\n\tif w.Header().Get(headerCreds) != \"\" {\n\t\tt.Errorf(\n\t\t\t\"Expected empty value for header '%s', got '%s'\",\n\t\t\theaderCreds,\n\t\t\tw.Header().Get(headerCreds),\n\t\t)\n\t}\n\tif w.Header().Get(headerMethods) != \"\" {\n\t\tt.Errorf(\n\t\t\t\"Expected empty value for header '%s', got '%s'\",\n\t\t\theaderMethods,\n\t\t\tw.Header().Get(headerMethods),\n\t\t)\n\t}\n\tif w.Header().Get(headerAllowHeaders) != \"\" {\n\t\tt.Errorf(\n\t\t\t\"Expected empty value for header '%s', got '%s'\",\n\t\t\theaderAllowHeaders,\n\t\t\tw.Header().Get(headerAllowHeaders),\n\t\t)\n\t}\n}\n\nfunc handler(w http.ResponseWriter, r *http.Request) {\n\t_, _ = w.Write([]byte(`hello`))\n}\n\nfunc getRoutes() []*webgo.Route {\n\treturn []*webgo.Route{\n\t\t{\n\t\t\tName:     \"hello\",\n\t\t\tPattern:  \"/hello\",\n\t\t\tMethod:   http.MethodGet,\n\t\t\tHandlers: []http.HandlerFunc{handler},\n\t\t},\n\t}\n}\nfunc setup(port string, routes []*webgo.Route) (*webgo.Router, error) {\n\tcfg := &webgo.Config{\n\t\tPort:            \"9696\",\n\t\tReadTimeout:     time.Second * 1,\n\t\tWriteTimeout:    time.Second * 1,\n\t\tShutdownTimeout: time.Second * 10,\n\t\tCertFile:        \"tests/ssl/server.crt\",\n\t\tKeyFile:         \"tests/ssl/server.key\",\n\t}\n\trouter := webgo.NewRouter(cfg, routes...)\n\n\treturn router, nil\n}\n"
  },
  {
    "path": "responses.go",
    "content": "package webgo\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"html/template\"\n\t\"net/http\"\n)\n\nvar (\n\tjsonErrPayload = []byte{}\n)\n\n// ErrorData used to render the error page\ntype ErrorData struct {\n\tErrCode        int\n\tErrDescription string\n}\n\n// dOutput is the standard/valid output wrapped in `{data: <payload>, status: <http response status>}`\ntype dOutput struct {\n\tData   interface{} `json:\"data\"`\n\tStatus int         `json:\"status\"`\n}\n\n// errOutput is the error output wrapped in `{errors:<errors>, status: <http response status>}`\ntype errOutput struct {\n\tErrors interface{} `json:\"errors\"`\n\tStatus int         `json:\"status\"`\n}\n\nconst (\n\t// HeaderContentType is the key for mentioning the response header content type\n\tHeaderContentType = \"Content-Type\"\n\t// JSONContentType is the MIME type when the response is JSON\n\tJSONContentType = \"application/json\"\n\t// HTMLContentType is the MIME type when the response is HTML\n\tHTMLContentType = \"text/html; charset=UTF-8\"\n\n\t// ErrInternalServer to send when there's an internal server error\n\tErrInternalServer = \"Internal server error\"\n)\n\n// SendHeader is used to send only a response header, i.e no response body\nfunc SendHeader(w http.ResponseWriter, rCode int) {\n\tw.WriteHeader(rCode)\n}\n\nfunc crwAsserter(w http.ResponseWriter, rCode int) http.ResponseWriter {\n\tif crw, ok := w.(*customResponseWriter); ok {\n\t\tcrw.statusCode = rCode\n\t\treturn crw\n\t}\n\n\treturn newCRW(w, rCode)\n}\n\n// Send sends a completely custom response without wrapping in the\n// `{data: <data>, status: <int>` struct\nfunc Send(w http.ResponseWriter, contentType string, data interface{}, rCode int) {\n\tw = crwAsserter(w, rCode)\n\tw.Header().Set(HeaderContentType, contentType)\n\t_, err := fmt.Fprint(w, data)\n\tif err != nil {\n\t\tw.WriteHeader(http.StatusInternalServerError)\n\t\t_, _ = w.Write([]byte(ErrInternalServer))\n\t\tLOGHANDLER.Error(err)\n\t}\n}\n\n// SendResponse is used to respond to any request (JSON response) based on the code, data etc.\nfunc SendResponse(w http.ResponseWriter, data interface{}, rCode int) {\n\tw = crwAsserter(w, rCode)\n\tw.Header().Add(HeaderContentType, JSONContentType)\n\terr := json.NewEncoder(w).Encode(dOutput{Data: data, Status: rCode})\n\tif err == nil {\n\t\treturn\n\t}\n\n\t// assuming the error was related to JSON encoding, so reattempting to respond\n\t// with a static payload. This could still fail in case of network write or other error(s)\n\tw = crwAsserter(w, http.StatusInternalServerError)\n\t_, _ = w.Write(jsonErrPayload)\n\tLOGHANDLER.Error(err)\n}\n\n// SendError is used to respond to any request with an error\nfunc SendError(w http.ResponseWriter, data interface{}, rCode int) {\n\tw = crwAsserter(w, rCode)\n\tw.Header().Add(HeaderContentType, JSONContentType)\n\terr := json.NewEncoder(w).Encode(errOutput{data, rCode})\n\tif err == nil {\n\t\treturn\n\t}\n\n\t// assuming the error was related to JSON encoding, so reattempting to respond\n\t// with a static payload. This could still fail in case of network write or other error(s)\n\tw = crwAsserter(w, http.StatusInternalServerError)\n\t_, _ = w.Write(jsonErrPayload)\n\tLOGHANDLER.Error(err)\n}\n\n// Render is used for rendering templates (HTML)\nfunc Render(w http.ResponseWriter, data interface{}, rCode int, tpl *template.Template) {\n\tw = crwAsserter(w, rCode)\n\n\t// In case of HTML response, setting appropriate header type for text/HTML response\n\tw.Header().Set(HeaderContentType, HTMLContentType)\n\n\t// Rendering an HTML template with appropriate data\n\terr := tpl.Execute(w, data)\n\tif err != nil {\n\t\tSend(w, \"text/plain\", ErrInternalServer, http.StatusInternalServerError)\n\t\tLOGHANDLER.Error(err.Error())\n\t}\n}\n\n// R200 - Successful/OK response\nfunc R200(w http.ResponseWriter, data interface{}) {\n\tSendResponse(w, data, http.StatusOK)\n}\n\n// R201 - New item created\nfunc R201(w http.ResponseWriter, data interface{}) {\n\tSendResponse(w, data, http.StatusCreated)\n}\n\n// R204 - empty, no content\nfunc R204(w http.ResponseWriter) {\n\tSendHeader(w, http.StatusNoContent)\n}\n\n// R302 - Temporary redirect\nfunc R302(w http.ResponseWriter, data interface{}) {\n\tSendResponse(w, data, http.StatusFound)\n}\n\n// R400 - Invalid request, any incorrect/erraneous value in the request body\nfunc R400(w http.ResponseWriter, data interface{}) {\n\tSendError(w, data, http.StatusBadRequest)\n}\n\n// R403 - Unauthorized access\nfunc R403(w http.ResponseWriter, data interface{}) {\n\tSendError(w, data, http.StatusForbidden)\n}\n\n// R404 - Resource not found\nfunc R404(w http.ResponseWriter, data interface{}) {\n\tSendError(w, data, http.StatusNotFound)\n}\n\n// R406 - Unacceptable header. For any error related to values set in header\nfunc R406(w http.ResponseWriter, data interface{}) {\n\tSendError(w, data, http.StatusNotAcceptable)\n}\n\n// R451 - Resource taken down because of a legal request\nfunc R451(w http.ResponseWriter, data interface{}) {\n\tSendError(w, data, http.StatusUnavailableForLegalReasons)\n}\n\n// R500 - Internal server error\nfunc R500(w http.ResponseWriter, data interface{}) {\n\tSendError(w, data, http.StatusInternalServerError)\n}\n"
  },
  {
    "path": "responses_test.go",
    "content": "package webgo\n\nimport (\n\t\"encoding/json\"\n\t\"html/template\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"reflect\"\n\t\"testing\"\n)\n\nfunc TestSendHeader(t *testing.T) {\n\tt.Parallel()\n\n\tw := httptest.NewRecorder()\n\tSendHeader(w, http.StatusNoContent)\n\tif w.Result().StatusCode != http.StatusNoContent {\n\t\tt.Errorf(\"Expected code '%d', got '%d'\", http.StatusNoContent, w.Result().StatusCode)\n\t}\n}\n\nfunc TestSendError(t *testing.T) {\n\tt.Parallel()\n\tw := httptest.NewRecorder()\n\tpayload := map[string]string{\"message\": \"hello world\"}\n\tSendError(w, payload, http.StatusBadRequest)\n\n\tresp := struct {\n\t\tErrors map[string]string\n\t}{}\n\n\tbody, err := io.ReadAll(w.Body)\n\tif err != nil {\n\t\tt.Error(err.Error())\n\t\treturn\n\t}\n\n\terr = json.Unmarshal(body, &resp)\n\tif err != nil {\n\t\tt.Error(err.Error())\n\t\treturn\n\t}\n\n\tif !reflect.DeepEqual(payload, resp.Errors) {\n\t\tt.Errorf(\n\t\t\t\"Expected '%v', got '%v'. Raw response: '%s'\",\n\t\t\tpayload,\n\t\t\tresp.Errors,\n\t\t\tstring(body),\n\t\t)\n\t}\n\tif w.Result().StatusCode != http.StatusBadRequest {\n\t\tt.Errorf(\n\t\t\t\"Expected response status code %d, got %d. Raw response: '%s'\",\n\t\t\thttp.StatusBadRequest,\n\t\t\tw.Result().StatusCode,\n\t\t\tstring(body),\n\t\t)\n\t}\n\n\t// testing invalid response body\n\tw = httptest.NewRecorder()\n\n\tinvResp := struct {\n\t\tErrors string\n\t}{}\n\tinvalidPayload := make(chan int)\n\tSendError(w, invalidPayload, http.StatusBadRequest)\n\tbody, err = io.ReadAll(w.Body)\n\tif err != nil {\n\t\tt.Error(err.Error())\n\t\treturn\n\t}\n\terr = json.Unmarshal(body, &invResp)\n\tif err != nil {\n\t\tt.Error(err.Error())\n\t\treturn\n\t}\n\n\tif invResp.Errors != `Internal server error` {\n\t\tt.Errorf(\n\t\t\t\"Expected 'Internal server error', got '%v'. Raw response: '%s'\",\n\t\t\tinvResp.Errors,\n\t\t\tstring(body),\n\t\t)\n\t}\n\n\tif w.Result().StatusCode != http.StatusInternalServerError {\n\t\tt.Errorf(\n\t\t\t\"Expected response status code %d, got %d. Raw response: '%s'\",\n\t\t\thttp.StatusInternalServerError,\n\t\t\tw.Result().StatusCode,\n\t\t\tstring(body),\n\t\t)\n\t}\n\n}\n\nfunc TestSendResponse(t *testing.T) {\n\tt.Parallel()\n\tw := httptest.NewRecorder()\n\tpayload := map[string]string{\"hello\": \"world\"}\n\n\tSendResponse(w, payload, http.StatusOK)\n\tbody, err := io.ReadAll(w.Body)\n\tif err != nil {\n\t\tt.Error(err.Error())\n\t\treturn\n\t}\n\n\tresp := struct {\n\t\tData map[string]string\n\t}{}\n\n\terr = json.Unmarshal(body, &resp)\n\tif err != nil {\n\t\tt.Error(err.Error())\n\t}\n\n\tif !reflect.DeepEqual(payload, resp.Data) {\n\t\tt.Errorf(\n\t\t\t\"Expected '%v', got '%v'. Raw response: '%s'\",\n\t\t\tpayload,\n\t\t\tresp.Data,\n\t\t\tstring(body),\n\t\t)\n\t}\n\n\tif w.Result().StatusCode != http.StatusOK {\n\t\tt.Errorf(\n\t\t\t\"Expected response status code %d, got %d. Raw response: '%s'\",\n\t\t\thttp.StatusOK,\n\t\t\tw.Result().StatusCode,\n\t\t\tstring(body),\n\t\t)\n\t}\n\n\t// testing invalid response payload\n\tw = httptest.NewRecorder()\n\tSendResponse(w, make(chan int), http.StatusOK)\n\tbody, err = io.ReadAll(w.Body)\n\tif err != nil {\n\t\tt.Error(err.Error())\n\t\treturn\n\t}\n\n\tinvalidresp := struct {\n\t\tErrors string\n\t}{}\n\n\terr = json.Unmarshal(body, &invalidresp)\n\tif err != nil {\n\t\tt.Error(err.Error())\n\t}\n\n\tif !reflect.DeepEqual(`Internal server error`, invalidresp.Errors) {\n\t\tt.Errorf(\n\t\t\t\"Expected '%v', got '%v'. Raw response: '%s'\",\n\t\t\tpayload,\n\t\t\tinvalidresp.Errors,\n\t\t\tstring(body),\n\t\t)\n\t}\n\n\tif w.Result().StatusCode != http.StatusInternalServerError {\n\t\tt.Errorf(\n\t\t\t\"Expected response status code %d, got %d. Raw response: '%s'\",\n\t\t\thttp.StatusInternalServerError,\n\t\t\tw.Result().StatusCode,\n\t\t\tstring(body),\n\t\t)\n\t}\n}\n\nfunc TestSend(t *testing.T) {\n\tt.Parallel()\n\tw := httptest.NewRecorder()\n\tpayload := map[string]string{\"hello\": \"world\"}\n\treqBody, _ := json.Marshal(payload)\n\n\tSend(w, JSONContentType, string(reqBody), http.StatusOK)\n\tbody, err := io.ReadAll(w.Body)\n\tif err != nil {\n\t\tt.Error(err.Error())\n\t\treturn\n\t}\n\n\tresp := map[string]string{}\n\n\terr = json.Unmarshal(body, &resp)\n\tif err != nil {\n\t\tt.Error(err.Error())\n\t}\n\n\tif !reflect.DeepEqual(payload, resp) {\n\t\tt.Errorf(\n\t\t\t\"Expected '%v', got '%v'. Raw response: '%s'\",\n\t\t\tpayload,\n\t\t\tresp,\n\t\t\tstring(body),\n\t\t)\n\t}\n\n\tif w.Result().StatusCode != http.StatusOK {\n\t\tt.Errorf(\n\t\t\t\"Expected response status code %d, got %d. Raw response: '%s'\",\n\t\t\thttp.StatusOK,\n\t\t\tw.Result().StatusCode,\n\t\t\tstring(body),\n\t\t)\n\t}\n}\n\nfunc TestRender(t *testing.T) {\n\tt.Parallel()\n\tw := httptest.NewRecorder()\n\tdata := struct {\n\t\tHello string\n\t}{\n\t\tHello: \"world\",\n\t}\n\ttpl := template.New(\"txttemp\")\n\ttpl, err := tpl.Parse(`{{.Hello}}`)\n\tif err != nil {\n\t\tt.Error(err.Error())\n\t\treturn\n\t}\n\tRender(w, data, http.StatusOK, tpl)\n\n\tbody, err := io.ReadAll(w.Body)\n\tif err != nil {\n\t\tt.Error(err.Error())\n\t\treturn\n\t}\n\tif w.Code != http.StatusOK {\n\t\tt.Errorf(\n\t\t\t\"Expected response status code %d, got %d. Raw response: '%s'\",\n\t\t\thttp.StatusOK,\n\t\t\tw.Code,\n\t\t\tstring(body),\n\t\t)\n\t}\n\n\tw = httptest.NewRecorder()\n\tinvaliddata := 0\n\n\ttpl = template.New(\"invalid\")\n\ttpl, err = tpl.Parse(`{{.Hello}}`)\n\tif err != nil {\n\t\tt.Error(err.Error())\n\t\treturn\n\t}\n\tRender(w, invaliddata, http.StatusOK, tpl)\n\n\tbody, err = io.ReadAll(w.Body)\n\tif err != nil {\n\t\tt.Error(err.Error())\n\t\treturn\n\t}\n\n\tstr := string(body)\n\twant := `Internal server error`\n\tif str != want {\n\t\tt.Errorf(\n\t\t\t\"Expected '%s', got '%s'. Raw response: '%s'\",\n\t\t\twant,\n\t\t\tstr,\n\t\t\tstr,\n\t\t)\n\t}\n\tif w.Code != http.StatusInternalServerError {\n\t\tt.Errorf(\n\t\t\t\"Expected response status code %d, got %d. Raw response: '%s'\",\n\t\t\thttp.StatusInternalServerError,\n\t\t\tw.Code,\n\t\t\tstring(body),\n\t\t)\n\t}\n}\n\nfunc TestResponsehelpers(t *testing.T) {\n\tt.Parallel()\n\tw := httptest.NewRecorder()\n\twant := \"hello world\"\n\tresp := struct {\n\t\tData   string\n\t\tErrors string\n\t\tStatus int\n\t}{}\n\n\tR200(w, want)\n\n\tbody, err := io.ReadAll(w.Body)\n\tif err != nil {\n\t\tt.Error(err.Error())\n\t\treturn\n\t}\n\n\terr = json.Unmarshal(body, &resp)\n\tif err != nil {\n\t\tt.Error(err.Error())\n\t\treturn\n\t}\n\n\tif resp.Data != want {\n\t\tt.Errorf(\n\t\t\t\"Expected '%s', got '%s'\",\n\t\t\twant,\n\t\t\tresp.Data,\n\t\t)\n\t}\n\tif w.Code != http.StatusOK {\n\t\tt.Errorf(\n\t\t\t\"Expected response status code %d, got %d. Raw response: '%s'\",\n\t\t\thttp.StatusOK,\n\t\t\tw.Code,\n\t\t\tstring(body),\n\t\t)\n\t}\n\n\t// R201\n\tw = httptest.NewRecorder()\n\tresp.Data = \"\"\n\tR201(w, want)\n\n\tbody, err = io.ReadAll(w.Body)\n\tif err != nil {\n\t\tt.Error(err.Error())\n\t\treturn\n\t}\n\n\terr = json.Unmarshal(body, &resp)\n\tif err != nil {\n\t\tt.Error(err.Error())\n\t\treturn\n\t}\n\n\tif resp.Data != want {\n\t\tt.Errorf(\n\t\t\t\"Expected '%s', got '%s'\",\n\t\t\twant,\n\t\t\tresp.Data,\n\t\t)\n\t}\n\tif w.Code != http.StatusCreated {\n\t\tt.Errorf(\n\t\t\t\"Expected response status code %d, got %d. Raw response: '%s'\",\n\t\t\thttp.StatusCreated,\n\t\t\tw.Code,\n\t\t\tstring(body),\n\t\t)\n\t}\n\n\t// R204\n\tw = httptest.NewRecorder()\n\tresp.Data = \"\"\n\tR204(w)\n\n\tbody, err = io.ReadAll(w.Body)\n\tif err != nil {\n\t\tt.Error(err.Error())\n\t\treturn\n\t}\n\tif string(body) != \"\" {\n\t\tt.Errorf(\n\t\t\t\"Expected empty response, got '%s'\",\n\t\t\tstring(body),\n\t\t)\n\t}\n\tif w.Code != http.StatusNoContent {\n\t\tt.Errorf(\n\t\t\t\"Expected response status code %d, got %d. Raw response: '%s'\",\n\t\t\thttp.StatusNoContent,\n\t\t\tw.Code,\n\t\t\tstring(body),\n\t\t)\n\t}\n\n\t// R302\n\tw = httptest.NewRecorder()\n\tresp.Data = \"\"\n\tR302(w, want)\n\n\tbody, err = io.ReadAll(w.Body)\n\tif err != nil {\n\t\tt.Error(err.Error())\n\t\treturn\n\t}\n\n\terr = json.Unmarshal(body, &resp)\n\tif err != nil {\n\t\tt.Error(err.Error())\n\t\treturn\n\t}\n\n\tif resp.Data != want {\n\t\tt.Errorf(\n\t\t\t\"Expected '%s', got '%s'\",\n\t\t\twant,\n\t\t\tresp.Data,\n\t\t)\n\t}\n\tif w.Code != http.StatusFound {\n\t\tt.Errorf(\n\t\t\t\"Expected response status code %d, got %d. Raw response: '%s'\",\n\t\t\thttp.StatusFound,\n\t\t\tw.Code,\n\t\t\tstring(body),\n\t\t)\n\t}\n\n\t// R400\n\tw = httptest.NewRecorder()\n\tresp.Data = \"\"\n\tresp.Errors = \"\"\n\tR400(w, want)\n\n\tbody, err = io.ReadAll(w.Body)\n\tif err != nil {\n\t\tt.Error(err.Error())\n\t\treturn\n\t}\n\n\terr = json.Unmarshal(body, &resp)\n\tif err != nil {\n\t\tt.Error(err.Error())\n\t\treturn\n\t}\n\n\tif resp.Errors != want {\n\t\tt.Errorf(\n\t\t\t\"Expected '%s', got '%s'\",\n\t\t\twant,\n\t\t\tresp.Errors,\n\t\t)\n\t}\n\tif w.Code != http.StatusBadRequest {\n\t\tt.Errorf(\n\t\t\t\"Expected response status code %d, got %d. Raw response: '%s'\",\n\t\t\thttp.StatusBadRequest,\n\t\t\tw.Code,\n\t\t\tstring(body),\n\t\t)\n\t}\n\n\t// R403\n\tw = httptest.NewRecorder()\n\tresp.Data = \"\"\n\tresp.Errors = \"\"\n\tR403(w, want)\n\n\tbody, err = io.ReadAll(w.Body)\n\tif err != nil {\n\t\tt.Error(err.Error())\n\t\treturn\n\t}\n\n\terr = json.Unmarshal(body, &resp)\n\tif err != nil {\n\t\tt.Error(err.Error())\n\t\treturn\n\t}\n\n\tif resp.Errors != want {\n\t\tt.Errorf(\n\t\t\t\"Expected '%s', got '%s'\",\n\t\t\twant,\n\t\t\tresp.Errors,\n\t\t)\n\t}\n\tif w.Code != http.StatusForbidden {\n\t\tt.Errorf(\n\t\t\t\"Expected response status code %d, got %d. Raw response: '%s'\",\n\t\t\thttp.StatusForbidden,\n\t\t\tw.Code,\n\t\t\tstring(body),\n\t\t)\n\t}\n\n\t// R404\n\tw = httptest.NewRecorder()\n\tresp.Data = \"\"\n\tresp.Errors = \"\"\n\tR404(w, want)\n\n\tbody, err = io.ReadAll(w.Body)\n\tif err != nil {\n\t\tt.Error(err.Error())\n\t\treturn\n\t}\n\n\terr = json.Unmarshal(body, &resp)\n\tif err != nil {\n\t\tt.Error(err.Error())\n\t\treturn\n\t}\n\n\tif resp.Errors != want {\n\t\tt.Errorf(\n\t\t\t\"Expected '%s', got '%s'\",\n\t\t\twant,\n\t\t\tresp.Errors,\n\t\t)\n\t}\n\tif w.Code != http.StatusNotFound {\n\t\tt.Errorf(\n\t\t\t\"Expected response status code %d, got %d. Raw response: '%s'\",\n\t\t\thttp.StatusNotFound,\n\t\t\tw.Code,\n\t\t\tstring(body),\n\t\t)\n\t}\n\n\t// R406\n\tw = httptest.NewRecorder()\n\tresp.Data = \"\"\n\tresp.Errors = \"\"\n\tR406(w, want)\n\n\tbody, err = io.ReadAll(w.Body)\n\tif err != nil {\n\t\tt.Error(err.Error())\n\t\treturn\n\t}\n\n\terr = json.Unmarshal(body, &resp)\n\tif err != nil {\n\t\tt.Error(err.Error())\n\t\treturn\n\t}\n\n\tif resp.Errors != want {\n\t\tt.Errorf(\n\t\t\t\"Expected '%s', got '%s'\",\n\t\t\twant,\n\t\t\tresp.Errors,\n\t\t)\n\t}\n\tif w.Code != http.StatusNotAcceptable {\n\t\tt.Errorf(\n\t\t\t\"Expected response status code %d, got %d. Raw response: '%s'\",\n\t\t\thttp.StatusNotAcceptable,\n\t\t\tw.Code,\n\t\t\tstring(body),\n\t\t)\n\t}\n\n\t// R451\n\tw = httptest.NewRecorder()\n\tresp.Data = \"\"\n\tresp.Errors = \"\"\n\tR451(w, want)\n\n\tbody, err = io.ReadAll(w.Body)\n\tif err != nil {\n\t\tt.Error(err.Error())\n\t\treturn\n\t}\n\n\terr = json.Unmarshal(body, &resp)\n\tif err != nil {\n\t\tt.Error(err.Error())\n\t\treturn\n\t}\n\n\tif resp.Errors != want {\n\t\tt.Errorf(\n\t\t\t\"Expected '%s', got '%s'\",\n\t\t\twant,\n\t\t\tresp.Errors,\n\t\t)\n\t}\n\tif w.Code != http.StatusUnavailableForLegalReasons {\n\t\tt.Errorf(\n\t\t\t\"Expected response status code %d, got %d. Raw response: '%s'\",\n\t\t\thttp.StatusUnavailableForLegalReasons,\n\t\t\tw.Code,\n\t\t\tstring(body),\n\t\t)\n\t}\n\n}\n"
  },
  {
    "path": "route.go",
    "content": "package webgo\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strings\"\n)\n\n// Route defines a route for each API\ntype Route struct {\n\t// Name is unique identifier for the route\n\tName string\n\t// Method is the HTTP request method/type\n\tMethod string\n\t// Pattern is the URI pattern to match\n\tPattern string\n\t// TrailingSlash if set to true, the URI will be matched with or without\n\t// a trailing slash. IMPORTANT: It does not redirect.\n\tTrailingSlash bool\n\n\t// FallThroughPostResponse if enabled will execute all the handlers even if a response was already sent to the client\n\tFallThroughPostResponse bool\n\n\t// Handlers is a slice of http.HandlerFunc which can be middlewares or anything else. Though only 1 of them will be allowed to respond to client.\n\t// subsequent writes from the following handlers will be ignored\n\tHandlers []http.HandlerFunc\n\n\thasWildcard bool\n\tfragments   []uriFragment\n\tparamsCount int\n\n\t// skipMiddleware if true, middleware added using `router` will not be applied to this Route.\n\t// This is used only when a Route is set using the RouteGroup, which can have its own set of middleware\n\tskipMiddleware bool\n\t// middlewareList is used at the last stage, i.e. right before starting the server\n\tmiddlewarelist []Middleware\n\n\tinitialized bool\n\n\tserve http.HandlerFunc\n}\ntype uriFragment struct {\n\tisVariable  bool\n\thasWildcard bool\n\t// fragment will be the key name, if it's a variable/named URI parameter\n\tfragment string\n}\n\nfunc (r *Route) parseURIWithParams() {\n\t// if there are no URI params, then there's no need to set route parts\n\tif !strings.Contains(r.Pattern, \":\") {\n\t\treturn\n\t}\n\n\tfragments := strings.Split(r.Pattern, \"/\")\n\tif len(fragments) == 1 {\n\t\treturn\n\t}\n\n\trFragments := make([]uriFragment, 0, len(fragments))\n\tfor _, fragment := range fragments[1:] {\n\t\thasParam := false\n\t\thasWildcard := false\n\n\t\tif strings.Contains(fragment, \":\") {\n\t\t\thasParam = true\n\t\t\tr.paramsCount++\n\t\t}\n\t\tif strings.Contains(fragment, \"*\") {\n\t\t\tr.hasWildcard = true\n\t\t\thasWildcard = true\n\t\t}\n\n\t\tkey := strings.ReplaceAll(fragment, \":\", \"\")\n\t\tkey = strings.ReplaceAll(key, \"*\", \"\")\n\t\trFragments = append(\n\t\t\trFragments,\n\t\t\turiFragment{\n\t\t\t\tisVariable:  hasParam,\n\t\t\t\thasWildcard: hasWildcard,\n\t\t\t\tfragment:    key,\n\t\t\t})\n\t}\n\tr.fragments = rFragments\n}\n\nfunc (r *Route) setupMiddleware(reverse bool) {\n\tif reverse {\n\t\tfor i := range r.middlewarelist {\n\t\t\tm := r.middlewarelist[i]\n\t\t\tsrv := r.serve\n\t\t\tr.serve = func(rw http.ResponseWriter, req *http.Request) {\n\t\t\t\tm(rw, req, srv)\n\t\t\t}\n\t\t}\n\t} else {\n\t\tfor i := len(r.middlewarelist) - 1; i >= 0; i-- {\n\t\t\tm := r.middlewarelist[i]\n\t\t\tsrv := r.serve\n\t\t\tr.serve = func(rw http.ResponseWriter, req *http.Request) {\n\t\t\t\tm(rw, req, srv)\n\t\t\t}\n\t\t}\n\t}\n\t// clear middlewarelist since it's already setup for the route\n\tr.middlewarelist = nil\n}\n\n// init does all the initializations required for the route\nfunc (r *Route) init() error {\n\tif r.initialized {\n\t\treturn nil\n\t}\n\tr.initialized = true\n\n\tr.parseURIWithParams()\n\tr.serve = defaultRouteServe(r)\n\treturn nil\n}\n\n// matchPath matches the requestURI with the URI pattern of the route\nfunc (r *Route) matchPath(requestURI string) (bool, map[string]string) {\n\tp := bytes.NewBufferString(r.Pattern)\n\tif r.TrailingSlash {\n\t\tp.WriteString(\"/\")\n\t} else {\n\t\tif requestURI[len(requestURI)-1] == '/' {\n\t\t\treturn false, nil\n\t\t}\n\t}\n\n\tif r.Pattern == requestURI || p.String() == requestURI {\n\t\treturn true, nil\n\t}\n\n\treturn r.matchWithWildcard(requestURI)\n}\n\nfunc (r *Route) matchWithWildcard(requestURI string) (bool, map[string]string) {\n\t// if r.fragments is empty, it means there are no variables in the URI pattern\n\t// hence no point checking\n\tif len(r.fragments) == 0 {\n\t\treturn false, nil\n\t}\n\n\tparams := make(map[string]string, r.paramsCount)\n\turiFragments := strings.Split(requestURI, \"/\")[1:]\n\tfragmentsLastIdx := len(r.fragments) - 1\n\tfragmentIdx := 0\n\turiParameter := make([]string, 0, len(uriFragments))\n\n\tfor idx, fragment := range uriFragments {\n\t\t// if part is empty, it means it's end of URI with trailing slash\n\t\tif fragment == \"\" {\n\t\t\tbreak\n\t\t}\n\n\t\tif fragmentIdx > fragmentsLastIdx {\n\t\t\treturn false, nil\n\t\t}\n\n\t\tcurrentFragment := r.fragments[fragmentIdx]\n\t\tif !currentFragment.isVariable && currentFragment.fragment != fragment {\n\t\t\treturn false, nil\n\t\t}\n\n\t\turiParameter = append(uriParameter, fragment)\n\t\tif currentFragment.isVariable {\n\t\t\tparams[currentFragment.fragment] = strings.Join(uriParameter, \"/\")\n\t\t}\n\n\t\tif !currentFragment.hasWildcard {\n\t\t\turiParameter = make([]string, 0, len(uriFragments)-idx)\n\t\t\tfragmentIdx++\n\t\t\tcontinue\n\t\t}\n\n\t\tnextIdx := fragmentIdx + 1\n\t\tif nextIdx > fragmentsLastIdx {\n\t\t\tcontinue\n\t\t}\n\t\tnextPart := r.fragments[nextIdx]\n\n\t\t// if the URI has more fragments/params after wildcard,\n\t\t// the immediately following part after wildcard cannot be a variable or another wildcard.\n\t\tif !nextPart.isVariable && nextPart.fragment == fragment {\n\t\t\t// remove the last added 'part' from parameters, as it's part of the static URI\n\t\t\tparams[currentFragment.fragment] = strings.Join(uriParameter[:len(uriParameter)-1], \"/\")\n\t\t\turiParameter = make([]string, 0, len(uriFragments)-idx)\n\t\t\tfragmentIdx += 2\n\t\t}\n\t}\n\n\tif len(params) != r.paramsCount {\n\t\treturn false, nil\n\t}\n\n\treturn true, params\n}\n\nfunc (r *Route) use(mm ...Middleware) {\n\tif r.middlewarelist == nil {\n\t\tr.middlewarelist = make([]Middleware, 0, len(mm))\n\t}\n\tr.middlewarelist = append(r.middlewarelist, mm...)\n}\n\nfunc routeServeChainedHandlers(r *Route) http.HandlerFunc {\n\treturn func(rw http.ResponseWriter, req *http.Request) {\n\n\t\tcrw, ok := rw.(*customResponseWriter)\n\t\tif !ok {\n\t\t\tcrw = newCRW(rw, http.StatusOK)\n\t\t}\n\n\t\tfor _, handler := range r.Handlers {\n\t\t\tif crw.written && !r.FallThroughPostResponse {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\thandler(crw, req)\n\t\t}\n\t}\n}\n\nfunc defaultRouteServe(r *Route) http.HandlerFunc {\n\tif len(r.Handlers) > 1 {\n\t\treturn routeServeChainedHandlers(r)\n\t}\n\n\t// when there is only 1 handler, custom response writer is not required to check if response\n\t// is already written or fallthrough is enabled\n\treturn r.Handlers[0]\n}\n\ntype RouteGroup struct {\n\troutes []*Route\n\t// skipRouterMiddleware if set to true, middleware applied to the router will not be applied\n\t// to this route group.\n\tskipRouterMiddleware bool\n\t// PathPrefix is the URI prefix for all routes in this group\n\tPathPrefix string\n}\n\nfunc (rg *RouteGroup) Add(rr ...Route) {\n\tfor idx := range rr {\n\t\troute := rr[idx]\n\t\troute.skipMiddleware = rg.skipRouterMiddleware\n\t\troute.Pattern = fmt.Sprintf(\"%s%s\", rg.PathPrefix, route.Pattern)\n\t\trg.routes = append(rg.routes, &route)\n\t}\n}\n\nfunc (rg *RouteGroup) Use(mm ...Middleware) {\n\tfor _, route := range rg.routes {\n\t\troute.use(mm...)\n\t}\n}\n\nfunc (rg *RouteGroup) Routes() []*Route {\n\treturn rg.routes\n}\n\nfunc NewRouteGroup(pathPrefix string, skipRouterMiddleware bool, rr ...Route) *RouteGroup {\n\trg := RouteGroup{\n\t\tPathPrefix:           pathPrefix,\n\t\tskipRouterMiddleware: skipRouterMiddleware,\n\t}\n\trg.Add(rr...)\n\treturn &rg\n}\n"
  },
  {
    "path": "route_test.go",
    "content": "package webgo\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"reflect\"\n\t\"testing\"\n)\n\nfunc TestRouteGroupsPathPrefix(t *testing.T) {\n\tt.Parallel()\n\troutes := []Route{\n\t\t{\n\t\t\tName:     \"r1\",\n\t\t\tPattern:  \"/a\",\n\t\t\tMethod:   http.MethodGet,\n\t\t\tHandlers: []http.HandlerFunc{dummyHandler},\n\t\t},\n\t\t{\n\t\t\tName:     \"r2\",\n\t\t\tPattern:  \"/b/:c\",\n\t\t\tMethod:   http.MethodGet,\n\t\t\tHandlers: []http.HandlerFunc{dummyHandler},\n\t\t},\n\t\t{\n\t\t\tName:     \"r3\",\n\t\t\tPattern:  \"/:w*\",\n\t\t\tMethod:   http.MethodGet,\n\t\t\tHandlers: []http.HandlerFunc{dummyHandler},\n\t\t},\n\t}\n\n\tconst prefix = \"/v7.0.0\"\n\texpectedSkipMiddleware := true\n\trg := NewRouteGroup(\"/v7.0.0\", expectedSkipMiddleware, routes...)\n\n\tlist := rg.Routes()\n\tfor idx := range list {\n\t\troute := list[idx]\n\t\toriginalRoute := routes[idx]\n\t\texpectedPattern := fmt.Sprintf(\"%s%s\", prefix, originalRoute.Pattern)\n\t\tif route.Pattern != expectedPattern {\n\t\t\tt.Errorf(\"Expected pattern %q, got %q\", expectedPattern, route.Pattern)\n\t\t}\n\t\tif route.skipMiddleware != expectedSkipMiddleware {\n\t\t\tt.Errorf(\"Expected skip %v, got %v\", expectedSkipMiddleware, route.skipMiddleware)\n\t\t}\n\t}\n}\n\nfunc dummyHandler(w http.ResponseWriter, r *http.Request) {}\n\nfunc BenchmarkMatchWithWildcard(b *testing.B) {\n\troute := Route{\n\t\tName:                    \"widlcard\",\n\t\tMethod:                  http.MethodGet,\n\t\tTrailingSlash:           true,\n\t\tFallThroughPostResponse: true,\n\t\tPattern:                 \"/:w*/static1/:myvar/:w2*\",\n\t\tHandlers:                []http.HandlerFunc{dummyHandler},\n\t}\n\n\turi := \"/hello/world/how/are/you/static1/hello2/world2/how2/are2/you2/static2\"\n\terr := route.init()\n\tif err != nil {\n\t\tb.Error(err)\n\t\treturn\n\t}\n\n\tfor i := 0; i < b.N; i++ {\n\t\tok, _ := route.matchPath(uri)\n\t\tif !ok {\n\t\t\tb.Errorf(\"Expected match, got no match\")\n\t\t\tbreak\n\t\t}\n\t}\n}\n\nfunc TestMatchWithWildcard(t *testing.T) {\n\troute := Route{\n\t\tName:                    \"widlcard\",\n\t\tMethod:                  http.MethodGet,\n\t\tTrailingSlash:           true,\n\t\tFallThroughPostResponse: true,\n\t\tPattern:                 \"/:w*/static1/:myvar/:w2*/static2\",\n\t\tHandlers:                []http.HandlerFunc{dummyHandler},\n\t}\n\terr := route.init()\n\tif err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n\n\turi := \"/hello/world/how/are/you/static1/hello2/world2/how2/are2/you2/static2\"\n\twantParams := map[string]string{\n\t\t\"w\":     \"hello/world/how/are/you\",\n\t\t\"myvar\": \"hello2\",\n\t\t\"w2\":    \"world2/how2/are2/you2\",\n\t}\n\tmatched, params := route.matchPath(uri)\n\tif !matched {\n\t\tt.Errorf(\"Expected match, got no match\")\n\t\treturn\n\t}\n\tif !reflect.DeepEqual(params, wantParams) {\n\t\tt.Errorf(\"Expected params %v, got %v\", wantParams, params)\n\t\treturn\n\t}\n\n\tt.Run(\"no match\", func(t *testing.T) {\n\t\troute := Route{\n\t\t\tName:                    \"widlcard\",\n\t\t\tMethod:                  http.MethodGet,\n\t\t\tTrailingSlash:           true,\n\t\t\tFallThroughPostResponse: true,\n\t\t\tPattern:                 \"/:w*/static1/:myvar/:w2*/static2\",\n\t\t\tHandlers:                []http.HandlerFunc{dummyHandler},\n\t\t}\n\t\terr := route.init()\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\treturn\n\t\t}\n\n\t\turi := \"/hello/world/how/are/you/static2/hello2/world2/how2/are2/you2/static2\"\n\t\tmatched, params := route.matchPath(uri)\n\t\tif matched {\n\t\t\tt.Errorf(\"Expected no match, got match\")\n\t\t\treturn\n\t\t}\n\t\tif params != nil {\n\t\t\tt.Errorf(\"Expected params %v, got %v\", nil, params)\n\t\t\treturn\n\t\t}\n\t})\n\tt.Run(\"match with more params\", func(t *testing.T) {\n\t\troute := Route{\n\t\t\tName:                    \"widlcard\",\n\t\t\tMethod:                  http.MethodGet,\n\t\t\tTrailingSlash:           true,\n\t\t\tFallThroughPostResponse: true,\n\t\t\tPattern:                 \"/:w*/static1/:myvar/:w2*/static2/:myvar2/:w3*/static3\",\n\t\t\tHandlers:                []http.HandlerFunc{dummyHandler},\n\t\t}\n\t\terr := route.init()\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\treturn\n\t\t}\n\n\t\turi := \"/hello/world/how/are/you/static1/hello2/world2/how2/are2/you2/static2/hello3/world3/how3/are3/you3/static3\"\n\t\twantParams := map[string]string{\n\t\t\t\"w\":      \"hello/world/how/are/you\",\n\t\t\t\"myvar\":  \"hello2\",\n\t\t\t\"w2\":     \"world2/how2/are2/you2\",\n\t\t\t\"myvar2\": \"hello3\",\n\t\t\t\"w3\":     \"world3/how3/are3/you3\",\n\t\t}\n\t\tmatched, params := route.matchPath(uri)\n\t\tif !matched {\n\t\t\tt.Errorf(\"Expected match, got no match\")\n\t\t\treturn\n\t\t}\n\t\tif !reflect.DeepEqual(params, wantParams) {\n\t\t\tt.Errorf(\"Expected params %v, got %v\", wantParams, params)\n\t\t\treturn\n\t\t}\n\t})\n\tt.Run(\"match - end with wildcard\", func(t *testing.T) {\n\t\troute := Route{\n\t\t\tName:                    \"widlcard\",\n\t\t\tMethod:                  http.MethodGet,\n\t\t\tTrailingSlash:           true,\n\t\t\tFallThroughPostResponse: true,\n\t\t\tPattern:                 \"/:w*/static1/:myvar/:w2*\",\n\t\t\tHandlers:                []http.HandlerFunc{dummyHandler},\n\t\t}\n\t\terr := route.init()\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\treturn\n\t\t}\n\n\t\turi := \"/hello/world/how/are/you/static1/hello2/world2/how2/are2/you2/static2\"\n\t\twantParams := map[string]string{\n\t\t\t\"w\":     \"hello/world/how/are/you\",\n\t\t\t\"myvar\": \"hello2\",\n\t\t\t\"w2\":    \"world2/how2/are2/you2/static2\",\n\t\t}\n\t\tmatched, params := route.matchPath(uri)\n\t\tif !matched {\n\t\t\tt.Errorf(\"Expected match, got no match\")\n\t\t\treturn\n\t\t}\n\t\tif !reflect.DeepEqual(params, wantParams) {\n\t\t\tt.Errorf(\"Expected params %v, got %v\", wantParams, params)\n\t\t\treturn\n\t\t}\n\t})\n\n\tt.Run(\"root URI, no match\", func(t *testing.T) {\n\t\troute := Route{\n\t\t\tName:                    \"\",\n\t\t\tMethod:                  http.MethodGet,\n\t\t\tTrailingSlash:           true,\n\t\t\tFallThroughPostResponse: true,\n\t\t\tPattern:                 \"/-/health\",\n\t\t\tHandlers:                []http.HandlerFunc{dummyHandler},\n\t\t}\n\t\terr := route.init()\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\treturn\n\t\t}\n\t\tmatched, _ := route.matchPath(\"/\")\n\t\tif matched {\n\t\t\tt.Errorf(\"Expected no match, got match\")\n\t\t\treturn\n\t\t}\n\t})\n\tt.Run(\"root URI, should match\", func(t *testing.T) {\n\t\troute := Route{\n\t\t\tName:                    \"\",\n\t\t\tMethod:                  http.MethodGet,\n\t\t\tTrailingSlash:           true,\n\t\t\tFallThroughPostResponse: true,\n\t\t\tPattern:                 \"/\",\n\t\t\tHandlers:                []http.HandlerFunc{dummyHandler},\n\t\t}\n\t\terr := route.init()\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\treturn\n\t\t}\n\t\tmatched, _ := route.matchPath(\"/\")\n\t\tif !matched {\n\t\t\tt.Errorf(\"Expected match, got no match\")\n\t\t\treturn\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "router.go",
    "content": "package webgo\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"sync\"\n)\n\n// httpResponseWriter has all the functions to be implemented by the custom\n// responsewriter used\ntype httpResponseWriter interface {\n\thttp.ResponseWriter\n\thttp.Flusher\n\thttp.Hijacker\n\thttp.Pusher\n}\n\nfunc init() {\n\tvar err error\n\tjsonErrPayload, err = json.Marshal(errOutput{\n\t\tErrors: ErrInternalServer,\n\t\tStatus: http.StatusInternalServerError,\n\t})\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// ensure the custom response writer implements all the required functions\n\tcrw := &customResponseWriter{}\n\t_ = httpResponseWriter(crw)\n}\n\nvar (\n\tvalidHTTPMethods = []string{\n\t\thttp.MethodOptions,\n\t\thttp.MethodHead,\n\t\thttp.MethodGet,\n\t\thttp.MethodPost,\n\t\thttp.MethodPut,\n\t\thttp.MethodPatch,\n\t\thttp.MethodDelete,\n\t}\n\n\tctxPool = &sync.Pool{\n\t\tNew: func() interface{} {\n\t\t\treturn new(ContextPayload)\n\t\t},\n\t}\n\tcrwPool = &sync.Pool{\n\t\tNew: func() interface{} {\n\t\t\treturn new(customResponseWriter)\n\t\t},\n\t}\n)\n\n// customResponseWriter is a custom HTTP response writer\ntype customResponseWriter struct {\n\thttp.ResponseWriter\n\tstatusCode    int\n\twritten       bool\n\theaderWritten bool\n}\n\n// WriteHeader is the interface implementation to get HTTP response code and add\n// it to the custom response writer\nfunc (crw *customResponseWriter) WriteHeader(code int) {\n\tif crw.headerWritten {\n\t\treturn\n\t}\n\n\tcrw.headerWritten = true\n\tcrw.statusCode = code\n\tcrw.ResponseWriter.WriteHeader(code)\n}\n\n// Write is the interface implementation to respond to the HTTP request,\n// but check if a response was already sent.\nfunc (crw *customResponseWriter) Write(body []byte) (int, error) {\n\tcrw.WriteHeader(crw.statusCode)\n\tcrw.written = true\n\treturn crw.ResponseWriter.Write(body)\n}\n\n// Flush calls the http.Flusher to clear/flush the buffer\nfunc (crw *customResponseWriter) Flush() {\n\tif rw, ok := crw.ResponseWriter.(http.Flusher); ok {\n\t\trw.Flush()\n\t}\n}\n\n// Hijack implements the http.Hijacker interface\nfunc (crw *customResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {\n\tif hj, ok := crw.ResponseWriter.(http.Hijacker); ok {\n\t\treturn hj.Hijack()\n\t}\n\n\treturn nil, nil, errors.New(\"unable to create hijacker\")\n}\n\nfunc (crw *customResponseWriter) Push(target string, opts *http.PushOptions) error {\n\tif n, ok := crw.ResponseWriter.(http.Pusher); ok {\n\t\treturn n.Push(target, opts)\n\t}\n\treturn errors.New(\"pusher not implemented\")\n}\n\nfunc (crw *customResponseWriter) reset() {\n\tcrw.statusCode = 0\n\tcrw.written = false\n\tcrw.headerWritten = false\n\tcrw.ResponseWriter = nil\n}\n\n// Middleware is the signature of WebGo's middleware\ntype Middleware func(http.ResponseWriter, *http.Request, http.HandlerFunc)\n\n// discoverRoute returns the correct 'route', for the given request\nfunc discoverRoute(path string, routes []*Route) (*Route, map[string]string) {\n\tfor _, route := range routes {\n\t\tif ok, params := route.matchPath(path); ok {\n\t\t\treturn route, params\n\t\t}\n\t}\n\treturn nil, nil\n}\n\n// Router is the HTTP router\ntype Router struct {\n\toptHandlers    []*Route\n\theadHandlers   []*Route\n\tgetHandlers    []*Route\n\tpostHandlers   []*Route\n\tputHandlers    []*Route\n\tpatchHandlers  []*Route\n\tdeleteHandlers []*Route\n\tallHandlers    map[string][]*Route\n\n\t// NotFound is the generic handler for 404 resource not found response\n\tNotFound http.HandlerFunc\n\n\t// NotImplemented is the generic handler for 501 method not implemented\n\tNotImplemented http.HandlerFunc\n\n\t// config has all the app config\n\tconfig *Config\n\n\t// httpServer is the server handler for the active HTTP server\n\thttpServer *http.Server\n\t// httpsServer is the server handler for the active HTTPS server\n\thttpsServer *http.Server\n}\n\n// methodRoutes returns the list of Routes handling the HTTP method given the request\nfunc (rtr *Router) methodRoutes(method string) (routes []*Route) {\n\tswitch method {\n\tcase http.MethodOptions:\n\t\treturn rtr.optHandlers\n\tcase http.MethodHead:\n\t\treturn rtr.headHandlers\n\tcase http.MethodGet:\n\t\treturn rtr.getHandlers\n\tcase http.MethodPost:\n\t\treturn rtr.postHandlers\n\tcase http.MethodPut:\n\t\treturn rtr.putHandlers\n\tcase http.MethodPatch:\n\t\treturn rtr.patchHandlers\n\tcase http.MethodDelete:\n\t\treturn rtr.deleteHandlers\n\t}\n\n\treturn nil\n}\n\nfunc (rtr *Router) ServeHTTP(rw http.ResponseWriter, r *http.Request) {\n\t// a custom response writer is used to set appropriate HTTP status code in case of\n\t// encoding errors. i.e. if there's a JSON encoding issue while responding,\n\t// the HTTP status code would say 200, and and the JSON payload {\"status\": 500}\n\tcrw := newCRW(rw, http.StatusOK)\n\n\troutes := rtr.methodRoutes(r.Method)\n\tif routes == nil {\n\t\t// serve 501 when HTTP method is not implemented\n\t\tcrw.statusCode = http.StatusNotImplemented\n\t\trtr.NotImplemented(crw, r)\n\t\treleaseCRW(crw)\n\t\treturn\n\t}\n\n\tpath := r.URL.EscapedPath()\n\troute, params := discoverRoute(path, routes)\n\tif route == nil {\n\t\t// serve 404 when there are no matching routes\n\t\tcrw.statusCode = http.StatusNotFound\n\t\trtr.NotFound(crw, r)\n\t\treleaseCRW(crw)\n\t\treturn\n\t}\n\n\tctxPayload := newContext()\n\tctxPayload.Route = route\n\tctxPayload.URIParams = params\n\n\t// webgo context is injected to the HTTP request context\n\t*r = *r.WithContext(\n\t\tcontext.WithValue(\n\t\t\tr.Context(),\n\t\t\twgoCtxKey,\n\t\t\tctxPayload,\n\t\t),\n\t)\n\n\tdefer releasePoolResources(crw, ctxPayload)\n\troute.serve(crw, r)\n}\n\n// Use adds a middleware layer\nfunc (rtr *Router) Use(mm ...Middleware) {\n\tfor _, handlers := range rtr.allHandlers {\n\t\tfor idx := range handlers {\n\t\t\troute := handlers[idx]\n\t\t\tif route.skipMiddleware {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\troute.use(mm...)\n\t\t}\n\t}\n}\n\n// UseOnSpecialHandlers adds middleware to the 2 special handlers of webgo\nfunc (rtr *Router) UseOnSpecialHandlers(mm ...Middleware) {\n\t// v3.2.1 introduced the feature of adding middleware to both notfound & not implemented\n\t// handlers\n\t/*\n\t\t- It was added considering an `accesslog` middleware, where all requests should be logged\n\t\t# This is now being moved to a separate function considering an authentication middleware, where all requests\n\t\t  including 404 & 501 would respond with `not authenticated` if you do not have special handling\n\t\t  within the middleware. It is a cleaner implementation to avoid this and let users add their\n\t\t  middleware separately to NOTFOUND & NOTIMPLEMENTED handlers\n\t*/\n\n\tfor idx := range mm {\n\t\tm := mm[idx]\n\t\tnf := rtr.NotFound\n\t\trtr.NotFound = func(rw http.ResponseWriter, req *http.Request) {\n\t\t\tm(rw, req, nf)\n\t\t}\n\n\t\tni := rtr.NotImplemented\n\t\trtr.NotImplemented = func(rw http.ResponseWriter, req *http.Request) {\n\t\t\tm(rw, req, ni)\n\t\t}\n\t}\n}\n\n// Add is a convenience method used to add a new route to an already initialized router\n// Important: `.Use` should be used only after all routes are added\nfunc (rtr *Router) Add(routes ...*Route) {\n\thmap := httpHandlers(routes)\n\trtr.optHandlers = append(rtr.optHandlers, hmap[http.MethodOptions]...)\n\trtr.headHandlers = append(rtr.headHandlers, hmap[http.MethodHead]...)\n\trtr.getHandlers = append(rtr.getHandlers, hmap[http.MethodGet]...)\n\trtr.postHandlers = append(rtr.postHandlers, hmap[http.MethodPost]...)\n\trtr.putHandlers = append(rtr.putHandlers, hmap[http.MethodPut]...)\n\trtr.patchHandlers = append(rtr.patchHandlers, hmap[http.MethodPatch]...)\n\trtr.deleteHandlers = append(rtr.deleteHandlers, hmap[http.MethodDelete]...)\n\n\tall := rtr.allHandlers\n\tif all == nil {\n\t\tall = map[string][]*Route{}\n\t}\n\n\tfor _, key := range supportedHTTPMethods {\n\t\tnewlist, hasKey := hmap[key]\n\t\tif !hasKey {\n\t\t\tcontinue\n\t\t}\n\t\tif all[key] == nil {\n\t\t\tall[key] = make([]*Route, 0, len(hmap))\n\t\t}\n\t\tall[key] = append(all[key], newlist...)\n\t}\n\n\trtr.allHandlers = all\n}\n\nfunc newCRW(rw http.ResponseWriter, rCode int) *customResponseWriter {\n\tcrw := crwPool.Get().(*customResponseWriter)\n\tcrw.ResponseWriter = rw\n\tcrw.statusCode = rCode\n\treturn crw\n}\n\nfunc releaseCRW(crw *customResponseWriter) {\n\tcrw.reset()\n\tcrwPool.Put(crw)\n}\n\nfunc newContext() *ContextPayload {\n\treturn ctxPool.Get().(*ContextPayload)\n}\n\nfunc releaseContext(cp *ContextPayload) {\n\tcp.reset()\n\tctxPool.Put(cp)\n}\n\nfunc releasePoolResources(crw *customResponseWriter, cp *ContextPayload) {\n\treleaseCRW(crw)\n\treleaseContext(cp)\n}\n\n// NewRouter initializes & returns a new router instance with all the configurations and routes set\nfunc NewRouter(cfg *Config, routes ...*Route) *Router {\n\tr := &Router{\n\t\tNotFound: http.NotFound,\n\t\tNotImplemented: func(rw http.ResponseWriter, req *http.Request) {\n\t\t\tSend(rw, \"\", \"501 Not Implemented\", http.StatusNotImplemented)\n\t\t},\n\t\tconfig: cfg,\n\t}\n\n\tr.Add(routes...)\n\n\treturn r\n}\n\n// checkDuplicateRoutes checks if any of the routes have duplicate name or URI pattern\nfunc checkDuplicateRoutes(idx int, route *Route, routes []*Route) {\n\t// checking if the URI pattern is duplicated\n\tfor i := 0; i < idx; i++ {\n\t\trt := routes[i]\n\n\t\tif rt.Name == route.Name {\n\t\t\tLOGHANDLER.Info(\n\t\t\t\tfmt.Sprintf(\n\t\t\t\t\t\"Duplicate route name('%s') detected\",\n\t\t\t\t\trt.Name,\n\t\t\t\t),\n\t\t\t)\n\t\t}\n\n\t\tif rt.Method != route.Method {\n\t\t\tcontinue\n\t\t}\n\n\t\t// regex pattern match\n\t\tif ok, _ := rt.matchPath(route.Pattern); !ok {\n\t\t\tcontinue\n\t\t}\n\n\t\tLOGHANDLER.Warn(\n\t\t\tfmt.Sprintf(\n\t\t\t\t\"Duplicate URI pattern detected.\\nPattern: '%s'\\nDuplicate pattern: '%s'\",\n\t\t\t\trt.Pattern,\n\t\t\t\troute.Pattern,\n\t\t\t),\n\t\t)\n\t\tLOGHANDLER.Warn(\"Only the first route to match the URI pattern would handle the request\")\n\t}\n}\n\n// httpHandlers returns all the handlers in a map, for each HTTP method\nfunc httpHandlers(routes []*Route) map[string][]*Route {\n\thandlers := map[string][]*Route{}\n\n\thandlers[http.MethodHead] = []*Route{}\n\thandlers[http.MethodGet] = []*Route{}\n\n\tfor idx, route := range routes {\n\t\tfound := false\n\t\tfor _, validMethod := range validHTTPMethods {\n\t\t\tif route.Method == validMethod {\n\t\t\t\tfound = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif !found {\n\t\t\tLOGHANDLER.Fatal(\n\t\t\t\tfmt.Sprintf(\n\t\t\t\t\t\"Unsupported HTTP method provided. Method: '%s'\",\n\t\t\t\t\troute.Method,\n\t\t\t\t),\n\t\t\t)\n\t\t\treturn nil\n\t\t}\n\n\t\tif len(route.Handlers) == 0 {\n\t\t\tLOGHANDLER.Fatal(\n\t\t\t\tfmt.Sprintf(\n\t\t\t\t\t\"No handlers provided for the route '%s', method '%s'\",\n\t\t\t\t\troute.Pattern,\n\t\t\t\t\troute.Method,\n\t\t\t\t),\n\t\t\t)\n\t\t\treturn nil\n\t\t}\n\n\t\terr := route.init()\n\t\tif err != nil {\n\t\t\tLOGHANDLER.Fatal(\"Unsupported URI pattern.\", route.Pattern, err)\n\t\t\treturn nil\n\t\t}\n\n\t\tcheckDuplicateRoutes(idx, route, routes)\n\n\t\thandlers[route.Method] = append(handlers[route.Method], route)\n\t}\n\n\treturn handlers\n}\n"
  },
  {
    "path": "router_test.go",
    "content": "package webgo\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestRouter_ServeHTTP(t *testing.T) {\n\tt.Parallel()\n\tport := \"9696\"\n\trouter, err := setup(t, port)\n\tif err != nil {\n\t\tt.Error(err.Error())\n\t\treturn\n\t}\n\tm := func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {\n\t\tw.Header().Add(\"middleware\", \"true\")\n\t\tnext(w, r)\n\t}\n\trouter.Use(m)\n\trouter.UseOnSpecialHandlers(m)\n\trouter.SetupMiddleware()\n\n\tlist := testTable()\n\n\tbaseAPI := fmt.Sprintf(\"http://localhost:%s\", port)\n\n\tfor _, l := range list {\n\t\turl := baseAPI\n\t\tif l.Path != \"\" {\n\t\t\tswitch l.TestType {\n\t\t\tcase \"checkpath\",\n\t\t\t\t\"checkpathnotrailingslash\",\n\t\t\t\t\"chaining\",\n\t\t\t\t\"notfound\",\n\t\t\t\t\"chaining-nofallthrough\":\n\t\t\t\t{\n\t\t\t\t\turl = strings.Join([]string{url, l.Path}, \"\")\n\t\t\t\t}\n\t\t\tcase \"checkparams\", \"widlcardwithouttrailingslash\":\n\t\t\t\t{\n\t\t\t\t\tfor idx, key := range l.ParamKeys {\n\t\t\t\t\t\t// in case of wildcard params, they have to be replaced first for proper URL construction\n\t\t\t\t\t\tl.Path = strings.Replace(l.Path, \":\"+key+\"*\", l.Params[idx], 1)\n\t\t\t\t\t\tl.Path = strings.Replace(l.Path, \":\"+key, l.Params[idx], 1)\n\t\t\t\t\t}\n\t\t\t\t\turl = strings.Join([]string{url, l.Path}, \"\")\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\trespRec := httptest.NewRecorder()\n\t\treq := httptest.NewRequest(\n\t\t\tl.Method,\n\t\t\turl,\n\t\t\tl.Body,\n\t\t)\n\t\trouter.ServeHTTP(respRec, req)\n\n\t\tswitch l.TestType {\n\t\tcase \"checkpath\", \"checkpathnotrailingslash\":\n\t\t\t{\n\t\t\t\terr = checkPath(req, respRec)\n\t\t\t}\n\t\tcase \"widlcardwithouttrailingslash\":\n\t\t\t{\n\t\t\t\terr = checkPathWildCard(req, respRec)\n\t\t\t}\n\t\tcase \"chaining\":\n\t\t\t{\n\t\t\t\terr = checkChaining(req, respRec)\n\t\t\t}\n\t\tcase \"checkparams\":\n\t\t\t{\n\t\t\t\terr = checkParams(req, respRec, l.ParamKeys, l.Params)\n\t\t\t}\n\t\tcase \"notimplemented\":\n\t\t\t{\n\t\t\t\terr = checkNotImplemented(req, respRec)\n\t\t\t}\n\t\tcase \"notfound\":\n\t\t\t{\n\t\t\t\terr = checkNotFound(req, respRec)\n\t\t\t}\n\t\t}\n\n\t\tif err != nil && !l.WantErr {\n\t\t\tt.Errorf(\n\t\t\t\t\"'%s' (%s '%s'): %s\",\n\t\t\t\tl.Name,\n\t\t\t\tl.Method,\n\t\t\t\turl,\n\t\t\t\terr.Error(),\n\t\t\t)\n\t\t\tif l.Err != nil {\n\t\t\t\tif !errors.Is(err, l.Err) {\n\t\t\t\t\tt.Errorf(\n\t\t\t\t\t\t\"expected error '%s', got %s\",\n\t\t\t\t\t\tl.Err.Error(),\n\t\t\t\t\t\terr.Error(),\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t}\n\t\t} else if err == nil && l.WantErr {\n\t\t\tt.Errorf(\n\t\t\t\t\"'%s' (%s '%s') expected error, but received nil\",\n\t\t\t\tl.Name,\n\t\t\t\tl.Method,\n\t\t\t\turl,\n\t\t\t)\n\t\t}\n\n\t\terr = checkMiddleware(req, respRec)\n\t\tif err != nil {\n\t\t\tt.Error(err.Error())\n\t\t}\n\t}\n}\n\nfunc setup(t *testing.T, port string) (*Router, error) {\n\tt.Helper()\n\tcfg := &Config{\n\t\tPort:            port,\n\t\tReadTimeout:     time.Second * 1,\n\t\tWriteTimeout:    time.Second * 1,\n\t\tShutdownTimeout: time.Second * 10,\n\t\tCertFile:        \"tests/ssl/server.crt\",\n\t\tKeyFile:         \"tests/ssl/server.key\",\n\t}\n\trouter := NewRouter(cfg, getRoutes(t)...)\n\treturn router, nil\n}\n\nfunc getRoutes(t *testing.T) []*Route {\n\tt.Helper()\n\n\tlist := testTable()\n\trr := make([]*Route, 0, len(list))\n\tfor _, l := range list {\n\t\tswitch l.TestType {\n\t\tcase \"checkpath\", \"checkparams\", \"checkparamswildcard\":\n\t\t\t{\n\t\t\t\trr = append(rr,\n\t\t\t\t\t&Route{\n\t\t\t\t\t\tName:                    l.Name,\n\t\t\t\t\t\tMethod:                  l.Method,\n\t\t\t\t\t\tPattern:                 l.Path,\n\t\t\t\t\t\tTrailingSlash:           true,\n\t\t\t\t\t\tFallThroughPostResponse: false,\n\t\t\t\t\t\tHandlers:                []http.HandlerFunc{successHandler},\n\t\t\t\t\t},\n\t\t\t\t)\n\t\t\t}\n\t\tcase \"checkpathnotrailingslash\", \"widlcardwithouttrailingslash\":\n\t\t\t{\n\t\t\t\trr = append(rr,\n\t\t\t\t\t&Route{\n\t\t\t\t\t\tName:                    l.Name,\n\t\t\t\t\t\tMethod:                  l.Method,\n\t\t\t\t\t\tPattern:                 l.Path,\n\t\t\t\t\t\tTrailingSlash:           false,\n\t\t\t\t\t\tFallThroughPostResponse: false,\n\t\t\t\t\t\tHandlers:                []http.HandlerFunc{successHandler},\n\t\t\t\t\t},\n\t\t\t\t)\n\n\t\t\t}\n\n\t\tcase \"chaining\":\n\t\t\t{\n\t\t\t\trr = append(\n\t\t\t\t\trr,\n\t\t\t\t\t&Route{\n\t\t\t\t\t\tName:                    l.Name,\n\t\t\t\t\t\tMethod:                  l.Method,\n\t\t\t\t\t\tPattern:                 l.Path,\n\t\t\t\t\t\tTrailingSlash:           false,\n\t\t\t\t\t\tFallThroughPostResponse: false,\n\t\t\t\t\t\tHandlers:                []http.HandlerFunc{chainHandler, successHandler},\n\t\t\t\t\t},\n\t\t\t\t)\n\t\t\t}\n\t\tcase \"chaining-nofallthrough\":\n\t\t\t{\n\t\t\t\t{\n\t\t\t\t\trr = append(\n\t\t\t\t\t\trr,\n\t\t\t\t\t\t&Route{\n\t\t\t\t\t\t\tName:                    l.Name,\n\t\t\t\t\t\t\tMethod:                  l.Method,\n\t\t\t\t\t\t\tPattern:                 l.Path,\n\t\t\t\t\t\t\tTrailingSlash:           false,\n\t\t\t\t\t\t\tFallThroughPostResponse: false,\n\t\t\t\t\t\t\tHandlers:                []http.HandlerFunc{chainHandler, chainNoFallthroughHandler, successHandler},\n\t\t\t\t\t\t},\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn rr\n}\n\nfunc chainHandler(w http.ResponseWriter, r *http.Request) {\n\tw.Header().Add(\"chained\", \"true\")\n}\n\nfunc chainNoFallthroughHandler(w http.ResponseWriter, r *http.Request) {\n\tw.Header().Add(\"chained\", \"true\")\n\t_, _ = w.Write([]byte(`yay, blocked!`))\n}\n\nfunc successHandler(w http.ResponseWriter, r *http.Request) {\n\twctx := Context(r)\n\tparams := wctx.Params()\n\tR200(\n\t\tw,\n\t\tmap[string]interface{}{\n\t\t\t\"path\":   r.URL.Path,\n\t\t\t\"params\": params,\n\t\t},\n\t)\n}\n\nfunc checkPath(req *http.Request, resp *httptest.ResponseRecorder) error {\n\twant := req.URL.EscapedPath()\n\trbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error reading response, '%s'\", err.Error())\n\t}\n\n\tbody := struct {\n\t\tData struct {\n\t\t\tPath   string\n\t\t\tParams map[string]string\n\t\t}\n\t}{}\n\terr = json.Unmarshal(rbody, &body)\n\tif err != nil {\n\t\treturn fmt.Errorf(\n\t\t\t\"json decode failed '%s', got response: '%s'\",\n\t\t\terr.Error(),\n\t\t\tstring(rbody),\n\t\t)\n\t}\n\n\tif want != body.Data.Path {\n\t\treturn fmt.Errorf(\"wanted URI path '%s', got '%s'\", want, body.Data.Path)\n\t}\n\n\treturn nil\n}\n\nfunc checkPathWildCard(req *http.Request, resp *httptest.ResponseRecorder) error {\n\twant := req.URL.EscapedPath()\n\trbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error reading response, '%s'\", err.Error())\n\t}\n\n\tbody := struct {\n\t\tData struct {\n\t\t\tPath   string\n\t\t\tParams map[string]string\n\t\t}\n\t}{}\n\terr = json.Unmarshal(rbody, &body)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"json decode failed '%s', got response: '%s'\", err.Error(), string(rbody))\n\t}\n\n\tif want != body.Data.Path {\n\t\treturn fmt.Errorf(\"wanted URI path '%s', got '%s'\", want, body.Data.Path)\n\t}\n\n\tif len(body.Data.Params) != 1 {\n\t\treturn fmt.Errorf(\"expected no.of params: %d, got %d. response: '%s'\", 1, len(body.Data.Params), string(rbody))\n\t}\n\n\twantWildcardParamValue := \"\"\n\tparts := strings.Split(want, \"/\")[2:]\n\twantWildcardParamValue = strings.Join(parts, \"/\")\n\tif body.Data.Params[\"a\"] != wantWildcardParamValue {\n\t\treturn fmt.Errorf(\n\t\t\t\"wildcard value\\nexpected: %s\\ngot: %s\",\n\t\t\twantWildcardParamValue,\n\t\t\tbody.Data.Params[\"a\"],\n\t\t)\n\t}\n\n\treturn nil\n}\n\nfunc checkParams(req *http.Request, resp *httptest.ResponseRecorder, keys []string, expected []string) error {\n\trbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error reading response, '%s'\", err.Error())\n\t}\n\n\tbody := struct {\n\t\tData struct {\n\t\t\tParams map[string]string\n\t\t}\n\t}{}\n\terr = json.Unmarshal(rbody, &body)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"json decode failed '%s', for response '%s'\", err.Error(), string(rbody))\n\t}\n\n\tfor idx, key := range keys {\n\t\twant := expected[idx]\n\t\tif body.Data.Params[key] != want {\n\t\t\treturn fmt.Errorf(\n\t\t\t\t\"expected value for '%s' is '%s', got '%s'\",\n\t\t\t\tkey,\n\t\t\t\twant,\n\t\t\t\tbody.Data.Params[key],\n\t\t\t)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc checkNotImplemented(req *http.Request, resp *httptest.ResponseRecorder) error {\n\tif resp.Result().StatusCode != http.StatusNotImplemented {\n\t\treturn fmt.Errorf(\n\t\t\t\"expected code %d, got %d\",\n\t\t\thttp.StatusNotImplemented,\n\t\t\tresp.Code,\n\t\t)\n\t}\n\treturn nil\n}\n\nfunc checkNotFound(req *http.Request, resp *httptest.ResponseRecorder) error {\n\tif resp.Result().StatusCode != http.StatusNotFound {\n\t\treturn fmt.Errorf(\n\t\t\t\"expected code %d, got %d\",\n\t\t\thttp.StatusNotFound,\n\t\t\tresp.Code,\n\t\t)\n\t}\n\treturn nil\n}\n\nfunc checkChaining(req *http.Request, resp *httptest.ResponseRecorder) error {\n\tif resp.Header().Get(\"chained\") != \"true\" {\n\t\treturn fmt.Errorf(\n\t\t\t\"Expected header value for 'chained', to be 'true', got '%s'\",\n\t\t\tresp.Header().Get(\"chained\"),\n\t\t)\n\t}\n\treturn nil\n}\n\nfunc checkMiddleware(req *http.Request, resp *httptest.ResponseRecorder) error {\n\tif resp.Header().Get(\"middleware\") != \"true\" {\n\t\treturn fmt.Errorf(\n\t\t\t\"Expected header value for 'middleware', to be 'true', got '%s'\",\n\t\t\tresp.Header().Get(\"middleware\"),\n\t\t)\n\t}\n\treturn nil\n}\n\nfunc testTable() []struct {\n\tName      string\n\tTestType  string\n\tPath      string\n\tMethod    string\n\tWant      interface{}\n\tWantErr   bool\n\tErr       error\n\tParamKeys []string\n\tParams    []string\n\tBody      io.Reader\n} {\n\treturn []struct {\n\t\tName      string\n\t\tTestType  string\n\t\tPath      string\n\t\tMethod    string\n\t\tWant      interface{}\n\t\tWantErr   bool\n\t\tErr       error\n\t\tParamKeys []string\n\t\tParams    []string\n\t\tBody      io.Reader\n\t}{\n\t\t{\n\t\t\tName:     \"Check root path without params\",\n\t\t\tTestType: \"checkpath\",\n\t\t\tPath:     \"/\",\n\t\t\tMethod:   http.MethodGet,\n\t\t\tWantErr:  false,\n\t\t},\n\t\t{\n\t\t\tName:     \"Check root path without params - duplicate\",\n\t\t\tTestType: \"checkpath\",\n\t\t\tPath:     \"/\",\n\t\t\tMethod:   http.MethodGet,\n\t\t\tWantErr:  false,\n\t\t},\n\t\t{\n\t\t\tName:     \"Check nested path without params - 1\",\n\t\t\tTestType: \"checkpath\",\n\t\t\tPath:     \"/a\",\n\t\t\tMethod:   http.MethodGet,\n\t\t\tWantErr:  false,\n\t\t},\n\t\t{\n\t\t\tName:     \"Check nested path without params - 2\",\n\t\t\tTestType: \"checkpath\",\n\t\t\tPath:     \"/a/b\",\n\t\t\tMethod:   http.MethodGet,\n\t\t\tWantErr:  false,\n\t\t},\n\t\t{\n\t\t\tName:     \"Check nested path without params - 3\",\n\t\t\tTestType: \"checkpath\",\n\t\t\tPath:     \"/a/b/-/c\",\n\t\t\tMethod:   http.MethodGet,\n\t\t\tWantErr:  false,\n\t\t},\n\t\t{\n\t\t\tName:     \"Check nested path without params - 4\",\n\t\t\tTestType: \"checkpath\",\n\t\t\tPath:     \"/a/b/-/c/~/d\",\n\t\t\tMethod:   http.MethodGet,\n\t\t\tWantErr:  false,\n\t\t},\n\t\t{\n\t\t\tName:     \"Check nested path without params - 5\",\n\t\t\tTestType: \"checkpath\",\n\t\t\tPath:     \"/a/b/-/c/~/d/./e\",\n\t\t\tMethod:   http.MethodGet,\n\t\t\tWantErr:  false,\n\t\t},\n\t\t{\n\t\t\tName:     \"Check nested path without params - 5\",\n\t\t\tTestType: \"checkpathnotrailingslash\",\n\t\t\tPath:     \"/a/b/-/c/~/d/./e/notrail\",\n\t\t\tMethod:   http.MethodGet,\n\t\t\tWantErr:  false,\n\t\t},\n\t\t{\n\t\t\tName:     \"Check nested path without params - OPTION\",\n\t\t\tTestType: \"checkpathnotrailingslash\",\n\t\t\tPath:     \"/a/b/-/c/~/d/./e\",\n\t\t\tMethod:   http.MethodOptions,\n\t\t\tWantErr:  false,\n\t\t},\n\t\t{\n\t\t\tName:     \"Check nested path without params - HEAD\",\n\t\t\tTestType: \"checkpathnotrailingslash\",\n\t\t\tPath:     \"/a/b/-/c/~/d/./e\",\n\t\t\tMethod:   http.MethodHead,\n\t\t\tWantErr:  false,\n\t\t},\n\t\t{\n\t\t\tName:     \"Check nested path without params - POST\",\n\t\t\tTestType: \"checkpathnotrailingslash\",\n\t\t\tPath:     \"/a/b/-/c/~/d/./e\",\n\t\t\tMethod:   http.MethodPost,\n\t\t\tWantErr:  false,\n\t\t},\n\t\t{\n\t\t\tName:     \"Check nested path without params - PUT\",\n\t\t\tTestType: \"checkpathnotrailingslash\",\n\t\t\tPath:     \"/a/b/-/c/~/d/./e\",\n\t\t\tMethod:   http.MethodPut,\n\t\t\tWantErr:  false,\n\t\t},\n\t\t{\n\t\t\tName:     \"Check nested path without params - PATCH\",\n\t\t\tTestType: \"checkpathnotrailingslash\",\n\t\t\tPath:     \"/a/b/-/c/~/d/./e\",\n\t\t\tMethod:   http.MethodPatch,\n\t\t\tWantErr:  false,\n\t\t},\n\t\t{\n\t\t\tName:     \"Check nested path without params - DELETE\",\n\t\t\tTestType: \"checkpathnotrailingslash\",\n\t\t\tPath:     \"/a/b/-/c/~/d/./e\",\n\t\t\tMethod:   http.MethodDelete,\n\t\t\tWantErr:  false,\n\t\t},\n\t\t{\n\t\t\tName:      \"Check with params - 1\",\n\t\t\tTestType:  \"checkparams\",\n\t\t\tPath:      \"/params/:a\",\n\t\t\tMethod:    http.MethodGet,\n\t\t\tParamKeys: []string{\"a\"},\n\t\t\tParams:    []string{\"hello\"},\n\t\t\tWantErr:   false,\n\t\t},\n\t\t{\n\t\t\tName:      \"Check with params - 2\",\n\t\t\tTestType:  \"checkparams\",\n\t\t\tPath:      \"/params/:a/:b\",\n\t\t\tMethod:    http.MethodGet,\n\t\t\tParamKeys: []string{\"a\", \"b\"},\n\t\t\tParams:    []string{\"hello\", \"world\"},\n\t\t\tWantErr:   false,\n\t\t},\n\t\t{\n\t\t\tName:      \"Check with wildcard\",\n\t\t\tTestType:  \"checkparams\",\n\t\t\tPath:      \"/wildcard/:a*\",\n\t\t\tMethod:    http.MethodGet,\n\t\t\tParamKeys: []string{\"a\"},\n\t\t\tParams:    []string{\"w1/hello/world/hi/there\"},\n\t\t\tWantErr:   false,\n\t\t},\n\t\t{\n\t\t\tName:      \"Check with wildcard - 2\",\n\t\t\tTestType:  \"checkparams\",\n\t\t\tPath:      \"/wildcard2/:a*\",\n\t\t\tMethod:    http.MethodGet,\n\t\t\tParamKeys: []string{\"a\"},\n\t\t\tParams:    []string{\"w2/hello/world/hi/there/-/~/./again\"},\n\t\t\tWantErr:   false,\n\t\t},\n\t\t{\n\t\t\tName:      \"Check with wildcard - 3\",\n\t\t\tTestType:  \"widlcardwithouttrailingslash\",\n\t\t\tPath:      \"/wildcard3/:a*\",\n\t\t\tMethod:    http.MethodGet,\n\t\t\tParamKeys: []string{\"a\"},\n\t\t\tParams:    []string{\"w3/hello/world/hi/there/-/~/./again/\"},\n\t\t\tWantErr:   true,\n\t\t},\n\t\t{\n\t\t\tName:      \"Check with wildcard - 4\",\n\t\t\tTestType:  \"widlcardwithouttrailingslash\",\n\t\t\tPath:      \"/wildcard3/:a*\",\n\t\t\tMethod:    http.MethodGet,\n\t\t\tParamKeys: []string{\"a\"},\n\t\t\tParams:    []string{\"w4/hello/world/hi/there/-/~/./again\"},\n\t\t\tWantErr:   false,\n\t\t},\n\t\t{\n\t\t\tName:     \"Check not implemented\",\n\t\t\tTestType: \"notimplemented\",\n\t\t\tPath:     \"/notimplemented\",\n\t\t\tMethod:   \"HELLO\",\n\t\t\tWantErr:  false,\n\t\t},\n\t\t{\n\t\t\tName:     \"Check not found\",\n\t\t\tTestType: \"notfound\",\n\t\t\tPath:     \"/notfound\",\n\t\t\tMethod:   http.MethodGet,\n\t\t\tWantErr:  false,\n\t\t},\n\t\t{\n\t\t\tName:     \"Check chaining\",\n\t\t\tTestType: \"chaining\",\n\t\t\tPath:     \"/chained\",\n\t\t\tMethod:   http.MethodGet,\n\t\t\tWantErr:  false,\n\t\t},\n\t\t{\n\t\t\tName:     \"Check chaining\",\n\t\t\tTestType: \"chaining-nofallthrough\",\n\t\t\tPath:     \"/chained/nofallthrough\",\n\t\t\tMethod:   http.MethodGet,\n\t\t\tWantErr:  false,\n\t\t},\n\t}\n}\n\ntype testLogger struct {\n\tout bytes.Buffer\n}\n\nfunc (tl *testLogger) Debug(data ...interface{}) {\n\ttl.out.Write([]byte(fmt.Sprint(data...)))\n}\nfunc (tl *testLogger) Info(data ...interface{}) {\n\ttl.out.Write([]byte(fmt.Sprint(data...)))\n}\nfunc (tl *testLogger) Warn(data ...interface{}) {\n\ttl.out.Write([]byte(fmt.Sprint(data...)))\n}\nfunc (tl *testLogger) Error(data ...interface{}) {\n\ttl.out.Write([]byte(fmt.Sprint(data...)))\n}\nfunc (tl *testLogger) Fatal(data ...interface{}) {\n\ttl.out.Write([]byte(fmt.Sprint(data...)))\n}\n\nfunc Test_httpHandlers(t *testing.T) {\n\t// t.Parallel()\n\ttl := &testLogger{\n\t\tout: bytes.Buffer{},\n\t}\n\tLOGHANDLER = tl\n\n\t// test invalid method\n\thttpHandlers(\n\t\t[]*Route{\n\t\t\t{\n\t\t\t\tName:    \"invalid method\",\n\t\t\t\tPattern: \"/hello/world\",\n\t\t\t\tMethod:  \"HELLO\",\n\t\t\t},\n\t\t})\n\tgot := tl.out.String()\n\twant := \"Unsupported HTTP method provided. Method: 'HELLO'\"\n\tif got != want {\n\t\tt.Errorf(\n\t\t\t\"Expected the error to end with '%s', got '%s'\",\n\t\t\twant,\n\t\t\tgot,\n\t\t)\n\t}\n\ttl.out.Reset()\n\n\t// test empty handlers\n\thttpHandlers(\n\t\t[]*Route{\n\t\t\t{\n\t\t\t\tName:    \"empty handlers\",\n\t\t\t\tPattern: \"/hello/world\",\n\t\t\t\tMethod:  http.MethodGet,\n\t\t\t},\n\t\t})\n\tstr := tl.out.String()\n\twant = \"provided for the route '/hello/world', method 'GET'\"\n\tgot = str[len(str)-len(want):]\n\tif got != want {\n\t\tt.Errorf(\n\t\t\t\"Expected the error to end with '%s', got '%s'\",\n\t\t\twant,\n\t\t\tgot,\n\t\t)\n\t}\n\ttl.out.Reset()\n}\n\nfunc TestWildcardMadness(t *testing.T) {\n\tport := \"9696\"\n\tt.Helper()\n\tcfg := &Config{\n\t\tPort:            port,\n\t\tReadTimeout:     time.Second * 1,\n\t\tWriteTimeout:    time.Second * 1,\n\t\tShutdownTimeout: time.Second * 10,\n\t\tCertFile:        \"tests/ssl/server.crt\",\n\t\tKeyFile:         \"tests/ssl/server.key\",\n\t}\n\trouter := NewRouter(cfg, []*Route{\n\t\t{\n\t\t\tName:          \"wildcard madness\",\n\t\t\tPattern:       \"/hello/:w*/world/:p1/:w2*/hi/there\",\n\t\t\tHandlers:      []http.HandlerFunc{successHandler},\n\t\t\tMethod:        http.MethodGet,\n\t\t\tTrailingSlash: true,\n\t\t},\n\t}...)\n\n\tbaseAPI := fmt.Sprintf(\"http://localhost:%s\", port)\n\turl := fmt.Sprintf(\n\t\t\"%s%s\",\n\t\tbaseAPI,\n\t\t\"/hello/a/b/c/-d/~/e/world/fgh/i/j/k~/l-/hi/there/\",\n\t)\n\n\treq, _ := http.NewRequest(http.MethodGet, url, nil)\n\trespRec := httptest.NewRecorder()\n\trouter.ServeHTTP(respRec, req)\n\n\trbody, err := io.ReadAll(respRec.Body)\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\tif respRec.Code != http.StatusOK {\n\t\tt.Errorf(\"expected status code: %d, got: %d. response: '%s'\", http.StatusOK, respRec.Code, string(rbody))\n\t}\n\n\turl = fmt.Sprintf(\n\t\t\"%s%s\",\n\t\tbaseAPI,\n\t\t\"/hello/a/b/c/-d/~/e/world/fgh/i/j/k~/l-/hi/there\",\n\t)\n\n\treq, _ = http.NewRequest(http.MethodGet, url, nil)\n\trespRec = httptest.NewRecorder()\n\trouter.ServeHTTP(respRec, req)\n\n\tif respRec.Code != http.StatusOK {\n\t\tt.Errorf(\"expected status code: %d, got: %d\", http.StatusOK, respRec.Code)\n\t}\n\n\terr = checkParams(\n\t\treq,\n\t\trespRec,\n\t\t[]string{\"w\", \"p1\", \"w2\"},\n\t\t[]string{\n\t\t\t\"a/b/c/-d/~/e\",\n\t\t\t\"fgh\",\n\t\t\t\"i/j/k~/l-\",\n\t\t})\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n}\n"
  },
  {
    "path": "tests/config.json",
    "content": "{\n\t\"host\": \"127.0.0.1\",\n\t\"port\": \"9696\",\n\t\"httpsPort\": \"8443\",\n\t\"certFile\": \"./ssl/server.crt\",\n\t\"keyFile\": \"./ssl/server.key\",\n\t\"readTimeout\": 15000000000,\n\t\"writeTimeout\": 60000000000,\n\t\"insecureSkipVerify\": true\n}\n"
  },
  {
    "path": "tests/ssl/server.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDkjCCAnoCCQDhnAb7Y802KzANBgkqhkiG9w0BAQsFADCBiTELMAkGA1UEBhMC\nSU4xDjAMBgNVBAgMBUluZGlhMQ4wDAYDVQQHDAVJbmRpYTEMMAoGA1UECgwDS0JO\nMRMwEQYDVQQLDApPcGVuU291cmNlMRIwEAYDVQQDDAkxMjcuMC4wLjExIzAhBgkq\nhkiG9w0BCQEWFGJua2FtYWxlc2hAZ21haWwuY29tMCAXDTE4MDEzMDEwMDQwMVoY\nDzIyOTExMTE0MTAwNDAxWjCBiTELMAkGA1UEBhMCSU4xDjAMBgNVBAgMBUluZGlh\nMQ4wDAYDVQQHDAVJbmRpYTEMMAoGA1UECgwDS0JOMRMwEQYDVQQLDApPcGVuU291\ncmNlMRIwEAYDVQQDDAkxMjcuMC4wLjExIzAhBgkqhkiG9w0BCQEWFGJua2FtYWxl\nc2hAZ21haWwuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2t0D\nkDIjlAh/kTzu2u8tIhpBQyjXUbw0Kv8T11eWlumkvIBKuNCdei7hkFxLop9Ei0Jt\n019uFpRzrjtyXtZ67XDDiBXdLiT1YW7Z/UysNz6FIAt1jPkxEnrX2WbP16puZmmL\na0/vCvwj4xCDlc3bosUkaVknwzaxf4Lb3m9oMKIRQcgovVRnKrq5YJaaPmjZG1Th\nAGuazRuR/S1OF4sImNwmGoiLDvgra3TeEyLGb1j3eVysqmEulaa2zHVEQPI3OtSJ\nE8Pp7sYqjguAMTWk2HsTMv42z1ITR9KX9JXvTapv15WMV4LB/0iHbGLQPT6uggCV\nEklndtf2q0jGFYjvSQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQALS5ZQ0CjbXlZc\nAX0vGxSlQ4jYoaDjrIqH5CizYGGVnmBkX/+2n1a+xp8kJp/76hiPoKiYBHX5I3tN\nXQxU/1DDrjn8y7M5pS2PU/B5q+Uy3FgGnIU9J41hCdagUKeTupUvVtxqVuInpX5w\n5JGW1eqLcCK5E5XBY85VpjdOWnOaZXuQmfgye/jKO4XtqDB20jtno7Jo3EIhllFT\nSzxDk/Hfr8jeW9rd1/Q4/UCNOXPP2TllDnsRJtQzkY3h1sLtNRlGYMn5gFI3CbT/\nxfbkKRmC8OmEyxe2m/qzMS2tJMadZEubuXtheA6W7YB76cmmVKIxL+h1ektVERTA\n8doRA1AK\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "tests/ssl/server.csr",
    "content": "-----BEGIN CERTIFICATE REQUEST-----\nMIICzzCCAbcCAQAwgYkxCzAJBgNVBAYTAklOMQ4wDAYDVQQIDAVJbmRpYTEOMAwG\nA1UEBwwFSW5kaWExDDAKBgNVBAoMA0tCTjETMBEGA1UECwwKT3BlblNvdXJjZTES\nMBAGA1UEAwwJMTI3LjAuMC4xMSMwIQYJKoZIhvcNAQkBFhRibmthbWFsZXNoQGdt\nYWlsLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANrdA5AyI5QI\nf5E87trvLSIaQUMo11G8NCr/E9dXlpbppLyASrjQnXou4ZBcS6KfRItCbdNfbhaU\nc647cl7Weu1ww4gV3S4k9WFu2f1MrDc+hSALdYz5MRJ619lmz9eqbmZpi2tP7wr8\nI+MQg5XN26LFJGlZJ8M2sX+C295vaDCiEUHIKL1UZyq6uWCWmj5o2RtU4QBrms0b\nkf0tTheLCJjcJhqIiw74K2t03hMixm9Y93lcrKphLpWmtsx1REDyNzrUiRPD6e7G\nKo4LgDE1pNh7EzL+Ns9SE0fSl/SV702qb9eVjFeCwf9Ih2xi0D0+roIAlRJJZ3bX\n9qtIxhWI70kCAwEAAaAAMA0GCSqGSIb3DQEBCwUAA4IBAQC2Jj9EyBFSzyQQLSu8\n3MFbHHbaAcvZpjAmPuPFLlaY3j7iXwsGbK4JRHR4fKKZA/M9RTNq+v7QtMkIEyAK\ndNKHjjK/zHfZbXCOYORFmfuR7xfG6FvjvLj2QtHasS0ogjDduqX/wt2aapR+2Q9W\nvRgJVc4nCnyboA/f/u96hk45UxrA87g73bnaAiPgYcx4wUvhWqZOglw7nC38oj5g\nCfelDqu+i4paf4pglfo8r/Dx3OAKIwFO4uHtGLPDNu27USwEO0/89dDNF5c6MmwI\ncSCaEMY1AyRAOHm2jiPYEOtobRH07/SIhskNUszn6FwSOQuUDN00Cn3NsE2ZiPMd\n4Gao\n-----END CERTIFICATE REQUEST-----\n"
  },
  {
    "path": "tests/ssl/server.key",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nMIIEogIBAAKCAQEA2t0DkDIjlAh/kTzu2u8tIhpBQyjXUbw0Kv8T11eWlumkvIBK\nuNCdei7hkFxLop9Ei0Jt019uFpRzrjtyXtZ67XDDiBXdLiT1YW7Z/UysNz6FIAt1\njPkxEnrX2WbP16puZmmLa0/vCvwj4xCDlc3bosUkaVknwzaxf4Lb3m9oMKIRQcgo\nvVRnKrq5YJaaPmjZG1ThAGuazRuR/S1OF4sImNwmGoiLDvgra3TeEyLGb1j3eVys\nqmEulaa2zHVEQPI3OtSJE8Pp7sYqjguAMTWk2HsTMv42z1ITR9KX9JXvTapv15WM\nV4LB/0iHbGLQPT6uggCVEklndtf2q0jGFYjvSQIDAQABAoIBACcKCVqtNt9u7KJR\nhiGTTC+rEz0RiebQdVW+DiH5Q6lDn9jn4Ww5+f0TY7TGYc9uLWHRxZlQimiIrmHD\nxNDZ3S+BT790dvGGMibhCQ0/ofBwvHpM0PkGchRjySDEUAqeIfcumGnZ5j/FXflg\ntrf/8k+EbsxD1O3jUaH3C5UPtjwGOBsXnKUQiDIqd27QXCFCecDts2AVIP4uejPY\nTqzdnK18k6WvCLs776RZmhDa0tWE0vgDuqYkAQgoYyRRtpWsQ1MyK2dUA9GVnenv\nQ7NgOSaEEGRNx5Ko4uXQkNnW7xHxsQT45iIoDvw/GF5Mgii9ZweZoTou6EXNop58\nudVa2akCgYEA91p2LeYyICq3vVLF3MWlytxs+asKTtja3NdwVZUMRGMat6GWO7D4\nEqzjt9lSow0d+JhVE9/hyS4MytmVhvRDAnw+cfmfhxlG2CSldeETjKFfAU8YkJQ+\npmx1lgbXqe9N2rUPqBMadMcQcJ/iTzvR1XmnPeKeG7sZdutJR3UVlWMCgYEA4oOZ\nHR6WLvCJUnvA0hvli98s1dGrB3gNiwGuX+Qz1n5YbDByGpWnNgoBACe/rUvmoThk\nQlYcmw3ItqGFq7TGImeoeOoQhTlpyMOYz28wDf5+8jdUJ5nNGFrfEsg2hUtsl3EO\nCSp5iHjGmDxMow2cVHpA4hL6eQyCh7f0GaBkTmMCgYB70m4MhgqbraazAIeJ/+sB\nxRxMU0HivI27NaHHRciRR2cte5dAJFPazW9lLkY+1yckteUJAO7/Da1bslY263nL\n+bQsy//+2jlro9SsUNK/eFydxCGQ5pUCLJMkWiKFsASyMic3RPDeenQRXQgmD9T3\n32FICnSJfzy9GgVh3wvB7wKBgFFRvV5e5LvlTud11juYGEimzonUw/nid7o32EpE\nuvd+VHBC1DQHFgiofsN3gbDNVvb6L8RA9fQUdsJaKosCUz92x1zhaxzpB7kzv2B5\nIl9jxl9eza+J37+mn/82MZyY/1s/EzLnNMpx0ZpFy52d/Um2uiRve8yJWTMwL0oj\n8t7RAoGAcsazEWd6eJz1vmaeMT5YBUQeD82y9IHHFyp6h02wy2ackEjqNTiqjVWG\nrzRDWJZWjTJkB+EY7/g7GqiEnAIJ4YqJccew9tJlaM2q58fvGCV/OLNYzzhBJkWo\n6YpvCa+uQVJMFejL0WnSiewd9g9HCVi1EHNasb1Zp+BTgyfE1rs=\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": "webgo.go",
    "content": "/*\nPackage webgo is a lightweight framework for building web apps. It has a multiplexer,\nmiddleware plugging mechanism & context management of its own. The primary goal\nof webgo is to get out of the developer's way as much as possible. i.e. it does\nnot enforce you to build your app in any particular pattern, instead just helps you\nget all the trivial things done faster and easier.\n\ne.g.\n1. Getting named URI parameters.\n2. Multiplexer for regex matching of URI and such.\n3. Inject special app level configurations or any such objects to the request context as required.\n*/\npackage webgo\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"net/http\"\n)\n\nvar supportedHTTPMethods = []string{\n\thttp.MethodOptions,\n\thttp.MethodHead,\n\thttp.MethodGet,\n\thttp.MethodPost,\n\thttp.MethodPut,\n\thttp.MethodPatch,\n\thttp.MethodDelete,\n}\n\n// ctxkey is a custom string type to store the WebGo context inside HTTP request context\ntype ctxkey string\n\nconst wgoCtxKey = ctxkey(\"webgocontext\")\n\n// ContextPayload is the WebgoContext. A new instance of ContextPayload is injected inside every request's context object\ntype ContextPayload struct {\n\tRoute     *Route\n\tErr       error\n\tURIParams map[string]string\n}\n\n// Params returns the URI parameters of the respective route\nfunc (cp *ContextPayload) Params() map[string]string {\n\treturn cp.URIParams\n}\n\nfunc (cp *ContextPayload) reset() {\n\tcp.Route = nil\n\tcp.Err = nil\n}\n\n// SetError sets the err within the context\nfunc (cp *ContextPayload) SetError(err error) {\n\tcp.Err = err\n}\n\n// Error returns the error set within the context\nfunc (cp *ContextPayload) Error() error {\n\treturn cp.Err\n}\n\n// Context returns the ContextPayload injected inside the HTTP request context\nfunc Context(r *http.Request) *ContextPayload {\n\treturn r.Context().Value(wgoCtxKey).(*ContextPayload)\n}\n\n// SetError is a helper function to set the error in webgo context\nfunc SetError(r *http.Request, err error) {\n\tctx := Context(r)\n\tctx.SetError(err)\n}\n\n// GetError is a helper function to get the error from webgo context\nfunc GetError(r *http.Request) error {\n\treturn Context(r).Error()\n}\n\n// ResponseStatus returns the response status code. It works only if the http.ResponseWriter\n// is not wrapped in another response writer before calling ResponseStatus\nfunc ResponseStatus(rw http.ResponseWriter) int {\n\tcrw, ok := rw.(*customResponseWriter)\n\tif !ok {\n\t\treturn http.StatusOK\n\t}\n\treturn crw.statusCode\n}\nfunc (router *Router) setupServer() {\n\tcfg := router.config\n\trouter.httpsServer = &http.Server{\n\t\tAddr:         \"\",\n\t\tHandler:      router,\n\t\tReadTimeout:  cfg.ReadTimeout,\n\t\tWriteTimeout: cfg.WriteTimeout,\n\t\tTLSConfig: &tls.Config{\n\t\t\tInsecureSkipVerify: cfg.InsecureSkipVerify,\n\t\t},\n\t}\n\trouter.httpServer = &http.Server{\n\t\tAddr:         \"\",\n\t\tHandler:      router,\n\t\tReadTimeout:  cfg.ReadTimeout,\n\t\tWriteTimeout: cfg.WriteTimeout,\n\t}\n\trouter.SetupMiddleware()\n}\n\n// SetupMiddleware initializes all the middleware added using \"Use\".\n// This function need not be called explicitly, if using router.Start()\n// or router.StartHTTPS(). Instead if the router is being passed to an external server\n// then the SetupMiddleware function should be called\nfunc (router *Router) SetupMiddleware() {\n\t// load middleware for all routes\n\tfor _, routes := range router.allHandlers {\n\t\tfor _, route := range routes {\n\t\t\troute.setupMiddleware(router.config.ReverseMiddleware)\n\t\t}\n\t}\n}\n\n// StartHTTPS starts the server with HTTPS enabled\nfunc (router *Router) StartHTTPS() {\n\tcfg := router.config\n\tif cfg.CertFile == \"\" {\n\t\tLOGHANDLER.Fatal(\"No certificate provided for HTTPS\")\n\t}\n\n\tif cfg.KeyFile == \"\" {\n\t\tLOGHANDLER.Fatal(\"No key file provided for HTTPS\")\n\t}\n\n\trouter.setupServer()\n\n\thost := cfg.Host\n\tif len(cfg.HTTPSPort) > 0 {\n\t\thost += \":\" + cfg.HTTPSPort\n\t}\n\trouter.httpsServer.Addr = host\n\n\tLOGHANDLER.Info(\"HTTPS server, listening on\", router.httpsServer.Addr)\n\terr := router.httpsServer.ListenAndServeTLS(cfg.CertFile, cfg.KeyFile)\n\tif err != nil && err != http.ErrServerClosed {\n\t\tLOGHANDLER.Error(\"HTTPS server exited with error:\", err.Error())\n\t}\n}\n\n// Start starts the HTTP server with the appropriate configurations\nfunc (router *Router) Start() {\n\trouter.setupServer()\n\n\tcfg := router.config\n\thost := cfg.Host\n\tif len(cfg.Port) > 0 {\n\t\thost += \":\" + cfg.Port\n\t}\n\trouter.httpServer.Addr = host\n\n\tLOGHANDLER.Info(\"HTTP server, listening on\", router.httpServer.Addr)\n\terr := router.httpServer.ListenAndServe()\n\tif err != nil && err != http.ErrServerClosed {\n\t\tLOGHANDLER.Error(\"HTTP server exited with error:\", err.Error())\n\t}\n}\n\n// Shutdown gracefully shuts down HTTP server\nfunc (router *Router) Shutdown() error {\n\tif router.httpServer == nil {\n\t\treturn nil\n\t}\n\ttimer := router.config.ShutdownTimeout\n\n\tctx, cancel := context.WithTimeout(context.TODO(), timer)\n\tdefer cancel()\n\n\terr := router.httpServer.Shutdown(ctx)\n\tif err != nil {\n\t\tLOGHANDLER.Error(err)\n\t}\n\treturn err\n}\n\n// ShutdownHTTPS gracefully shuts down HTTPS server\nfunc (router *Router) ShutdownHTTPS() error {\n\tif router.httpsServer == nil {\n\t\treturn nil\n\t}\n\ttimer := router.config.ShutdownTimeout\n\n\tctx, cancel := context.WithTimeout(context.TODO(), timer)\n\tdefer cancel()\n\n\terr := router.httpsServer.Shutdown(ctx)\n\tif err != nil && err != http.ErrServerClosed {\n\t\tLOGHANDLER.Error(err)\n\t}\n\treturn err\n}\n\n// OriginalResponseWriter returns the Go response writer stored within the webgo custom response\n// writer\nfunc OriginalResponseWriter(rw http.ResponseWriter) http.ResponseWriter {\n\tcrw, ok := rw.(*customResponseWriter)\n\tif !ok {\n\t\treturn nil\n\t}\n\n\treturn crw.ResponseWriter\n}\n"
  },
  {
    "path": "webgo_test.go",
    "content": "/*\nPackage webgo is a lightweight framework for building web apps. It has a multiplexer,\nmiddleware plugging mechanism & context management of its own. The primary goal\nof webgo is to get out of the developer's way as much as possible. i.e. it does\nnot enforce you to build your app in any particular pattern, instead just helps you\nget all the trivial things done faster and easier.\n\ne.g.\n1. Getting named URI parameters.\n2. Multiplexer for regex matching of URI and such.\n3. Inject special app level configurations or any such objects to the request context as required.\n*/\npackage webgo\n\nimport (\n\t\"errors\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestResponseStatus(t *testing.T) {\n\tt.Parallel()\n\tw := newCRW(httptest.NewRecorder(), http.StatusOK)\n\tSendError(w, nil, http.StatusNotFound)\n\tif http.StatusNotFound != ResponseStatus(w) {\n\t\tt.Errorf(\n\t\t\t\"Expected status '%d', got '%d'\",\n\t\t\thttp.StatusNotFound,\n\t\t\tResponseStatus(w),\n\t\t)\n\t}\n\n\t// ideally we should get 200 from ResponseStatus; but it can get accurate status code only\n\t// when `customresponsewriter` is used\n\trw := httptest.NewRecorder()\n\tSendError(rw, nil, http.StatusNotFound)\n\tif http.StatusOK != ResponseStatus(rw) {\n\t\tt.Errorf(\n\t\t\t\"Expected status '%d', got '%d'\",\n\t\t\thttp.StatusOK,\n\t\t\tResponseStatus(rw),\n\t\t)\n\t}\n}\n\nfunc TestStart(t *testing.T) {\n\tt.Parallel()\n\trouter, _ := setup(t, \"9696\")\n\tgo router.Start()\n\ttime.Sleep(time.Second * 2)\n\terr := router.Shutdown()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\nfunc TestStartHTTPS(t *testing.T) {\n\tt.Parallel()\n\trouter, _ := setup(t, \"8443\")\n\tgo router.StartHTTPS()\n\ttime.Sleep(time.Second * 2)\n\terr := router.ShutdownHTTPS()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\nfunc TestErrorHandling(t *testing.T) {\n\tt.Parallel()\n\terr := errors.New(\"hello world, failed\")\n\trouter, _ := setup(t, \"7878\")\n\tw := httptest.NewRecorder()\n\tr, _ := http.NewRequest(http.MethodGet, \"/\", nil)\n\trouter.ServeHTTP(w, r)\n\n\tSetError(r, err)\n\tgotErr := GetError(r)\n\n\tif !errors.Is(err, gotErr) {\n\t\tt.Fatalf(\"expected err %v, got %v\", err, gotErr)\n\t}\n}\n\nfunc BenchmarkRouter(b *testing.B) {\n\tGlobalLoggerConfig(nil, nil, LogCfgDisableDebug, LogCfgDisableInfo, LogCfgDisableWarn)\n\tt := &testing.T{}\n\trouter, err := setup(t, \"1595\")\n\tif err != nil {\n\t\tb.Error(err)\n\t\treturn\n\t}\n\tw := httptest.NewRecorder()\n\tr, _ := http.NewRequest(http.MethodGet, \"/a/b/-/c/~/d/./e\", nil)\n\tfor i := 0; i < b.N; i++ {\n\t\trouter.ServeHTTP(w, r)\n\t\tif w.Result().StatusCode != http.StatusOK {\n\t\t\tb.Error(\"expected status 200, got\", w.Result().StatusCode)\n\t\t\treturn\n\t\t}\n\t}\n}\n"
  }
]