├── .github ├── CODEOWNERS ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml └── workflows │ ├── go.yml │ ├── lint.yml │ └── pages.yml ├── .gitignore ├── .golangci.yml ├── .markdownlint.yaml ├── .typos.toml ├── BENCHMARK.md ├── CONTRIBUTING.md ├── LICENSE.txt ├── Makefile ├── README.md ├── SECURITY.md ├── assets └── Zuplo.png ├── check-all-modules.sh ├── cmd └── fuego │ ├── commands │ ├── controller.go │ ├── controller_test.go │ └── service.go │ ├── go.mod │ ├── go.sum │ ├── main.go │ └── templates │ ├── embed.go │ └── newEntity │ ├── controller.go │ ├── entity.go │ └── service.go ├── ctx.go ├── ctx_params_test.go ├── ctx_test.go ├── default_middlewares.go ├── default_middlewares_test.go ├── deserialization.go ├── deserialization_test.go ├── doc.go ├── documentation ├── .gitignore ├── .nvmrc ├── README.md ├── babel.config.js ├── docs │ ├── guides │ │ ├── _category_.json │ │ ├── alternative-routers-support │ │ │ ├── _category_.json │ │ │ ├── echo.md │ │ │ └── gin.md │ │ ├── controllers.md │ │ ├── errors.md │ │ ├── middlewares.md │ │ ├── openapi.md │ │ ├── options.md │ │ ├── routing.md │ │ ├── serialization.md │ │ ├── testing.md │ │ ├── transformation.md │ │ └── validation.md │ ├── index.md │ ├── internals │ │ ├── 01-data-flow.mdx │ │ ├── 02-architecture.md │ │ ├── _category_.json │ │ └── architecture.png │ └── tutorials │ │ ├── 01-hello-world.md │ │ ├── 02-crud.md │ │ ├── 03-hot-reload.md │ │ ├── _category_.json │ │ └── rendering │ │ ├── gomponents.md │ │ ├── index.md │ │ ├── std.md │ │ └── templ.md ├── docusaurus.config.ts ├── package-lock.json ├── package.json ├── sidebars.ts ├── src │ ├── components │ │ ├── FlowChart.js │ │ └── HomepageFeatures │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ ├── css │ │ └── custom.css │ └── pages │ │ ├── index.module.css │ │ ├── index.tsx │ │ └── markdown-page.md ├── static │ ├── .nojekyll │ └── img │ │ ├── docusaurus-social-card.jpg │ │ ├── fuego-big.png │ │ ├── fuego.ico │ │ ├── fuego.png │ │ ├── fuego.svg │ │ ├── hello-world-openapi.jpeg │ │ └── logo.svg └── tsconfig.json ├── engine.go ├── engine_test.go ├── errors.go ├── errors_test.go ├── examples ├── acme-tls │ ├── go.mod │ ├── go.sum │ └── main.go ├── basic │ ├── go.mod │ ├── go.sum │ └── main.go ├── crud-gorm │ ├── .gitignore │ ├── go.mod │ ├── go.sum │ ├── handlers │ │ └── handler.go │ ├── main.go │ ├── mocks │ │ └── mock_userqueries.go │ ├── models │ │ └── user.go │ └── queries │ │ └── query.go ├── custom-errors │ ├── go.mod │ ├── go.sum │ └── main.go ├── custom-serializer │ ├── go.mod │ ├── go.sum │ └── main.go ├── echo-compat │ ├── go.mod │ ├── go.sum │ ├── handlers.go │ ├── main.go │ ├── main_test.go │ └── test.go ├── full-app-gourmet │ ├── .air.toml │ ├── .env │ ├── .gitignore │ ├── .prettierignore │ ├── .prettierrc │ ├── Dockerfile │ ├── Makefile │ ├── controller │ │ ├── dosing.go │ │ ├── dummy_controllers.go │ │ ├── id.go │ │ ├── ingredient.go │ │ ├── recipe.go │ │ ├── routes.go │ │ └── users.go │ ├── errors_custom.go │ ├── favicon.ico │ ├── go.mod │ ├── go.sum │ ├── main.go │ ├── operations │ │ ├── .gitignore │ │ ├── backup.sh │ │ ├── deploy.sh │ │ ├── gourmet.service │ │ ├── logs.sh │ │ └── upload-db.sh │ ├── package.json │ ├── server │ │ └── server.go │ ├── sqlc.yml │ ├── static │ │ ├── .gitignore │ │ ├── dinner-placeholder.webp │ │ ├── embed.go │ │ ├── embed_test.go │ │ ├── favicon.ico │ │ ├── hero.webp │ │ ├── lobster.svg │ │ ├── manifest.json │ │ ├── plan.webp │ │ └── robots.txt │ ├── store │ │ ├── db.go │ │ ├── dosing.go │ │ ├── dosing.sql.go │ │ ├── ingredient.go │ │ ├── ingredient.sql.go │ │ ├── init.go │ │ ├── migrations │ │ │ ├── 000001_migration.down.sql │ │ │ ├── 000001_migration.up.sql │ │ │ ├── 000002_ingredient_default_dosing.down.sql │ │ │ ├── 000002_ingredient_default_dosing.up.sql │ │ │ ├── 000003_ingredient_categories.down.sql │ │ │ ├── 000003_ingredient_categories.up.sql │ │ │ ├── 000004_recipes_categories.down.sql │ │ │ ├── 000004_recipes_categories.up.sql │ │ │ ├── 000005_whenToEat.down.sql │ │ │ ├── 000005_whenToEat.up.sql │ │ │ └── embed.go │ │ ├── models.go │ │ ├── queries │ │ │ ├── dosing.sql │ │ │ ├── ingredient.sql │ │ │ └── recipe.sql │ │ ├── recipe.go │ │ ├── recipe.sql.go │ │ ├── slug.go │ │ ├── slug_test.go │ │ └── types │ │ │ ├── category.go │ │ │ ├── translations.go │ │ │ ├── unit.go │ │ │ └── whenToEat.go │ ├── tailwind.config.js │ ├── tailwind.css │ ├── templa │ │ ├── admin │ │ │ ├── ingredient.form.templ │ │ │ ├── ingredient.list.templ │ │ │ ├── ingredient.new.templ │ │ │ ├── ingredient.page.templ │ │ │ ├── layout.templ │ │ │ ├── navbar.admin.templ │ │ │ ├── recipe.form.templ │ │ │ ├── recipe.list.templ │ │ │ ├── recipe.new.templ │ │ │ └── recipe.page.templ │ │ ├── components │ │ │ ├── card.templ │ │ │ ├── footer.templ │ │ │ ├── head.templ │ │ │ ├── scripts.templ │ │ │ ├── searchForm.templ │ │ │ ├── select.category.ingredient.templ │ │ │ ├── select.templ │ │ │ ├── slider.templ │ │ │ ├── stars.templ │ │ │ └── unitSelector.templ │ │ ├── generate.go │ │ ├── home.templ │ │ ├── ingredient.list.templ │ │ ├── layout.templ │ │ ├── logo.templ │ │ ├── navbar.templ │ │ ├── planner.templ │ │ ├── recipe.list.templ │ │ ├── recipe.page.templ │ │ ├── search.page.templ │ │ └── search.templ │ ├── templates │ │ ├── embed.go │ │ ├── layouts │ │ │ ├── admin.layout.html │ │ │ └── main.layout.html │ │ ├── pages │ │ │ ├── admin.page.html │ │ │ ├── admin │ │ │ │ ├── ingredients.page.html │ │ │ │ ├── recipes.page.html │ │ │ │ └── single-recipe.page.html │ │ │ ├── index.page.html │ │ │ ├── ingredients.page.html │ │ │ ├── recipe.page.html │ │ │ ├── recipes.page.html │ │ │ └── search.page.html │ │ └── partials │ │ │ ├── dosing │ │ │ └── preselected-unit.partial.html │ │ │ ├── footer.partial.html │ │ │ ├── head.partial.html │ │ │ ├── htmx │ │ │ └── htmx.partial.html │ │ │ ├── ingredients │ │ │ ├── add-ingredient.partial.admin.html │ │ │ ├── ingredient-card.partial.html │ │ │ ├── ingredient-slider.partial.html │ │ │ └── ingredients-list.partial.html │ │ │ ├── navbar.partial.html │ │ │ ├── recipes │ │ │ ├── add-recipe.partial.admin.html │ │ │ ├── recipe-card.partial.html │ │ │ ├── recipe-line.partial.html │ │ │ ├── recipe-slider.partial.html │ │ │ ├── recipes-grid.partial.html │ │ │ └── recipes-list.partial.html │ │ │ ├── scripts.partial.html │ │ │ └── search-result.partial.html │ ├── views │ │ ├── admin.go │ │ ├── admin.ingredient.go │ │ ├── admin.recipe.go │ │ ├── dosing.go │ │ ├── ingredients.go │ │ ├── partials.go │ │ ├── planner.go │ │ ├── recipe.go │ │ ├── recipe_test.go │ │ └── views.go │ └── yarn.lock ├── generate-opengraph-image │ ├── Raleway-Regular.ttf │ ├── controller │ │ └── opengraph.go │ ├── go.mod │ ├── go.sum │ └── main.go ├── gin-compat │ ├── adaptor_test.go │ ├── go.mod │ ├── go.sum │ ├── handlers.go │ ├── main.go │ └── main_test.go ├── hello-world │ ├── go.mod │ ├── go.sum │ └── main.go ├── openapi-generate │ ├── cmd │ │ └── main.go │ ├── go.mod │ ├── go.sum │ ├── scripts │ │ └── genspec.go │ ├── server │ │ └── server.go │ └── tools.go ├── openapi │ ├── go.mod │ ├── go.sum │ └── main.go ├── petstore │ ├── controllers │ │ ├── middlewares.go │ │ ├── pets.go │ │ └── pets_test.go │ ├── go.mod │ ├── go.sum │ ├── lib │ │ ├── server.go │ │ ├── server_test.go │ │ └── testdata │ │ │ └── doc │ │ │ └── openapi.golden.json │ ├── main.go │ ├── models │ │ └── Pet.go │ └── services │ │ ├── in_memory_pets.go │ │ └── in_memory_pets_test.go └── with-listener │ ├── go.mod │ ├── go.sum │ └── main.go ├── extra ├── fuegoecho │ ├── adaptor.go │ ├── context.go │ ├── go.mod │ └── go.sum ├── fuegogin │ ├── adaptor.go │ ├── adaptor_test.go │ ├── adaptor_wrapped_router_test.go │ ├── context.go │ ├── go.mod │ └── go.sum ├── markdown │ ├── go.mod │ ├── go.sum │ ├── markdown.go │ └── markdown_test.go ├── sql │ ├── go.mod │ ├── go.sum │ ├── sql_errors.go │ └── sql_errors_test.go └── sqlite3 │ ├── go.mod │ ├── go.sum │ ├── sqlite_errors.go │ └── sqlite_errors_test.go ├── generic_mux.go ├── generics_test.go ├── go.mod ├── go.sum ├── go.work ├── html.go ├── html_test.go ├── internal └── common_context.go ├── mdsf.json ├── middleware ├── basicauth │ ├── basicauth.go │ ├── basicauth_test.go │ ├── go.mod │ └── go.sum └── cache │ ├── cache.go │ ├── cache_test.go │ ├── go.mod │ ├── go.sum │ ├── in_memory.go │ ├── writer.go │ └── writer_test.go ├── mock_context.go ├── mock_context_test.go ├── multi_return.go ├── multi_return_test.go ├── net_http_mux.go ├── net_http_mux_test.go ├── op-logo.svg ├── openapi.go ├── openapi_description.go ├── openapi_handler.go ├── openapi_handler_test.go ├── openapi_operations.go ├── openapi_operations_test.go ├── openapi_test.go ├── option.go ├── option └── option.go ├── option_test.go ├── param.go ├── param └── param.go ├── param_test.go ├── parameter_registration_test.go ├── params.go ├── params_test.go ├── perf.go ├── perf_test.go ├── route.go ├── schema_customizer.go ├── security.go ├── security_test.go ├── serialization.go ├── serialization_test.go ├── serve.go ├── serve_test.go ├── server.go ├── server_test.go ├── static ├── embed.go └── fuego.svg ├── testdata └── test.html ├── testing-from-outside ├── cors_test.go ├── go.mod └── go.sum ├── tests_test.go ├── validate_params.go ├── validate_params_test.go ├── validation.go └── validation_test.go /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # CODEOWNERS file for Fuego project 2 | # See https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners 3 | 4 | # Default owners for everything in the repo 5 | * @EwenQuim @dylanhitt 6 | 7 | # Documentation 8 | /documentation/ @EwenQuim @ccoVeille 9 | *.md @EwenQuim @ccoVeille 10 | 11 | # CI/CD and GitHub specific files 12 | /.github/ @EwenQuim 13 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [EwenQuim] 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve Fuego 4 | title: "" 5 | labels: bug 6 | assignees: "" 7 | --- 8 | 9 | **To Reproduce** 10 | Steps to reproduce the behavior: 11 | 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Framework version (please check if it happens with the last 24 | Fuego version before posting):** 25 | 26 | **Go version (please check if it happens with the last Go version before posting):** 27 | 28 | **Additional context** 29 | Add any other context about the problem here. 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. 12 | Ex. I'm always frustrated when [...] 13 | 14 | **Describe the solution you'd like** 15 | A clear and concise description of what you want to happen. 16 | 17 | **Describe alternatives you've considered** 18 | A clear and concise description of any alternative solutions or features you've considered. 19 | 20 | **Additional context** 21 | Add any other context or screenshots about the feature request here. 22 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directories: 5 | - "/documentation" 6 | - "/examples/**/*" 7 | groups: 8 | all: 9 | patterns: 10 | - "*" # Group all updates into a single larger pull request. 11 | open-pull-requests-limit: 10 # avoid spam, if no one reacts 12 | schedule: 13 | interval: "weekly" 14 | 15 | - package-ecosystem: "gomod" 16 | directories: 17 | - / 18 | - /cmd/**/* 19 | - /examples/**/* 20 | - /extra/**/* 21 | groups: 22 | all: 23 | patterns: 24 | - "*" 25 | open-pull-requests-limit: 10 26 | schedule: 27 | interval: "weekly" 28 | 29 | - package-ecosystem: github-actions 30 | directory: / 31 | groups: 32 | all: 33 | patterns: 34 | - "*" 35 | open-pull-requests-limit: 10 36 | schedule: 37 | interval: weekly 38 | 39 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a golang project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | 4 | name: Go 5 | 6 | on: 7 | pull_request: 8 | push: 9 | branches: ["main"] 10 | 11 | jobs: 12 | tests: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - name: Set up Go 18 | uses: actions/setup-go@v5 19 | with: 20 | go-version-file: "./go.work" 21 | 22 | - name: Build 23 | run: make build 24 | 25 | - name: Test 26 | run: make cover 27 | 28 | - name: Install goveralls 29 | run: go install github.com/mattn/goveralls@latest 30 | 31 | - name: Send coverage 32 | env: 33 | COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | run: goveralls -coverprofile=coverage.out -service=github 35 | 36 | golangci-lint: 37 | runs-on: ubuntu-latest 38 | steps: 39 | - uses: actions/checkout@v4 40 | 41 | - uses: actions/setup-go@v5 42 | with: 43 | go-version-file: "./go.work" 44 | 45 | - run: make lint FIX="" 46 | 47 | govulncheck: 48 | runs-on: ubuntu-latest 49 | steps: 50 | - uses: actions/checkout@v4 51 | 52 | - uses: actions/setup-go@v5 53 | with: 54 | go-version-file: "./go.work" 55 | 56 | - run: make dependencies-analyze 57 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: ["main"] 7 | 8 | jobs: 9 | lint: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | 14 | - name: markdownlint-cli 15 | uses: nosborn/github-action-markdown-cli@v3.4.0 16 | with: 17 | files: . 18 | config_file: .markdownlint.yaml 19 | dot: true 20 | 21 | - name: typos-action spellchecker 22 | uses: crate-ci/typos@v1.33.1 23 | -------------------------------------------------------------------------------- /.github/workflows/pages.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Docusaurus site to Pages 2 | 3 | on: 4 | # Runs on pushes targeting the default branch 5 | push: 6 | branches: ["main", "pages/**"] 7 | paths: 8 | - "documentation/**" 9 | 10 | # Allows you to run this workflow manually from the Actions tab 11 | workflow_dispatch: 12 | 13 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 14 | permissions: 15 | contents: read 16 | pages: write 17 | id-token: write 18 | 19 | # Allow one concurrent deployment 20 | concurrency: 21 | group: "pages" 22 | cancel-in-progress: true 23 | 24 | jobs: 25 | # Build job 26 | build: 27 | runs-on: ubuntu-latest 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@v4 31 | 32 | - name: Setup Node 33 | uses: actions/setup-node@v4 34 | with: 35 | node-version: "20" 36 | 37 | - name: Install dependencies 38 | run: npm ci 39 | working-directory: ./documentation 40 | 41 | - name: Build Docusaurus site 42 | run: npm run build 43 | working-directory: ./documentation 44 | 45 | - name: Upload artifact for deployment 46 | uses: actions/upload-pages-artifact@v3 47 | with: 48 | path: ./documentation/build 49 | 50 | # Deployment job 51 | deploy: 52 | needs: build 53 | runs-on: ubuntu-latest 54 | environment: 55 | name: github-pages 56 | steps: 57 | - name: Deploy to GitHub Pages 58 | id: deployment 59 | uses: actions/deploy-pages@v4 60 | 61 | - name: Print page URL 62 | run: echo "Deployed to ${PAGES_URL}" 63 | env: 64 | PAGES_URL: ${{ steps.deployment.outputs.page_url }} 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .vscode 3 | .idea 4 | coverage.out 5 | .DS_Store 6 | **/openapi.json 7 | .env.local 8 | recipe.db 9 | bin 10 | go.work.sum 11 | /cmd/fuego/fuego 12 | *.db 13 | -------------------------------------------------------------------------------- /.markdownlint.yaml: -------------------------------------------------------------------------------- 1 | # MD010/no-hard-tabs : Hard tabs : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md010.md 2 | # 3 | # The rendered markdown is viewed in github and 4 | # the Docusaurus site which render hard-tabs fine. 5 | # It is annoying to have to deal with so many spaces 6 | # specifically in golang code blocks. 7 | MD010: false 8 | 9 | # MD013/line-length : Line length : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md013.md 10 | MD013: false 11 | 12 | # MD033/no-inline-html : Inline HTML : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md033.md 13 | # 14 | # It's more convenient to work with HTML in some cases. 15 | # We use HTML in a lot of our docs. 16 | MD033: false 17 | -------------------------------------------------------------------------------- /.typos.toml: -------------------------------------------------------------------------------- 1 | [default] 2 | locale = "en-us" 3 | 4 | [files] 5 | # excluded file 6 | extend-exclude = [ 7 | "go.sum", "go.mod", # these files are specific to Go, they shouldn't get parsed for typos 8 | ] 9 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | First of all, if you are reading this, thank you for considering contributing to 4 | the project! It's people like you that make it a reality. ❤️ 5 | 6 | In return, we will do our best to make the contribution process as easy and 7 | transparent as possible. 8 | 9 | ## How to contribute 10 | 11 | There are many ways to contribute, from writing tutorials or blog posts, 12 | improving the documentation, submitting bug reports and feature requests 13 | or writing code which can be incorporated into the project itself. 14 | 15 | ## Installation 16 | 17 | 1. **Fork** the repository 18 | 2. **Clone** the forked repository 19 | 3. ~~Install the dependencies~~ lol nope just kidding, only `go` is needed 20 | 4. Coding time! 21 | 5. Run `make ci` to **run all the tests**, coverage and check the code style 22 | 6. If everything is fine, **commit** your changes and push them to your fork 23 | 7. Open a **pull request**! 24 | 25 | ## Contributors 26 | 27 | Thanks to [everyone who have contributed][contributors-url] to this project! 28 | 29 | [contributors-url]: https://github.com/go-fuego/fuego/graphs/contributors 30 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 go-fuego 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | The following table outlines which versions of Fuego are actively supported with security updates. Please ensure that you are using a supported version to benefit from the latest patches and improvements. 6 | 7 | | Version | Supported | 8 | | ----------------------------------------------- | ---------------------- | 9 | | 0.x.y (x being the latest version released) | :white_check_mark: Yes | 10 | | 0.x.y (x being NOT the latest version released) | :x: No | 11 | 12 | ## Reporting a Vulnerability 13 | 14 | Fuego relies on its community to ensure its security. Here is how to report a vulnerability: 15 | 16 | 1. **Send a Pull Request (PR):** If possible, immediately send a PR addressing the vulnerability and tag the maintainers for a quick review. 17 | 2. **Dependency Issues:** For supply chain or dependency-related vulnerabilities, update the all modules with `make check-all-modules` and submit a PR. 18 | 3. **Direct Contact:** If you cannot send a PR or the issue requires further discussion, please contact the maintainers directly by email. 19 | 20 | ### Important Notes 21 | 22 | - Please do not publicly disclose the vulnerability until it has been addressed and patched. 23 | - We are committed to transparency and will publicly acknowledge reporters in the release notes unless requested otherwise. 24 | 25 | Your cooperation helps ensure Fuego remains a secure and reliable framework for everyone. 26 | -------------------------------------------------------------------------------- /assets/Zuplo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-fuego/fuego/b482389bf8263ee19095f38b45d4f76fbc85dd50/assets/Zuplo.png -------------------------------------------------------------------------------- /check-all-modules.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | mods=$(go list -f '{{.Dir}}' -m) 6 | for mod in $mods; do 7 | cd "$mod" 8 | echo "=== Updating $mod" 9 | go get -u ./... 10 | go mod tidy 11 | go test ./... 12 | go build -o /dev/null ./... 13 | cd - 14 | echo 15 | done 16 | -------------------------------------------------------------------------------- /cmd/fuego/commands/controller_test.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestCreateController(t *testing.T) { 11 | res, err := createNewEntityDomainFile("books", "controller.go", "booksController.go") 12 | require.NoError(t, err) 13 | require.Contains(t, res, "package books") 14 | require.Contains(t, res, `fuego.Get(booksGroup, "/{id}", rs.getBooks)`) 15 | require.Contains(t, res, `func (rs BooksResources) postBooks(c fuego.ContextWithBody[BooksCreate]) (Books, error)`) 16 | require.FileExists(t, "./domains/books/booksController.go") 17 | os.Remove("./domains/books/booksController.go") 18 | } 19 | -------------------------------------------------------------------------------- /cmd/fuego/commands/service.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/urfave/cli/v2" 7 | ) 8 | 9 | func Service() *cli.Command { 10 | return &cli.Command{ 11 | Name: "service", 12 | Usage: "creates a new service file", 13 | Aliases: []string{"s"}, 14 | Action: serviceCommandAction, 15 | } 16 | } 17 | 18 | func serviceCommandAction(cCtx *cli.Context) error { 19 | entityName := cCtx.Args().First() 20 | 21 | if entityName == "" { 22 | entityName = "newController" 23 | fmt.Println("Note: You can add an entity name as an argument. Example: `fuego service books`") 24 | } 25 | 26 | _, err := createNewEntityDomainFile(entityName, "entity.go", entityName+".go") 27 | if err != nil { 28 | return err 29 | } 30 | 31 | _, err = createNewEntityDomainFile(entityName, "service.go", entityName+"Service.go") 32 | if err != nil { 33 | return err 34 | } 35 | 36 | fmt.Printf("🔥 Service %s created successfully\n", entityName) 37 | return nil 38 | } 39 | -------------------------------------------------------------------------------- /cmd/fuego/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-fuego/fuego/cmd/fuego 2 | 3 | go 1.24.2 4 | 5 | require ( 6 | github.com/go-fuego/fuego v0.18.8 7 | github.com/stretchr/testify v1.10.0 8 | github.com/urfave/cli/v2 v2.27.6 9 | golang.org/x/text v0.25.0 10 | ) 11 | 12 | require ( 13 | github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect 14 | github.com/davecgh/go-spew v1.1.1 // indirect 15 | github.com/gabriel-vasile/mimetype v1.4.8 // indirect 16 | github.com/getkin/kin-openapi v0.132.0 // indirect 17 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 18 | github.com/go-openapi/swag v0.23.0 // indirect 19 | github.com/go-playground/locales v0.14.1 // indirect 20 | github.com/go-playground/universal-translator v0.18.1 // indirect 21 | github.com/go-playground/validator/v10 v10.26.0 // indirect 22 | github.com/golang-jwt/jwt/v5 v5.2.2 // indirect 23 | github.com/google/uuid v1.6.0 // indirect 24 | github.com/gorilla/schema v1.4.1 // indirect 25 | github.com/josharian/intern v1.0.0 // indirect 26 | github.com/leodido/go-urn v1.4.0 // indirect 27 | github.com/mailru/easyjson v0.9.0 // indirect 28 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect 29 | github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect 30 | github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect 31 | github.com/perimeterx/marshmallow v1.1.5 // indirect 32 | github.com/pmezard/go-difflib v1.0.0 // indirect 33 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 34 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect 35 | golang.org/x/crypto v0.37.0 // indirect 36 | golang.org/x/net v0.38.0 // indirect 37 | golang.org/x/sys v0.32.0 // indirect 38 | gopkg.in/yaml.v3 v3.0.1 // indirect 39 | ) 40 | -------------------------------------------------------------------------------- /cmd/fuego/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | 8 | "github.com/urfave/cli/v2" 9 | 10 | "github.com/go-fuego/fuego/cmd/fuego/commands" 11 | ) 12 | 13 | func main() { 14 | app := &cli.App{ 15 | Name: "fuego", 16 | Usage: "The framework for busy Go developers", 17 | Action: func(c *cli.Context) error { 18 | fmt.Println("The 🔥 CLI!") 19 | return nil 20 | }, 21 | Commands: []*cli.Command{ 22 | commands.Controller(), 23 | commands.Service(), 24 | }, 25 | } 26 | 27 | if err := app.Run(os.Args); err != nil { 28 | log.Fatal(err) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /cmd/fuego/templates/embed.go: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | import ( 4 | "embed" 5 | ) 6 | 7 | //go:embed */*.go 8 | var FS embed.FS 9 | -------------------------------------------------------------------------------- /cmd/fuego/templates/newEntity/controller.go: -------------------------------------------------------------------------------- 1 | package newEntity 2 | 3 | import ( 4 | "github.com/go-fuego/fuego" 5 | ) 6 | 7 | type NewEntityResources struct { 8 | // TODO add resources 9 | NewEntityService NewEntityService 10 | } 11 | 12 | func (rs NewEntityResources) Routes(s *fuego.Server) { 13 | newEntityGroup := fuego.Group(s, "/newEntity") 14 | 15 | fuego.Get(newEntityGroup, "/", rs.getAllNewEntity) 16 | fuego.Post(newEntityGroup, "/", rs.postNewEntity) 17 | 18 | fuego.Get(newEntityGroup, "/{id}", rs.getNewEntity) 19 | fuego.Put(newEntityGroup, "/{id}", rs.putNewEntity) 20 | fuego.Delete(newEntityGroup, "/{id}", rs.deleteNewEntity) 21 | } 22 | 23 | func (rs NewEntityResources) getAllNewEntity(c fuego.ContextNoBody) ([]NewEntity, error) { 24 | return rs.NewEntityService.GetAllNewEntity() 25 | } 26 | 27 | func (rs NewEntityResources) postNewEntity(c fuego.ContextWithBody[NewEntityCreate]) (NewEntity, error) { 28 | body, err := c.Body() 29 | if err != nil { 30 | return NewEntity{}, err 31 | } 32 | 33 | return rs.NewEntityService.CreateNewEntity(body) 34 | } 35 | 36 | func (rs NewEntityResources) getNewEntity(c fuego.ContextNoBody) (NewEntity, error) { 37 | id := c.PathParam("id") 38 | 39 | return rs.NewEntityService.GetNewEntity(id) 40 | } 41 | 42 | func (rs NewEntityResources) putNewEntity(c fuego.ContextWithBody[NewEntityUpdate]) (NewEntity, error) { 43 | id := c.PathParam("id") 44 | 45 | body, err := c.Body() 46 | if err != nil { 47 | return NewEntity{}, err 48 | } 49 | 50 | return rs.NewEntityService.UpdateNewEntity(id, body) 51 | } 52 | 53 | func (rs NewEntityResources) deleteNewEntity(c fuego.ContextNoBody) (any, error) { 54 | return rs.NewEntityService.DeleteNewEntity(c.PathParam("id")) 55 | } 56 | -------------------------------------------------------------------------------- /cmd/fuego/templates/newEntity/entity.go: -------------------------------------------------------------------------------- 1 | package newEntity 2 | 3 | type NewEntity struct { 4 | ID string `json:"id"` 5 | Name string `json:"name"` 6 | } 7 | 8 | type NewEntityCreate struct { 9 | Name string `json:"name"` 10 | } 11 | 12 | type NewEntityUpdate struct { 13 | Name string `json:"name"` 14 | } 15 | 16 | type NewEntityService interface { 17 | GetNewEntity(id string) (NewEntity, error) 18 | CreateNewEntity(NewEntityCreate) (NewEntity, error) 19 | GetAllNewEntity() ([]NewEntity, error) 20 | UpdateNewEntity(id string, input NewEntityUpdate) (NewEntity, error) 21 | DeleteNewEntity(id string) (any, error) 22 | } 23 | -------------------------------------------------------------------------------- /ctx_params_test.go: -------------------------------------------------------------------------------- 1 | package fuego_test 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "strconv" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | 11 | "github.com/go-fuego/fuego" 12 | "github.com/go-fuego/fuego/option" 13 | "github.com/go-fuego/fuego/param" 14 | ) 15 | 16 | func TestParam(t *testing.T) { 17 | t.Run("Query params default values", func(t *testing.T) { 18 | s := fuego.NewServer() 19 | 20 | fuego.Get(s, "/test", func(c fuego.ContextNoBody) (string, error) { 21 | name := c.QueryParam("name") 22 | age := c.QueryParamInt("age") 23 | isok := c.QueryParamBool("is_ok") 24 | 25 | return name + strconv.Itoa(age) + strconv.FormatBool(isok), nil 26 | }, 27 | option.Query("name", "Name", param.Required(), param.Default("hey"), param.Example("example1", "you")), 28 | option.QueryInt("age", "Age", param.Nullable(), param.Default(18), param.Example("example1", 1)), 29 | option.QueryBool("is_ok", "Is OK?", param.Default(true), param.Example("example1", true)), 30 | ) 31 | 32 | t.Run("Default should correctly set parameter in controller", func(t *testing.T) { 33 | r := httptest.NewRequest("GET", "/test", nil) 34 | w := httptest.NewRecorder() 35 | s.Mux.ServeHTTP(w, r) 36 | 37 | require.Equal(t, http.StatusOK, w.Code) 38 | require.Equal(t, "hey18true", w.Body.String()) 39 | }) 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /default_middlewares_test.go: -------------------------------------------------------------------------------- 1 | package fuego 2 | 3 | import ( 4 | "log/slog" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | 9 | "github.com/thejerf/slogassert" 10 | ) 11 | 12 | type TestResponseWriter struct{} 13 | 14 | func (w *TestResponseWriter) Header() http.Header { 15 | panic("not implemented") 16 | } 17 | 18 | func (w *TestResponseWriter) Write(b []byte) (int, error) { 19 | panic("not implemented") 20 | } 21 | 22 | func (w *TestResponseWriter) WriteHeader(statusCode int) { 23 | panic("not implemented") 24 | } 25 | 26 | func TestFlush(t *testing.T) { 27 | t.Run("is implemented", func(t *testing.T) { 28 | handler := slogassert.New(t, slog.LevelWarn, nil) 29 | slog.SetDefault(slog.New(handler)) 30 | 31 | rw := newResponseWriter(httptest.NewRecorder()) 32 | rw.Flush() 33 | handler.AssertEmpty() 34 | }) 35 | t.Run("is not implemented", func(t *testing.T) { 36 | handler := slogassert.New(t, slog.LevelWarn, nil) 37 | slog.SetDefault(slog.New(handler)) 38 | 39 | rw := newResponseWriter(&TestResponseWriter{}) 40 | rw.Flush() 41 | handler.AssertMessage("Flush not implemented, skipping") 42 | handler.AssertEmpty() 43 | }) 44 | } 45 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package fuego provides a set of tools to build HTTP servers in Go, that automatically 2 | // generate OpenAPI 3.0 documentation and support for multiple web frameworks. 3 | package fuego 4 | -------------------------------------------------------------------------------- /documentation/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /documentation/.nvmrc: -------------------------------------------------------------------------------- 1 | v20.9.0 2 | -------------------------------------------------------------------------------- /documentation/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus](https://docusaurus.io/), 4 | a modern static website generator. 5 | 6 | ## Installation 7 | 8 | ```sh 9 | yarn 10 | ``` 11 | 12 | ### Local Development 13 | 14 | ```sh 15 | yarn dev 16 | ``` 17 | 18 | This command starts a local development server and opens up a browser window. 19 | Most changes are reflected live without having to restart the server. 20 | 21 | ### Build 22 | 23 | ```sh 24 | yarn build 25 | ``` 26 | 27 | This command generates static content into the `build` directory and 28 | can be served using any static contents hosting service. 29 | 30 | ### Deployment 31 | 32 | Using SSH: 33 | 34 | ```sh 35 | USE_SSH=true yarn deploy 36 | ``` 37 | 38 | Not using SSH: 39 | 40 | ```sh 41 | GIT_USER= yarn deploy 42 | ``` 43 | 44 | If you are using GitHub pages for hosting, this command is a convenient 45 | way to build the website and push to the `gh-pages` branch. 46 | -------------------------------------------------------------------------------- /documentation/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /documentation/docs/guides/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "🚀 Guides", 3 | "position": 3, 4 | "link": { 5 | "type": "generated-index" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /documentation/docs/guides/alternative-routers-support/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Plug to existing Routers", 3 | "link": { 4 | "type": "generated-index" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /documentation/docs/guides/alternative-routers-support/echo.md: -------------------------------------------------------------------------------- 1 | # Echo 2 | 3 | Fuego can be seamlessly integrated with echo framework using the `fuegoecho` adaptor. 4 | 5 | Instead of utilizing the default server **Setup** with `fuego.NewServer()`, you will use the **Engine** with `fuego.NewEngine()`, alongside your Echo router. 6 | 7 | The integration process mirrors the default server setup, with the key difference being that you will declare routes using `fuegoecho.Get`, `fuegoecho.Post`, etc., rather than `fuego.Get`, `fuego.Post`. 8 | 9 | ## Incremental Migration 10 | 11 | Follow these steps to gradually integrate Fuego into your Echo application: 12 | 13 | 1. Instantiate the engine using `fuego.NewEngine()`. 14 | 2. Replace `echo.GET` with `fuegoecho.GetEcho` to wrap your routes with the OpenAPI declaration, without modifying your existing controllers. 15 | 3. Incrementally replace your existing controllers with Fuego controllers (`fuegoecho.Get`), enabling automatic generation of OpenAPI documentation, validation, and content-negotiation for each controller you replace. 16 | 4. Enjoy the enhanced functionality provided by Fuego while maintaining compatibility with your existing Echo application. 17 | 18 | ## Example 19 | 20 | For a comprehensive, up-to-date example, please refer to the [Echo example](https://github.com/go-fuego/fuego/tree/main/examples/echo-compat). 21 | -------------------------------------------------------------------------------- /documentation/docs/guides/alternative-routers-support/gin.md: -------------------------------------------------------------------------------- 1 | # Gin 2 | 3 | Fuego can be used with Gin by using the `fuegogin` adaptor. 4 | 5 | Instead of using the **Server** `fuego.NewServer()`, you will use the **Engine** `fuego.NewEngine()` along with your router. 6 | 7 | The usage is similar to the default server, but you will need to declare the routes with `fuegogin.Get`, `fuegogin.Post`... instead of `fuego.Get`, `fuego.Post`... 8 | 9 | ## Migrate incrementally 10 | 11 | 1. Spawn an engine with `fuego.NewEngine()`. 12 | 2. Use `fuegogin.GetGin` instead of `gin.GET` to wrap the routes with OpenAPI declaration of the route, **without even touching the existing controllers**. 13 | 3. Replace the controllers **one by one** with Fuego controllers. You'll get complete OpenAPI documentation, validation, Content-Negotiation for each controller you replace! 14 | 4. Enjoy the benefits of Fuego with your existing Gin application! 15 | 16 | ## Example 17 | 18 | Please refer to the [Gin example](https://github.com/go-fuego/fuego/tree/main/examples/gin-compat) for a complete and up-to-date example. 19 | -------------------------------------------------------------------------------- /documentation/docs/guides/validation.md: -------------------------------------------------------------------------------- 1 | # Validation 2 | 3 | Validation is the process of ensuring that the data provided to the application 4 | is correct and meaningful. 5 | 6 | With fuego, you have several options for validating data. 7 | 8 | - struct tags with `go-validator` 9 | - custom validation functions 10 | 11 | ## Struct tags 12 | 13 | You can use struct tags to validate the data coming into your application. 14 | This is a common pattern in Go and is used by many libraries, 15 | and we use [`go-playground/validator`](https://github.com/go-playground/validator) to do so. 16 | 17 | ```go 18 | type User struct { 19 | FirstName string `json:"first_name" validate:"required"` 20 | LastName string `json:"last_name" validate:"required"` 21 | Age int `json:"age" validate:"gte=0,lte=130"` 22 | Email string `json:"email" validate:"email"` 23 | } 24 | ``` 25 | 26 | ## Custom validation 27 | 28 | You can also use Fuego's [Transformation](./transformation.md) methods to validate the data. 29 | 30 | ```go 31 | package main 32 | 33 | import ( 34 | "context" 35 | "errors" 36 | "strings" 37 | 38 | "github.com/go-fuego/fuego" 39 | ) 40 | 41 | type User struct { 42 | FirstName string `json:"first_name"` 43 | LastName string `json:"last_name"` 44 | } 45 | 46 | func (u *User) InTransform(ctx context.Context) error { 47 | u.FirstName = strings.ToUpper(u.FirstName) 48 | u.LastName = strings.TrimSpace(u.LastName) 49 | 50 | if u.FirstName == "" { 51 | return errors.New("first name is required") 52 | } 53 | return nil 54 | } 55 | 56 | var _ fuego.InTransformer = (*User)(nil) // Ensure *User implements fuego.InTransformer 57 | // This check is a classic example of Go's interface implementation check and we highly recommend to use it 58 | ``` 59 | -------------------------------------------------------------------------------- /documentation/docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | # 🔥 Fuego 6 | 7 |

8 | Fuego Logo 9 |

10 | 11 | Let's discover **Fuego in less than 5 minutes**. 12 | 13 | ## Quick peek without installing 14 | 15 | Try our [Hello World](./tutorials/01-hello-world.md)! 16 | 17 | ```bash 18 | go run github.com/go-fuego/fuego/examples/hello-world@latest 19 | ``` 20 | 21 | This simple code snippet runs a 'Hello World' server. 22 | See how much Fuego generates from just a few lines of code! 23 | You'll even get a URL to view the result directly in your browser 24 | 25 | ```go showLineNumbers 26 | package main 27 | 28 | import ( 29 | "github.com/go-fuego/fuego" 30 | ) 31 | 32 | func main() { 33 | s := fuego.NewServer() 34 | 35 | fuego.Get(s, "/", helloWorld) 36 | 37 | s.Run() 38 | } 39 | 40 | func helloWorld(c fuego.ContextNoBody) (string, error) { 41 | return "Hello, World!", nil 42 | } 43 | ``` 44 | 45 | ![Swagger UI](../static/img/hello-world-openapi.jpeg) 46 | 47 | ## Try examples with real source code in 3 sec 48 | 49 | Just copy/paste these commands in your terminal, 50 | you'll be iterating on a real example in no time. 51 | 52 | ```bash 53 | git clone git@github.com:go-fuego/fuego.git 54 | cd fuego/examples/petstore 55 | go run . 56 | ``` 57 | 58 | ### What you'll need 59 | 60 | - [Golang v1.22](https://golang.org/doc/go1.22) or above 61 | _(Fuego relies on a new feature of the net/http package only available after 1.22)_. 62 | -------------------------------------------------------------------------------- /documentation/docs/internals/01-data-flow.mdx: -------------------------------------------------------------------------------- 1 | import FlowChart from '@site/src/components/FlowChart'; 2 | 3 | # Data Flow 4 | 5 | Here's a high-level overview of how data flows through a Fuego application. 6 | 7 | Please note that every step can send an error to the error handler. 8 | 9 | 10 | -------------------------------------------------------------------------------- /documentation/docs/internals/02-architecture.md: -------------------------------------------------------------------------------- 1 | # Architecture 2 | 3 | Fuego's architecture rely on the following components: 4 | 5 | - **Engine**: The engine is responsible for handling the request and response. It is the core of Fuego. 6 | - It contains the **OpenAPI** struct with the Description and OpenAPI-related utilities. 7 | - It also contains the centralized Error Handler. 8 | - **Server**: The default `net/http` server that Fuego uses to listen for incoming requests. 9 | - Responsible for routes, groups and middlewares. 10 | - **Adaptors**: If you use Gin, Echo, or any other web framework, you can use an adaptor to use Fuego with them. 11 | - **Context**: The context is a generic typed interface that represents the state that the user can access & modify in the controller. 12 | 13 | ![Fuego Architecture](./architecture.png) 14 | -------------------------------------------------------------------------------- /documentation/docs/internals/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "⚙ Internals & Contributing", 3 | "position": 4, 4 | "link": { 5 | "type": "generated-index" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /documentation/docs/internals/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-fuego/fuego/b482389bf8263ee19095f38b45d4f76fbc85dd50/documentation/docs/internals/architecture.png -------------------------------------------------------------------------------- /documentation/docs/tutorials/01-hello-world.md: -------------------------------------------------------------------------------- 1 | # Hello world 2 | 3 | Let's discover **Fuego** in a few lines. 4 | 5 | ## Quick start 6 | 7 | If you don't want to copy/paste the code on your local setup, you can run the 8 | following command: 9 | 10 | ```bash 11 | go run github.com/go-fuego/fuego/examples/hello-world@latest 12 | ``` 13 | 14 | Useful URLs (including OpenAPI spec & Swagger UI) are given in the terminal: 15 | you'll be able to see the result in your browser. 16 | 17 | ## Start from scratch 18 | 19 | First, create a directory for your project: 20 | 21 | ```bash 22 | mkdir hello-fuego 23 | cd hello-fuego 24 | ``` 25 | 26 | Then, create a `go.mod` file: 27 | 28 | ```bash 29 | go mod init hello-fuego 30 | ``` 31 | 32 | Finally, create a `main.go` file with the following content: 33 | 34 | ```go 35 | package main 36 | 37 | import ( 38 | "github.com/go-fuego/fuego" 39 | ) 40 | 41 | func main() { 42 | s := fuego.NewServer() 43 | 44 | fuego.Get(s, "/", func(c fuego.ContextNoBody) (string, error) { 45 | return "Hello, World!", nil 46 | }) 47 | 48 | s.Run() 49 | } 50 | ``` 51 | 52 | You can now run your server: 53 | 54 | ```bash 55 | go mod tidy 56 | go run . 57 | ``` 58 | -------------------------------------------------------------------------------- /documentation/docs/tutorials/03-hot-reload.md: -------------------------------------------------------------------------------- 1 | # Hot reload 2 | 3 | Hot reload is a feature that allows you to update your code and 4 | see the changes in real-time without restarting the server. 5 | This is very useful for development, 6 | as it allows you to see the changes you make to your code immediately. 7 | 8 | To enable hot reload, you need to install the `air` command-line tool. 9 | 10 | ```sh 11 | go install github.com/air-verse/air@latest 12 | ``` 13 | 14 | Optionally, create a `.air.toml` configuration file to customize the hot reload behavior. 15 | 16 | ```sh 17 | air init 18 | ``` 19 | 20 | Simply the following command to start the server with hot reload. 21 | 22 | ```sh 23 | air 24 | ``` 25 | -------------------------------------------------------------------------------- /documentation/docs/tutorials/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "🍰 Tutorials", 3 | "position": 2, 4 | "link": { 5 | "type": "generated-index" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /documentation/docs/tutorials/rendering/gomponents.md: -------------------------------------------------------------------------------- 1 | # Gomponents 2 | 3 | Fuego supports rendering HTML templates with [Gomponents](https://github.com/maragudk/gomponents). 4 | 5 | Just use the `fuego.Gomponent` type as a return type for your handler, 6 | and return the gomponent. 7 | 8 | ```go 9 | import ( 10 | "github.com/go-fuego/fuego" 11 | "github.com/go-fuego/fuego/examples/full-app-gourmet/store" 12 | ) 13 | 14 | // highlight-next-line 15 | func (rs Resource) adminIngredients(c fuego.ContextNoBody) (fuego.Gomponent, error) { 16 | searchParams := components.SearchParams{ 17 | Name: c.QueryParam("name"), 18 | PerPage: c.QueryParamInt("perPage", 20), 19 | Page: c.QueryParamInt("page", 1), 20 | URL: "/admin/ingredients", 21 | Lang: c.MainLang(), 22 | } 23 | 24 | ingredients, err := rs.IngredientsQueries.SearchIngredients(c.Context(), store.SearchIngredientsParams{ 25 | Name: "%" + searchParams.Name + "%", 26 | Limit: int64(searchParams.PerPage), 27 | Offset: int64(searchParams.Page-1) * int64(searchParams.PerPage), 28 | }) 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | // highlight-next-line 34 | return admin.IngredientList(ingredients, searchParams), nil 35 | } 36 | ``` 37 | 38 | Note that the `fuego.Gomponent` type is a simple alias for `fuego.Renderer`: 39 | any type that implements the `Render(io.Writer) error` 40 | method can be used as a return type for a handler. 41 | -------------------------------------------------------------------------------- /documentation/docs/tutorials/rendering/index.md: -------------------------------------------------------------------------------- 1 | # HTML Rendering 2 | 3 | Fuego is not only capable to handle XML and JSON, it can also render HTML. 4 | 5 | It supports templating with [html/template](https://pkg.go.dev/html/template), 6 | [Templ](https://github.com/a-h/templ), and [Gomponents](https://github.com/maragudk/gomponents). 7 | 8 | ## Content Negotiation 9 | 10 | Remember that Fuego handles [Content Negotiation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation), 11 | so you can serve different content types based on the `Accept` header of the request. 12 | 13 | Fuego also provides a helper to render HTML or JSON data for a single controller! 14 | 15 | ```go 16 | package main 17 | 18 | import ( 19 | "github.com/go-fuego/fuego" 20 | ) 21 | 22 | func main() { 23 | s := fuego.NewServer() 24 | 25 | fuego.Get(s, "/", func(c fuego.ContextNoBody) (interface{}, error) { 26 | return fuego.DataOrHTML( 27 | data, // When asking for JSON/XML, this data will be returned 28 | MyTemplateInjectedWithData(data), // When asking for HTML, this template will be rendered 29 | ), nil 30 | }) 31 | 32 | s.Run() 33 | } 34 | ``` 35 | -------------------------------------------------------------------------------- /documentation/docs/tutorials/rendering/std.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | # html/template 6 | 7 | Fuego supports rendering HTML templates with the 8 | [html/template](https://pkg.go.dev/html/template) package. 9 | 10 | Just use the `fuego.HTML` type as a return type for your handler, and return 11 | `c.Render()` with the template name and data. 12 | 13 | ```go 14 | import ( 15 | "github.com/go-fuego/fuego" 16 | "github.com/go-fuego/fuego/examples/full-app-gourmet/store/types" 17 | ) 18 | 19 | // highlight-next-line 20 | func (rs Resource) unitPreselected(c fuego.ContextNoBody) (fuego.CtxRenderer, error) { 21 | id := c.QueryParam("IngredientID") 22 | 23 | ingredient, err := rs.IngredientsQueries.GetIngredient(c.Context(), id) 24 | if err != nil { 25 | return "", err 26 | } 27 | 28 | // highlight-start 29 | return c.Render("preselected-unit.partial.html", fuego.H{ 30 | "Units": types.UnitValues, 31 | "SelectedUnit": ingredient.DefaultUnit, 32 | }) 33 | // highlight-end 34 | } 35 | ``` 36 | -------------------------------------------------------------------------------- /documentation/docs/tutorials/rendering/templ.md: -------------------------------------------------------------------------------- 1 | # Templ 2 | 3 | Fuego supports templating with [Templ](https://github.com/a-h/templ). 4 | 5 | Simply return a Templ component from your handler, 6 | with the `fuego.Templ` return type. 7 | 8 | Example from [a recipe app](https://github.com/go-fuego/fuego/tree/main/examples/full-app-gourmet): 9 | 10 | ```go 11 | import ( 12 | "github.com/go-fuego/fuego" 13 | "github.com/go-fuego/fuego/examples/full-app-gourmet/store" 14 | ) 15 | 16 | // highlight-next-line 17 | func (rs Resource) adminIngredients(c fuego.ContextNoBody) (fuego.Templ, error) { 18 | searchParams := components.SearchParams{ 19 | Name: c.QueryParam("name"), 20 | PerPage: c.QueryParamInt("perPage", 20), 21 | Page: c.QueryParamInt("page", 1), 22 | URL: "/admin/ingredients", 23 | Lang: c.MainLang(), 24 | } 25 | 26 | ingredients, err := rs.IngredientsQueries.SearchIngredients(c.Context(), store.SearchIngredientsParams{ 27 | Name: "%" + searchParams.Name + "%", 28 | Limit: int64(searchParams.PerPage), 29 | Offset: int64(searchParams.Page-1) * int64(searchParams.PerPage), 30 | }) 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | // highlight-next-line 36 | return admin.IngredientList(ingredients, searchParams), nil 37 | } 38 | ``` 39 | 40 | Note that the `fuego.Templ` type is a simple alias for `fuego.CtxRenderer`: 41 | any type that implements the `Render(context.Context, io.Writer) error` 42 | method can be used as a return type for a handler. 43 | -------------------------------------------------------------------------------- /documentation/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fuego", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "dev": "docusaurus start --port 3009", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "clear": "docusaurus clear", 12 | "serve": "docusaurus serve", 13 | "write-translations": "docusaurus write-translations", 14 | "write-heading-ids": "docusaurus write-heading-ids", 15 | "typecheck": "tsc" 16 | }, 17 | "dependencies": { 18 | "@docusaurus/core": "^3.8.0", 19 | "@docusaurus/preset-classic": "^3.8.0", 20 | "@docusaurus/theme-mermaid": "^3.8.0", 21 | "@mdx-js/react": "^3.1.0", 22 | "clsx": "^2.1.1", 23 | "docusaurus-lunr-search": "^3.6.0", 24 | "prism-react-renderer": "^2.4.1", 25 | "react": "^19.1.0", 26 | "react-dom": "^19.1.0" 27 | }, 28 | "devDependencies": { 29 | "@docusaurus/module-type-aliases": "^3.7.0", 30 | "@docusaurus/tsconfig": "^3.8.0", 31 | "@docusaurus/types": "^3.7.0", 32 | "typescript": "~5.8.3" 33 | }, 34 | "browserslist": { 35 | "production": [ 36 | ">0.5%", 37 | "not dead", 38 | "not op_mini all" 39 | ], 40 | "development": [ 41 | "last 3 chrome version", 42 | "last 3 firefox version", 43 | "last 5 safari version" 44 | ] 45 | }, 46 | "engines": { 47 | "node": ">=20.0" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /documentation/sidebars.ts: -------------------------------------------------------------------------------- 1 | import type {SidebarsConfig} from '@docusaurus/plugin-content-docs'; 2 | 3 | /** 4 | * Creating a sidebar enables you to: 5 | - create an ordered group of docs 6 | - render a sidebar for each doc of that group 7 | - provide next/previous navigation 8 | 9 | The sidebars can be generated from the filesystem, or explicitly defined here. 10 | 11 | Create as many sidebars as you want. 12 | */ 13 | const sidebars: SidebarsConfig = { 14 | // By default, Docusaurus generates a sidebar from the docs folder structure 15 | tutorialSidebar: [{type: 'autogenerated', dirName: '.'}], 16 | 17 | // But you can create a sidebar manually 18 | /* 19 | tutorialSidebar: [ 20 | 'intro', 21 | 'hello', 22 | { 23 | type: 'category', 24 | label: 'Tutorial', 25 | items: ['tutorial-basics/create-a-document'], 26 | }, 27 | ], 28 | */ 29 | }; 30 | 31 | export default sidebars; 32 | -------------------------------------------------------------------------------- /documentation/src/components/HomepageFeatures/index.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | import styles from "./styles.module.css"; 3 | 4 | export default function HomepageFeatures(): JSX.Element { 5 | return ( 6 |
7 |
8 |

Serialization

9 |

Simple serialization of data structures to JSON and back.

10 |
11 |
12 |

OpenAPI generation

13 |

Generate OpenAPI 3.0 specifications from your data structures.

14 |
15 |
16 |

Validation

17 |

Easily validate data structures against your own rules.

18 |
19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /documentation/src/components/HomepageFeatures/styles.module.css: -------------------------------------------------------------------------------- 1 | .features { 2 | display: flex; 3 | flex-direction: row; 4 | flex-wrap: wrap; 5 | width: 100%; 6 | } 7 | 8 | .block { 9 | min-width: 200px; 10 | display: flex; 11 | flex-direction: column; 12 | align-items: center; 13 | justify-content: center; 14 | padding: 1rem; 15 | width: 100%; 16 | } 17 | 18 | .block_odd { 19 | background-color: #953a09; 20 | color: #fff; 21 | } 22 | 23 | .featureSvg { 24 | height: 200px; 25 | width: 200px; 26 | } 27 | -------------------------------------------------------------------------------- /documentation/src/css/custom.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Any CSS included here will be global. The classic template 3 | * bundles Infima by default. Infima is a CSS framework designed to 4 | * work well for content-centric websites. 5 | */ 6 | 7 | /* You can override the default Infima variables here. */ 8 | :root { 9 | --ifm-color-primary: #953a09; 10 | --ifm-color-primary-dark: #863408; 11 | --ifm-color-primary-darker: #7f3108; 12 | --ifm-color-primary-darkest: #682906; 13 | --ifm-color-primary-light: #a4400a; 14 | --ifm-color-primary-lighter: #ab430a; 15 | --ifm-color-primary-lightest: #c24b0c; 16 | --ifm-code-font-size: 95%; 17 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); 18 | } 19 | 20 | /* For readability concerns, you should choose a lighter palette in dark mode. */ 21 | [data-theme="dark"] { 22 | --ifm-color-primary: #ffb752; 23 | --ifm-color-primary-dark: #ffa930; 24 | --ifm-color-primary-darker: #ffa21f; 25 | --ifm-color-primary-darkest: #ec8a00; 26 | --ifm-color-primary-light: #ffc574; 27 | --ifm-color-primary-lighter: #ffcc85; 28 | --ifm-color-primary-lightest: #ffe1b7; 29 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); 30 | } 31 | 32 | /* 33 | 34 | Mobile: <996px 35 | Small : 996px - 1159px 36 | Medium : 1160px - 1429px 37 | Large : >1430px 38 | 39 | */ 40 | 41 | .display-mobile { 42 | display: block; 43 | } 44 | 45 | @media (min-width: 996px) { 46 | .display-mobile { 47 | display: none; 48 | } 49 | } 50 | 51 | .display-small { 52 | display: none; 53 | } 54 | 55 | @media (min-width: 996px) and (max-width: 1159px) { 56 | .display-small { 57 | display: block; 58 | } 59 | } 60 | 61 | .display-medium { 62 | display: none; 63 | } 64 | 65 | @media (min-width: 1161px) and (max-width: 1429px) { 66 | .display-medium { 67 | display: block; 68 | } 69 | } 70 | 71 | .display-large { 72 | display: none; 73 | } 74 | 75 | @media (min-width: 1430px) { 76 | .display-large { 77 | display: block; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /documentation/src/pages/index.module.css: -------------------------------------------------------------------------------- 1 | /** 2 | * CSS files with the .module.css suffix will be treated as CSS modules 3 | * and scoped locally. 4 | */ 5 | 6 | .heroBanner { 7 | padding: 1rem; 8 | text-align: center; 9 | position: relative; 10 | overflow: hidden; 11 | } 12 | 13 | @media screen and (min-width: 996px) { 14 | .heroBanner { 15 | padding: 4rem 2rem; 16 | } 17 | } 18 | 19 | .buttons { 20 | display: flex; 21 | align-items: center; 22 | justify-content: center; 23 | } 24 | 25 | .main { 26 | padding: 0 1rem 1rem; 27 | display: flex; 28 | flex-direction: column; 29 | align-items: center; 30 | justify-content: center; 31 | } 32 | 33 | @media screen and (min-width: 996px) { 34 | .main { 35 | padding: 0 3rem 3rem; 36 | } 37 | 38 | } 39 | 40 | .video { 41 | margin-top: 5rem; 42 | width: 100%; 43 | max-width: 50rem; 44 | } 45 | -------------------------------------------------------------------------------- /documentation/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | import Link from "@docusaurus/Link"; 3 | import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; 4 | import Layout from "@theme/Layout"; 5 | import Heading from "@theme/Heading"; 6 | 7 | import styles from "./index.module.css"; 8 | 9 | function HomepageHeader() { 10 | const { siteConfig } = useDocusaurusContext(); 11 | return ( 12 |
13 |
14 | 15 | 16 | 17 | {siteConfig.title} 18 | 19 |

{siteConfig.tagline}

20 |
21 |
22 | ); 23 | } 24 | 25 | export default function Home(): JSX.Element { 26 | const { siteConfig } = useDocusaurusContext(); 27 | return ( 28 | 29 | 30 |
31 |
32 | 33 | Tutorial - 5 min ⏱️ 34 | 35 |
36 | 47 |
48 |
49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /documentation/src/pages/markdown-page.md: -------------------------------------------------------------------------------- 1 | # Markdown page example 2 | 3 | You don't need React to write simple standalone pages. 4 | -------------------------------------------------------------------------------- /documentation/static/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-fuego/fuego/b482389bf8263ee19095f38b45d4f76fbc85dd50/documentation/static/.nojekyll -------------------------------------------------------------------------------- /documentation/static/img/docusaurus-social-card.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-fuego/fuego/b482389bf8263ee19095f38b45d4f76fbc85dd50/documentation/static/img/docusaurus-social-card.jpg -------------------------------------------------------------------------------- /documentation/static/img/fuego-big.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-fuego/fuego/b482389bf8263ee19095f38b45d4f76fbc85dd50/documentation/static/img/fuego-big.png -------------------------------------------------------------------------------- /documentation/static/img/fuego.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-fuego/fuego/b482389bf8263ee19095f38b45d4f76fbc85dd50/documentation/static/img/fuego.ico -------------------------------------------------------------------------------- /documentation/static/img/fuego.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-fuego/fuego/b482389bf8263ee19095f38b45d4f76fbc85dd50/documentation/static/img/fuego.png -------------------------------------------------------------------------------- /documentation/static/img/hello-world-openapi.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-fuego/fuego/b482389bf8263ee19095f38b45d4f76fbc85dd50/documentation/static/img/hello-world-openapi.jpeg -------------------------------------------------------------------------------- /documentation/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // This file is not used in compilation. It is here just for a nice editor experience. 3 | "extends": "@docusaurus/tsconfig", 4 | "compilerOptions": { 5 | "baseUrl": "." 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /examples/basic/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-fuego/fuego/examples/basic 2 | 3 | go 1.24.2 4 | 5 | require ( 6 | github.com/go-chi/chi/v5 v5.2.1 7 | github.com/go-fuego/fuego v0.18.8 8 | github.com/rs/cors v1.11.1 9 | ) 10 | 11 | require ( 12 | github.com/gabriel-vasile/mimetype v1.4.8 // indirect 13 | github.com/getkin/kin-openapi v0.132.0 // indirect 14 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 15 | github.com/go-openapi/swag v0.23.0 // indirect 16 | github.com/go-playground/locales v0.14.1 // indirect 17 | github.com/go-playground/universal-translator v0.18.1 // indirect 18 | github.com/go-playground/validator/v10 v10.26.0 // indirect 19 | github.com/golang-jwt/jwt/v5 v5.2.2 // indirect 20 | github.com/google/uuid v1.6.0 // indirect 21 | github.com/gorilla/schema v1.4.1 // indirect 22 | github.com/josharian/intern v1.0.0 // indirect 23 | github.com/leodido/go-urn v1.4.0 // indirect 24 | github.com/mailru/easyjson v0.9.0 // indirect 25 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect 26 | github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect 27 | github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect 28 | github.com/perimeterx/marshmallow v1.1.5 // indirect 29 | golang.org/x/crypto v0.37.0 // indirect 30 | golang.org/x/net v0.38.0 // indirect 31 | golang.org/x/sys v0.32.0 // indirect 32 | golang.org/x/text v0.25.0 // indirect 33 | gopkg.in/yaml.v3 v3.0.1 // indirect 34 | ) 35 | -------------------------------------------------------------------------------- /examples/crud-gorm/.gitignore: -------------------------------------------------------------------------------- 1 | tmp/ 2 | doc/ 3 | -------------------------------------------------------------------------------- /examples/crud-gorm/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-fuego/fuego/examples/crud-gorm 2 | 3 | go 1.24.2 4 | 5 | require ( 6 | github.com/go-fuego/fuego v0.18.8 7 | github.com/golang/mock v1.6.0 8 | gorm.io/driver/sqlite v1.5.7 9 | gorm.io/gorm v1.30.0 10 | ) 11 | 12 | require ( 13 | github.com/gabriel-vasile/mimetype v1.4.8 // indirect 14 | github.com/getkin/kin-openapi v0.132.0 // indirect 15 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 16 | github.com/go-openapi/swag v0.23.0 // indirect 17 | github.com/go-playground/locales v0.14.1 // indirect 18 | github.com/go-playground/universal-translator v0.18.1 // indirect 19 | github.com/go-playground/validator/v10 v10.26.0 // indirect 20 | github.com/golang-jwt/jwt/v5 v5.2.2 // indirect 21 | github.com/google/uuid v1.6.0 // indirect 22 | github.com/gorilla/schema v1.4.1 // indirect 23 | github.com/jinzhu/inflection v1.0.0 // indirect 24 | github.com/jinzhu/now v1.1.5 // indirect 25 | github.com/josharian/intern v1.0.0 // indirect 26 | github.com/leodido/go-urn v1.4.0 // indirect 27 | github.com/mailru/easyjson v0.9.0 // indirect 28 | github.com/mattn/go-sqlite3 v1.14.28 // indirect 29 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect 30 | github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect 31 | github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect 32 | github.com/perimeterx/marshmallow v1.1.5 // indirect 33 | golang.org/x/crypto v0.37.0 // indirect 34 | golang.org/x/net v0.38.0 // indirect 35 | golang.org/x/sys v0.32.0 // indirect 36 | golang.org/x/text v0.25.0 // indirect 37 | gopkg.in/yaml.v3 v3.0.1 // indirect 38 | ) 39 | -------------------------------------------------------------------------------- /examples/crud-gorm/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/go-fuego/fuego" 5 | "github.com/go-fuego/fuego/examples/crud-gorm/handlers" 6 | "github.com/go-fuego/fuego/examples/crud-gorm/models" 7 | "github.com/go-fuego/fuego/examples/crud-gorm/queries" 8 | "github.com/go-fuego/fuego/option" 9 | 10 | "gorm.io/driver/sqlite" 11 | "gorm.io/gorm" 12 | ) 13 | 14 | func main() { 15 | db, err := gorm.Open(sqlite.Open("users.db"), &gorm.Config{}) 16 | if err != nil { 17 | panic("error connecting to database") 18 | } 19 | 20 | db.AutoMigrate(&models.User{}) 21 | 22 | server := fuego.NewServer() 23 | 24 | userQueries := &queries.UserQueries{DB: db} 25 | handlers := &handlers.UserResources{UserQueries: userQueries} 26 | 27 | fuego.Get(server, "/", func(c fuego.ContextNoBody) (string, error) { 28 | return "Hello, World!", nil 29 | }) 30 | fuego.Get(server, "/users", handlers.GetUsers) 31 | fuego.Post(server, "/users", handlers.CreateUser, option.DefaultStatusCode(201)) 32 | fuego.Get(server, "/users/{id}", handlers.GetUserByID) 33 | fuego.Put(server, "/users/{id}", handlers.UpdateUser) 34 | fuego.Delete(server, "/users/{id}", handlers.DeleteUser, option.DefaultStatusCode(204)) 35 | 36 | server.Run() 37 | } 38 | -------------------------------------------------------------------------------- /examples/crud-gorm/models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "gorm.io/gorm" 4 | 5 | type User struct { 6 | gorm.Model 7 | Name string `json:"name" gorm:"not null"` 8 | Email string `json:"email" gorm:"unique;not null"` 9 | } 10 | -------------------------------------------------------------------------------- /examples/crud-gorm/queries/query.go: -------------------------------------------------------------------------------- 1 | package queries 2 | 3 | import ( 4 | "errors" 5 | 6 | "gorm.io/gorm" 7 | 8 | "github.com/go-fuego/fuego" 9 | "github.com/go-fuego/fuego/examples/crud-gorm/models" 10 | ) 11 | 12 | type UserQueries struct { 13 | DB *gorm.DB 14 | } 15 | 16 | func (q *UserQueries) GetUserByID(id uint) (*models.User, error) { 17 | var user models.User 18 | err := q.DB.First(&user, id).Error 19 | if err != nil { 20 | if errors.Is(err, gorm.ErrRecordNotFound) { 21 | return nil, fuego.NotFoundError{ 22 | Title: "User not found", 23 | Detail: "No user with the provided ID was found.", 24 | Err: err, 25 | } 26 | } 27 | return nil, err 28 | 29 | } 30 | return &user, nil 31 | } 32 | 33 | func (q *UserQueries) GetUserByEmail(email string) (*models.User, error) { 34 | var user models.User 35 | err := q.DB.Where("email = ?", email).First(&user).Error 36 | if err != nil { 37 | return nil, err 38 | } 39 | return &user, nil 40 | } 41 | 42 | func (q *UserQueries) GetUsers() ([]models.User, error) { 43 | var users []models.User 44 | if err := q.DB.Find(&users).Error; err != nil { 45 | return nil, err 46 | } 47 | return users, nil 48 | } 49 | 50 | func (q *UserQueries) CreateUser(user *models.User) (*models.User, error) { 51 | err := q.DB.Create(user).Error 52 | if err != nil { 53 | return nil, fuego.InternalServerError{ 54 | Detail: "Failed to create the user.", 55 | Err: err, 56 | } 57 | } 58 | return user, nil 59 | } 60 | 61 | func (q *UserQueries) UpdateUser(user *models.User) (*models.User, error) { 62 | err := q.DB.Save(user).Error 63 | if err != nil { 64 | return nil, fuego.InternalServerError{ 65 | Detail: "Failed to update the user.", 66 | Err: err, 67 | } 68 | } 69 | return user, nil 70 | } 71 | 72 | func (q *UserQueries) DeleteUser(id uint) error { 73 | return q.DB.Delete(&models.User{}, id).Error 74 | } 75 | -------------------------------------------------------------------------------- /examples/custom-errors/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-fuego/fuego/examples/custom-errors 2 | 3 | go 1.24.2 4 | 5 | require ( 6 | github.com/go-chi/chi/v5 v5.2.1 7 | github.com/go-fuego/fuego v0.18.8 8 | github.com/rs/cors v1.11.1 9 | ) 10 | 11 | require ( 12 | github.com/gabriel-vasile/mimetype v1.4.8 // indirect 13 | github.com/getkin/kin-openapi v0.132.0 // indirect 14 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 15 | github.com/go-openapi/swag v0.23.0 // indirect 16 | github.com/go-playground/locales v0.14.1 // indirect 17 | github.com/go-playground/universal-translator v0.18.1 // indirect 18 | github.com/go-playground/validator/v10 v10.26.0 // indirect 19 | github.com/golang-jwt/jwt/v5 v5.2.2 // indirect 20 | github.com/google/uuid v1.6.0 // indirect 21 | github.com/gorilla/schema v1.4.1 // indirect 22 | github.com/josharian/intern v1.0.0 // indirect 23 | github.com/leodido/go-urn v1.4.0 // indirect 24 | github.com/mailru/easyjson v0.9.0 // indirect 25 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect 26 | github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect 27 | github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect 28 | github.com/perimeterx/marshmallow v1.1.5 // indirect 29 | golang.org/x/crypto v0.37.0 // indirect 30 | golang.org/x/net v0.38.0 // indirect 31 | golang.org/x/sys v0.32.0 // indirect 32 | golang.org/x/text v0.25.0 // indirect 33 | gopkg.in/yaml.v3 v3.0.1 // indirect 34 | ) 35 | -------------------------------------------------------------------------------- /examples/custom-errors/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | "strings" 7 | 8 | chiMiddleware "github.com/go-chi/chi/v5/middleware" 9 | "github.com/rs/cors" 10 | 11 | "github.com/go-fuego/fuego" 12 | "github.com/go-fuego/fuego/option" 13 | ) 14 | 15 | type MyError struct { 16 | Err error `json:"error"` 17 | Message string `json:"message"` 18 | } 19 | 20 | var ( 21 | _ fuego.ErrorWithStatus = MyError{} 22 | _ fuego.ErrorWithDetail = MyError{} 23 | ) 24 | 25 | func (e MyError) Error() string { return e.Err.Error() } 26 | 27 | func (e MyError) StatusCode() int { return http.StatusTeapot } 28 | 29 | func (e MyError) DetailMsg() string { 30 | return strings.Split(e.Error(), " ")[1] 31 | } 32 | 33 | func main() { 34 | s := fuego.NewServer( 35 | fuego.WithAddr("localhost:8088"), 36 | ) 37 | 38 | fuego.Use(s, cors.Default().Handler) 39 | fuego.Use(s, chiMiddleware.Compress(5, "text/html", "text/css")) 40 | 41 | fuego.Get(s, "/custom-err", func(c fuego.ContextNoBody) (string, error) { 42 | return "hello", MyError{Err: errors.New("my error")} 43 | }, 44 | option.AddError(http.StatusTeapot, "my custom teapot error", MyError{}), 45 | ) 46 | 47 | s.Run() 48 | } 49 | -------------------------------------------------------------------------------- /examples/custom-serializer/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-fuego/fuego/examples/custom-serializer 2 | 3 | go 1.24.2 4 | 5 | require ( 6 | github.com/go-fuego/fuego v0.18.8 7 | github.com/json-iterator/go v1.1.12 8 | ) 9 | 10 | require ( 11 | github.com/gabriel-vasile/mimetype v1.4.8 // indirect 12 | github.com/getkin/kin-openapi v0.132.0 // indirect 13 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 14 | github.com/go-openapi/swag v0.23.0 // indirect 15 | github.com/go-playground/locales v0.14.1 // indirect 16 | github.com/go-playground/universal-translator v0.18.1 // indirect 17 | github.com/go-playground/validator/v10 v10.26.0 // indirect 18 | github.com/golang-jwt/jwt/v5 v5.2.2 // indirect 19 | github.com/google/uuid v1.6.0 // indirect 20 | github.com/gorilla/schema v1.4.1 // indirect 21 | github.com/josharian/intern v1.0.0 // indirect 22 | github.com/leodido/go-urn v1.4.0 // indirect 23 | github.com/mailru/easyjson v0.9.0 // indirect 24 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 25 | github.com/modern-go/reflect2 v1.0.2 // indirect 26 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect 27 | github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect 28 | github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect 29 | github.com/perimeterx/marshmallow v1.1.5 // indirect 30 | golang.org/x/crypto v0.37.0 // indirect 31 | golang.org/x/net v0.38.0 // indirect 32 | golang.org/x/sys v0.32.0 // indirect 33 | golang.org/x/text v0.25.0 // indirect 34 | gopkg.in/yaml.v3 v3.0.1 // indirect 35 | ) 36 | -------------------------------------------------------------------------------- /examples/custom-serializer/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | 6 | jsoniter "github.com/json-iterator/go" 7 | 8 | "github.com/go-fuego/fuego" 9 | ) 10 | 11 | var json = jsoniter.ConfigCompatibleWithStandardLibrary 12 | 13 | func main() { 14 | s := fuego.NewServer() 15 | 16 | s.Serialize = func(w http.ResponseWriter, _ *http.Request, ans any) error { 17 | w.Header().Set("Content-Type", "text/plain") 18 | return json.NewEncoder(w).Encode(ans) 19 | } 20 | 21 | fuego.Get(s, "/", helloWorld) 22 | 23 | s.Run() 24 | } 25 | 26 | func helloWorld(c fuego.ContextNoBody) (string, error) { 27 | return "Hello, World!", nil 28 | } 29 | -------------------------------------------------------------------------------- /examples/echo-compat/handlers.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/labstack/echo/v4" 8 | 9 | "github.com/go-fuego/fuego" 10 | ) 11 | 12 | func echoController(c echo.Context) error { 13 | return c.String(http.StatusOK, "pong") 14 | } 15 | 16 | func fuegoControllerGet(c fuego.ContextNoBody) (HelloResponse, error) { 17 | return HelloResponse{ 18 | Message: "Hello", 19 | }, nil 20 | } 21 | 22 | func fuegoControllerPost(c fuego.ContextWithBody[HelloRequest]) (*HelloResponse, error) { 23 | body, err := c.Body() 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | if body.Word == "forbidden" { 29 | return nil, fuego.BadRequestError{Title: "Forbidden word"} 30 | } 31 | 32 | name := c.QueryParam("name") 33 | 34 | return &HelloResponse{ 35 | Message: fmt.Sprintf("Hello %s, %s", body.Word, name), 36 | }, nil 37 | } 38 | 39 | func DefaultOpenAPIHandler(specURL string) echo.HandlerFunc { 40 | return func(ctx echo.Context) error { 41 | ctx.Response().Header().Set(echo.HeaderContentType, "text/html; charset=utf-8") 42 | return ctx.String(http.StatusOK, fuego.DefaultOpenAPIHTML(specURL)) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /examples/echo-compat/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | 8 | "github.com/go-fuego/fuego" 9 | ) 10 | 11 | func TestFuegoControllerPost(t *testing.T) { 12 | testCtx := fuego.NewMockContext(HelloRequest{Word: "World"}, any(nil)) 13 | testCtx.SetQueryParam("name", "Ewen") 14 | 15 | response, err := fuegoControllerPost(testCtx) 16 | require.NoError(t, err) 17 | require.Equal(t, "Hello World, Ewen", response.Message) 18 | } 19 | -------------------------------------------------------------------------------- /examples/echo-compat/test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestFuegoEcho(t *testing.T) { 12 | e, _ := server() 13 | 14 | t.Run("simply test echo", func(t *testing.T) { 15 | r := httptest.NewRequest("GET", "/echo", nil) 16 | w := httptest.NewRecorder() 17 | 18 | e.ServeHTTP(w, r) 19 | 20 | require.Equal(t, 200, w.Code) 21 | }) 22 | 23 | t.Run("test fuego plugin", func(t *testing.T) { 24 | r := httptest.NewRequest("GET", "/fuego", nil) 25 | w := httptest.NewRecorder() 26 | 27 | e.ServeHTTP(w, r) 28 | 29 | require.Equal(t, http.StatusOK, w.Code) 30 | require.JSONEq(t, `{"message":"Hello"}`, w.Body.String()) 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/.air.toml: -------------------------------------------------------------------------------- 1 | root = "." 2 | testdata_dir = "testdata" 3 | tmp_dir = "tmp" 4 | 5 | [build] 6 | args_bin = [] 7 | bin = "./tmp/main" 8 | cmd = "templ generate && go build -o ./tmp/main ." 9 | delay = 0 10 | exclude_dir = ["node_modules"] 11 | exclude_file = ["store/db.go"] 12 | exclude_regex = ["_test.go", "_templ.go", ".sql.go", "models.go"] 13 | exclude_unchanged = false 14 | follow_symlink = false 15 | full_bin = "" 16 | include_dir = [] 17 | include_ext = ["go", "tpl", "tmpl", "html", "templ"] 18 | include_file = [] 19 | kill_delay = "0s" 20 | log = "build-errors.log" 21 | poll = false 22 | poll_interval = 0 23 | rerun = false 24 | rerun_delay = 500 25 | send_interrupt = false 26 | stop_on_error = false 27 | 28 | [color] 29 | app = "" 30 | build = "yellow" 31 | main = "magenta" 32 | runner = "green" 33 | watcher = "cyan" 34 | 35 | [log] 36 | main_only = false 37 | time = false 38 | 39 | [misc] 40 | clean_on_exit = false 41 | 42 | [screen] 43 | clear_on_rebuild = false 44 | keep_scroll = true 45 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/.env: -------------------------------------------------------------------------------- 1 | ADMIN_USER=admin 2 | ADMIN_PASSWORD=admin 3 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/.gitignore: -------------------------------------------------------------------------------- 1 | full-app-gourmet 2 | docs 3 | .vscode 4 | tmp 5 | *.db 6 | node_modules 7 | tmp/* 8 | *_templ.go 9 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/.prettierignore: -------------------------------------------------------------------------------- 1 | doc 2 | static/**.min.js 3 | static/**.min.css 4 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["prettier-plugin-go-template", "prettier-plugin-tailwindcss"], 3 | "overrides": [ 4 | { 5 | "files": ["*.html"], 6 | "options": { 7 | "parser": "go-template" 8 | } 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:latest AS builder 2 | WORKDIR /app 3 | COPY go.mod go.sum /app/ 4 | RUN go mod download 5 | COPY . . 6 | RUN --mount=type=cache,target="/root/.cache/go-build" CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o gourmet . 7 | 8 | FROM alpine:latest 9 | WORKDIR /app 10 | COPY --from=builder /app/gourmet . 11 | ENTRYPOINT [ "/app/gourmet" ] 12 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/Makefile: -------------------------------------------------------------------------------- 1 | dev: 2 | air -- -debug 3 | 4 | prepare-db: backup 5 | cp gourmet.bak.db recipe.db 6 | 7 | db-upload: 8 | operations/upload-db.sh 9 | 10 | preview: build 11 | ./bin/gourmet 12 | 13 | prettier: 14 | npx prettier --write . 15 | 16 | css: 17 | tailwindcss -i ./tailwind.css -o ./static/tailwind.min.css --minify 18 | 19 | css-watch: 20 | tailwindcss -i ./tailwind.css -o ./static/tailwind.min.css --minify --watch 21 | 22 | 23 | build: css 24 | go generate ./... 25 | go build -v -ldflags="-s -w" -o gourmet-app 26 | 27 | # Build for Prod 28 | docker-build: 29 | go generate ./... 30 | docker build --platform linux/amd64 -t ewenquim/gourmet . 31 | 32 | docker-push: 33 | docker push ewenquim/gourmet 34 | 35 | docker-build-and-push: docker-build docker-push 36 | 37 | docker-preview: 38 | docker run --env-file .env --rm -p 8083:8083 ewenquim/gourmet 39 | 40 | deploy: 41 | make backup & GOARCH=amd64 GOOS=linux make build 42 | operations/deploy.sh 43 | open https://gourmet.quimerch.com 44 | make logs 45 | 46 | logs: 47 | operations/logs.sh 48 | 49 | backup: 50 | operations/backup.sh 51 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/controller/dosing.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/go-fuego/fuego" 7 | "github.com/go-fuego/fuego/examples/full-app-gourmet/store" 8 | ) 9 | 10 | type dosingResource struct { 11 | Queries DosingRepository 12 | } 13 | 14 | func (rs dosingResource) MountRoutes(s *fuego.Server) { 15 | dosingGroup := fuego.Group(s, "/dosings") 16 | fuego.Post(dosingGroup, "/new", rs.newDosing) 17 | } 18 | 19 | func (rs dosingResource) newDosing(c fuego.ContextWithBody[store.CreateDosingParams]) (store.Dosing, error) { 20 | body, err := c.Body() 21 | if err != nil { 22 | return store.Dosing{}, err 23 | } 24 | 25 | dosing, err := rs.Queries.CreateDosing(c.Context(), body) 26 | if err != nil { 27 | return store.Dosing{}, err 28 | } 29 | 30 | return dosing, nil 31 | } 32 | 33 | type DosingRepository interface { 34 | CreateDosing(ctx context.Context, arg store.CreateDosingParams) (store.Dosing, error) 35 | GetDosings(ctx context.Context) ([]store.Dosing, error) 36 | } 37 | 38 | var _ DosingRepository = (*store.Queries)(nil) 39 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/controller/dummy_controllers.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/go-fuego/fuego" 7 | ) 8 | 9 | type test struct { 10 | Name string `json:"name"` 11 | } 12 | 13 | func slow(c fuego.ContextNoBody) (test, error) { 14 | time.Sleep(2 * time.Second) 15 | return test{Name: "hello"}, nil 16 | } 17 | 18 | func placeholderController(c fuego.ContextNoBody) (string, error) { 19 | return "hello", nil 20 | } 21 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/controller/id.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/hex" 6 | ) 7 | 8 | func generateID() string { 9 | id := make([]byte, 10) 10 | _, _ = rand.Read(id) 11 | return hex.EncodeToString(id) 12 | } 13 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/errors_custom.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "net/http" 4 | 5 | type MyError struct { 6 | Err error // developer readable error message 7 | } 8 | 9 | func (e MyError) Status() int { 10 | return http.StatusTeapot 11 | } 12 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-fuego/fuego/b482389bf8263ee19095f38b45d4f76fbc85dd50/examples/full-app-gourmet/favicon.ico -------------------------------------------------------------------------------- /examples/full-app-gourmet/operations/.gitignore: -------------------------------------------------------------------------------- 1 | *.db 2 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/operations/backup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Description: Deploy the app to the VPS 3 | 4 | 5 | SSH_KEY_PATH=~/.ssh/id_rsa 6 | TARGET_HOST=vps 7 | 8 | # exit when any command fails 9 | set -e 10 | 11 | # echo "===> Updating remote server dependencies" 12 | # ssh -i $SSH_KEY_PATH $TARGET_HOST 'sudo apt-get update && sudo apt-get upgrade -y && sudo apt-get autoremove -y' 13 | 14 | echo "===> Copy the SQLite database" 15 | scp -i $SSH_KEY_PATH $TARGET_HOST:/home/ubuntu/gourmet.db gourmet.bak.db 16 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/operations/gourmet.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Gourmet service 3 | 4 | [Service] 5 | User=ubuntu 6 | WorkingDirectory=/home/ubuntu/gourmet 7 | ExecStart=/home/ubuntu/gourmet/gourmet-app -port 8074 -db /home/ubuntu/gourmet.db 8 | Restart=on-failure 9 | RestartSec=5s 10 | 11 | [Install] 12 | WantedBy=multi-user.target 13 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/operations/logs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Description: Deploy the app to the VPS 3 | 4 | 5 | SSH_KEY_PATH=~/.ssh/id_rsa 6 | TARGET_HOST=vps 7 | 8 | # exit when any command fails 9 | set -e 10 | 11 | echo "===> Checking the status of the service" 12 | ssh -i $SSH_KEY_PATH $TARGET_HOST 'journalctl -u gourmet -f' 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/operations/upload-db.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Description: Upload the SQLite database to the server 3 | 4 | SSH_KEY_PATH=~/.ssh/id_rsa 5 | TARGET_HOST=vps 6 | 7 | # exit when any command fails 8 | set -e 9 | 10 | echo "===> Copy the SQLite database" 11 | scp -i $SSH_KEY_PATH recipe.db $TARGET_HOST:/home/ubuntu/gourmet.db 12 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "prettier": "^3.5.3", 4 | "prettier-plugin-go-template": "^0.0.15", 5 | "prettier-plugin-tailwindcss": "^0.6.12" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/sqlc.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | sql: 3 | - engine: "sqlite" 4 | schema: "store/migrations/" 5 | queries: "store/queries/" 6 | gen: 7 | go: 8 | package: "store" 9 | out: "store" 10 | emit_json_tags: true 11 | overrides: 12 | - column: "dosing.quantity" 13 | go_struct_tag: 'validate:"required,gt=0"' 14 | - column: "dosing.unit" 15 | go_struct_tag: 'validate:"required"' 16 | go_type: 17 | import: "github.com/go-fuego/fuego/examples/full-app-gourmet/store/types" 18 | type: "Unit" 19 | - column: "ingredient.category" 20 | go_type: 21 | import: "github.com/go-fuego/fuego/examples/full-app-gourmet/store/types" 22 | type: "Category" 23 | - column: "ingredient.default_unit" 24 | go_type: 25 | import: "github.com/go-fuego/fuego/examples/full-app-gourmet/store/types" 26 | type: "Unit" 27 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/static/.gitignore: -------------------------------------------------------------------------------- 1 | tailwind.min.css 2 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/static/dinner-placeholder.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-fuego/fuego/b482389bf8263ee19095f38b45d4f76fbc85dd50/examples/full-app-gourmet/static/dinner-placeholder.webp -------------------------------------------------------------------------------- /examples/full-app-gourmet/static/embed.go: -------------------------------------------------------------------------------- 1 | package static 2 | 3 | import ( 4 | "embed" 5 | "net/http" 6 | ) 7 | 8 | //go:embed * 9 | var StaticFiles embed.FS 10 | 11 | // Handler returns a http.Handler that will serve files from 12 | // the given file system. 13 | func Handler() http.Handler { 14 | return http.FileServer(http.FS(StaticFiles)) 15 | } 16 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/static/embed_test.go: -------------------------------------------------------------------------------- 1 | package static 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | ) 8 | 9 | func TestHandler(t *testing.T) { 10 | h := Handler() 11 | 12 | t.Run("200 with raw handler", func(t *testing.T) { 13 | w := httptest.NewRecorder() 14 | r := httptest.NewRequest("GET", "/tailwind.min.css", nil) 15 | h.ServeHTTP(w, r) 16 | 17 | if w.Code != 200 { 18 | t.Errorf("Handler() = %v, want %v", w.Code, 200) 19 | } 20 | }) 21 | 22 | t.Run("404 with raw handler", func(t *testing.T) { 23 | w := httptest.NewRecorder() 24 | r := httptest.NewRequest("GET", "/not-existing", nil) 25 | h.ServeHTTP(w, r) 26 | 27 | if w.Code != 404 { 28 | t.Errorf("Handler() = %v, want %v", w.Code, 404) 29 | } 30 | }) 31 | 32 | t.Run("200 with mux handler", func(t *testing.T) { 33 | mux := http.NewServeMux() 34 | mux.Handle("/static/", http.StripPrefix("/static", h)) 35 | 36 | w := httptest.NewRecorder() 37 | r := httptest.NewRequest("GET", "/static/tailwind.min.css", nil) 38 | mux.ServeHTTP(w, r) 39 | 40 | if w.Code != 200 { 41 | t.Errorf("Handler() = %v, want %v", w.Code, 200) 42 | } 43 | 44 | t.Log(w.Body.String()) 45 | }) 46 | } 47 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-fuego/fuego/b482389bf8263ee19095f38b45d4f76fbc85dd50/examples/full-app-gourmet/static/favicon.ico -------------------------------------------------------------------------------- /examples/full-app-gourmet/static/hero.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-fuego/fuego/b482389bf8263ee19095f38b45d4f76fbc85dd50/examples/full-app-gourmet/static/hero.webp -------------------------------------------------------------------------------- /examples/full-app-gourmet/static/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Gourmet", 3 | "short_name": "Gourmet", 4 | "description": "Gourmet - Healthy Recipes", 5 | "start_url": "/", 6 | "background_color": "white", 7 | "theme_color": "#282828", 8 | "display": "fullscreen", 9 | "icons": [ 10 | { 11 | "src": "favicon.ico", 12 | "sizes": "192x192", 13 | "type": "image/png" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/static/plan.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-fuego/fuego/b482389bf8263ee19095f38b45d4f76fbc85dd50/examples/full-app-gourmet/static/plan.webp -------------------------------------------------------------------------------- /examples/full-app-gourmet/static/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / 3 | Disallow: /admin/ 4 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/store/db.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.27.0 4 | 5 | package store 6 | 7 | import ( 8 | "context" 9 | "database/sql" 10 | ) 11 | 12 | type DBTX interface { 13 | ExecContext(context.Context, string, ...interface{}) (sql.Result, error) 14 | PrepareContext(context.Context, string) (*sql.Stmt, error) 15 | QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) 16 | QueryRowContext(context.Context, string, ...interface{}) *sql.Row 17 | } 18 | 19 | func New(db DBTX) *Queries { 20 | return &Queries{db: db} 21 | } 22 | 23 | type Queries struct { 24 | db DBTX 25 | } 26 | 27 | func (q *Queries) WithTx(tx *sql.Tx) *Queries { 28 | return &Queries{ 29 | db: tx, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/store/dosing.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/go-fuego/fuego" 9 | "github.com/go-fuego/fuego/examples/full-app-gourmet/store/types" 10 | ) 11 | 12 | var _ fuego.InTransformer = (*CreateDosingParams)(nil) 13 | 14 | func (d *CreateDosingParams) InTransform(context.Context) error { 15 | d.Unit = types.Unit(strings.ToLower(string(d.Unit))) 16 | 17 | if !d.Unit.Valid() { 18 | return types.InvalidUnitError{Unit: d.Unit} 19 | } 20 | 21 | if !(d.Quantity > 0 || 22 | d.Quantity == 0 && d.Unit == types.UnitNone) { 23 | return fmt.Errorf("quantity must be greater than 0 for unit %s", d.Unit) 24 | } 25 | 26 | return nil 27 | } 28 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/store/dosing.sql.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.27.0 4 | // source: dosing.sql 5 | 6 | package store 7 | 8 | import ( 9 | "context" 10 | 11 | "github.com/go-fuego/fuego/examples/full-app-gourmet/store/types" 12 | ) 13 | 14 | const createDosing = `-- name: CreateDosing :one 15 | INSERT INTO dosing (recipe_id, ingredient_id, quantity, unit) VALUES (?, ?, ?, ?) RETURNING recipe_id, ingredient_id, quantity, unit 16 | ` 17 | 18 | type CreateDosingParams struct { 19 | RecipeID string `json:"recipe_id"` 20 | IngredientID string `json:"ingredient_id"` 21 | Quantity int64 `json:"quantity" validate:"required,gt=0"` 22 | Unit types.Unit `json:"unit" validate:"required"` 23 | } 24 | 25 | func (q *Queries) CreateDosing(ctx context.Context, arg CreateDosingParams) (Dosing, error) { 26 | row := q.db.QueryRowContext(ctx, createDosing, 27 | arg.RecipeID, 28 | arg.IngredientID, 29 | arg.Quantity, 30 | arg.Unit, 31 | ) 32 | var i Dosing 33 | err := row.Scan( 34 | &i.RecipeID, 35 | &i.IngredientID, 36 | &i.Quantity, 37 | &i.Unit, 38 | ) 39 | return i, err 40 | } 41 | 42 | const getDosings = `-- name: GetDosings :many 43 | SELECT recipe_id, ingredient_id, quantity, unit FROM dosing 44 | ` 45 | 46 | func (q *Queries) GetDosings(ctx context.Context) ([]Dosing, error) { 47 | rows, err := q.db.QueryContext(ctx, getDosings) 48 | if err != nil { 49 | return nil, err 50 | } 51 | defer rows.Close() 52 | var items []Dosing 53 | for rows.Next() { 54 | var i Dosing 55 | if err := rows.Scan( 56 | &i.RecipeID, 57 | &i.IngredientID, 58 | &i.Quantity, 59 | &i.Unit, 60 | ); err != nil { 61 | return nil, err 62 | } 63 | items = append(items, i) 64 | } 65 | if err := rows.Close(); err != nil { 66 | return nil, err 67 | } 68 | if err := rows.Err(); err != nil { 69 | return nil, err 70 | } 71 | return items, nil 72 | } 73 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/store/ingredient.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "github.com/go-fuego/fuego" 8 | ) 9 | 10 | var _ fuego.InTransformer = (*CreateIngredientParams)(nil) 11 | 12 | func (c *CreateIngredientParams) InTransform(context.Context) error { 13 | c.Name = strings.TrimSpace(c.Name) 14 | 15 | c.ID = slug(c.Name) 16 | 17 | return nil 18 | } 19 | 20 | func (i Ingredient) Months() string { 21 | months := []string{} 22 | if i.AvailableJan { 23 | months = append(months, "Jan") 24 | } 25 | if i.AvailableFeb { 26 | months = append(months, "Feb") 27 | } 28 | if i.AvailableMar { 29 | months = append(months, "Mar") 30 | } 31 | if i.AvailableApr { 32 | months = append(months, "Apr") 33 | } 34 | if i.AvailableMay { 35 | months = append(months, "May") 36 | } 37 | if i.AvailableJun { 38 | months = append(months, "Jun") 39 | } 40 | if i.AvailableJul { 41 | months = append(months, "Jul") 42 | } 43 | if i.AvailableAug { 44 | months = append(months, "Aug") 45 | } 46 | if i.AvailableSep { 47 | months = append(months, "Sep") 48 | } 49 | if i.AvailableOct { 50 | months = append(months, "Oct") 51 | } 52 | if i.AvailableNov { 53 | months = append(months, "Nov") 54 | } 55 | if i.AvailableDec { 56 | months = append(months, "Dec") 57 | } 58 | 59 | if len(months) == 0 { 60 | return "None" 61 | } 62 | 63 | return strings.Join(months, ", ") 64 | } 65 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/store/init.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "database/sql" 5 | _ "embed" 6 | "errors" 7 | "log" 8 | "log/slog" 9 | 10 | "github.com/golang-migrate/migrate/v4" 11 | _ "github.com/golang-migrate/migrate/v4/database/sqlite" // SQLite driver for migration 12 | _ "github.com/golang-migrate/migrate/v4/source/file" // Migration files 13 | "github.com/golang-migrate/migrate/v4/source/iofs" // Migration files 14 | _ "modernc.org/sqlite" 15 | 16 | "github.com/go-fuego/fuego/examples/full-app-gourmet/store/migrations" 17 | ) 18 | 19 | // InitDB initialize the database. 20 | // SchemaPath is imported from schema.sql 21 | func InitDB(path string) *sql.DB { 22 | db, err := sql.Open("sqlite", path) 23 | if err != nil { 24 | slog.Error("cannot open db connection", "err", err) 25 | } 26 | 27 | err = db.Ping() 28 | if err != nil { 29 | slog.Error("cannot ping db", "err", err) 30 | } 31 | 32 | d, err := iofs.New(migrations.FS, ".") 33 | if err != nil { 34 | log.Fatal(err) 35 | } 36 | 37 | m, err := migrate.NewWithSourceInstance("embed://", d, "sqlite://"+path) 38 | if err != nil { 39 | slog.Error("cannot migrate db", "err", err) 40 | panic("cannot migrate db") 41 | } 42 | 43 | err = m.Up() 44 | if !errors.Is(err, migrate.ErrNoChange) { 45 | if err != nil { 46 | slog.Error("database migration failed", 47 | slog.Any("error", err), 48 | slog.String("database", "migrating: failure")) 49 | 50 | panic("database migration failed") 51 | } 52 | slog.Info("", slog.String("database", "migrating: success")) 53 | } else { 54 | slog.Info("", slog.String("database", "migrating: no change required")) 55 | } 56 | 57 | slog.Info("Database connected", "address", path) 58 | 59 | slog.Info("Database initialized") 60 | 61 | return db 62 | } 63 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/store/migrations/000001_migration.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE recipe; 2 | 3 | DROP TABLE ingredient; 4 | 5 | CREATE TABLE dosing; 6 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/store/migrations/000001_migration.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS recipe ( 2 | id TEXT PRIMARY KEY, 3 | created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 4 | 5 | name TEXT NOT NULL UNIQUE, 6 | description TEXT NOT NULL, 7 | instructions TEXT NOT NULL 8 | ); 9 | 10 | 11 | CREATE TABLE IF NOT EXISTS ingredient ( 12 | id TEXT PRIMARY KEY, 13 | created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 14 | name TEXT NOT NULL, 15 | description TEXT NOT NULL 16 | ); 17 | 18 | CREATE TABLE IF NOT EXISTS dosing ( 19 | recipe_id TEXT NOT NULL, 20 | ingredient_id TEXT NOT NULL, 21 | quantity INTEGER NOT NULL, 22 | unit TEXT NOT NULL, 23 | PRIMARY KEY (recipe_id, ingredient_id), 24 | FOREIGN KEY (recipe_id) REFERENCES recipe(id), 25 | FOREIGN KEY (ingredient_id) REFERENCES ingredient(id) 26 | ); 27 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/store/migrations/000002_ingredient_default_dosing.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE ingredient DROP COLUMN default_unit; 2 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/store/migrations/000002_ingredient_default_dosing.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE ingredient ADD COLUMN default_unit text NOT NULL DEFAULT '-'; 2 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/store/migrations/000003_ingredient_categories.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE ingredient DROP COLUMN category; 2 | ALTER TABLE ingredient DROP COLUMN available_all_year; 3 | ALTER TABLE ingredient DROP COLUMN available_jan; 4 | ALTER TABLE ingredient DROP COLUMN available_feb; 5 | ALTER TABLE ingredient DROP COLUMN available_mar; 6 | ALTER TABLE ingredient DROP COLUMN available_apr; 7 | ALTER TABLE ingredient DROP COLUMN available_may; 8 | ALTER TABLE ingredient DROP COLUMN available_jun; 9 | ALTER TABLE ingredient DROP COLUMN available_jul; 10 | ALTER TABLE ingredient DROP COLUMN available_aug; 11 | ALTER TABLE ingredient DROP COLUMN available_sep; 12 | ALTER TABLE ingredient DROP COLUMN available_oct; 13 | ALTER TABLE ingredient DROP COLUMN available_nov; 14 | ALTER TABLE ingredient DROP COLUMN available_dec; 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/store/migrations/000003_ingredient_categories.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE ingredient ADD COLUMN category text NOT NULL DEFAULT 'other'; 2 | ALTER TABLE ingredient ADD COLUMN available_all_year boolean NOT NULL DEFAULT false; 3 | ALTER TABLE ingredient ADD COLUMN available_jan boolean NOT NULL DEFAULT false; 4 | ALTER TABLE ingredient ADD COLUMN available_feb boolean NOT NULL DEFAULT false; 5 | ALTER TABLE ingredient ADD COLUMN available_mar boolean NOT NULL DEFAULT false; 6 | ALTER TABLE ingredient ADD COLUMN available_apr boolean NOT NULL DEFAULT false; 7 | ALTER TABLE ingredient ADD COLUMN available_may boolean NOT NULL DEFAULT false; 8 | ALTER TABLE ingredient ADD COLUMN available_jun boolean NOT NULL DEFAULT false; 9 | ALTER TABLE ingredient ADD COLUMN available_jul boolean NOT NULL DEFAULT false; 10 | ALTER TABLE ingredient ADD COLUMN available_aug boolean NOT NULL DEFAULT false; 11 | ALTER TABLE ingredient ADD COLUMN available_sep boolean NOT NULL DEFAULT false; 12 | ALTER TABLE ingredient ADD COLUMN available_oct boolean NOT NULL DEFAULT false; 13 | ALTER TABLE ingredient ADD COLUMN available_nov boolean NOT NULL DEFAULT false; 14 | ALTER TABLE ingredient ADD COLUMN available_dec boolean NOT NULL DEFAULT false; 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/store/migrations/000004_recipes_categories.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE recipe DROP COLUMN category; 2 | ALTER TABLE recipe DROP COLUMN class; 3 | ALTER TABLE recipe DROP COLUMN published; 4 | ALTER TABLE recipe DROP COLUMN created_by; 5 | ALTER TABLE recipe DROP COLUMN calories; 6 | ALTER TABLE recipe DROP COLUMN cost; 7 | ALTER TABLE recipe DROP COLUMN prep_time; 8 | ALTER TABLE recipe DROP COLUMN cook_time; 9 | ALTER TABLE recipe DROP COLUMN servings; 10 | ALTER TABLE recipe DROP COLUMN image_url; 11 | ALTER TABLE recipe DROP COLUMN disclaimer; 12 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/store/migrations/000004_recipes_categories.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE recipe ADD COLUMN category text NOT NULL DEFAULT 'other'; -- Breakfast, Lunch, Dinner, Dessert, Snack, Other 2 | ALTER TABLE recipe ADD COLUMN class text NOT NULL DEFAULT 'other'; -- Pasta, Soup, Salad, Sandwich, Other 3 | ALTER TABLE recipe ADD COLUMN published boolean NOT NULL DEFAULT false; 4 | ALTER TABLE recipe ADD COLUMN created_by text NOT NULL DEFAULT 'admin'; 5 | ALTER TABLE recipe ADD COLUMN calories integer NOT NULL DEFAULT 0; 6 | ALTER TABLE recipe ADD COLUMN cost integer NOT NULL DEFAULT 0; 7 | ALTER TABLE recipe ADD COLUMN prep_time integer NOT NULL DEFAULT 0; 8 | ALTER TABLE recipe ADD COLUMN cook_time integer NOT NULL DEFAULT 0; 9 | ALTER TABLE recipe ADD COLUMN servings integer NOT NULL DEFAULT 0; 10 | ALTER TABLE recipe ADD COLUMN image_url text NOT NULL DEFAULT ''; 11 | ALTER TABLE recipe ADD COLUMN disclaimer text NOT NULL DEFAULT ''; 12 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/store/migrations/000005_whenToEat.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE recipe DROP COLUMN when_to_eat; 2 | ALTER TABLE recipe ADD COLUMN class text NOT NULL DEFAULT 'other'; 3 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/store/migrations/000005_whenToEat.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE recipe DROP COLUMN class; 2 | ALTER TABLE recipe ADD COLUMN when_to_eat text NOT NULL DEFAULT 'other'; -- Breakfast, Lunch, Dinner, Dessert, Snack, Other 3 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/store/migrations/embed.go: -------------------------------------------------------------------------------- 1 | package migrations 2 | 3 | import ( 4 | "embed" 5 | ) 6 | 7 | //go:embed *.sql 8 | var FS embed.FS 9 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/store/queries/dosing.sql: -------------------------------------------------------------------------------- 1 | -- name: GetDosings :many 2 | SELECT * FROM dosing; 3 | 4 | 5 | -- name: CreateDosing :one 6 | INSERT INTO dosing (recipe_id, ingredient_id, quantity, unit) VALUES (?, ?, ?, ?) RETURNING *; 7 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/store/queries/recipe.sql: -------------------------------------------------------------------------------- 1 | -- name: GetRecipe :one 2 | SELECT * FROM recipe WHERE id = ?; 3 | 4 | -- name: GetRecipes :many 5 | SELECT * FROM recipe; 6 | 7 | -- name: SearchRecipes :many 8 | -- Search anything that contains the given string 9 | SELECT * FROM recipe WHERE 10 | (name LIKE '%' || @search || '%') 11 | AND published = @published 12 | AND calories <= @max_calories 13 | AND prep_time + cook_time <= @max_time 14 | ORDER BY name ASC 15 | LIMIT @limit 16 | OFFSET @offset; 17 | 18 | -- name: CreateRecipe :one 19 | INSERT INTO recipe ( 20 | id, 21 | name, 22 | description, 23 | instructions, 24 | prep_time, 25 | cook_time, 26 | category, 27 | image_url, 28 | published, 29 | servings, 30 | when_to_eat 31 | ) 32 | VALUES (?,?,?,?,?,?,?,?,?,?,?) RETURNING *; 33 | 34 | -- name: DeleteRecipe :exec 35 | DELETE FROM recipe WHERE id = ?; 36 | 37 | -- name: GetRandomRecipes :many 38 | SELECT * FROM recipe ORDER BY RANDOM() DESC LIMIT 10; 39 | 40 | -- name: UpdateRecipe :one 41 | UPDATE recipe SET 42 | name=COALESCE(sqlc.arg(name), name), 43 | description=COALESCE(sqlc.narg(description), description), 44 | instructions=COALESCE(sqlc.narg(instructions), instructions), 45 | category=COALESCE(sqlc.arg(category), category), 46 | when_to_eat=COALESCE(sqlc.arg(when_to_eat), when_to_eat), 47 | image_url=COALESCE(sqlc.arg(image_url), image_url), 48 | cook_time=COALESCE(sqlc.arg(cook_time), cook_time), 49 | prep_time=COALESCE(sqlc.arg(prep_time), prep_time), 50 | servings=COALESCE(sqlc.arg(servings), servings), 51 | published=COALESCE(sqlc.arg(published), published) 52 | WHERE id = @id 53 | RETURNING *; 54 | 55 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/store/recipe.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "github.com/go-fuego/fuego" 8 | ) 9 | 10 | var _ fuego.InTransformer = (*CreateRecipeParams)(nil) 11 | 12 | // InTransform implements fuego.InTransformer. 13 | func (c *CreateRecipeParams) InTransform(context.Context) error { 14 | c.Name = strings.TrimSpace(c.Name) 15 | 16 | c.ID = slug(c.Name) 17 | 18 | return nil 19 | } 20 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/store/slug.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "net/url" 5 | "strings" 6 | "unicode" 7 | 8 | "golang.org/x/text/runes" 9 | "golang.org/x/text/transform" 10 | "golang.org/x/text/unicode/norm" 11 | ) 12 | 13 | var transformer = transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC) 14 | 15 | // slug returns a slugified version of name. 16 | func slug(name string) string { 17 | name = strings.TrimSpace(name) 18 | 19 | id := strings.ToLower(name) 20 | id = strings.ReplaceAll(id, " ", "-") 21 | id = strings.ReplaceAll(id, "/", "-") 22 | 23 | var err error 24 | id, _, err = transform.String(transformer, id) 25 | if err != nil { 26 | panic(err) 27 | } 28 | id = url.PathEscape(id) 29 | 30 | return id 31 | } 32 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/store/slug_test.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import "testing" 4 | 5 | func TestSlug(t *testing.T) { 6 | tests := []struct { 7 | name string 8 | want string 9 | }{ 10 | { 11 | name: "simple", 12 | want: "simple", 13 | }, 14 | { 15 | name: "simple with space", 16 | want: "simple-with-space", 17 | }, 18 | { 19 | name: "simple with space and accént", 20 | want: "simple-with-space-and-accent", 21 | }, 22 | { 23 | name: "simple with space and accent and ✅", 24 | want: "simple-with-space-and-accent-and-%E2%9C%85", 25 | }, 26 | } 27 | for _, tt := range tests { 28 | t.Run(tt.name, func(t *testing.T) { 29 | if got := slug(tt.name); got != tt.want { 30 | t.Errorf("slug() = %v, want %v", got, tt.want) 31 | } 32 | }) 33 | } 34 | } 35 | 36 | func FuzzSlug(f *testing.F) { 37 | f.Add("simple") 38 | f.Add("simple with space") 39 | f.Add("simple with space and accént") 40 | f.Add("simple with space and accent and ✅") 41 | f.Fuzz(func(t *testing.T, name string) { 42 | slug(name) 43 | }) 44 | } 45 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/store/types/translations.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | type Locale string 4 | 5 | const ( 6 | LocaleEn Locale = "en" 7 | LocaleFr Locale = "fr" 8 | LocaleEmoji Locale = "emoji" 9 | ) 10 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/store/types/unit.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import "fmt" 4 | 5 | type Unit string 6 | 7 | const ( 8 | UnitNone Unit = "-" // for ingredients that are not dosed, like salt, pepper, ... 9 | UnitPiece Unit = "piece" 10 | UnitGram Unit = "g" 11 | UnitMilliliter Unit = "ml" 12 | ) 13 | 14 | // UnitValues is a slice of all valid units 15 | var UnitValues = []Unit{ 16 | UnitNone, 17 | UnitPiece, 18 | UnitGram, 19 | UnitMilliliter, 20 | } 21 | 22 | type InvalidUnitError struct { 23 | Unit Unit 24 | } 25 | 26 | func (e InvalidUnitError) Error() string { 27 | return fmt.Sprintf("invalid unit %s. Valid units are: %v", e.Unit, UnitValues) 28 | } 29 | 30 | func (u Unit) Valid() bool { 31 | for _, v := range UnitValues { 32 | if v == u { 33 | return true 34 | } 35 | } 36 | return false 37 | } 38 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/store/types/whenToEat.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import "fmt" 4 | 5 | type WhenToEat string 6 | 7 | const ( 8 | WhenToEatNone WhenToEat = "-" 9 | WhenToEatStarter WhenToEat = "starter" // entrée 10 | WhenToEatDish WhenToEat = "dish" // plat 11 | WhenToEatDessert WhenToEat = "dessert" // dessert 12 | ) 13 | 14 | // WhenToEatValues is a slice of all valid WhenToEats 15 | var WhenToEatValues = []WhenToEat{ 16 | WhenToEatNone, 17 | WhenToEatStarter, 18 | WhenToEatDish, 19 | WhenToEatDessert, 20 | } 21 | 22 | type InvalidWhenToEatError struct { 23 | WhenToEat WhenToEat 24 | } 25 | 26 | func (e InvalidWhenToEatError) Error() string { 27 | return fmt.Sprintf("invalid WhenToEat %s. Valid WhenToEats are: %v", e.WhenToEat, WhenToEatValues) 28 | } 29 | 30 | func (u WhenToEat) Valid() bool { 31 | for _, v := range WhenToEatValues { 32 | if v == u { 33 | return true 34 | } 35 | } 36 | return false 37 | } 38 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ["**/*.html", "**/*.templ"], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [], 8 | }; 9 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer utilities { 6 | body { 7 | @apply font-light antialiased; 8 | } 9 | 10 | .input { 11 | @apply block w-full rounded-md border border-zinc-300 px-3 py-2 font-light placeholder-zinc-400 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 dark:border-zinc-700 dark:bg-zinc-600 sm:text-sm; 12 | } 13 | 14 | .form { 15 | @apply flex max-w-lg flex-col gap-1 md:gap-2; 16 | } 17 | 18 | .label { 19 | @apply block text-sm font-medium text-zinc-700 dark:text-zinc-200; 20 | } 21 | 22 | .btn { 23 | @apply inline-flex items-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2; 24 | } 25 | 26 | .btn-secondary { 27 | @apply border-indigo-600 bg-white text-indigo-600 hover:border-indigo-700 hover:bg-zinc-100 hover:text-indigo-700; 28 | } 29 | 30 | .btn-red-bg { 31 | @apply border-white bg-transparent; 32 | } 33 | 34 | .btn-danger { 35 | @apply inline-flex items-center rounded-md border border-transparent bg-red-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2; 36 | } 37 | 38 | h1 { 39 | @apply mb-1 mt-2 text-3xl font-bold md:mb-2 md:mt-4; 40 | } 41 | 42 | h2 { 43 | @apply mb-1 mt-2 text-2xl font-bold md:mb-2 md:mt-4; 44 | } 45 | 46 | h3 { 47 | @apply mb-1 mt-2 text-xl font-bold md:mb-2 md:mt-4; 48 | } 49 | 50 | .markdown { 51 | ul { 52 | @apply list-inside list-disc; 53 | } 54 | 55 | ol { 56 | @apply list-inside list-decimal; 57 | } 58 | } 59 | 60 | .card { 61 | @apply divide-y divide-zinc-200 overflow-hidden rounded-lg bg-white shadow; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/templa/admin/ingredient.form.templ: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "github.com/go-fuego/fuego/examples/full-app-gourmet/templa/components" 5 | "github.com/go-fuego/fuego/examples/full-app-gourmet/store" 6 | ) 7 | 8 | type IngredientFormProps struct { 9 | Ingredient store.Ingredient 10 | IsCreating bool 11 | FormAction string 12 | HXTrigger string 13 | } 14 | 15 | templ IngredientForm(props IngredientFormProps) { 16 |
30 | 31 | 32 | 33 | 34 | 35 | @components.CategorySelector(props.Ingredient.Category) 36 | 37 | @components.UnitSelector(props.Ingredient.DefaultUnit) 38 | 48 | 49 |
50 | } 51 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/templa/admin/ingredient.new.templ: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import "github.com/go-fuego/fuego/examples/full-app-gourmet/store" 4 | 5 | templ IngredientNew() { 6 | @htmlPage("Ingredient - Creation", true) { 7 |

Create Ingredient

8 | @IngredientForm(IngredientFormProps{ 9 | FormAction: "/admin/ingredients/new", 10 | IsCreating: true, 11 | Ingredient: store.Ingredient{ 12 | AvailableAllYear: true, 13 | }, 14 | }) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/templa/admin/ingredient.page.templ: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "github.com/go-fuego/fuego/examples/full-app-gourmet/store" 5 | ) 6 | 7 | templ IngredientPage(ingredient store.Ingredient) { 8 | @htmlPage("Ingredient - "+ingredient.Name, true) { 9 |

Edit Ingredient

10 | @IngredientForm(IngredientFormProps{ 11 | Ingredient: ingredient, 12 | FormAction:"/admin/ingredients/" + ingredient.ID, 13 | IsCreating: false, 14 | }) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/templa/admin/layout.templ: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "github.com/go-fuego/fuego/examples/full-app-gourmet/templa/components" 5 | ) 6 | 7 | templ htmlPage(title string, admin bool) { 8 | 9 | 10 | 11 | @components.Head(title) 12 | 13 | 14 |
15 | @AdminNavbar() 16 |
17 | { children... } 18 |
19 |
20 | @components.Footer() 21 | @components.Scripts() 22 | 23 | 24 | } 25 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/templa/admin/navbar.admin.templ: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | templ AdminNavbar() { 4 | 41 | } 42 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/templa/admin/recipe.new.templ: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import "github.com/go-fuego/fuego/examples/full-app-gourmet/store" 4 | 5 | templ RecipeNew() { 6 | @htmlPage("Recipe - Creation", true) { 7 |

Create Recipe

8 | @RecipeForm(RecipeFormProps{ 9 | FormAction: "/admin/recipes/new", 10 | IsCreating: true, 11 | Recipe: store.Recipe{}, 12 | }) 13 | } 14 | } -------------------------------------------------------------------------------- /examples/full-app-gourmet/templa/admin/recipe.page.templ: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "github.com/go-fuego/fuego/examples/full-app-gourmet/store" 5 | ) 6 | 7 | type RecipePageProps struct { 8 | Recipe store.Recipe 9 | Dosings []store.GetIngredientsOfRecipeRow 10 | AllIngredients []store.Ingredient 11 | } 12 | 13 | templ RecipePage(props RecipePageProps) { 14 | @htmlPage("Recipe - "+props.Recipe.Name, true) { 15 |

Edit Recipe

16 | 27 | @RecipeForm(RecipeFormProps{ 28 | Recipe: props.Recipe, 29 | Dosings: props.Dosings, 30 | AllIngredients: props.AllIngredients, 31 | FormAction: "/admin/recipes/" + props.Recipe.ID, 32 | IsCreating: false, 33 | }) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/templa/components/card.templ: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | type CardProps struct { 4 | ImageURL string 5 | Link string 6 | Title string 7 | Body string 8 | WhenToEat string 9 | } 10 | 11 | templ Card(props CardProps) { 12 | 51 | } 52 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/templa/components/footer.templ: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | templ Footer() { 4 | 37 | } 38 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/templa/components/head.templ: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | templ Head(title string) { 4 | if title == "" { 5 | title = "Mon app" 6 | } 7 | { title } 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 46 | 47 | 48 | 49 | } 50 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/templa/components/scripts.templ: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | templ Scripts() { 4 | 5 | 35 | } 36 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/templa/components/searchForm.templ: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import "fmt" 4 | 5 | type SearchParams struct { 6 | URL string `json:"url"` 7 | Name string `json:"name"` 8 | Page int `json:"page"` 9 | PerPage int `json:"per_page"` 10 | Lang string `json:"lang"` // english by default 11 | } 12 | 13 | var searchBoxDefaultTranslations = map[string]string{ 14 | "Search": "Search", 15 | "Page": "Page", 16 | "Per Page": "Per Page", 17 | } 18 | 19 | var searchBoxTranslations = map[string]map[string]string{ 20 | "": searchBoxDefaultTranslations, 21 | "en": searchBoxDefaultTranslations, 22 | "fr": { 23 | "Search": "Rechercher", 24 | "Page": "Page", 25 | "Per Page": "Par Page", 26 | }, 27 | } 28 | 29 | templ SearchBox(parameters SearchParams) { 30 |
43 | 47 | 51 | 55 | 58 |
59 | } 60 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/templa/components/select.category.ingredient.templ: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import ( 4 | "github.com/go-fuego/fuego/examples/full-app-gourmet/store/types" 5 | ) 6 | 7 | templ CategorySelector(selectedCategory types.Category) { 8 | 25 | } 26 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/templa/components/select.templ: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | type SelectItem struct { 4 | Value string 5 | Label string 6 | } 7 | 8 | type SelectProps struct { 9 | Items []SelectItem 10 | SelectedValue string 11 | Name string // used in the name attribute of the Select 12 | } 13 | 14 | templ Select(props SelectProps) { 15 | 32 | } 33 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/templa/components/slider.templ: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import "github.com/go-fuego/fuego/examples/full-app-gourmet/store" 4 | 5 | templ Slider() { 6 | 9 | } 10 | 11 | templ SliderRecipes(recipes []store.Recipe) { 12 | @Slider() { 13 | for _, recipe := range recipes { 14 | @Card(CardProps{ 15 | Title: recipe.Name, 16 | WhenToEat: recipe.WhenToEat, 17 | Link: "/recipes/" + recipe.ID, 18 | ImageURL: recipe.ImageUrl, 19 | Body: recipe.Description, 20 | }) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/templa/components/stars.templ: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | // Stars is between 0 and 10. It will be translated to a number of stars, e.g. 7 will be 3 full stars and 1 half star. 4 | templ Stars(stars int) { 5 | 6 | for i := 0; i < stars/2; i++ { 7 | 8 | 9 | 10 | } 11 | if stars%2 == 1 { 12 | 13 | 14 | 15 | 16 | } 17 | for i := 0; i < (10-stars)/2; i++ { 18 | 19 | 20 | 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/templa/components/unitSelector.templ: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import ( 4 | "github.com/go-fuego/fuego/examples/full-app-gourmet/store/types" 5 | ) 6 | 7 | templ UnitSelector(selectedUnit types.Unit) { 8 | 20 | } 21 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/templa/generate.go: -------------------------------------------------------------------------------- 1 | package templa 2 | 3 | //go:generate templ generate 4 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/templa/home.templ: -------------------------------------------------------------------------------- 1 | package templa 2 | 3 | import ( 4 | "github.com/go-fuego/fuego/examples/full-app-gourmet/store" 5 | "github.com/go-fuego/fuego/examples/full-app-gourmet/templa/components" 6 | ) 7 | 8 | type HomeProps struct { 9 | Recipes []store.Recipe 10 | PopularRecipes []store.Recipe 11 | FastRecipes []store.Recipe 12 | HealthyRecipes []store.Recipe 13 | } 14 | 15 | templ Home(props HomeProps) { 16 | @page("Gourmet") { 17 |
22 |
23 |

Gourmet

24 |

Healthy recipes

25 |
26 |
27 |
31 |

Today's recipes 📅

32 | @components.SliderRecipes(props.Recipes) 33 |
34 | 41 |
45 |

Fast recipes ⚡️

46 | @components.SliderRecipes(props.FastRecipes) 47 |
48 |
52 |

Healthy recipes 🥗

53 | @components.SliderRecipes(props.HealthyRecipes) 54 |
55 | } 56 | } 57 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/templa/ingredient.list.templ: -------------------------------------------------------------------------------- 1 | package templa 2 | 3 | import ( 4 | "github.com/go-fuego/fuego/examples/full-app-gourmet/store" 5 | "github.com/go-fuego/fuego/examples/full-app-gourmet/templa/components" 6 | ) 7 | 8 | type IngredientPageProps struct { 9 | Ingredients []store.Ingredient 10 | Header string 11 | } 12 | 13 | templ IngredientPage(props IngredientPageProps) { 14 | @page("Ingredients") { 15 |
16 | @IngredientList(IngredientListProps{Ingredients: props.Ingredients}) 17 |
18 | } 19 | } 20 | 21 | type IngredientListProps struct { 22 | Ingredients []store.Ingredient 23 | } 24 | 25 | templ IngredientList(props IngredientListProps) { 26 | for _, ingredient := range props.Ingredients { 27 | @components.Card(components.CardProps{ 28 | Title: ingredient.Name, 29 | Link: "/ingredients/" + ingredient.ID, 30 | ImageURL: "", 31 | Body: ingredient.Description, 32 | }) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/templa/layout.templ: -------------------------------------------------------------------------------- 1 | package templa 2 | 3 | import ( 4 | "github.com/go-fuego/fuego/examples/full-app-gourmet/templa/components" 5 | ) 6 | 7 | templ page(title string) { 8 | 9 | 10 | 11 | @components.Head(title) 12 | 13 | 14 | 22 | @NavBar() 23 |
24 | { children... } 25 |
26 | @components.Footer() 27 | @components.Scripts() 28 | 29 | 30 | } 31 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/templa/logo.templ: -------------------------------------------------------------------------------- 1 | package templa 2 | 3 | templ Logo() { 4 | 5 | 6 | 7 | 8 | } 9 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/templa/recipe.list.templ: -------------------------------------------------------------------------------- 1 | package templa 2 | 3 | import ( 4 | "github.com/go-fuego/fuego/examples/full-app-gourmet/templa/components" 5 | "github.com/go-fuego/fuego/examples/full-app-gourmet/store" 6 | ) 7 | 8 | type RecipeListProps struct { 9 | Recipes []store.Recipe 10 | } 11 | 12 | // RecipeList is unused 13 | templ RecipeList(props RecipeListProps) { 14 | @page("Recipes") { 15 |
16 | for _, recipe := range props.Recipes { 17 | @components.Card(components.CardProps{ 18 | Title: recipe.Name, 19 | WhenToEat: recipe.WhenToEat, 20 | Link: "/recipes/" + recipe.ID, 21 | ImageURL: "", 22 | Body: recipe.Description, 23 | }) 24 | } 25 |
26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/templa/search.page.templ: -------------------------------------------------------------------------------- 1 | package templa 2 | 3 | templ SearchPage(props SearchProps) { 4 | @page("Recipe") { 5 | @Search(props) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/templates/embed.go: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | import ( 4 | "embed" 5 | ) 6 | 7 | //go:embed * 8 | var FS embed.FS 9 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/templates/layouts/admin.layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ template "head.partial.html" . }} 6 | 7 | 8 | 9 |
10 | 11 | 48 | 49 |
50 | {{ template "page" . }} 51 |
52 |
53 | 54 | {{ template "footer.partial.html" . }} 55 | {{ template "scripts.partial.html" . }} 56 | 57 | 58 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/templates/layouts/main.layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ template "head.partial.html" . }} 6 | 7 | 8 | 9 | 17 | 18 | {{ template "navbar.partial.html" . }} 19 | 20 | 21 |
22 | {{ template "page" . }} 23 |
24 | 25 | {{ template "footer.partial.html" . }} 26 | {{ template "scripts.partial.html" . }} 27 | 28 | 29 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/templates/pages/admin.page.html: -------------------------------------------------------------------------------- 1 | {{ template "main.layout.html" . }} 2 | 3 | {{ define "page" }} 4 |
5 | 6 |

Admin page

7 |

Here you can manage data.

8 |

Manage users

9 | 10 |
11 |

Add a recipe

12 |
13 | 14 | 15 | 16 | 23 | 24 | 25 |
26 | 27 |
28 | {{ template "recipes-list.partial.html" .Recipes }} 29 |
30 |
31 |
32 | {{ end }} 33 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/templates/pages/admin/ingredients.page.html: -------------------------------------------------------------------------------- 1 | {{ template "admin.layout.html" . }} 2 | 3 | {{ define "title" }}Admin - Ingredients{{ end }} 4 | 5 | {{ define "page" }} 6 | 7 |
8 |

Admin - Ingredients

9 | 10 |
11 |

Add an ingredient

12 | 13 | {{ template "add-ingredient.partial.admin.html" }} 14 |
15 | 16 |
17 |

All ingredients

18 | 19 | {{ template "ingredients-list.partial.html" .Ingredients }} 20 |
21 |
22 | {{ end }} 23 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/templates/pages/admin/recipes.page.html: -------------------------------------------------------------------------------- 1 | {{ template "admin.layout.html" . }} 2 | 3 | {{ define "title" }}Admin - Recipes{{ end }} 4 | 5 | {{ define "page" }} 6 | 7 |
8 |

Admin - Recipes

9 | 10 |
11 |

Add a recipe

12 | 13 | {{ template "add-recipe.partial.admin.html" }} 14 |
15 | 16 |
17 | 18 |
19 |

All recipes

20 | 21 | {{ template "recipes-list.partial.html" .Recipes }} 22 |
23 |
24 | {{ end }} 25 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/templates/pages/ingredients.page.html: -------------------------------------------------------------------------------- 1 | {{ template "main.layout.html" . }} 2 | 3 | {{ define "page" }} 4 |
5 |

List of all my favorite ingredients

6 |
    7 | {{ range . }} 8 |
  • 9 | 10 | {{ .Name }} 11 | {{ .Description }} 12 | 13 |
  • 14 | {{ end }} 15 |
16 |
17 | {{ end }} 18 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/templates/pages/recipe.page.html: -------------------------------------------------------------------------------- 1 | {{ template "main.layout.html" . }} 2 | 3 | {{ define "title" }} 4 | {{ .Recipe.Name }} 5 | {{ end }} 6 | 7 | {{ define "page" }} 8 |
9 |
10 |

{{ .Recipe.Name }}

11 | {{ if .Admin }} 12 | 20 | {{ end }} 21 |
22 |

{{ .Recipe.Description }}

23 | 24 | {{ if .Instructions }} 25 |

Instructions

26 |
27 | {{ .Instructions }} 28 |
29 | {{ end }} 30 | 31 | {{ if false }} 32 | {{ .Recipe.Name }} 37 | {{ end }} 38 | 39 | {{ if .Ingredients }} 40 |

Ingredients

41 |
    42 | {{ range .Ingredients }} 43 |
  • 44 | {{ .Ingredient.Name }} 45 | {{ if ne .Unit "unit" }} 46 | : {{ .Quantity }}{{ if .Unit }} 47 | {{ .Unit }} 48 | {{ end }} 49 | {{ end }} 50 |
  • 51 | {{ end }} 52 |
53 | {{ end }} 54 | 55 |
56 | {{ end }} 57 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/templates/pages/recipes.page.html: -------------------------------------------------------------------------------- 1 | {{ template "main.layout.html" . }} 2 | 3 | {{ define "title" }}My recipes{{ end }} 4 | 5 | {{ define "page" }} 6 |
7 |

All recipes

8 | {{ template "recipes-grid.partial.html" . }} 9 |
10 | {{ end }} 11 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/templates/pages/search.page.html: -------------------------------------------------------------------------------- 1 | {{ template "main.layout.html" . }} 2 | 3 | {{ define "title" }} 4 | Gourmet - 5 | {{ if .Search }} 6 | {{ .Search }} 7 | {{ else }} 8 | Search 9 | {{ end }} 10 | {{ end }} 11 | 12 | {{ define "page" }} 13 | 14 |
15 | {{ template "search-result.partial.html" . }} 16 |
17 | {{ end }} 18 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/templates/partials/dosing/preselected-unit.partial.html: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/templates/partials/footer.partial.html: -------------------------------------------------------------------------------- 1 | 38 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/templates/partials/head.partial.html: -------------------------------------------------------------------------------- 1 | {{ block "title" . }}Mon app{{ end }} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/templates/partials/htmx/htmx.partial.html: -------------------------------------------------------------------------------- 1 | 2 | 19 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/templates/partials/ingredients/add-ingredient.partial.admin.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/templates/partials/ingredients/ingredient-card.partial.html: -------------------------------------------------------------------------------- 1 | 19 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/templates/partials/ingredients/ingredient-slider.partial.html: -------------------------------------------------------------------------------- 1 | 2 | 9 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/templates/partials/ingredients/ingredients-list.partial.html: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/templates/partials/recipes/add-recipe.partial.admin.html: -------------------------------------------------------------------------------- 1 |
9 | 10 | 11 | 12 | 19 | 20 | 28 | 29 | 30 |
31 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/templates/partials/recipes/recipe-card.partial.html: -------------------------------------------------------------------------------- 1 | 19 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/templates/partials/recipes/recipe-line.partial.html: -------------------------------------------------------------------------------- 1 | 2 |
5 |
6 |
7 | 14 | {{ .Name }} 15 | 16 | {{ .Description }} 17 |
18 |
19 | 29 |
30 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/templates/partials/recipes/recipe-slider.partial.html: -------------------------------------------------------------------------------- 1 | 2 | 9 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/templates/partials/recipes/recipes-grid.partial.html: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/templates/partials/recipes/recipes-list.partial.html: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/templates/partials/scripts.partial.html: -------------------------------------------------------------------------------- 1 | {{ template "htmx.partial.html" }} 2 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/templates/partials/search-result.partial.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | {{ if .Search }} 4 | Search results for {{ .Search }} 5 | {{ else }} 6 | All recipes 7 | {{ end }} 8 |

9 | 10 |
11 |
14 | Filters 15 | 16 |
17 | 18 |

Type de repas

19 |
20 | {{ range .Filters.Types }} 21 | 25 | {{ end }} 26 |
27 | 28 | 29 |

Ingrédients

30 |
31 | {{ range .Filters.Ingredients }} 32 | 36 | {{ end }} 37 |
38 |
39 |
40 | 41 |
42 | {{ template "recipes-grid.partial.html" .Recipes }} 43 |
44 |
45 |
46 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/views/admin.go: -------------------------------------------------------------------------------- 1 | package views 2 | 3 | import ( 4 | "github.com/go-fuego/fuego" 5 | ) 6 | 7 | type AdminResource struct { 8 | DosingQueries DosingRepository 9 | RecipesQueries RecipeRepository 10 | IngredientsQueries IngredientRepository 11 | } 12 | 13 | func (rs Resource) pageAdmin(c fuego.ContextNoBody) (fuego.Templ, error) { 14 | return rs.adminRecipes(c) 15 | } 16 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/views/dosing.go: -------------------------------------------------------------------------------- 1 | package views 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/go-fuego/fuego/examples/full-app-gourmet/store" 7 | ) 8 | 9 | type DosingRepository interface { 10 | CreateDosing(ctx context.Context, arg store.CreateDosingParams) (store.Dosing, error) 11 | GetDosings(ctx context.Context) ([]store.Dosing, error) 12 | } 13 | 14 | var _ DosingRepository = (*store.Queries)(nil) 15 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/views/ingredients.go: -------------------------------------------------------------------------------- 1 | package views 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/go-fuego/fuego" 7 | "github.com/go-fuego/fuego/examples/full-app-gourmet/store" 8 | "github.com/go-fuego/fuego/examples/full-app-gourmet/templa" 9 | ) 10 | 11 | func (rs Resource) showIngredients(c fuego.ContextNoBody) (fuego.Templ, error) { 12 | ingredients, _ := rs.IngredientsQueries.GetIngredients(c.Context()) 13 | 14 | if c.Header("HX-Request") == "true" && c.Header("HX-Target") == "#page" { 15 | return templa.IngredientList(templa.IngredientListProps{ 16 | Ingredients: ingredients, 17 | }), nil 18 | } 19 | 20 | headerInfo, _ := rs.MetaQueries.GetHeaderInfo(c.Context()) 21 | 22 | return templa.IngredientPage(templa.IngredientPageProps{ 23 | Ingredients: ingredients, 24 | Header: headerInfo, 25 | }), nil 26 | } 27 | 28 | type IngredientRepository interface { 29 | CreateIngredient(ctx context.Context, arg store.CreateIngredientParams) (store.Ingredient, error) 30 | GetIngredient(ctx context.Context, id string) (store.Ingredient, error) 31 | GetIngredients(ctx context.Context) ([]store.Ingredient, error) 32 | GetIngredientsOfRecipe(ctx context.Context, recipeID string) ([]store.GetIngredientsOfRecipeRow, error) 33 | UpdateIngredient(ctx context.Context, arg store.UpdateIngredientParams) (store.Ingredient, error) 34 | SearchIngredients(ctx context.Context, arg store.SearchIngredientsParams) ([]store.Ingredient, error) 35 | } 36 | 37 | type MetaRepository interface { 38 | GetHeaderInfo(ctx context.Context) (string, error) 39 | } 40 | 41 | var _ IngredientRepository = (*store.Queries)(nil) 42 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/views/partials.go: -------------------------------------------------------------------------------- 1 | package views 2 | 3 | import ( 4 | "github.com/go-fuego/fuego" 5 | "github.com/go-fuego/fuego/examples/full-app-gourmet/store/types" 6 | ) 7 | 8 | func (rs Resource) unitPreselected(c fuego.ContextNoBody) (fuego.CtxRenderer, error) { 9 | id := c.QueryParam("IngredientID") 10 | 11 | ingredient, err := rs.IngredientsQueries.GetIngredient(c.Context(), id) 12 | if err != nil { 13 | return nil, err 14 | } 15 | 16 | return c.Render("preselected-unit.partial.html", fuego.H{ 17 | "Units": types.UnitValues, 18 | "SelectedUnit": ingredient.DefaultUnit, 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/views/planner.go: -------------------------------------------------------------------------------- 1 | package views 2 | 3 | import ( 4 | "github.com/go-fuego/fuego" 5 | "github.com/go-fuego/fuego/examples/full-app-gourmet/templa" 6 | ) 7 | 8 | func (rs Resource) planner(c fuego.ContextNoBody) (fuego.Templ, error) { 9 | recipes, err := rs.RecipesQueries.GetRecipes(c.Context()) 10 | if err != nil { 11 | return nil, err 12 | } 13 | 14 | fastRecipes, err := rs.RecipesQueries.GetRandomRecipes(c.Context()) 15 | if err != nil { 16 | return nil, err 17 | } 18 | 19 | healthyRecipes, err := rs.RecipesQueries.GetRandomRecipes(c.Context()) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | popularRecipes, err := rs.RecipesQueries.GetRandomRecipes(c.Context()) 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | return templa.Planner(templa.PlannerProps{ 30 | Recipes: recipes, 31 | PopularRecipes: popularRecipes, 32 | FastRecipes: fastRecipes, 33 | HealthyRecipes: healthyRecipes, 34 | }), nil 35 | } 36 | -------------------------------------------------------------------------------- /examples/full-app-gourmet/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | prettier-plugin-go-template@^0.0.15: 6 | version "0.0.15" 7 | resolved "https://registry.yarnpkg.com/prettier-plugin-go-template/-/prettier-plugin-go-template-0.0.15.tgz#474952ed72405e528f70bf9cf3f50938c97d8f86" 8 | integrity sha512-WqU92E1NokWYNZ9mLE6ijoRg6LtIGdLMePt2C7UBDjXeDH9okcRI3zRqtnWR4s5AloiqyvZ66jNBAa9tmRY5EQ== 9 | dependencies: 10 | ulid "^2.3.0" 11 | 12 | prettier-plugin-tailwindcss@^0.6.12: 13 | version "0.6.12" 14 | resolved "https://registry.yarnpkg.com/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.12.tgz#280cf901facf18014c79b8641f69623892006f0b" 15 | integrity sha512-OuTQKoqNwV7RnxTPwXWzOFXy6Jc4z8oeRZYGuMpRyG3WbuR3jjXdQFK8qFBMBx8UHWdHrddARz2fgUenild6aw== 16 | 17 | prettier@^3.5.3: 18 | version "3.5.3" 19 | resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.5.3.tgz#4fc2ce0d657e7a02e602549f053b239cb7dfe1b5" 20 | integrity sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw== 21 | 22 | ulid@^2.3.0: 23 | version "2.3.0" 24 | resolved "https://registry.yarnpkg.com/ulid/-/ulid-2.3.0.tgz#93063522771a9774121a84d126ecd3eb9804071f" 25 | integrity sha512-keqHubrlpvT6G2wH0OEfSW4mquYRcbe/J8NMmveoQOjUqmo+hXtO+ORCpWhdbZ7k72UtY61BL7haGxW6enBnjw== 26 | -------------------------------------------------------------------------------- /examples/generate-opengraph-image/Raleway-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-fuego/fuego/b482389bf8263ee19095f38b45d4f76fbc85dd50/examples/generate-opengraph-image/Raleway-Regular.ttf -------------------------------------------------------------------------------- /examples/generate-opengraph-image/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-fuego/fuego/examples/generate-opengraph-image 2 | 3 | go 1.24.2 4 | 5 | require ( 6 | github.com/getkin/kin-openapi v0.132.0 7 | github.com/go-fuego/fuego v0.18.8 8 | github.com/go-fuego/fuego/middleware/cache v0.0.0-20250205094027-802052722609 9 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 10 | golang.org/x/image v0.27.0 11 | ) 12 | 13 | require ( 14 | github.com/gabriel-vasile/mimetype v1.4.8 // indirect 15 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 16 | github.com/go-openapi/swag v0.23.0 // indirect 17 | github.com/go-playground/locales v0.14.1 // indirect 18 | github.com/go-playground/universal-translator v0.18.1 // indirect 19 | github.com/go-playground/validator/v10 v10.26.0 // indirect 20 | github.com/golang-jwt/jwt/v5 v5.2.2 // indirect 21 | github.com/google/uuid v1.6.0 // indirect 22 | github.com/gorilla/schema v1.4.1 // indirect 23 | github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect 24 | github.com/josharian/intern v1.0.0 // indirect 25 | github.com/leodido/go-urn v1.4.0 // indirect 26 | github.com/mailru/easyjson v0.9.0 // indirect 27 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect 28 | github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect 29 | github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect 30 | github.com/perimeterx/marshmallow v1.1.5 // indirect 31 | golang.org/x/crypto v0.37.0 // indirect 32 | golang.org/x/net v0.38.0 // indirect 33 | golang.org/x/sys v0.32.0 // indirect 34 | golang.org/x/text v0.25.0 // indirect 35 | gopkg.in/yaml.v3 v3.0.1 // indirect 36 | ) 37 | -------------------------------------------------------------------------------- /examples/generate-opengraph-image/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/getkin/kin-openapi/openapi3" 5 | 6 | "github.com/go-fuego/fuego" 7 | "github.com/go-fuego/fuego/examples/generate-opengraph-image/controller" 8 | "github.com/go-fuego/fuego/middleware/cache" 9 | "github.com/go-fuego/fuego/option" 10 | "github.com/go-fuego/fuego/param" 11 | ) 12 | 13 | // A custom option to add a custom response to the OpenAPI spec. 14 | // The route returns a PNG image. 15 | var optionReturnsPNG = func(br *fuego.BaseRoute) { 16 | response := openapi3.NewResponse() 17 | response.WithDescription("Generated image") 18 | response.WithContent(openapi3.NewContentWithSchema(nil, []string{"image/png"})) 19 | br.Operation.AddResponse(200, response) 20 | } 21 | 22 | func main() { 23 | s := fuego.NewServer( 24 | fuego.WithEngineOptions( 25 | fuego.WithOpenAPIConfig(fuego.OpenAPIConfig{ 26 | PrettyFormatJSON: true, 27 | }), 28 | ), 29 | ) 30 | 31 | fuego.GetStd(s, "/{title}", controller.OpenGraphHandler, 32 | optionReturnsPNG, 33 | option.Description("Generate an image with a title. Useful for Opengraph."), 34 | option.Path("title", "The title to write on the image", param.Example("example", "My awesome article!")), 35 | option.Middleware(cache.New()), 36 | ) 37 | 38 | s.Run() 39 | } 40 | -------------------------------------------------------------------------------- /examples/gin-compat/adaptor_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestFuegoGin(t *testing.T) { 12 | e, _ := server() 13 | 14 | t.Run("simply test gin", func(t *testing.T) { 15 | r := httptest.NewRequest("GET", "/gin", nil) 16 | w := httptest.NewRecorder() 17 | 18 | e.ServeHTTP(w, r) 19 | 20 | require.Equal(t, 200, w.Code) 21 | }) 22 | 23 | t.Run("test fuego plugin", func(t *testing.T) { 24 | r := httptest.NewRequest("GET", "/fuego", nil) 25 | w := httptest.NewRecorder() 26 | 27 | e.ServeHTTP(w, r) 28 | 29 | require.Equal(t, http.StatusOK, w.Code) 30 | require.JSONEq(t, `{"message":"Hello"}`, w.Body.String()) 31 | }) 32 | 33 | t.Run("test fuego plugin with gin group", func(t *testing.T) { 34 | r := httptest.NewRequest("GET", "/my-group/1/fuego", nil) 35 | w := httptest.NewRecorder() 36 | 37 | e.ServeHTTP(w, r) 38 | 39 | require.Equal(t, http.StatusOK, w.Code) 40 | require.JSONEq(t, `{"message":"Hello"}`, w.Body.String()) 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /examples/gin-compat/handlers.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/gin-gonic/gin" 7 | 8 | "github.com/go-fuego/fuego" 9 | ) 10 | 11 | func ginController(c *gin.Context) { 12 | c.String(200, "pong") 13 | } 14 | 15 | func fuegoControllerGet(c fuego.ContextNoBody) (HelloResponse, error) { 16 | return HelloResponse{ 17 | Message: "Hello", 18 | }, nil 19 | } 20 | 21 | func fuegoControllerPost(c fuego.ContextWithBody[HelloRequest]) (*HelloResponse, error) { 22 | body, err := c.Body() 23 | if err != nil { 24 | return nil, err 25 | } 26 | 27 | if body.Word == "forbidden" { 28 | return nil, fuego.BadRequestError{Title: "Forbidden word"} 29 | } 30 | 31 | _, _ = c.Context().(*gin.Context) // Access to the Gin context 32 | 33 | name := c.QueryParam("name") 34 | _ = c.QueryParam("not-existing-param-raises-warning") 35 | 36 | return &HelloResponse{ 37 | Message: fmt.Sprintf("Hello %s, %s", body.Word, name), 38 | }, nil 39 | } 40 | -------------------------------------------------------------------------------- /examples/gin-compat/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | 8 | "github.com/go-fuego/fuego" 9 | ) 10 | 11 | func TestFuegoControllerPost(t *testing.T) { 12 | testCtx := fuego.NewMockContext(HelloRequest{Word: "World"}, any(nil)) 13 | testCtx.QueryParams().Set("name", "Ewen") 14 | 15 | response, err := fuegoControllerPost(testCtx) 16 | require.NoError(t, err) 17 | require.Equal(t, "Hello World, Ewen", response.Message) 18 | } 19 | -------------------------------------------------------------------------------- /examples/hello-world/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-fuego/fuego/examples/hello-world 2 | 3 | go 1.24.2 4 | 5 | require github.com/go-fuego/fuego v0.18.8 6 | 7 | require ( 8 | github.com/gabriel-vasile/mimetype v1.4.8 // indirect 9 | github.com/getkin/kin-openapi v0.132.0 // indirect 10 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 11 | github.com/go-openapi/swag v0.23.0 // indirect 12 | github.com/go-playground/locales v0.14.1 // indirect 13 | github.com/go-playground/universal-translator v0.18.1 // indirect 14 | github.com/go-playground/validator/v10 v10.26.0 // indirect 15 | github.com/golang-jwt/jwt/v5 v5.2.2 // indirect 16 | github.com/google/uuid v1.6.0 // indirect 17 | github.com/gorilla/schema v1.4.1 // indirect 18 | github.com/josharian/intern v1.0.0 // indirect 19 | github.com/leodido/go-urn v1.4.0 // indirect 20 | github.com/mailru/easyjson v0.9.0 // indirect 21 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect 22 | github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect 23 | github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect 24 | github.com/perimeterx/marshmallow v1.1.5 // indirect 25 | golang.org/x/crypto v0.37.0 // indirect 26 | golang.org/x/net v0.38.0 // indirect 27 | golang.org/x/sys v0.32.0 // indirect 28 | golang.org/x/text v0.25.0 // indirect 29 | gopkg.in/yaml.v3 v3.0.1 // indirect 30 | ) 31 | -------------------------------------------------------------------------------- /examples/hello-world/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/go-fuego/fuego" 5 | ) 6 | 7 | func main() { 8 | s := fuego.NewServer() 9 | 10 | fuego.Get(s, "/", helloWorld) 11 | 12 | s.Run() 13 | } 14 | 15 | func helloWorld(c fuego.ContextNoBody) (string, error) { 16 | return "Hello, World!", nil 17 | } 18 | -------------------------------------------------------------------------------- /examples/openapi-generate/cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/go-fuego/fuego/examples/openapi-generate/server" 5 | ) 6 | 7 | func main() { 8 | s := server.GetServer() 9 | s.Run() 10 | } 11 | -------------------------------------------------------------------------------- /examples/openapi-generate/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-fuego/fuego/examples/openapi-generate 2 | 3 | go 1.24.2 4 | 5 | require github.com/go-fuego/fuego v0.18.8 6 | 7 | require ( 8 | github.com/gabriel-vasile/mimetype v1.4.8 // indirect 9 | github.com/getkin/kin-openapi v0.132.0 // indirect 10 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 11 | github.com/go-openapi/swag v0.23.0 // indirect 12 | github.com/go-playground/locales v0.14.1 // indirect 13 | github.com/go-playground/universal-translator v0.18.1 // indirect 14 | github.com/go-playground/validator/v10 v10.26.0 // indirect 15 | github.com/golang-jwt/jwt/v5 v5.2.2 // indirect 16 | github.com/google/uuid v1.6.0 // indirect 17 | github.com/gorilla/schema v1.4.1 // indirect 18 | github.com/josharian/intern v1.0.0 // indirect 19 | github.com/leodido/go-urn v1.4.0 // indirect 20 | github.com/mailru/easyjson v0.9.0 // indirect 21 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect 22 | github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect 23 | github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect 24 | github.com/perimeterx/marshmallow v1.1.5 // indirect 25 | golang.org/x/crypto v0.37.0 // indirect 26 | golang.org/x/net v0.38.0 // indirect 27 | golang.org/x/sys v0.32.0 // indirect 28 | golang.org/x/text v0.25.0 // indirect 29 | gopkg.in/yaml.v3 v3.0.1 // indirect 30 | ) 31 | -------------------------------------------------------------------------------- /examples/openapi-generate/scripts/genspec.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/go-fuego/fuego/examples/openapi-generate/server" 5 | ) 6 | 7 | func main() { 8 | // Get the server instance, configured earlier 9 | newServer := server.GetServer() 10 | 11 | // Simple configuration for OpenAPI spec generation 12 | newServer.OpenAPI.Config.DisableLocalSave = false 13 | newServer.OpenAPI.Config.PrettyFormatJSON = true 14 | newServer.OpenAPI.Config.JSONFilePath = "api/openapi.json" 15 | 16 | // Generate the OpenAPI spec 17 | newServer.OutputOpenAPISpec() 18 | } 19 | -------------------------------------------------------------------------------- /examples/openapi-generate/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/go-fuego/fuego" 5 | "github.com/go-fuego/fuego/option" 6 | ) 7 | 8 | func helloWorld(c fuego.ContextNoBody) (string, error) { 9 | return "Hello, World!", nil 10 | } 11 | 12 | // GetServer is a representation of "central" server configuration 13 | // that can be used in multiple places, e.g. in the main function and in the OpenAPI spec generation script. 14 | func GetServer() *fuego.Server { 15 | s := fuego.NewServer() 16 | // Disable local save of the OpenAPI spec after runtime 17 | s.Engine.OpenAPI.Config.DisableLocalSave = true 18 | 19 | fuego.Get(s, "/", helloWorld, 20 | option.Summary("A simple hello world"), 21 | option.Description("This is a simple hello world"), 22 | option.Deprecated(), 23 | ) 24 | 25 | return s 26 | } 27 | -------------------------------------------------------------------------------- /examples/openapi-generate/tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | // +build tools 3 | 4 | package tools 5 | 6 | //go:generate go run scripts/genspec.go 7 | -------------------------------------------------------------------------------- /examples/openapi/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-fuego/fuego/examples/openapi 2 | 3 | go 1.24.2 4 | 5 | require github.com/go-fuego/fuego v0.18.8 6 | 7 | require ( 8 | github.com/gabriel-vasile/mimetype v1.4.8 // indirect 9 | github.com/getkin/kin-openapi v0.132.0 // indirect 10 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 11 | github.com/go-openapi/swag v0.23.0 // indirect 12 | github.com/go-playground/locales v0.14.1 // indirect 13 | github.com/go-playground/universal-translator v0.18.1 // indirect 14 | github.com/go-playground/validator/v10 v10.26.0 // indirect 15 | github.com/golang-jwt/jwt/v5 v5.2.2 // indirect 16 | github.com/google/uuid v1.6.0 // indirect 17 | github.com/gorilla/schema v1.4.1 // indirect 18 | github.com/josharian/intern v1.0.0 // indirect 19 | github.com/leodido/go-urn v1.4.0 // indirect 20 | github.com/mailru/easyjson v0.9.0 // indirect 21 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect 22 | github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect 23 | github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect 24 | github.com/perimeterx/marshmallow v1.1.5 // indirect 25 | golang.org/x/crypto v0.37.0 // indirect 26 | golang.org/x/net v0.38.0 // indirect 27 | golang.org/x/sys v0.32.0 // indirect 28 | golang.org/x/text v0.25.0 // indirect 29 | gopkg.in/yaml.v3 v3.0.1 // indirect 30 | ) 31 | -------------------------------------------------------------------------------- /examples/openapi/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/go-fuego/fuego" 5 | "github.com/go-fuego/fuego/option" 6 | ) 7 | 8 | func main() { 9 | s := fuego.NewServer() 10 | 11 | fuego.Get(s, "/", helloWorld, 12 | option.Summary("A simple hello world"), 13 | option.Description("This is a simple hello world"), 14 | option.Deprecated(), 15 | ) 16 | 17 | s.Run() 18 | } 19 | 20 | func helloWorld(c fuego.ContextNoBody) (string, error) { 21 | return "Hello, World!", nil 22 | } 23 | -------------------------------------------------------------------------------- /examples/petstore/controllers/middlewares.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import "net/http" 4 | 5 | func dummyMiddleware(next http.Handler) http.Handler { 6 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 7 | // do something before 8 | next.ServeHTTP(w, r) 9 | // do something after 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /examples/petstore/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-fuego/fuego/examples/petstore 2 | 3 | go 1.24.2 4 | 5 | require ( 6 | github.com/go-fuego/fuego v0.18.8 7 | github.com/stretchr/testify v1.10.0 8 | gotest.tools/v3 v3.5.2 9 | ) 10 | 11 | require ( 12 | github.com/davecgh/go-spew v1.1.1 // indirect 13 | github.com/gabriel-vasile/mimetype v1.4.8 // indirect 14 | github.com/getkin/kin-openapi v0.132.0 // indirect 15 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 16 | github.com/go-openapi/swag v0.23.0 // indirect 17 | github.com/go-playground/locales v0.14.1 // indirect 18 | github.com/go-playground/universal-translator v0.18.1 // indirect 19 | github.com/go-playground/validator/v10 v10.26.0 // indirect 20 | github.com/golang-jwt/jwt/v5 v5.2.2 // indirect 21 | github.com/google/go-cmp v0.6.0 // indirect 22 | github.com/google/uuid v1.6.0 // indirect 23 | github.com/gorilla/schema v1.4.1 // indirect 24 | github.com/josharian/intern v1.0.0 // indirect 25 | github.com/leodido/go-urn v1.4.0 // indirect 26 | github.com/mailru/easyjson v0.9.0 // indirect 27 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect 28 | github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect 29 | github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect 30 | github.com/perimeterx/marshmallow v1.1.5 // indirect 31 | github.com/pmezard/go-difflib v1.0.0 // indirect 32 | golang.org/x/crypto v0.37.0 // indirect 33 | golang.org/x/net v0.38.0 // indirect 34 | golang.org/x/sys v0.32.0 // indirect 35 | golang.org/x/text v0.25.0 // indirect 36 | gopkg.in/yaml.v3 v3.0.1 // indirect 37 | ) 38 | -------------------------------------------------------------------------------- /examples/petstore/lib/server.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "net/http" 5 | "reflect" 6 | "strings" 7 | 8 | "github.com/getkin/kin-openapi/openapi3" 9 | "github.com/getkin/kin-openapi/openapi3gen" 10 | "github.com/go-fuego/fuego" 11 | controller "github.com/go-fuego/fuego/examples/petstore/controllers" 12 | "github.com/go-fuego/fuego/examples/petstore/services" 13 | "github.com/go-fuego/fuego/option" 14 | ) 15 | 16 | type NoContent struct { 17 | Empty string `json:"-"` 18 | } 19 | 20 | var uuidParser openapi3gen.SchemaCustomizerFn = func(name string, t reflect.Type, tag reflect.StructTag, schema *openapi3.Schema) error { 21 | validateTag := tag.Get("validate") 22 | if validateTag == "" { 23 | return nil 24 | } 25 | 26 | parts := strings.Split(validateTag, ",") 27 | for _, part := range parts { 28 | if part == "uuid" { 29 | schema.Format = "uuid" 30 | } 31 | } 32 | return nil 33 | } 34 | 35 | func NewPetStoreServer(options ...func(*fuego.Server)) *fuego.Server { 36 | options = append(options, fuego.WithRouteOptions( 37 | option.AddResponse(http.StatusNoContent, "No Content", fuego.Response{Type: NoContent{}}), 38 | )) 39 | options = append(options, fuego.WithEngineOptions( 40 | fuego.WithOpenAPIGeneratorSchemaCustomizer( 41 | uuidParser, 42 | ), 43 | )) 44 | 45 | s := fuego.NewServer(options...) 46 | 47 | petsResources := controller.PetsResources{ 48 | PetsService: services.NewInMemoryPetsService(), // Dependency injection: we can pass a service here (for example a database service) 49 | } 50 | petsResources.Routes(s) 51 | return s 52 | } 53 | -------------------------------------------------------------------------------- /examples/petstore/lib/server_test.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "testing" 7 | 8 | "github.com/getkin/kin-openapi/openapi3" 9 | "github.com/stretchr/testify/require" 10 | "gotest.tools/v3/golden" 11 | 12 | "github.com/go-fuego/fuego" 13 | ) 14 | 15 | func TestPetstoreOpenAPIGeneration(t *testing.T) { 16 | server := NewPetStoreServer( 17 | fuego.WithoutStartupMessages(), 18 | fuego.WithEngineOptions( 19 | fuego.WithOpenAPIConfig(fuego.OpenAPIConfig{ 20 | JSONFilePath: "testdata/doc/openapi.json", 21 | PrettyFormatJSON: true, 22 | Info: &openapi3.Info{ 23 | Title: "Pet Store", 24 | Version: "0.0.2", 25 | }, 26 | }), 27 | ), 28 | ) 29 | 30 | server.Engine.RegisterOpenAPIRoutes(server) 31 | server.OutputOpenAPISpec() 32 | err := server.OpenAPI.Description().Validate(context.Background()) 33 | require.NoError(t, err) 34 | 35 | generatedSpec, err := os.ReadFile("testdata/doc/openapi.json") 36 | require.NoError(t, err) 37 | 38 | golden.Assert(t, string(generatedSpec), "doc/openapi.golden.json") 39 | } 40 | -------------------------------------------------------------------------------- /examples/petstore/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/go-fuego/fuego/examples/petstore/lib" 5 | ) 6 | 7 | func main() { 8 | err := lib.NewPetStoreServer().Run() 9 | if err != nil { 10 | panic(err) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/with-listener/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-fuego/fuego/examples/with-listener 2 | 3 | go 1.24.2 4 | 5 | require github.com/go-fuego/fuego v0.18.8 6 | 7 | require ( 8 | github.com/gabriel-vasile/mimetype v1.4.8 // indirect 9 | github.com/getkin/kin-openapi v0.132.0 // indirect 10 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 11 | github.com/go-openapi/swag v0.23.0 // indirect 12 | github.com/go-playground/locales v0.14.1 // indirect 13 | github.com/go-playground/universal-translator v0.18.1 // indirect 14 | github.com/go-playground/validator/v10 v10.26.0 // indirect 15 | github.com/golang-jwt/jwt/v5 v5.2.2 // indirect 16 | github.com/google/uuid v1.6.0 // indirect 17 | github.com/gorilla/schema v1.4.1 // indirect 18 | github.com/josharian/intern v1.0.0 // indirect 19 | github.com/leodido/go-urn v1.4.0 // indirect 20 | github.com/mailru/easyjson v0.9.0 // indirect 21 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect 22 | github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect 23 | github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect 24 | github.com/perimeterx/marshmallow v1.1.5 // indirect 25 | golang.org/x/crypto v0.37.0 // indirect 26 | golang.org/x/net v0.38.0 // indirect 27 | golang.org/x/sys v0.32.0 // indirect 28 | golang.org/x/text v0.25.0 // indirect 29 | gopkg.in/yaml.v3 v3.0.1 // indirect 30 | ) 31 | -------------------------------------------------------------------------------- /examples/with-listener/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/go-fuego/fuego" 7 | "github.com/go-fuego/fuego/option" 8 | ) 9 | 10 | func main() { 11 | listener, err := net.Listen("tcp", "127.0.0.1:8080") 12 | if err != nil { 13 | panic(err) 14 | } 15 | s := fuego.NewServer(fuego.WithListener(listener)) 16 | 17 | fuego.Get(s, "/", helloWorld, 18 | option.Summary("A simple hello world"), 19 | option.Description("This is a simple hello world"), 20 | option.Deprecated(), 21 | ) 22 | 23 | s.Run() 24 | } 25 | 26 | func helloWorld(c fuego.ContextNoBody) (string, error) { 27 | return "Hello, World!", nil 28 | } 29 | -------------------------------------------------------------------------------- /extra/fuegoecho/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-fuego/fuego/extra/fuegoecho 2 | 3 | go 1.24.2 4 | 5 | require ( 6 | github.com/go-fuego/fuego v0.18.8 7 | github.com/labstack/echo/v4 v4.13.4 8 | ) 9 | 10 | require ( 11 | github.com/gabriel-vasile/mimetype v1.4.8 // indirect 12 | github.com/getkin/kin-openapi v0.132.0 // indirect 13 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 14 | github.com/go-openapi/swag v0.23.0 // indirect 15 | github.com/go-playground/locales v0.14.1 // indirect 16 | github.com/go-playground/universal-translator v0.18.1 // indirect 17 | github.com/go-playground/validator/v10 v10.26.0 // indirect 18 | github.com/golang-jwt/jwt/v5 v5.2.2 // indirect 19 | github.com/google/uuid v1.6.0 // indirect 20 | github.com/gorilla/schema v1.4.1 // indirect 21 | github.com/josharian/intern v1.0.0 // indirect 22 | github.com/labstack/gommon v0.4.2 // indirect 23 | github.com/leodido/go-urn v1.4.0 // indirect 24 | github.com/mailru/easyjson v0.9.0 // indirect 25 | github.com/mattn/go-colorable v0.1.14 // indirect 26 | github.com/mattn/go-isatty v0.0.20 // indirect 27 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect 28 | github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect 29 | github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect 30 | github.com/perimeterx/marshmallow v1.1.5 // indirect 31 | github.com/valyala/bytebufferpool v1.0.0 // indirect 32 | github.com/valyala/fasttemplate v1.2.2 // indirect 33 | golang.org/x/crypto v0.38.0 // indirect 34 | golang.org/x/net v0.40.0 // indirect 35 | golang.org/x/sys v0.33.0 // indirect 36 | golang.org/x/text v0.25.0 // indirect 37 | gopkg.in/yaml.v3 v3.0.1 // indirect 38 | ) 39 | -------------------------------------------------------------------------------- /extra/markdown/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-fuego/fuego/extra/markdown 2 | 3 | go 1.24.2 4 | 5 | require ( 6 | github.com/gomarkdown/markdown v0.0.0-20250202022148-4f606c78d442 7 | github.com/stretchr/testify v1.10.0 8 | ) 9 | 10 | require ( 11 | github.com/davecgh/go-spew v1.1.1 // indirect 12 | github.com/kr/pretty v0.3.1 // indirect 13 | github.com/pmezard/go-difflib v1.0.0 // indirect 14 | github.com/rogpeppe/go-internal v1.12.0 // indirect 15 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 16 | gopkg.in/yaml.v3 v3.0.1 // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /extra/markdown/markdown.go: -------------------------------------------------------------------------------- 1 | package markdown 2 | 3 | import ( 4 | "html/template" 5 | 6 | "github.com/gomarkdown/markdown" 7 | "github.com/gomarkdown/markdown/html" 8 | "github.com/gomarkdown/markdown/parser" 9 | ) 10 | 11 | // Markdown converts a markdown string to HTML. 12 | // Note: fuego does not protect against malicious content 13 | // sanitation is up the caller of this function. 14 | func Markdown(content string) template.HTML { 15 | if content == "" { 16 | return template.HTML("") 17 | } 18 | mdRenderer := html.NewRenderer(html.RendererOptions{Flags: html.CommonFlags | html.SkipHTML}) 19 | mdParser := parser.NewWithExtensions(parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock | parser.Footnotes | parser.DefinitionLists) 20 | 21 | //nolint:gosec // G203 // the caller of this function needs to sanitize their input 22 | return template.HTML(markdown.ToHTML([]byte(content), mdParser, mdRenderer)) 23 | } 24 | -------------------------------------------------------------------------------- /extra/markdown/markdown_test.go: -------------------------------------------------------------------------------- 1 | package markdown 2 | 3 | import ( 4 | "html/template" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestMarkdown(t *testing.T) { 11 | t.Run("empty string", func(t *testing.T) { 12 | html := Markdown("") 13 | require.Equal(t, template.HTML(""), html) 14 | }) 15 | 16 | t.Run("can render markdown", func(t *testing.T) { 17 | md := `# Hello 18 | Just **testing**.` 19 | 20 | html := Markdown(md) 21 | require.Equal(t, template.HTML("

Hello

\n\n
Just **testing**.\n
\n"), html) 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /extra/sql/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-fuego/fuego/extra/sql 2 | 3 | go 1.24.2 4 | 5 | require ( 6 | github.com/go-fuego/fuego v0.18.8 7 | github.com/stretchr/testify v1.10.0 8 | ) 9 | 10 | require ( 11 | github.com/davecgh/go-spew v1.1.1 // indirect 12 | github.com/gabriel-vasile/mimetype v1.4.8 // indirect 13 | github.com/getkin/kin-openapi v0.132.0 // indirect 14 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 15 | github.com/go-openapi/swag v0.23.0 // indirect 16 | github.com/go-playground/locales v0.14.1 // indirect 17 | github.com/go-playground/universal-translator v0.18.1 // indirect 18 | github.com/go-playground/validator/v10 v10.26.0 // indirect 19 | github.com/golang-jwt/jwt/v5 v5.2.2 // indirect 20 | github.com/google/uuid v1.6.0 // indirect 21 | github.com/gorilla/schema v1.4.1 // indirect 22 | github.com/josharian/intern v1.0.0 // indirect 23 | github.com/leodido/go-urn v1.4.0 // indirect 24 | github.com/mailru/easyjson v0.9.0 // indirect 25 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect 26 | github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect 27 | github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect 28 | github.com/perimeterx/marshmallow v1.1.5 // indirect 29 | github.com/pmezard/go-difflib v1.0.0 // indirect 30 | golang.org/x/crypto v0.37.0 // indirect 31 | golang.org/x/net v0.38.0 // indirect 32 | golang.org/x/sys v0.32.0 // indirect 33 | golang.org/x/text v0.25.0 // indirect 34 | gopkg.in/yaml.v3 v3.0.1 // indirect 35 | ) 36 | -------------------------------------------------------------------------------- /extra/sql/sql_errors.go: -------------------------------------------------------------------------------- 1 | // Package sql provides error handling for SQL operations. 2 | // It maps standard SQL errors to the corresponding fuego errors. 3 | package sql 4 | 5 | import ( 6 | "database/sql" 7 | "errors" 8 | "net/http" 9 | 10 | "github.com/go-fuego/fuego" 11 | ) 12 | 13 | // ErrorHandler maps standard SQL errors to the corresponding fuego errors. 14 | func ErrorHandler(err error) error { 15 | if err == nil { 16 | return nil 17 | } 18 | 19 | if errors.Is(err, sql.ErrNoRows) { 20 | return fuego.NotFoundError{ 21 | Err: err, 22 | Title: "Record Not Found", 23 | Detail: err.Error(), 24 | Status: http.StatusNotFound, 25 | } 26 | } 27 | 28 | if errors.Is(err, sql.ErrConnDone) { 29 | return fuego.InternalServerError{ 30 | Err: err, 31 | Title: "Connection Closed", 32 | Detail: err.Error(), 33 | Status: http.StatusInternalServerError, 34 | } 35 | } 36 | 37 | if errors.Is(err, sql.ErrTxDone) { 38 | return fuego.ConflictError{ 39 | Err: err, 40 | Title: "Transaction Completed", 41 | Detail: err.Error(), 42 | Status: http.StatusConflict, 43 | } 44 | } 45 | 46 | // For any other error, return the original error. 47 | return err 48 | } 49 | -------------------------------------------------------------------------------- /extra/sqlite3/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-fuego/fuego/extra/sqlite3 2 | 3 | go 1.24.2 4 | 5 | require ( 6 | github.com/go-fuego/fuego v0.18.8 7 | github.com/mattn/go-sqlite3 v1.14.28 8 | github.com/stretchr/testify v1.10.0 9 | ) 10 | 11 | require ( 12 | github.com/davecgh/go-spew v1.1.1 // indirect 13 | github.com/gabriel-vasile/mimetype v1.4.8 // indirect 14 | github.com/getkin/kin-openapi v0.132.0 // indirect 15 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 16 | github.com/go-openapi/swag v0.23.0 // indirect 17 | github.com/go-playground/locales v0.14.1 // indirect 18 | github.com/go-playground/universal-translator v0.18.1 // indirect 19 | github.com/go-playground/validator/v10 v10.26.0 // indirect 20 | github.com/golang-jwt/jwt/v5 v5.2.2 // indirect 21 | github.com/google/uuid v1.6.0 // indirect 22 | github.com/gorilla/schema v1.4.1 // indirect 23 | github.com/josharian/intern v1.0.0 // indirect 24 | github.com/leodido/go-urn v1.4.0 // indirect 25 | github.com/mailru/easyjson v0.9.0 // indirect 26 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect 27 | github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect 28 | github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect 29 | github.com/perimeterx/marshmallow v1.1.5 // indirect 30 | github.com/pmezard/go-difflib v1.0.0 // indirect 31 | golang.org/x/crypto v0.37.0 // indirect 32 | golang.org/x/net v0.38.0 // indirect 33 | golang.org/x/sys v0.32.0 // indirect 34 | golang.org/x/text v0.25.0 // indirect 35 | gopkg.in/yaml.v3 v3.0.1 // indirect 36 | ) 37 | -------------------------------------------------------------------------------- /generic_mux.go: -------------------------------------------------------------------------------- 1 | package fuego 2 | 3 | import ( 4 | "log/slog" 5 | ) 6 | 7 | // Registerer is an interface that allows registering routes. 8 | // It can be implementable by any router. 9 | type Registerer[T, B, P any] interface { 10 | Register() Route[T, B, P] 11 | } 12 | 13 | func Registers[B, T, P any](engine *Engine, a Registerer[B, T, P]) *Route[B, T, P] { 14 | route := a.Register() 15 | err := route.RegisterOpenAPIOperation(engine.OpenAPI) 16 | if err != nil { 17 | slog.Warn("error documenting openapi operation", "error", err) 18 | } 19 | return &route 20 | } 21 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-fuego/fuego 2 | 3 | go 1.24.2 4 | 5 | require ( 6 | github.com/getkin/kin-openapi v0.132.0 7 | github.com/go-playground/validator/v10 v10.26.0 8 | github.com/golang-jwt/jwt/v5 v5.2.2 9 | github.com/google/uuid v1.6.0 10 | github.com/gorilla/schema v1.4.1 11 | github.com/stretchr/testify v1.10.0 12 | github.com/thejerf/slogassert v0.3.4 13 | gopkg.in/yaml.v3 v3.0.1 14 | ) 15 | 16 | require ( 17 | github.com/davecgh/go-spew v1.1.1 // indirect 18 | github.com/gabriel-vasile/mimetype v1.4.8 // indirect 19 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 20 | github.com/go-openapi/swag v0.23.0 // indirect 21 | github.com/go-playground/locales v0.14.1 // indirect 22 | github.com/go-playground/universal-translator v0.18.1 // indirect 23 | github.com/josharian/intern v1.0.0 // indirect 24 | github.com/leodido/go-urn v1.4.0 // indirect 25 | github.com/mailru/easyjson v0.9.0 // indirect 26 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect 27 | github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect 28 | github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect 29 | github.com/perimeterx/marshmallow v1.1.5 // indirect 30 | github.com/pmezard/go-difflib v1.0.0 // indirect 31 | github.com/ugorji/go/codec v1.2.12 // indirect 32 | golang.org/x/crypto v0.37.0 // indirect 33 | golang.org/x/net v0.38.0 // indirect 34 | golang.org/x/sys v0.32.0 // indirect 35 | golang.org/x/text v0.25.0 // indirect 36 | ) 37 | 38 | retract ( 39 | v1.0.1 // Contains retractions only. 40 | v1.0.0 // Published accidentally. 41 | ) 42 | -------------------------------------------------------------------------------- /go.work: -------------------------------------------------------------------------------- 1 | go 1.24.2 2 | 3 | use ( 4 | . 5 | ./cmd/fuego 6 | ./examples/acme-tls 7 | ./examples/basic 8 | ./examples/crud-gorm 9 | ./examples/custom-errors 10 | ./examples/custom-serializer 11 | ./examples/echo-compat 12 | ./examples/full-app-gourmet 13 | ./examples/generate-opengraph-image 14 | ./examples/gin-compat 15 | ./examples/hello-world 16 | ./examples/openapi 17 | ./examples/petstore 18 | ./examples/with-listener 19 | ./extra/fuegoecho 20 | ./extra/fuegogin 21 | ./extra/markdown 22 | ./extra/sql 23 | ./extra/sqlite3 24 | ./middleware/basicauth 25 | ./middleware/cache 26 | ./testing-from-outside 27 | examples/openapi-generate 28 | ) 29 | -------------------------------------------------------------------------------- /mdsf.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/hougesen/mdsf/main/schemas/v0.4.2/mdsf.schema.json", 3 | "custom_file_extensions": {}, 4 | "format_finished_document": false, 5 | "javascript_runtime": "node", 6 | "language_aliases": { 7 | "bash": "shell", 8 | "sh": "shell" 9 | }, 10 | "languages": { 11 | "fish": "fish_indent", 12 | "json": "prettier", 13 | "shell": "shfmt", 14 | "yaml": "prettier", 15 | "go": [ 16 | [ 17 | "gci", 18 | "goimports" 19 | ], 20 | [ 21 | "gofumpt", 22 | "gofmt" 23 | ] 24 | ] 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /middleware/basicauth/basicauth.go: -------------------------------------------------------------------------------- 1 | package basicauth 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/go-fuego/fuego" 7 | ) 8 | 9 | type Config struct { 10 | Username string 11 | Password string 12 | AllowGet bool // Allow GET requests without auth 13 | } 14 | 15 | // Basic auth middleware 16 | func New(config Config) func(http.Handler) http.Handler { 17 | if config.Username == "" { 18 | panic("basicauth: username is required") 19 | } 20 | if config.Password == "" { 21 | panic("basicauth: password is required") 22 | } 23 | 24 | return func(h http.Handler) http.Handler { 25 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 26 | if r.Method == http.MethodGet && config.AllowGet { 27 | h.ServeHTTP(w, r) 28 | return 29 | } 30 | 31 | user, pass, ok := r.BasicAuth() 32 | 33 | if ok && user == config.Username && pass == config.Password { 34 | h.ServeHTTP(w, r) 35 | return 36 | } 37 | 38 | err := fuego.HTTPError{ 39 | Title: "unauthorized access", 40 | Detail: "wrong username or password", 41 | Status: http.StatusUnauthorized, 42 | } 43 | 44 | w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`) 45 | fuego.SendJSONError(w, nil, err) 46 | }) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /middleware/basicauth/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-fuego/fuego/middleware/basicauth 2 | 3 | go 1.24.2 4 | 5 | require ( 6 | github.com/go-fuego/fuego v0.18.0 7 | github.com/stretchr/testify v1.10.0 8 | ) 9 | 10 | require ( 11 | github.com/davecgh/go-spew v1.1.1 // indirect 12 | github.com/gabriel-vasile/mimetype v1.4.8 // indirect 13 | github.com/getkin/kin-openapi v0.131.0 // indirect 14 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 15 | github.com/go-openapi/swag v0.23.0 // indirect 16 | github.com/go-playground/locales v0.14.1 // indirect 17 | github.com/go-playground/universal-translator v0.18.1 // indirect 18 | github.com/go-playground/validator/v10 v10.24.0 // indirect 19 | github.com/golang-jwt/jwt/v5 v5.2.2 // indirect 20 | github.com/google/uuid v1.6.0 // indirect 21 | github.com/gorilla/schema v1.4.1 // indirect 22 | github.com/josharian/intern v1.0.0 // indirect 23 | github.com/leodido/go-urn v1.4.0 // indirect 24 | github.com/mailru/easyjson v0.9.0 // indirect 25 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect 26 | github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect 27 | github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect 28 | github.com/perimeterx/marshmallow v1.1.5 // indirect 29 | github.com/pmezard/go-difflib v1.0.0 // indirect 30 | golang.org/x/crypto v0.36.0 // indirect 31 | golang.org/x/net v0.38.0 // indirect 32 | golang.org/x/sys v0.31.0 // indirect 33 | golang.org/x/text v0.23.0 // indirect 34 | gopkg.in/yaml.v3 v3.0.1 // indirect 35 | ) 36 | -------------------------------------------------------------------------------- /middleware/cache/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-fuego/fuego/middleware/cache 2 | 3 | go 1.24.2 4 | 5 | require ( 6 | github.com/go-fuego/fuego v0.15.1 7 | github.com/hashicorp/golang-lru/v2 v2.0.7 8 | github.com/stretchr/testify v1.10.0 9 | ) 10 | 11 | require ( 12 | github.com/davecgh/go-spew v1.1.1 // indirect 13 | github.com/gabriel-vasile/mimetype v1.4.8 // indirect 14 | github.com/getkin/kin-openapi v0.129.0 // indirect 15 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 16 | github.com/go-openapi/swag v0.23.0 // indirect 17 | github.com/go-playground/locales v0.14.1 // indirect 18 | github.com/go-playground/universal-translator v0.18.1 // indirect 19 | github.com/go-playground/validator/v10 v10.24.0 // indirect 20 | github.com/golang-jwt/jwt/v5 v5.2.2 // indirect 21 | github.com/gorilla/schema v1.4.1 // indirect 22 | github.com/josharian/intern v1.0.0 // indirect 23 | github.com/leodido/go-urn v1.4.0 // indirect 24 | github.com/mailru/easyjson v0.9.0 // indirect 25 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect 26 | github.com/oasdiff/yaml v0.0.0-20241214135536-5f7845c759c8 // indirect 27 | github.com/oasdiff/yaml3 v0.0.0-20241214160948-977117996672 // indirect 28 | github.com/perimeterx/marshmallow v1.1.5 // indirect 29 | github.com/pmezard/go-difflib v1.0.0 // indirect 30 | github.com/ugorji/go/codec v1.2.12 // indirect 31 | golang.org/x/crypto v0.32.0 // indirect 32 | golang.org/x/net v0.34.0 // indirect 33 | golang.org/x/sys v0.30.0 // indirect 34 | golang.org/x/text v0.22.0 // indirect 35 | gopkg.in/yaml.v3 v3.0.1 // indirect 36 | ) 37 | -------------------------------------------------------------------------------- /middleware/cache/in_memory.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/hashicorp/golang-lru/v2/expirable" 7 | ) 8 | 9 | type TTLCache struct { 10 | cache *expirable.LRU[string, string] 11 | } 12 | 13 | var _ Storage = (*TTLCache)(nil) 14 | 15 | func NewInMemoryCache(duration time.Duration, maxObjects int) *TTLCache { 16 | return &TTLCache{ 17 | cache: expirable.NewLRU[string, string](maxObjects, nil, duration), 18 | } 19 | } 20 | 21 | func (t *TTLCache) Get(key string) (string, bool) { 22 | return t.cache.Get(key) 23 | } 24 | 25 | func (t *TTLCache) Set(key, value string) { 26 | t.cache.Add(key, value) 27 | } 28 | -------------------------------------------------------------------------------- /middleware/cache/writer.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | ) 7 | 8 | // MultiHTTPWriter is a http.ResponseWriter that writes the response to multiple writers 9 | type MultiHTTPWriter struct { 10 | http.ResponseWriter 11 | status int // status is the status code that will be written to the response 12 | cacheWriter io.Writer // cacheWriter is the writer that will be used to cache the response 13 | } 14 | 15 | var _ http.ResponseWriter = &MultiHTTPWriter{} 16 | 17 | func (m *MultiHTTPWriter) Write(p []byte) (int, error) { 18 | multiWriter := io.MultiWriter(m.ResponseWriter, m.cacheWriter) 19 | return multiWriter.Write(p) 20 | } 21 | 22 | func (m *MultiHTTPWriter) Unwrap() http.ResponseWriter { 23 | return m.ResponseWriter 24 | } 25 | 26 | func (m *MultiHTTPWriter) WriteHeader(statusCode int) { 27 | m.status = statusCode 28 | m.ResponseWriter.WriteHeader(statusCode) 29 | } 30 | -------------------------------------------------------------------------------- /middleware/cache/writer_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "bytes" 5 | "net/http/httptest" 6 | "testing" 7 | ) 8 | 9 | func TestWriter(t *testing.T) { 10 | w := httptest.NewRecorder() 11 | bytesBuffer := &bytes.Buffer{} 12 | m := &MultiHTTPWriter{ 13 | ResponseWriter: w, 14 | cacheWriter: bytesBuffer, 15 | } 16 | written := "hello world" 17 | 18 | n, err := m.Write([]byte(written)) 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | if len(written) != n { 23 | t.Errorf("Expected %d, got %d", len(written), n) 24 | } 25 | 26 | bytesWritten := m.cacheWriter.(*bytes.Buffer).String() 27 | if written != bytesWritten { 28 | t.Errorf("Expected %s, got %s", written, m.cacheWriter.(*bytes.Buffer).Bytes()) 29 | } 30 | } 31 | 32 | func TestWriter_Unwrap(t *testing.T) { 33 | w := httptest.NewRecorder() 34 | bytesBuffer := &bytes.Buffer{} 35 | m := &MultiHTTPWriter{ 36 | ResponseWriter: w, 37 | cacheWriter: bytesBuffer, 38 | } 39 | if m.Unwrap() != w { 40 | t.Errorf("Expected %T, got %T", w, m.Unwrap()) 41 | } 42 | } 43 | 44 | func TestWriter_WriteHeader(t *testing.T) { 45 | w := httptest.NewRecorder() 46 | bytesBuffer := &bytes.Buffer{} 47 | m := &MultiHTTPWriter{ 48 | ResponseWriter: w, 49 | cacheWriter: bytesBuffer, 50 | } 51 | m.WriteHeader(204) 52 | if m.status != 204 { 53 | t.Errorf("Expected %d, got %d", 204, m.status) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /multi_return.go: -------------------------------------------------------------------------------- 1 | package fuego 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "encoding/xml" 7 | "fmt" 8 | "io" 9 | 10 | "gopkg.in/yaml.v3" 11 | ) 12 | 13 | // DataOrTemplate is a struct that can return either data or a template 14 | // depending on the asked type. 15 | type DataOrTemplate[T any] struct { 16 | Data T 17 | Template any 18 | } 19 | 20 | var ( 21 | _ CtxRenderer = DataOrTemplate[any]{} // Can render HTML (template) 22 | _ json.Marshaler = DataOrTemplate[any]{} // Can render JSON (data) 23 | _ xml.Marshaler = DataOrTemplate[any]{} // Can render XML (data) 24 | _ yaml.Marshaler = DataOrTemplate[any]{} // Can render YAML (data) 25 | _ fmt.Stringer = DataOrTemplate[any]{} // Can render string (data) 26 | ) 27 | 28 | func (m DataOrTemplate[T]) MarshalJSON() ([]byte, error) { 29 | return json.Marshal(m.Data) 30 | } 31 | 32 | func (m DataOrTemplate[T]) MarshalXML(e *xml.Encoder, _ xml.StartElement) error { 33 | return e.Encode(m.Data) 34 | } 35 | 36 | func (m DataOrTemplate[T]) MarshalYAML() (any, error) { 37 | return m.Data, nil 38 | } 39 | 40 | func (m DataOrTemplate[T]) String() string { 41 | return fmt.Sprintf("%v", m.Data) 42 | } 43 | 44 | func (m DataOrTemplate[T]) Render(c context.Context, w io.Writer) error { 45 | switch renderer := m.Template.(type) { 46 | case CtxRenderer: 47 | return renderer.Render(c, w) 48 | case Renderer: 49 | return renderer.Render(w) 50 | default: 51 | panic("template must be either CtxRenderer or Renderer") 52 | } 53 | } 54 | 55 | // DataOrHTML is a helper function to create a [DataOrTemplate] return item without specifying the type. 56 | func DataOrHTML[T any](data T, template any) *DataOrTemplate[T] { 57 | return &DataOrTemplate[T]{ 58 | Data: data, 59 | Template: template, 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /openapi_handler.go: -------------------------------------------------------------------------------- 1 | package fuego 2 | 3 | import "net/http" 4 | 5 | func DefaultOpenAPIHandler(specURL string) http.Handler { 6 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 7 | w.Header().Set("Content-Type", "text/html; charset=utf-8") 8 | _, _ = w.Write([]byte(DefaultOpenAPIHTML(specURL))) 9 | }) 10 | } 11 | 12 | func DefaultOpenAPIHTML(specURL string) string { 13 | return ` 14 | 15 | 16 | 17 | 18 | 19 | 20 | OpenAPI specification 21 | 22 | 23 | 24 | 25 | 32 | 33 | ` 34 | } 35 | -------------------------------------------------------------------------------- /param.go: -------------------------------------------------------------------------------- 1 | package fuego 2 | 3 | func ParamRequired() func(param *OpenAPIParam) { 4 | return func(param *OpenAPIParam) { 5 | param.Required = true 6 | } 7 | } 8 | 9 | func ParamNullable() func(param *OpenAPIParam) { 10 | return func(param *OpenAPIParam) { 11 | param.Nullable = true 12 | } 13 | } 14 | 15 | func ParamString() func(param *OpenAPIParam) { 16 | return func(param *OpenAPIParam) { 17 | param.GoType = "string" 18 | } 19 | } 20 | 21 | func ParamInteger() func(param *OpenAPIParam) { 22 | return func(param *OpenAPIParam) { 23 | param.GoType = "integer" 24 | } 25 | } 26 | 27 | func ParamBool() func(param *OpenAPIParam) { 28 | return func(param *OpenAPIParam) { 29 | param.GoType = "boolean" 30 | } 31 | } 32 | 33 | func ParamDescription(description string) func(param *OpenAPIParam) { 34 | return func(param *OpenAPIParam) { 35 | param.Description = description 36 | } 37 | } 38 | 39 | func ParamDefault(value any) func(param *OpenAPIParam) { 40 | return func(param *OpenAPIParam) { 41 | param.Default = value 42 | } 43 | } 44 | 45 | // ParamExample adds an example to the parameter. As per the OpenAPI 3.0 standard, the example must be given a name. 46 | func ParamExample(exampleName string, value any) func(param *OpenAPIParam) { 47 | return func(param *OpenAPIParam) { 48 | if param.Examples == nil { 49 | param.Examples = make(map[string]any) 50 | } 51 | param.Examples[exampleName] = value 52 | } 53 | } 54 | 55 | // ParamStatusCodes sets the status codes for which this parameter is required. 56 | // Only used for response parameters. 57 | // If empty, it is required for 200 status codes. 58 | func ParamStatusCodes(codes ...int) func(param *OpenAPIParam) { 59 | return func(param *OpenAPIParam) { 60 | param.StatusCodes = codes 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /param/param.go: -------------------------------------------------------------------------------- 1 | // Package param provides a set of shortcuts to define parameters for the route Options. 2 | // See the [github.com/go-fuego/fuego/option] package for more information. 3 | package param 4 | 5 | import "github.com/go-fuego/fuego" 6 | 7 | // Required sets the parameter as required. 8 | // If the parameter is not present, the request will fail. 9 | var Required = fuego.ParamRequired 10 | 11 | // Nullable sets the parameter as nullable. 12 | var Nullable = fuego.ParamNullable 13 | 14 | // Integer sets the parameter type to integer. 15 | // The query parameter is transmitted as a string in the URL, but it is parsed as an integer. 16 | // Please prefer QueryInt for clarity. 17 | var Integer = fuego.ParamInteger 18 | 19 | // Bool sets the parameter type to boolean. 20 | // The query parameter is transmitted as a string in the URL, but it is parsed as a boolean. 21 | // Please prefer QueryBool for clarity. 22 | var Bool = fuego.ParamBool 23 | 24 | // Description sets the description for the parameter. 25 | var Description = fuego.ParamDescription 26 | 27 | // Default sets the default value for the parameter. 28 | // Type is checked at start-time. 29 | var Default = fuego.ParamDefault 30 | 31 | // Example adds an example to the parameter. As per the OpenAPI 3.0 standard, the example must be given a name. 32 | var Example = fuego.ParamExample 33 | 34 | // StatusCodes sets the status codes for which this parameter is required. 35 | // Only used for response parameters. 36 | // If empty, it is required for 200 status codes. 37 | var StatusCodes = fuego.ParamStatusCodes 38 | -------------------------------------------------------------------------------- /param_test.go: -------------------------------------------------------------------------------- 1 | package fuego_test 2 | 3 | import ( 4 | "strconv" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | 9 | "github.com/go-fuego/fuego" 10 | "github.com/go-fuego/fuego/option" 11 | ) 12 | 13 | func TestParams(t *testing.T) { 14 | t.Run("All options", func(t *testing.T) { 15 | s := fuego.NewServer() 16 | 17 | route := fuego.Get(s, "/test", func(c fuego.ContextNoBody) (string, error) { 18 | name := c.QueryParam("name") 19 | age := c.QueryParamInt("age") 20 | isok := c.QueryParamBool("is_ok") 21 | 22 | return name + strconv.Itoa(age) + strconv.FormatBool(isok), nil 23 | }, 24 | option.Query("name", "Name", fuego.ParamRequired(), fuego.ParamDefault("hey"), fuego.ParamExample("example1", "you")), 25 | option.QueryInt("age", "Age", fuego.ParamNullable(), fuego.ParamDefault(18), fuego.ParamExample("example1", 1)), 26 | option.QueryBool("is_ok", "Is OK?", fuego.ParamDefault(true), fuego.ParamExample("example1", true)), 27 | ) 28 | 29 | require.NotNil(t, route) 30 | require.NotNil(t, route.Params) 31 | require.Len(t, route.Params, 4) 32 | require.Equal(t, "Name", route.Params["name"].Description) 33 | require.True(t, route.Params["name"].Required) 34 | require.Equal(t, "hey", route.Params["name"].Default) 35 | require.Equal(t, "you", route.Params["name"].Examples["example1"]) 36 | require.Equal(t, "string", route.Params["name"].GoType) 37 | 38 | require.Equal(t, "Age", route.Params["age"].Description) 39 | require.True(t, route.Params["age"].Nullable) 40 | require.Equal(t, 18, route.Params["age"].Default) 41 | require.Equal(t, "integer", route.Params["age"].GoType) 42 | 43 | require.Equal(t, "Is OK?", route.Params["is_ok"].Description) 44 | require.True(t, route.Params["is_ok"].Default.(bool)) 45 | 46 | require.Equal(t, "Accept", route.Params["Accept"].Name) 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /parameter_registration_test.go: -------------------------------------------------------------------------------- 1 | package fuego 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func Test_RegisterOpenAPIOperation(t *testing.T) { 12 | handler := func(w http.ResponseWriter, r *http.Request) {} 13 | s := NewServer() 14 | 15 | t.Run("Nil operation handling", func(t *testing.T) { 16 | route := NewRoute[struct{}, struct{}, struct{}]( 17 | http.MethodGet, 18 | "/test", 19 | handler, 20 | s.Engine, 21 | ) 22 | route.Operation = nil 23 | err := route.RegisterParams() 24 | require.NoError(t, err) 25 | assert.NotNil(t, route.Operation) 26 | }) 27 | 28 | t.Run("Register with params", func(t *testing.T) { 29 | route := NewRoute[struct{}, struct{}, struct { 30 | QueryParam string `query:"queryParam"` 31 | HeaderParam string `header:"headerParam"` 32 | }]( 33 | http.MethodGet, 34 | "/some/path/{pathParam}", 35 | handler, 36 | s.Engine, 37 | ) 38 | err := route.RegisterParams() 39 | require.NoError(t, err) 40 | operation := route.Operation 41 | assert.NotNil(t, operation) 42 | assert.Len(t, operation.Parameters, 2) 43 | 44 | queryParam := operation.Parameters.GetByInAndName("query", "queryParam") 45 | assert.NotNil(t, queryParam) 46 | assert.Equal(t, "queryParam", queryParam.Name) 47 | 48 | headerParam := operation.Parameters.GetByInAndName("header", "headerParam") 49 | assert.NotNil(t, headerParam) 50 | assert.Equal(t, "headerParam", headerParam.Name) 51 | }) 52 | 53 | t.Run("RegisterParams do not raise error with interface types", func(t *testing.T) { 54 | route := NewRoute[struct{}, struct{}, any]( 55 | http.MethodGet, 56 | "/no-interfaces", 57 | handler, 58 | s.Engine, 59 | OptionDefaultStatusCode(201), 60 | ) 61 | 62 | err := route.RegisterParams() 63 | require.NoError(t, err) 64 | }) 65 | } 66 | -------------------------------------------------------------------------------- /params.go: -------------------------------------------------------------------------------- 1 | package fuego 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | ) 7 | 8 | var pathParamRegex = regexp.MustCompile(`{(.+?)}`) 9 | 10 | // parsePathParams gives the list of path parameters in a path. 11 | // Example : /item/{user}/{id} -> [user, id] 12 | func parsePathParams(path string) []string { 13 | matches := pathParamRegex.FindAllString(path, -1) 14 | for i, match := range matches { 15 | matches[i] = strings.Trim(match, "{}") 16 | } 17 | return matches 18 | } 19 | -------------------------------------------------------------------------------- /params_test.go: -------------------------------------------------------------------------------- 1 | package fuego 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestParsePathParams(t *testing.T) { 10 | require.Equal(t, []string(nil), parsePathParams("/")) 11 | require.Equal(t, []string(nil), parsePathParams("/item/")) 12 | require.Equal(t, []string{"user"}, parsePathParams("POST /item/{user}")) 13 | require.Equal(t, []string{"user"}, parsePathParams("/item/{user}")) 14 | require.Equal(t, []string{"user", "bookname..."}, parsePathParams("/item/{user}/{bookname...}")) 15 | require.Equal(t, []string{"user", "id"}, parsePathParams("/item/{user}/{id}")) 16 | require.Equal(t, []string{"$"}, parsePathParams("/item/{$}")) 17 | require.Equal(t, []string{"user"}, parsePathParams("POST alt.com/item/{user}")) 18 | } 19 | 20 | func BenchmarkParsePathParams(b *testing.B) { 21 | b.Run("empty", func(b *testing.B) { 22 | for range b.N { 23 | parsePathParams("/") 24 | } 25 | }) 26 | 27 | b.Run("several path params", func(b *testing.B) { 28 | for range b.N { 29 | parsePathParams("/item/{user}/{id}") 30 | } 31 | }) 32 | } 33 | 34 | func FuzzParsePathParams(f *testing.F) { 35 | f.Add("/item/{user}") 36 | f.Add("/item/") 37 | f.Add("/item/{user}/{id}") 38 | f.Add("POST /item/{user}") 39 | f.Add("") 40 | 41 | f.Fuzz(func(t *testing.T, data string) { 42 | parsePathParams(data) 43 | }) 44 | } 45 | -------------------------------------------------------------------------------- /perf.go: -------------------------------------------------------------------------------- 1 | package fuego 2 | 3 | import ( 4 | "strconv" 5 | "time" 6 | ) 7 | 8 | // Timing is a struct to represent a server timing. 9 | // Used in the Server-Timing header. 10 | type Timing struct { 11 | Name string 12 | Desc string 13 | Dur time.Duration 14 | } 15 | 16 | // String returns a string representation of a Timing, as defined in https://www.w3.org/TR/server-timing/#the-server-timing-header-field 17 | func (t Timing) String() string { 18 | s := t.Name + ";dur=" + strconv.Itoa(int(t.Dur.Milliseconds())) 19 | if t.Desc != "" { 20 | s += ";desc=\"" + t.Desc + "\"" 21 | } 22 | return s 23 | } 24 | -------------------------------------------------------------------------------- /perf_test.go: -------------------------------------------------------------------------------- 1 | package fuego 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestTiming_String(t *testing.T) { 11 | t.Run("no desc", func(t *testing.T) { 12 | timing := Timing{ 13 | Name: "test", 14 | Dur: time.Duration(100) * time.Millisecond, 15 | } 16 | 17 | require.Equal(t, "test;dur=100", timing.String()) 18 | }) 19 | 20 | t.Run("with desc", func(t *testing.T) { 21 | timing := Timing{ 22 | Name: "test", 23 | Dur: time.Duration(300) * time.Millisecond, 24 | Desc: "test desc", 25 | } 26 | 27 | require.Equal(t, "test;dur=300;desc=\"test desc\"", timing.String()) 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /static/embed.go: -------------------------------------------------------------------------------- 1 | // Package static provides embedded static files for the Fuego framework. 2 | package static 3 | 4 | import ( 5 | _ "embed" 6 | ) 7 | 8 | // Favicon is the embedded SVG favicon file. 9 | // Will not pollute your binary with a favicon.ico file, 10 | // unless you import it explicitly in your server: the Fuego framework does not use it. 11 | // 12 | //go:embed fuego.svg 13 | var Favicon []byte 14 | -------------------------------------------------------------------------------- /testdata/test.html: -------------------------------------------------------------------------------- 1 |
2 |

Test

3 |

Your name is: {{ .Name }}

4 |
5 | -------------------------------------------------------------------------------- /testing-from-outside/cors_test.go: -------------------------------------------------------------------------------- 1 | package testingfromoutside_test 2 | 3 | import ( 4 | "net/http/httptest" 5 | "testing" 6 | 7 | "github.com/rs/cors" 8 | "github.com/stretchr/testify/require" 9 | 10 | "github.com/go-fuego/fuego" 11 | ) 12 | 13 | func TestCors(t *testing.T) { 14 | s := fuego.NewServer( 15 | fuego.WithoutLogger(), 16 | fuego.WithGlobalMiddlewares(cors.New(cors.Options{ 17 | AllowedOrigins: []string{"*"}, 18 | AllowedMethods: []string{"GET"}, 19 | }).Handler), 20 | ) 21 | 22 | fuego.Get(s, "/", func(c fuego.ContextNoBody) (string, error) { 23 | return "Hello, World!", nil 24 | }) 25 | 26 | t.Run("CORS request INCOMPLETE TEST", func(t *testing.T) { 27 | r := httptest.NewRequest("GET", "http://example.com/", nil) 28 | w := httptest.NewRecorder() 29 | 30 | r.Header.Set("Origin", "http://example.com/") 31 | r.Header.Set("Access-Control-Request-Method", "GET") 32 | 33 | s.Mux.ServeHTTP(w, r) 34 | 35 | t.Log(w.Header()) 36 | body := w.Body.String() 37 | require.Equal(t, "Hello, World!", body) 38 | require.Equal(t, 200, w.Code) 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /testing-from-outside/go.mod: -------------------------------------------------------------------------------- 1 | module testing-from-outside 2 | 3 | go 1.24.2 4 | 5 | require ( 6 | github.com/go-fuego/fuego v0.13.4 7 | github.com/rs/cors v1.11.1 8 | github.com/stretchr/testify v1.10.0 9 | ) 10 | 11 | require ( 12 | github.com/davecgh/go-spew v1.1.1 // indirect 13 | github.com/gabriel-vasile/mimetype v1.4.8 // indirect 14 | github.com/getkin/kin-openapi v0.131.0 // indirect 15 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 16 | github.com/go-openapi/swag v0.23.0 // indirect 17 | github.com/go-playground/locales v0.14.1 // indirect 18 | github.com/go-playground/universal-translator v0.18.1 // indirect 19 | github.com/go-playground/validator/v10 v10.24.0 // indirect 20 | github.com/golang-jwt/jwt/v5 v5.2.2 // indirect 21 | github.com/gomarkdown/markdown v0.0.0-20231222211730-1d6d20845b47 // indirect 22 | github.com/gorilla/schema v1.4.1 // indirect 23 | github.com/josharian/intern v1.0.0 // indirect 24 | github.com/leodido/go-urn v1.4.0 // indirect 25 | github.com/mailru/easyjson v0.9.0 // indirect 26 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect 27 | github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect 28 | github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect 29 | github.com/perimeterx/marshmallow v1.1.5 // indirect 30 | github.com/pmezard/go-difflib v1.0.0 // indirect 31 | github.com/ugorji/go/codec v1.2.12 // indirect 32 | golang.org/x/crypto v0.37.0 // indirect 33 | golang.org/x/net v0.39.0 // indirect 34 | golang.org/x/sys v0.32.0 // indirect 35 | golang.org/x/text v0.24.0 // indirect 36 | gopkg.in/yaml.v3 v3.0.1 // indirect 37 | ) 38 | -------------------------------------------------------------------------------- /tests_test.go: -------------------------------------------------------------------------------- 1 | package fuego 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | // Contains random tests reported on the issues. 13 | 14 | func TestContentType(t *testing.T) { 15 | server := NewServer() 16 | 17 | t.Run("Sends application/problem+json when return type is HTTPError", func(t *testing.T) { 18 | GetStd(server, "/json-problems", func(w http.ResponseWriter, r *http.Request) { 19 | SendJSONError(w, nil, UnauthorizedError{ 20 | Title: "Unauthorized", 21 | }) 22 | }) 23 | 24 | req := httptest.NewRequest("GET", "/json-problems", nil) 25 | w := httptest.NewRecorder() 26 | server.Mux.ServeHTTP(w, req) 27 | 28 | require.Equal(t, "application/problem+json", w.Result().Header.Get("Content-Type")) 29 | require.Equal(t, 401, w.Code) 30 | require.Contains(t, w.Body.String(), "Unauthorized") 31 | }) 32 | 33 | t.Run("Sends application/json when return type is not HTTPError", func(t *testing.T) { 34 | GetStd(server, "/json", func(w http.ResponseWriter, r *http.Request) { 35 | SendJSONError(w, nil, errors.New("error")) 36 | }) 37 | 38 | req := httptest.NewRequest("GET", "/json", nil) 39 | w := httptest.NewRecorder() 40 | server.Mux.ServeHTTP(w, req) 41 | 42 | require.Equal(t, "application/json", w.Header().Get("Content-Type")) 43 | require.Equal(t, 500, w.Code) 44 | require.JSONEq(t, "{}\n", w.Body.String()) 45 | }) 46 | } 47 | -------------------------------------------------------------------------------- /validate_params.go: -------------------------------------------------------------------------------- 1 | package fuego 2 | 3 | import "fmt" 4 | 5 | type ValidableCtx interface { 6 | GetOpenAPIParams() map[string]OpenAPIParam 7 | HasQueryParam(key string) bool 8 | HasHeader(key string) bool 9 | HasCookie(key string) bool 10 | } 11 | 12 | // ValidateParams checks if all required parameters are present in the request. 13 | func ValidateParams(c ValidableCtx) error { 14 | for k, param := range c.GetOpenAPIParams() { 15 | if param.Default != nil { 16 | // skip: param has a default 17 | continue 18 | } 19 | 20 | if param.Required { 21 | switch param.Type { 22 | case QueryParamType: 23 | if !c.HasQueryParam(k) { 24 | err := fmt.Errorf("%s is a required query param", k) 25 | return BadRequestError{ 26 | Title: "Query Param Not Found", 27 | Err: err, 28 | Detail: "cannot parse request parameter: " + err.Error(), 29 | } 30 | } 31 | case HeaderParamType: 32 | if !c.HasHeader(k) { 33 | err := fmt.Errorf("%s is a required header", k) 34 | return BadRequestError{ 35 | Title: "Header Not Found", 36 | Err: err, 37 | Detail: "cannot parse request parameter: " + err.Error(), 38 | } 39 | } 40 | case CookieParamType: 41 | if !c.HasCookie(k) { 42 | err := fmt.Errorf("%s is a required cookie", k) 43 | return BadRequestError{ 44 | Title: "Cookie Not Found", 45 | Err: err, 46 | Detail: "cannot parse request parameter: " + err.Error(), 47 | } 48 | } 49 | } 50 | } 51 | } 52 | 53 | return nil 54 | } 55 | -------------------------------------------------------------------------------- /validate_params_test.go: -------------------------------------------------------------------------------- 1 | package fuego_test 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | 10 | "github.com/go-fuego/fuego" 11 | "github.com/go-fuego/fuego/option" 12 | "github.com/go-fuego/fuego/param" 13 | ) 14 | 15 | func TestParamsValidation(t *testing.T) { 16 | t.Run("Should enforce Required query parameter", func(t *testing.T) { 17 | s := fuego.NewServer() 18 | 19 | fuego.Get(s, "/test", dummyController, 20 | option.Query("name", "Name", param.Required(), param.Example("example1", "you")), 21 | ) 22 | r := httptest.NewRequest("GET", "/test", nil) 23 | w := httptest.NewRecorder() 24 | s.Mux.ServeHTTP(w, r) 25 | require.Equal(t, http.StatusBadRequest, w.Code) 26 | require.Contains(t, w.Body.String(), "name is a required query param") 27 | }) 28 | 29 | t.Run("Should enforce Required header", func(t *testing.T) { 30 | s := fuego.NewServer() 31 | 32 | fuego.Get(s, "/test", dummyController, 33 | option.Header("foo", "header that is foo", param.Required()), 34 | ) 35 | r := httptest.NewRequest("GET", "/test", nil) 36 | w := httptest.NewRecorder() 37 | s.Mux.ServeHTTP(w, r) 38 | require.Equal(t, http.StatusBadRequest, w.Code) 39 | require.Contains(t, w.Body.String(), "foo is a required header") 40 | }) 41 | 42 | t.Run("Should enforce Required cookie", func(t *testing.T) { 43 | s := fuego.NewServer() 44 | 45 | fuego.Get(s, "/test", dummyController, 46 | option.Cookie("bar", "cookie that is bar", param.Required()), 47 | ) 48 | r := httptest.NewRequest("GET", "/test", nil) 49 | w := httptest.NewRecorder() 50 | s.Mux.ServeHTTP(w, r) 51 | require.Equal(t, http.StatusBadRequest, w.Code) 52 | require.Contains(t, w.Body.String(), "bar is a required cookie") 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /validation_test.go: -------------------------------------------------------------------------------- 1 | package fuego 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | type validatableStruct struct { 11 | Name string `validate:"required,min=3,max=10"` 12 | Age int `validate:"min=18"` 13 | Required string `validate:"required"` 14 | Email string `validate:"email"` 15 | ExternalID string `validate:"uuid"` 16 | } 17 | 18 | func TestValidate(t *testing.T) { 19 | me := validatableStruct{ 20 | Name: "Napoleon Bonaparte", 21 | Age: 12, 22 | Email: "napoleon.bonaparte", 23 | } 24 | 25 | err := validate(me) 26 | t.Log(err) 27 | require.Error(t, err) 28 | 29 | var errStructValidation HTTPError 30 | require.ErrorAs(t, err, &errStructValidation) 31 | assert.Equal(t, 400, errStructValidation.Status) 32 | assert.Equal(t, "Validation Error", errStructValidation.Title) 33 | assert.Len(t, errStructValidation.Errors, 5) 34 | assert.Equal(t, "400 Validation Error (Name should be max=10, Age should be min=18, Required is required, Email should be a valid email, ExternalID should be a valid UUID)", errStructValidation.PublicError()) 35 | assert.EqualError(t, errStructValidation, `400 Validation Error (Name should be max=10, Age should be min=18, Required is required, Email should be a valid email, ExternalID should be a valid UUID): Key: 'validatableStruct.Name' Error:Field validation for 'Name' failed on the 'max' tag 36 | Key: 'validatableStruct.Age' Error:Field validation for 'Age' failed on the 'min' tag 37 | Key: 'validatableStruct.Required' Error:Field validation for 'Required' failed on the 'required' tag 38 | Key: 'validatableStruct.Email' Error:Field validation for 'Email' failed on the 'email' tag 39 | Key: 'validatableStruct.ExternalID' Error:Field validation for 'ExternalID' failed on the 'uuid' tag`) 40 | } 41 | --------------------------------------------------------------------------------