Repository: kataras/iris Branch: main Commit: 4291456991a5 Files: 1261 Total size: 4.9 MB Directory structure: gitextract_sx40lk7k/ ├── .deepsource.toml ├── .fossa.yml ├── .gitattributes ├── .github/ │ ├── CODEOWNERS │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ ├── custom.md │ │ └── feature_request.md │ ├── ISSUE_TEMPLATE.md │ ├── PULL_REQUEST_TEMPLATE.md │ ├── dependabot.yml │ ├── scripts/ │ │ └── setup_examples_test.bash │ └── workflows/ │ ├── ci.yml │ └── codeql-analysis.yml ├── .gitignore ├── AUTHORS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── FAQ.md ├── HISTORY.md ├── LICENSE ├── NOTICE ├── README.md ├── README_AR.md ├── README_ES.md ├── README_FA.md ├── README_FR.md ├── README_GR.md ├── README_JA.md ├── README_KO.md ├── README_PT_BR.md ├── README_RU.md ├── README_VN.md ├── README_ZH.md ├── README_ZH_HANS.md ├── README_ZH_HANT.md ├── SECURITY.md ├── VERSION ├── _benchmarks/ │ ├── README.md │ └── view/ │ ├── README.md │ ├── ace/ │ │ ├── main.go │ │ └── views/ │ │ ├── index.ace │ │ ├── layouts/ │ │ │ └── main.ace │ │ └── partials/ │ │ └── footer.ace │ ├── blocks/ │ │ ├── main.go │ │ └── views/ │ │ ├── index.html │ │ ├── layouts/ │ │ │ └── main.html │ │ └── partials/ │ │ └── footer.html │ ├── django/ │ │ ├── main.go │ │ └── views/ │ │ ├── index.html │ │ ├── layouts/ │ │ │ └── main.html │ │ └── partials/ │ │ └── footer.html │ ├── handlebars/ │ │ ├── main.go │ │ └── views/ │ │ ├── index.html │ │ ├── layouts/ │ │ │ └── main.html │ │ └── partials/ │ │ └── footer.html │ ├── html/ │ │ ├── main.go │ │ └── views/ │ │ ├── index.html │ │ ├── layouts/ │ │ │ └── main.html │ │ └── partials/ │ │ └── footer.html │ ├── jet/ │ │ ├── main.go │ │ └── views/ │ │ ├── index.jet │ │ ├── layouts/ │ │ │ └── main.jet │ │ └── partials/ │ │ └── footer.jet │ ├── pug/ │ │ ├── main.go │ │ └── views/ │ │ ├── index.pug │ │ ├── layouts/ │ │ │ └── main.pug │ │ └── partials/ │ │ └── footer.pug │ └── tests.yml ├── _examples/ │ ├── README.md │ ├── README_ZH_HANT.md │ ├── apidoc/ │ │ └── swagger/ │ │ └── README.md │ ├── auth/ │ │ ├── auth/ │ │ │ ├── README.md │ │ │ ├── auth.yml │ │ │ ├── main.go │ │ │ ├── user.go │ │ │ ├── user_provider.go │ │ │ └── views/ │ │ │ ├── layouts/ │ │ │ │ └── main.html │ │ │ ├── partials/ │ │ │ │ └── footer.html │ │ │ └── signin.html │ │ ├── basicauth/ │ │ │ ├── basic/ │ │ │ │ ├── main.go │ │ │ │ └── main_test.go │ │ │ ├── database/ │ │ │ │ ├── Dockerfile │ │ │ │ ├── README.md │ │ │ │ ├── docker-compose.yml │ │ │ │ ├── go.mod │ │ │ │ ├── go.sum │ │ │ │ ├── main.go │ │ │ │ └── migration/ │ │ │ │ └── db.sql │ │ │ ├── users_file_bcrypt/ │ │ │ │ ├── main.go │ │ │ │ └── users.yml │ │ │ └── users_list/ │ │ │ └── main.go │ │ ├── cors/ │ │ │ ├── main.go │ │ │ └── public/ │ │ │ ├── index.html │ │ │ └── main.js │ │ ├── goth/ │ │ │ ├── main.go │ │ │ └── templates/ │ │ │ ├── index.html │ │ │ └── user.html │ │ ├── hcaptcha/ │ │ │ ├── hosts │ │ │ ├── main.go │ │ │ └── templates/ │ │ │ └── register_form.html │ │ ├── jwt/ │ │ │ ├── basic/ │ │ │ │ └── main.go │ │ │ ├── blocklist/ │ │ │ │ └── main.go │ │ │ ├── middleware/ │ │ │ │ └── main.go │ │ │ ├── refresh-token/ │ │ │ │ ├── main.go │ │ │ │ ├── rsa_private_key.pem │ │ │ │ └── rsa_public_key.pem │ │ │ └── tutorial/ │ │ │ ├── README.md │ │ │ ├── api/ │ │ │ │ ├── auth.go │ │ │ │ ├── router.go │ │ │ │ └── todo.go │ │ │ ├── domain/ │ │ │ │ ├── model/ │ │ │ │ │ ├── role.go │ │ │ │ │ ├── todo.go │ │ │ │ │ └── user.go │ │ │ │ └── repository/ │ │ │ │ ├── samples.go │ │ │ │ ├── todo_repository.go │ │ │ │ └── user_repository.go │ │ │ ├── go-client/ │ │ │ │ ├── README.md │ │ │ │ ├── client.go │ │ │ │ └── main.go │ │ │ ├── go.mod │ │ │ ├── go.sum │ │ │ ├── main.go │ │ │ └── util/ │ │ │ ├── app.go │ │ │ ├── clock.go │ │ │ ├── password.go │ │ │ └── uuid.go │ │ ├── permissions/ │ │ │ └── main.go │ │ └── recaptcha/ │ │ ├── custom_form/ │ │ │ └── main.go │ │ └── main.go │ ├── bootstrapper/ │ │ ├── bootstrap/ │ │ │ └── bootstrapper.go │ │ ├── go.mod │ │ ├── go.sum │ │ ├── main.go │ │ ├── main_test.go │ │ ├── middleware/ │ │ │ └── identity/ │ │ │ └── identity.go │ │ ├── routes/ │ │ │ ├── follower.go │ │ │ ├── following.go │ │ │ ├── index.go │ │ │ ├── like.go │ │ │ └── routes.go │ │ └── views/ │ │ ├── index.html │ │ └── shared/ │ │ ├── error.html │ │ └── layout.html │ ├── caddy/ │ │ ├── Caddyfile │ │ ├── README.md │ │ ├── server1/ │ │ │ ├── main.go │ │ │ └── views/ │ │ │ ├── index.html │ │ │ └── shared/ │ │ │ └── layout.html │ │ └── server2/ │ │ └── main.go │ ├── compression/ │ │ ├── client/ │ │ │ └── main.go │ │ ├── client-using-iris/ │ │ │ └── main.go │ │ ├── main.go │ │ └── main_test.go │ ├── configuration/ │ │ ├── from-configuration-structure/ │ │ │ └── main.go │ │ ├── from-toml-file/ │ │ │ ├── configs/ │ │ │ │ └── iris.tml │ │ │ └── main.go │ │ ├── from-yaml-file/ │ │ │ ├── configs/ │ │ │ │ └── iris.yml │ │ │ ├── main.go │ │ │ └── shared-configuration/ │ │ │ └── main.go │ │ ├── functional/ │ │ │ └── main.go │ │ ├── multi-environments/ │ │ │ ├── README.md │ │ │ ├── api/ │ │ │ │ ├── configuration.go │ │ │ │ └── server.go │ │ │ ├── cmd/ │ │ │ │ └── root_command.go │ │ │ ├── go.mod │ │ │ ├── go.sum │ │ │ ├── main.go │ │ │ ├── server.dev.yml │ │ │ └── server.yml │ │ └── viper/ │ │ ├── config/ │ │ │ └── config.go │ │ ├── config.yml │ │ ├── go.mod │ │ ├── go.sum │ │ └── main.go │ ├── convert-handlers/ │ │ ├── negroni-like/ │ │ │ └── main.go │ │ ├── nethttp/ │ │ │ ├── main.go │ │ │ └── wrapper/ │ │ │ └── main.go │ │ └── real-usecase-raven/ │ │ ├── wrapping-the-router/ │ │ │ └── main.go │ │ └── writing-middleware/ │ │ └── main.go │ ├── cookies/ │ │ ├── basic/ │ │ │ ├── main.go │ │ │ └── main_test.go │ │ ├── options/ │ │ │ ├── main.go │ │ │ └── main_test.go │ │ └── securecookie/ │ │ ├── main.go │ │ └── main_test.go │ ├── database/ │ │ ├── mongodb/ │ │ │ ├── Dockerfile │ │ │ ├── README.md │ │ │ ├── api/ │ │ │ │ └── store/ │ │ │ │ └── movie.go │ │ │ ├── docker-compose.yml │ │ │ ├── env/ │ │ │ │ └── env.go │ │ │ ├── go.mod │ │ │ ├── go.sum │ │ │ ├── httputil/ │ │ │ │ └── error.go │ │ │ ├── main.go │ │ │ └── store/ │ │ │ └── movie.go │ │ ├── mysql/ │ │ │ ├── Dockerfile │ │ │ ├── README.md │ │ │ ├── api/ │ │ │ │ ├── api.go │ │ │ │ ├── category_handler.go │ │ │ │ ├── helper.go │ │ │ │ ├── httperror.go │ │ │ │ ├── middleware/ │ │ │ │ │ └── .gitkeep │ │ │ │ └── product_handler.go │ │ │ ├── cache/ │ │ │ │ └── groupcache.go │ │ │ ├── docker-compose.yml │ │ │ ├── entity/ │ │ │ │ ├── category.go │ │ │ │ └── product.go │ │ │ ├── go.mod │ │ │ ├── go.sum │ │ │ ├── main.go │ │ │ ├── migration/ │ │ │ │ ├── api_category/ │ │ │ │ │ ├── create_category.json │ │ │ │ │ ├── insert_products_category.json │ │ │ │ │ ├── update_category.json │ │ │ │ │ └── update_partial_category.json │ │ │ │ ├── api_postman.json │ │ │ │ ├── api_product/ │ │ │ │ │ ├── create_product.json │ │ │ │ │ ├── update_partial_product.json │ │ │ │ │ └── update_product.json │ │ │ │ └── db.sql │ │ │ ├── service/ │ │ │ │ ├── category_service.go │ │ │ │ ├── category_service_test.go │ │ │ │ └── product_service.go │ │ │ └── sql/ │ │ │ ├── mysql.go │ │ │ ├── service.go │ │ │ └── sql.go │ │ ├── orm/ │ │ │ ├── gorm/ │ │ │ │ ├── REAMDE.md │ │ │ │ └── main.go │ │ │ ├── reform/ │ │ │ │ ├── controllers/ │ │ │ │ │ └── person_controller.go │ │ │ │ ├── go.mod │ │ │ │ ├── go.sum │ │ │ │ ├── main.go │ │ │ │ ├── models/ │ │ │ │ │ ├── person.go │ │ │ │ │ └── person_reform.go │ │ │ │ └── postman_collection.json │ │ │ └── sqlx/ │ │ │ └── main.go │ │ └── sqlx/ │ │ └── main.go │ ├── dependency-injection/ │ │ ├── basic/ │ │ │ ├── main.go │ │ │ └── middleware/ │ │ │ ├── main.go │ │ │ └── main_test.go │ │ ├── context-register-dependency/ │ │ │ └── main.go │ │ ├── jwt/ │ │ │ ├── contrib/ │ │ │ │ ├── go.mod │ │ │ │ ├── go.sum │ │ │ │ └── main.go │ │ │ └── main.go │ │ ├── overview/ │ │ │ ├── datamodels/ │ │ │ │ └── movie.go │ │ │ ├── datasource/ │ │ │ │ └── movies.go │ │ │ ├── main.go │ │ │ ├── repositories/ │ │ │ │ └── movie_repository.go │ │ │ ├── services/ │ │ │ │ └── movie_service.go │ │ │ └── web/ │ │ │ ├── middleware/ │ │ │ │ └── basicauth.go │ │ │ ├── routes/ │ │ │ │ ├── hello.go │ │ │ │ └── movies.go │ │ │ └── views/ │ │ │ └── hello/ │ │ │ ├── index.html │ │ │ └── name.html │ │ ├── sessions/ │ │ │ ├── main.go │ │ │ └── routes/ │ │ │ └── index.go │ │ └── smart-contract/ │ │ └── main.go │ ├── desktop/ │ │ ├── blink/ │ │ │ └── main.go │ │ ├── lorca/ │ │ │ └── main.go │ │ └── webview/ │ │ ├── go.mod │ │ ├── go.sum │ │ └── main.go │ ├── dropzonejs/ │ │ ├── README.md │ │ ├── README_PART2.md │ │ ├── meta.yml │ │ └── src/ │ │ ├── main.go │ │ ├── public/ │ │ │ ├── css/ │ │ │ │ └── dropzone.css │ │ │ └── js/ │ │ │ └── dropzone.js │ │ └── views/ │ │ └── upload.html │ ├── file-server/ │ │ ├── basic/ │ │ │ ├── assets/ │ │ │ │ ├── app2/ │ │ │ │ │ ├── app22/ │ │ │ │ │ │ └── just_a_text_no_index.txt │ │ │ │ │ ├── app2app3/ │ │ │ │ │ │ └── index.html │ │ │ │ │ └── index.html │ │ │ │ ├── css/ │ │ │ │ │ └── main.css │ │ │ │ ├── index.html │ │ │ │ └── js/ │ │ │ │ └── main.js │ │ │ ├── assets.system/ │ │ │ │ ├── css/ │ │ │ │ │ └── main.css │ │ │ │ └── test.txt │ │ │ ├── main.go │ │ │ └── main_test.go │ │ ├── embedding-files-into-app/ │ │ │ ├── assets/ │ │ │ │ ├── css/ │ │ │ │ │ └── main.css │ │ │ │ └── js/ │ │ │ │ └── main.js │ │ │ ├── main.go │ │ │ └── main_test.go │ │ ├── embedding-files-into-app-bindata/ │ │ │ ├── assets/ │ │ │ │ ├── css/ │ │ │ │ │ └── main.css │ │ │ │ └── js/ │ │ │ │ └── main.js │ │ │ ├── bindata.go │ │ │ ├── main.go │ │ │ └── main_test.go │ │ ├── embedding-gzipped-files-into-app-bindata/ │ │ │ ├── bindata.go │ │ │ ├── main.go │ │ │ └── main_test.go │ │ ├── favicon/ │ │ │ └── main.go │ │ ├── file-server/ │ │ │ ├── main.go │ │ │ └── views/ │ │ │ ├── dirlist.html │ │ │ └── upload.html │ │ ├── http2push/ │ │ │ ├── assets/ │ │ │ │ ├── app2/ │ │ │ │ │ ├── app2app3/ │ │ │ │ │ │ ├── css/ │ │ │ │ │ │ │ └── main.css │ │ │ │ │ │ ├── dirs/ │ │ │ │ │ │ │ ├── dir1/ │ │ │ │ │ │ │ │ └── text.txt │ │ │ │ │ │ │ ├── dir2/ │ │ │ │ │ │ │ │ └── text.txt │ │ │ │ │ │ │ └── text.txt │ │ │ │ │ │ └── index.html │ │ │ │ │ ├── index.html │ │ │ │ │ └── mydir/ │ │ │ │ │ └── text.txt │ │ │ │ ├── css/ │ │ │ │ │ └── main.css │ │ │ │ ├── index.html │ │ │ │ └── js/ │ │ │ │ └── main.js │ │ │ ├── main.go │ │ │ ├── mycert.crt │ │ │ └── mykey.key │ │ ├── http2push-embedded/ │ │ │ ├── bindata.go │ │ │ └── main.go │ │ ├── http2push-embedded-gzipped/ │ │ │ ├── bindata.go │ │ │ └── main.go │ │ ├── send-files/ │ │ │ └── main.go │ │ ├── single-page-application/ │ │ │ ├── basic/ │ │ │ │ ├── main.go │ │ │ │ └── public/ │ │ │ │ ├── index.html │ │ │ │ └── index.js │ │ │ ├── embedded-single-page-application/ │ │ │ │ ├── bindata.go │ │ │ │ ├── data/ │ │ │ │ │ ├── public/ │ │ │ │ │ │ ├── app.js │ │ │ │ │ │ ├── app2/ │ │ │ │ │ │ │ └── index.html │ │ │ │ │ │ └── css/ │ │ │ │ │ │ └── main.css │ │ │ │ │ └── views/ │ │ │ │ │ └── index.html │ │ │ │ ├── main.go │ │ │ │ └── main_test.go │ │ │ └── embedded-single-page-application-with-other-routes/ │ │ │ ├── bindata.go │ │ │ ├── main.go │ │ │ └── public/ │ │ │ ├── app.js │ │ │ ├── css/ │ │ │ │ └── main.css │ │ │ └── index.html │ │ ├── spa-vue-router/ │ │ │ ├── frontend/ │ │ │ │ ├── css/ │ │ │ │ │ └── page.css │ │ │ │ ├── index.html │ │ │ │ └── js/ │ │ │ │ └── app.js │ │ │ └── main.go │ │ ├── subdomain/ │ │ │ ├── assets/ │ │ │ │ ├── app2/ │ │ │ │ │ ├── app22/ │ │ │ │ │ │ └── just_a_text_no_index.txt │ │ │ │ │ ├── app2app3/ │ │ │ │ │ │ └── index.html │ │ │ │ │ └── index.html │ │ │ │ ├── css/ │ │ │ │ │ └── main.css │ │ │ │ ├── index.html │ │ │ │ └── js/ │ │ │ │ └── jquery-2.1.1.js │ │ │ ├── hosts │ │ │ ├── main.go │ │ │ └── main_test.go │ │ ├── upload-file/ │ │ │ ├── main.go │ │ │ └── templates/ │ │ │ └── upload_form.html │ │ ├── upload-files/ │ │ │ ├── main.go │ │ │ ├── main_test.go │ │ │ └── templates/ │ │ │ └── upload_form.html │ │ └── webdav/ │ │ ├── main.go │ │ ├── newdir/ │ │ │ └── .gitkeep │ │ └── test.txt │ ├── graphql/ │ │ └── schema-first/ │ │ ├── README.md │ │ ├── go.mod │ │ ├── go.sum │ │ ├── gqlgen.yml │ │ ├── graph/ │ │ │ ├── generated.go │ │ │ ├── model/ │ │ │ │ └── models_gen.go │ │ │ ├── resolver.go │ │ │ ├── schema.graphqls │ │ │ └── schema.resolvers.go │ │ ├── server.go │ │ └── tools.go │ ├── http-client/ │ │ └── weatherapi/ │ │ ├── client/ │ │ │ ├── client.go │ │ │ └── response.go │ │ └── main.go │ ├── http-server/ │ │ ├── README.md │ │ ├── custom-httpserver/ │ │ │ ├── easy-way/ │ │ │ │ └── main.go │ │ │ ├── multi/ │ │ │ │ └── main.go │ │ │ └── std-way/ │ │ │ └── main.go │ │ ├── custom-listener/ │ │ │ └── main.go │ │ ├── graceful-shutdown/ │ │ │ ├── custom-notifier/ │ │ │ │ └── main.go │ │ │ └── default-notifier/ │ │ │ └── main.go │ │ ├── h2c/ │ │ │ ├── go.mod │ │ │ ├── go.sum │ │ │ └── main.go │ │ ├── http3-quic/ │ │ │ ├── go.mod │ │ │ ├── go.sum │ │ │ ├── localhost.cert │ │ │ ├── localhost.key │ │ │ └── main.go │ │ ├── iris-configurator-and-host-configurator/ │ │ │ └── main.go │ │ ├── listen-addr/ │ │ │ ├── main.go │ │ │ └── omit-server-errors/ │ │ │ ├── main.go │ │ │ └── main_test.go │ │ ├── listen-addr-public/ │ │ │ └── main.go │ │ ├── listen-letsencrypt/ │ │ │ └── main.go │ │ ├── listen-tls/ │ │ │ ├── main.go │ │ │ ├── mycert.crt │ │ │ └── mykey.key │ │ ├── listen-unix/ │ │ │ └── main.go │ │ ├── notify-on-shutdown/ │ │ │ └── main.go │ │ ├── socket-sharding/ │ │ │ └── main.go │ │ └── timeout/ │ │ └── main.go │ ├── i18n/ │ │ ├── basic/ │ │ │ ├── hosts │ │ │ ├── locales/ │ │ │ │ ├── el-GR/ │ │ │ │ │ ├── locale_el-GR.ini │ │ │ │ │ ├── locale_multi_first_el-GR.yml │ │ │ │ │ └── locale_multi_second_el-GR.ini │ │ │ │ ├── en-US/ │ │ │ │ │ ├── locale_en-US.ini │ │ │ │ │ ├── locale_multi_first_en-US.yml │ │ │ │ │ └── locale_multi_second_en-US.ini │ │ │ │ └── zh-CN/ │ │ │ │ └── locale_zh-CN.ini │ │ │ ├── main.go │ │ │ ├── main_test.go │ │ │ └── views/ │ │ │ └── index.html │ │ ├── plurals/ │ │ │ ├── locales/ │ │ │ │ └── en-US/ │ │ │ │ ├── 1648.ini │ │ │ │ └── welcome.yml │ │ │ ├── main.go │ │ │ └── main_test.go │ │ ├── template/ │ │ │ ├── locales/ │ │ │ │ ├── el-GR/ │ │ │ │ │ ├── other.ini │ │ │ │ │ └── user.ini │ │ │ │ └── en-US/ │ │ │ │ ├── other.ini │ │ │ │ └── user.ini │ │ │ ├── main.go │ │ │ └── main_test.go │ │ └── template-embedded/ │ │ ├── embedded/ │ │ │ └── locales/ │ │ │ ├── el-GR/ │ │ │ │ ├── other.ini │ │ │ │ └── user.ini │ │ │ └── en-US/ │ │ │ ├── other.ini │ │ │ └── user.ini │ │ ├── main.go │ │ └── main_test.go │ ├── kafka-api/ │ │ ├── Dockerfile │ │ ├── README.md │ │ ├── docker-compose.yml │ │ ├── go.mod │ │ ├── go.sum │ │ ├── main.go │ │ └── postman_collection.json │ ├── logging/ │ │ ├── file-logger/ │ │ │ └── main.go │ │ ├── json-logger/ │ │ │ ├── main.go │ │ │ └── main_test.go │ │ ├── request-logger/ │ │ │ ├── accesslog/ │ │ │ │ ├── access.log.sample │ │ │ │ ├── main.go │ │ │ │ └── public/ │ │ │ │ └── index.html │ │ │ ├── accesslog-broker/ │ │ │ │ └── main.go │ │ │ ├── accesslog-csv/ │ │ │ │ ├── access_log.csv.sample │ │ │ │ └── main.go │ │ │ ├── accesslog-formatter/ │ │ │ │ └── main.go │ │ │ ├── accesslog-proxy/ │ │ │ │ ├── main.go │ │ │ │ └── target/ │ │ │ │ └── main.go │ │ │ ├── accesslog-simple/ │ │ │ │ ├── access.log.sample │ │ │ │ └── main.go │ │ │ ├── accesslog-slack/ │ │ │ │ ├── go.mod │ │ │ │ ├── go.sum │ │ │ │ ├── main.go │ │ │ │ └── slack_formatter.go │ │ │ └── accesslog-template/ │ │ │ └── main.go │ │ └── rollbar/ │ │ ├── go.mod │ │ ├── go.sum │ │ └── main.go │ ├── monitor/ │ │ ├── monitor-middleware/ │ │ │ └── main.go │ │ └── statsviz/ │ │ ├── go.mod │ │ ├── go.sum │ │ └── main.go │ ├── mvc/ │ │ ├── authenticated-controller/ │ │ │ ├── main.go │ │ │ └── main_test.go │ │ ├── basic/ │ │ │ ├── main.go │ │ │ └── wildcard/ │ │ │ └── main.go │ │ ├── error-handler/ │ │ │ └── main.go │ │ ├── error-handler-custom-result/ │ │ │ ├── main.go │ │ │ └── views/ │ │ │ ├── 404.html │ │ │ └── 500.html │ │ ├── error-handler-hijack/ │ │ │ ├── main.go │ │ │ └── views/ │ │ │ ├── 404.html │ │ │ └── 500.html │ │ ├── error-handler-http/ │ │ │ ├── main.go │ │ │ ├── main_test.go │ │ │ └── views/ │ │ │ ├── 404.html │ │ │ ├── 500.html │ │ │ └── unexpected-error.html │ │ ├── error-handler-preflight/ │ │ │ ├── main.go │ │ │ └── views/ │ │ │ ├── 404.html │ │ │ └── 500.html │ │ ├── grpc-compatible/ │ │ │ ├── README.md │ │ │ ├── grpc-client/ │ │ │ │ └── main.go │ │ │ ├── helloworld/ │ │ │ │ ├── README.md │ │ │ │ ├── helloworld.pb.go │ │ │ │ └── helloworld.proto │ │ │ ├── http-client/ │ │ │ │ └── main.go │ │ │ ├── main.go │ │ │ ├── main_test.go │ │ │ ├── server.crt │ │ │ └── server.key │ │ ├── grpc-compatible-bidirectional-stream/ │ │ │ ├── client/ │ │ │ │ └── main.go │ │ │ ├── go.mod │ │ │ ├── go.sum │ │ │ ├── helloworld/ │ │ │ │ ├── helloworld.pb.go │ │ │ │ └── helloworld_grpc.pb.go │ │ │ ├── helloworld.proto │ │ │ ├── server/ │ │ │ │ └── main.go │ │ │ ├── server.crt │ │ │ └── server.key │ │ ├── hello-world/ │ │ │ ├── main.go │ │ │ └── main_test.go │ │ ├── login/ │ │ │ ├── datamodels/ │ │ │ │ └── user.go │ │ │ ├── datasource/ │ │ │ │ └── users.go │ │ │ ├── main.go │ │ │ ├── repositories/ │ │ │ │ └── user_repository.go │ │ │ ├── services/ │ │ │ │ └── user_service.go │ │ │ └── web/ │ │ │ ├── controllers/ │ │ │ │ ├── user_controller.go │ │ │ │ └── users_controller.go │ │ │ ├── middleware/ │ │ │ │ └── basicauth.go │ │ │ ├── public/ │ │ │ │ └── css/ │ │ │ │ └── site.css │ │ │ ├── viewmodels/ │ │ │ │ └── README.md │ │ │ └── views/ │ │ │ ├── shared/ │ │ │ │ ├── error.html │ │ │ │ └── layout.html │ │ │ └── user/ │ │ │ ├── login.html │ │ │ ├── me.html │ │ │ └── register.html │ │ ├── login-mvc-single-responsibility/ │ │ │ ├── main.go │ │ │ ├── public/ │ │ │ │ └── css/ │ │ │ │ └── site.css │ │ │ ├── user/ │ │ │ │ ├── auth.go │ │ │ │ ├── controller.go │ │ │ │ ├── datasource.go │ │ │ │ └── model.go │ │ │ └── views/ │ │ │ ├── shared/ │ │ │ │ ├── error.html │ │ │ │ └── layout.html │ │ │ └── user/ │ │ │ ├── login.html │ │ │ ├── me.html │ │ │ ├── notfound.html │ │ │ └── register.html │ │ ├── middleware/ │ │ │ ├── main.go │ │ │ ├── per-method/ │ │ │ │ └── main.go │ │ │ └── without-ctx-next/ │ │ │ └── main.go │ │ ├── overview/ │ │ │ ├── Dockerfile │ │ │ ├── README.md │ │ │ ├── controller/ │ │ │ │ └── greet_controller.go │ │ │ ├── database/ │ │ │ │ ├── database.go │ │ │ │ ├── mysql.go │ │ │ │ └── sqlite.go │ │ │ ├── docker-compose.yml │ │ │ ├── environment/ │ │ │ │ └── environment.go │ │ │ ├── go.mod │ │ │ ├── go.sum │ │ │ ├── main.go │ │ │ ├── model/ │ │ │ │ ├── request.go │ │ │ │ └── response.go │ │ │ └── service/ │ │ │ └── greet_service.go │ │ ├── regexp/ │ │ │ └── main.go │ │ ├── repository/ │ │ │ ├── datamodels/ │ │ │ │ ├── README.md │ │ │ │ └── movie.go │ │ │ ├── datasource/ │ │ │ │ ├── README.md │ │ │ │ └── movies.go │ │ │ ├── main.go │ │ │ ├── models/ │ │ │ │ └── README.md │ │ │ ├── repositories/ │ │ │ │ ├── README.md │ │ │ │ └── movie_repository.go │ │ │ ├── services/ │ │ │ │ ├── README.md │ │ │ │ └── movie_service.go │ │ │ └── web/ │ │ │ ├── controllers/ │ │ │ │ ├── hello_controller.go │ │ │ │ └── movie_controller.go │ │ │ ├── middleware/ │ │ │ │ └── basicauth.go │ │ │ ├── viewmodels/ │ │ │ │ └── README.md │ │ │ └── views/ │ │ │ └── hello/ │ │ │ ├── index.html │ │ │ └── name.html │ │ ├── request-default-values/ │ │ │ └── main.go │ │ ├── session-controller/ │ │ │ ├── main.go │ │ │ └── main_test.go │ │ ├── singleton/ │ │ │ └── main.go │ │ ├── versioned-controller/ │ │ │ ├── main.go │ │ │ └── main_test.go │ │ ├── vuejs-todo-mvc/ │ │ │ ├── README.md │ │ │ └── src/ │ │ │ ├── go.mod │ │ │ ├── go.sum │ │ │ ├── todo/ │ │ │ │ ├── item.go │ │ │ │ └── service.go │ │ │ └── web/ │ │ │ ├── controllers/ │ │ │ │ └── todo_controller.go │ │ │ ├── main.go │ │ │ └── public/ │ │ │ ├── css/ │ │ │ │ └── index │ │ │ ├── index.html │ │ │ └── js/ │ │ │ ├── app.js │ │ │ └── lib/ │ │ │ └── .gitkeep │ │ ├── websocket/ │ │ │ ├── browser/ │ │ │ │ └── index.html │ │ │ └── main.go │ │ └── websocket-auth/ │ │ ├── auth.yml │ │ ├── browser/ │ │ │ └── index.html │ │ ├── main.go │ │ ├── user.go │ │ ├── user_provider.go │ │ └── views/ │ │ ├── layouts/ │ │ │ └── main.html │ │ ├── partials/ │ │ │ └── footer.html │ │ └── signin.html │ ├── pprof/ │ │ └── main.go │ ├── project/ │ │ ├── README.md │ │ ├── api/ │ │ │ ├── configuration.go │ │ │ ├── router.go │ │ │ ├── server.go │ │ │ └── users/ │ │ │ └── api.go │ │ ├── cmd/ │ │ │ ├── cmd.go │ │ │ └── help.go │ │ ├── go.mod │ │ ├── go.sum │ │ ├── main.go │ │ ├── pkg/ │ │ │ ├── database/ │ │ │ │ └── database.go │ │ │ └── http/ │ │ │ └── handlers/ │ │ │ └── cors.go │ │ ├── server.yml │ │ └── user/ │ │ ├── repository.go │ │ └── user.go │ ├── recover/ │ │ ├── main.go │ │ └── panic-and-custom-error-handler-with-compression/ │ │ └── main.go │ ├── request-body/ │ │ ├── form-query-headers-params-decoder/ │ │ │ └── main.go │ │ ├── read-body/ │ │ │ ├── main.go │ │ │ └── main_test.go │ │ ├── read-custom-per-type/ │ │ │ ├── main.go │ │ │ └── main_test.go │ │ ├── read-custom-via-unmarshaler/ │ │ │ ├── main.go │ │ │ └── main_test.go │ │ ├── read-form/ │ │ │ ├── checkboxes/ │ │ │ │ ├── main.go │ │ │ │ └── templates/ │ │ │ │ └── form.html │ │ │ ├── main.go │ │ │ └── templates/ │ │ │ └── form.html │ │ ├── read-headers/ │ │ │ ├── main.go │ │ │ └── main_test.go │ │ ├── read-json/ │ │ │ └── main.go │ │ ├── read-json-stream/ │ │ │ └── main.go │ │ ├── read-json-struct-validation/ │ │ │ └── main.go │ │ ├── read-many/ │ │ │ └── main.go │ │ ├── read-msgpack/ │ │ │ └── main.go │ │ ├── read-params/ │ │ │ ├── main.go │ │ │ └── main_test.go │ │ ├── read-query/ │ │ │ └── main.go │ │ ├── read-url/ │ │ │ ├── main.go │ │ │ └── main_test.go │ │ ├── read-xml/ │ │ │ ├── main.go │ │ │ └── main_test.go │ │ └── read-yaml/ │ │ ├── main.go │ │ └── main_test.go │ ├── request-ratelimit/ │ │ ├── rate-middleware/ │ │ │ └── main.go │ │ └── ulule-limiter/ │ │ └── main.go │ ├── request-referrer/ │ │ └── main.go │ ├── response-writer/ │ │ ├── cache/ │ │ │ ├── client-side/ │ │ │ │ └── main.go │ │ │ └── simple/ │ │ │ └── main.go │ │ ├── content-negotiation/ │ │ │ ├── main.go │ │ │ └── main_test.go │ │ ├── http2push/ │ │ │ ├── main.go │ │ │ ├── mycert.crt │ │ │ ├── mykey.key │ │ │ └── public/ │ │ │ └── main.js │ │ ├── json-third-party/ │ │ │ ├── go.mod │ │ │ ├── go.sum │ │ │ └── main.go │ │ ├── protobuf/ │ │ │ ├── README.md │ │ │ ├── go.mod │ │ │ ├── go.sum │ │ │ ├── main.go │ │ │ └── protos/ │ │ │ ├── hello.pb.go │ │ │ └── hello.proto │ │ ├── sse/ │ │ │ ├── main.go │ │ │ └── optional.sse.mini.js.html │ │ ├── sse-third-party/ │ │ │ └── main.go │ │ ├── sse-third-party-2/ │ │ │ ├── index.html │ │ │ └── main.go │ │ ├── stream-writer/ │ │ │ └── main.go │ │ └── write-rest/ │ │ └── main.go │ ├── routing/ │ │ ├── basic/ │ │ │ ├── .dockerignore │ │ │ ├── Dockerfile │ │ │ ├── README.md │ │ │ ├── docker-compose.yml │ │ │ ├── main.go │ │ │ └── main_test.go │ │ ├── conditional-chain/ │ │ │ ├── main.go │ │ │ └── main_test.go │ │ ├── custom-context/ │ │ │ └── main.go │ │ ├── custom-router/ │ │ │ └── main.go │ │ ├── custom-wrapper/ │ │ │ ├── main.go │ │ │ ├── main_test.go │ │ │ └── public/ │ │ │ ├── app.js │ │ │ ├── css/ │ │ │ │ └── main.css │ │ │ └── index.html │ │ ├── dynamic-path/ │ │ │ ├── at-username/ │ │ │ │ └── main.go │ │ │ ├── main.go │ │ │ ├── root-wildcard/ │ │ │ │ └── main.go │ │ │ └── same-pattern-different-func/ │ │ │ ├── main.go │ │ │ ├── main_test.go │ │ │ └── use-global/ │ │ │ ├── main.go │ │ │ └── main_test.go │ │ ├── hello-world/ │ │ │ └── main.go │ │ ├── http-errors/ │ │ │ ├── main.go │ │ │ └── reset-body/ │ │ │ ├── main.go │ │ │ └── main_test.go │ │ ├── http-wire-errors/ │ │ │ ├── main.go │ │ │ └── service/ │ │ │ └── main.go │ │ ├── intelligence/ │ │ │ ├── main.go │ │ │ └── manual/ │ │ │ └── main.go │ │ ├── macros/ │ │ │ └── main.go │ │ ├── main.go │ │ ├── main_test.go │ │ ├── overview/ │ │ │ ├── main.go │ │ │ └── public/ │ │ │ └── assets/ │ │ │ └── css/ │ │ │ └── main.css │ │ ├── overview-2/ │ │ │ ├── main.go │ │ │ └── views/ │ │ │ └── user/ │ │ │ ├── create_verification.html │ │ │ └── profile.html │ │ ├── party-controller/ │ │ │ ├── go.mod │ │ │ ├── go.sum │ │ │ ├── main.go │ │ │ └── pkg/ │ │ │ └── weatherapi/ │ │ │ ├── client.go │ │ │ └── response.go │ │ ├── remove-handler/ │ │ │ ├── main.go │ │ │ └── main_test.go │ │ ├── remove-route/ │ │ │ └── main.go │ │ ├── reverse/ │ │ │ └── main.go │ │ ├── rewrite/ │ │ │ ├── hosts │ │ │ ├── main.go │ │ │ └── redirects.yml │ │ ├── route-handlers-execution-rules/ │ │ │ └── main.go │ │ ├── route-register-rule/ │ │ │ ├── main.go │ │ │ └── main_test.go │ │ ├── route-state/ │ │ │ └── main.go │ │ ├── sitemap/ │ │ │ ├── main.go │ │ │ └── main_test.go │ │ ├── subdomains/ │ │ │ ├── http-errors-view/ │ │ │ │ ├── main.go │ │ │ │ ├── main_test.go │ │ │ │ └── views/ │ │ │ │ ├── error.html │ │ │ │ ├── index.html │ │ │ │ ├── layouts/ │ │ │ │ │ ├── layout.html │ │ │ │ │ └── test.layout.html │ │ │ │ └── partials/ │ │ │ │ ├── 404.html │ │ │ │ └── 500.html │ │ │ ├── multi/ │ │ │ │ ├── hosts │ │ │ │ └── main.go │ │ │ ├── redirect/ │ │ │ │ ├── hosts │ │ │ │ ├── main.go │ │ │ │ └── multi-instances/ │ │ │ │ ├── go.mod │ │ │ │ ├── go.sum │ │ │ │ ├── main.go │ │ │ │ ├── other/ │ │ │ │ │ └── server.go │ │ │ │ └── root/ │ │ │ │ └── server.go │ │ │ ├── single/ │ │ │ │ ├── hosts │ │ │ │ └── main.go │ │ │ ├── wildcard/ │ │ │ │ ├── hosts │ │ │ │ └── main.go │ │ │ └── www/ │ │ │ ├── hosts │ │ │ ├── main.go │ │ │ ├── main_test.go │ │ │ └── www-method/ │ │ │ └── main.go │ │ ├── versioning/ │ │ │ ├── main.go │ │ │ ├── v1/ │ │ │ │ └── index.html │ │ │ ├── v2/ │ │ │ │ └── index.html │ │ │ └── v3/ │ │ │ └── index.html │ │ └── writing-a-middleware/ │ │ ├── globally/ │ │ │ └── main.go │ │ ├── per-route/ │ │ │ └── main.go │ │ ├── share-funcs/ │ │ │ ├── main.go │ │ │ └── main_test.go │ │ └── share-services/ │ │ ├── main.go │ │ └── main_test.go │ ├── sessions/ │ │ ├── basic/ │ │ │ └── main.go │ │ ├── database/ │ │ │ ├── badger/ │ │ │ │ └── main.go │ │ │ ├── boltdb/ │ │ │ │ └── main.go │ │ │ └── redis/ │ │ │ ├── Dockerfile │ │ │ ├── docker-compose.yml │ │ │ ├── go.mod │ │ │ ├── go.sum │ │ │ └── main.go │ │ ├── flash-messages/ │ │ │ └── main.go │ │ ├── overview/ │ │ │ ├── example/ │ │ │ │ └── example.go │ │ │ └── main.go │ │ ├── securecookie/ │ │ │ ├── main.go │ │ │ └── main_test.go │ │ └── viewdata/ │ │ ├── main.go │ │ └── views/ │ │ └── index.html │ ├── testing/ │ │ ├── ginkgotest/ │ │ │ ├── ginkgotest_suite_test.go │ │ │ ├── go.mod │ │ │ ├── go.sum │ │ │ ├── main.go │ │ │ └── main_test.go │ │ └── httptest/ │ │ ├── main.go │ │ └── main_test.go │ ├── url-shortener/ │ │ ├── README.md │ │ ├── factory.go │ │ ├── main.go │ │ ├── main_test.go │ │ ├── resources/ │ │ │ └── css/ │ │ │ └── style.css │ │ ├── store.go │ │ └── templates/ │ │ └── index.html │ ├── view/ │ │ ├── context-view-data/ │ │ │ ├── main.go │ │ │ └── templates/ │ │ │ ├── index.html │ │ │ └── layouts/ │ │ │ └── layout.html │ │ ├── context-view-engine/ │ │ │ ├── main.go │ │ │ └── views/ │ │ │ ├── admin/ │ │ │ │ ├── index.html │ │ │ │ └── layouts/ │ │ │ │ └── main.html │ │ │ ├── on-fly/ │ │ │ │ └── index.html │ │ │ └── public/ │ │ │ ├── 500.html │ │ │ ├── index.html │ │ │ ├── layouts/ │ │ │ │ ├── error.html │ │ │ │ └── main.html │ │ │ └── partials/ │ │ │ └── footer.html │ │ ├── embedding-templates-into-app/ │ │ │ ├── embedded/ │ │ │ │ └── templates/ │ │ │ │ ├── layouts/ │ │ │ │ │ ├── layout.html │ │ │ │ │ └── mylayout.html │ │ │ │ ├── page1.html │ │ │ │ └── partials/ │ │ │ │ └── page1_partial1.html │ │ │ └── main.go │ │ ├── embedding-templates-into-app-bindata/ │ │ │ ├── bindata.go │ │ │ ├── main.go │ │ │ └── templates/ │ │ │ ├── layouts/ │ │ │ │ ├── layout.html │ │ │ │ └── mylayout.html │ │ │ ├── page1.html │ │ │ └── partials/ │ │ │ └── page1_partial1.html │ │ ├── fallback/ │ │ │ ├── main.go │ │ │ └── view/ │ │ │ └── fallback.html │ │ ├── herotemplate/ │ │ │ ├── README.md │ │ │ ├── app.go │ │ │ └── template/ │ │ │ ├── index.html │ │ │ ├── index.html.go │ │ │ ├── user.html │ │ │ ├── user.html.go │ │ │ ├── userlist.html │ │ │ ├── userlist.html.go │ │ │ ├── userlistwriter.html │ │ │ └── userlistwriter.html.go │ │ ├── layout/ │ │ │ ├── ace/ │ │ │ │ ├── main.go │ │ │ │ └── views/ │ │ │ │ ├── index.ace │ │ │ │ ├── layouts/ │ │ │ │ │ └── main.ace │ │ │ │ └── partials/ │ │ │ │ └── footer.ace │ │ │ ├── blocks/ │ │ │ │ ├── main.go │ │ │ │ └── views/ │ │ │ │ ├── index.html │ │ │ │ ├── layouts/ │ │ │ │ │ └── main.html │ │ │ │ └── partials/ │ │ │ │ └── footer.html │ │ │ ├── django/ │ │ │ │ ├── main.go │ │ │ │ └── views/ │ │ │ │ ├── index.html │ │ │ │ ├── layouts/ │ │ │ │ │ └── main.html │ │ │ │ └── partials/ │ │ │ │ └── footer.html │ │ │ ├── handlebars/ │ │ │ │ ├── main.go │ │ │ │ └── views/ │ │ │ │ ├── index.html │ │ │ │ ├── layouts/ │ │ │ │ │ └── main.html │ │ │ │ └── partials/ │ │ │ │ └── footer.html │ │ │ ├── html/ │ │ │ │ ├── main.go │ │ │ │ └── views/ │ │ │ │ ├── index.html │ │ │ │ ├── layouts/ │ │ │ │ │ └── main.html │ │ │ │ └── partials/ │ │ │ │ └── footer.html │ │ │ ├── jet/ │ │ │ │ ├── main.go │ │ │ │ └── views/ │ │ │ │ ├── index.jet │ │ │ │ ├── layouts/ │ │ │ │ │ └── main.jet │ │ │ │ └── partials/ │ │ │ │ └── footer.jet │ │ │ └── pug/ │ │ │ ├── main.go │ │ │ └── views/ │ │ │ ├── index.pug │ │ │ ├── layouts/ │ │ │ │ └── main.pug │ │ │ └── partials/ │ │ │ └── footer.pug │ │ ├── overview/ │ │ │ ├── main.go │ │ │ └── templates/ │ │ │ ├── example.html │ │ │ ├── functions.html │ │ │ └── hi.html │ │ ├── parse-template/ │ │ │ ├── django/ │ │ │ │ └── main.go │ │ │ ├── handlebars/ │ │ │ │ └── main.go │ │ │ ├── jet/ │ │ │ │ └── main.go │ │ │ ├── main.go │ │ │ └── views/ │ │ │ └── layouts/ │ │ │ └── main.html │ │ ├── quicktemplate/ │ │ │ ├── README.md │ │ │ ├── controllers/ │ │ │ │ ├── execute_template.go │ │ │ │ ├── hello.go │ │ │ │ └── index.go │ │ │ ├── main.go │ │ │ ├── main_test.go │ │ │ ├── models/ │ │ │ │ └── .gitkeep │ │ │ └── templates/ │ │ │ ├── base.qtpl │ │ │ ├── base.qtpl.go │ │ │ ├── hello.qtpl │ │ │ ├── hello.qtpl.go │ │ │ ├── index.qtpl │ │ │ └── index.qtpl.go │ │ ├── templ/ │ │ │ ├── go.mod │ │ │ ├── go.sum │ │ │ ├── hello.templ │ │ │ ├── hello_templ.go │ │ │ └── main.go │ │ ├── template_ace_0/ │ │ │ ├── main.go │ │ │ └── views/ │ │ │ ├── index.ace │ │ │ ├── layouts/ │ │ │ │ └── main.ace │ │ │ └── partials/ │ │ │ ├── footer.ace │ │ │ └── header.ace │ │ ├── template_blocks_0/ │ │ │ ├── main.go │ │ │ └── views/ │ │ │ ├── 500.html │ │ │ ├── index.html │ │ │ ├── layouts/ │ │ │ │ ├── error.html │ │ │ │ └── main.html │ │ │ └── partials/ │ │ │ └── footer.html │ │ ├── template_blocks_1_embedded/ │ │ │ ├── bindata.go │ │ │ └── main.go │ │ ├── template_blocks_2/ │ │ │ ├── main.go │ │ │ └── src/ │ │ │ └── public/ │ │ │ └── html/ │ │ │ ├── files/ │ │ │ │ └── list.html │ │ │ ├── layouts/ │ │ │ │ ├── main.html │ │ │ │ └── secondary.html │ │ │ └── menu/ │ │ │ └── menu.html │ │ ├── template_django_0/ │ │ │ ├── main.go │ │ │ └── templates/ │ │ │ └── hi.html │ │ ├── template_django_1/ │ │ │ ├── main.go │ │ │ └── views/ │ │ │ └── page.html │ │ ├── template_handlebars_0/ │ │ │ ├── main.go │ │ │ └── templates/ │ │ │ └── example.html │ │ ├── template_html_0/ │ │ │ ├── main.go │ │ │ └── templates/ │ │ │ └── hi.html │ │ ├── template_html_1/ │ │ │ ├── main.go │ │ │ └── templates/ │ │ │ ├── layout.html │ │ │ └── mypage.html │ │ ├── template_html_2/ │ │ │ ├── README.md │ │ │ ├── main.go │ │ │ └── templates/ │ │ │ ├── layouts/ │ │ │ │ ├── layout.html │ │ │ │ └── mylayout.html │ │ │ ├── page1.html │ │ │ └── partials/ │ │ │ └── page1_partial1.html │ │ ├── template_html_3/ │ │ │ ├── main.go │ │ │ └── templates/ │ │ │ └── page.html │ │ ├── template_html_4/ │ │ │ ├── hosts │ │ │ ├── main.go │ │ │ └── templates/ │ │ │ └── page.html │ │ ├── template_html_5/ │ │ │ ├── main.go │ │ │ └── views/ │ │ │ ├── about.html │ │ │ ├── home.html │ │ │ ├── layout.html │ │ │ └── user/ │ │ │ └── index.html │ │ ├── template_jet_0/ │ │ │ ├── README.md │ │ │ ├── main.go │ │ │ └── views/ │ │ │ ├── layouts/ │ │ │ │ └── application.jet │ │ │ └── todos/ │ │ │ ├── index.jet │ │ │ └── show.jet │ │ ├── template_jet_1_embedded/ │ │ │ ├── bindata.go │ │ │ ├── main.go │ │ │ └── views/ │ │ │ ├── includes/ │ │ │ │ ├── _partial.jet │ │ │ │ └── blocks.jet │ │ │ ├── index.jet │ │ │ └── layouts/ │ │ │ └── application.jet │ │ ├── template_jet_2/ │ │ │ ├── main.go │ │ │ └── views/ │ │ │ └── page.jet │ │ ├── template_jet_3/ │ │ │ ├── main.go │ │ │ └── views/ │ │ │ └── index.jet │ │ ├── template_pug_0/ │ │ │ ├── main.go │ │ │ └── templates/ │ │ │ └── index.pug │ │ ├── template_pug_1/ │ │ │ ├── main.go │ │ │ └── templates/ │ │ │ ├── footer.pug │ │ │ ├── header.pug │ │ │ └── index.pug │ │ ├── template_pug_2_embedded/ │ │ │ ├── bindata.go │ │ │ ├── main.go │ │ │ └── templates/ │ │ │ ├── index.pug │ │ │ └── layout.pug │ │ └── write-to/ │ │ ├── main.go │ │ └── views/ │ │ ├── email/ │ │ │ └── simple.html │ │ └── shared/ │ │ └── email.html │ ├── webassembly/ │ │ ├── client/ │ │ │ ├── go-wasm-runtime.js │ │ │ ├── hello.html │ │ │ ├── hello_go116.go │ │ │ └── main.js │ │ └── main.go │ └── websocket/ │ ├── README.md │ ├── basic/ │ │ ├── README.md │ │ ├── browser/ │ │ │ └── index.html │ │ ├── browserify/ │ │ │ ├── app.js │ │ │ ├── bundle.js │ │ │ ├── client.html │ │ │ └── package.json │ │ ├── go-client/ │ │ │ └── client.go │ │ ├── go.mod │ │ ├── go.sum │ │ ├── nodejs-client/ │ │ │ ├── client.js │ │ │ └── package.json │ │ └── server.go │ ├── gorilla-filewatch/ │ │ ├── go.mod │ │ ├── go.sum │ │ ├── main.go │ │ ├── testfile.txt │ │ └── views/ │ │ └── home.html │ ├── native-messages/ │ │ ├── main.go │ │ ├── static/ │ │ │ └── js/ │ │ │ └── chat.js │ │ └── templates/ │ │ └── client.html │ ├── online-visitors/ │ │ ├── main.go │ │ ├── static/ │ │ │ └── assets/ │ │ │ └── js/ │ │ │ └── visitors.js │ │ └── templates/ │ │ ├── index.html │ │ └── other.html │ ├── secure/ │ │ └── README.md │ └── socketio/ │ ├── asset/ │ │ └── index.html │ ├── go.mod │ ├── go.sum │ └── main.go ├── _proposals/ │ ├── route_builder.md │ └── xerrors_party.md ├── aliases.go ├── apps/ │ ├── README.md │ ├── apps.go │ ├── switch.go │ ├── switch_hosts.go │ ├── switch_hosts_test.go │ ├── switch_options.go │ ├── switch_options_test.go │ ├── switch_scheme.go │ └── switch_test.go ├── auth/ │ ├── auth.go │ ├── configuration.go │ ├── provider.go │ └── user.go ├── cache/ │ ├── browser.go │ ├── browser_test.go │ ├── cache.go │ ├── cache_test.go │ ├── cfg/ │ │ └── cfg.go │ ├── client/ │ │ ├── client.go │ │ ├── handler.go │ │ ├── rule/ │ │ │ ├── chained.go │ │ │ ├── conditional.go │ │ │ ├── header.go │ │ │ ├── not_satisfied.go │ │ │ ├── rule.go │ │ │ ├── satisfied.go │ │ │ └── validator.go │ │ └── ruleset.go │ ├── entry/ │ │ ├── entry.go │ │ ├── pool.go │ │ ├── response.go │ │ └── store.go │ ├── ruleset/ │ │ └── ruleset.go │ └── uri/ │ └── uribuilder.go ├── cli.go ├── configuration.go ├── configuration_test.go ├── context/ │ ├── accept_header.go │ ├── application.go │ ├── compress.go │ ├── configuration.go │ ├── context.go │ ├── context_func.go │ ├── context_go118.go │ ├── context_user.go │ ├── counter.go │ ├── fs.go │ ├── handler.go │ ├── i18n.go │ ├── pool.go │ ├── problem.go │ ├── request_params.go │ ├── response_recorder.go │ ├── response_writer.go │ ├── route.go │ ├── status.go │ ├── strconv.go │ └── view.go ├── context_wrapper.go ├── core/ │ ├── errgroup/ │ │ ├── errgroup.go │ │ └── errgroup_test.go │ ├── handlerconv/ │ │ ├── from_std.go │ │ └── from_std_test.go │ ├── host/ │ │ ├── interrupt.go │ │ ├── proxy.go │ │ ├── proxy_test.go │ │ ├── supervisor.go │ │ ├── supervisor_task_example_test.go │ │ ├── supervisor_test.go │ │ ├── task.go │ │ └── waiter.go │ ├── memstore/ │ │ ├── gob.go │ │ ├── lifetime.go │ │ ├── memstore.go │ │ └── memstore_test.go │ ├── netutil/ │ │ ├── addr.go │ │ ├── addr_test.go │ │ ├── client.go │ │ ├── ip.go │ │ ├── ip_test.go │ │ ├── server.go │ │ ├── tcp.go │ │ ├── tcp_soreuse_control_unix.go │ │ ├── tcp_soreuse_control_wasm.go │ │ └── tcp_soreuse_control_windows.go │ └── router/ │ ├── TestUseRouterParentDisallow_fuzz_test.go │ ├── api_builder.go │ ├── api_builder_benchmark_test.go │ ├── api_container.go │ ├── fs.go │ ├── handler.go │ ├── handler_debug.go │ ├── handler_execution_rules.go │ ├── handler_execution_rules_test.go │ ├── mime.go │ ├── party.go │ ├── path.go │ ├── path_test.go │ ├── route.go │ ├── route_register_rule_test.go │ ├── route_test.go │ ├── router.go │ ├── router_handlers_order_test.go │ ├── router_subdomain_redirect.go │ ├── router_test.go │ ├── router_wildcard_root_test.go │ ├── router_wrapper.go │ ├── router_wrapper_test.go │ ├── status_test.go │ └── trie.go ├── doc.go ├── go.mod ├── go.sum ├── hero/ │ ├── binding.go │ ├── binding_test.go │ ├── container.go │ ├── container_test.go │ ├── dependency.go │ ├── dependency_source.go │ ├── dependency_test.go │ ├── func_result.go │ ├── func_result_test.go │ ├── handler.go │ ├── handler_test.go │ ├── param_test.go │ ├── reflect.go │ ├── reflect_test.go │ ├── struct.go │ └── struct_test.go ├── httptest/ │ ├── aliases.go │ ├── httptest.go │ ├── netutils.go │ ├── server.go │ └── status.go ├── i18n/ │ ├── i18n.go │ ├── internal/ │ │ ├── aliases.go │ │ ├── catalog.go │ │ ├── locale.go │ │ ├── message.go │ │ ├── plural.go │ │ ├── template.go │ │ └── var.go │ └── loader.go ├── iris.go ├── iris_guide.go ├── macro/ │ ├── handler/ │ │ ├── handler.go │ │ └── handler_test.go │ ├── interpreter/ │ │ ├── ast/ │ │ │ └── ast.go │ │ ├── lexer/ │ │ │ ├── lexer.go │ │ │ └── lexer_test.go │ │ ├── parser/ │ │ │ ├── parser.go │ │ │ └── parser_test.go │ │ └── token/ │ │ └── token.go │ ├── macro.go │ ├── macro_test.go │ ├── macros.go │ └── template.go ├── middleware/ │ ├── README.md │ ├── accesslog/ │ │ ├── accesslog.go │ │ ├── accesslog_test.go │ │ ├── broker.go │ │ ├── csv.go │ │ ├── csv_test.go │ │ ├── json.go │ │ ├── json_easy.go │ │ ├── log.go │ │ └── template.go │ ├── basicauth/ │ │ ├── basicauth.go │ │ ├── basicauth_test.go │ │ ├── error.go │ │ ├── header.go │ │ ├── header_test.go │ │ ├── user.go │ │ └── user_test.go │ ├── cors/ │ │ └── cors.go │ ├── grpc/ │ │ └── grpc.go │ ├── hcaptcha/ │ │ ├── ARTICLE.md │ │ └── hcaptcha.go │ ├── jwt/ │ │ ├── ARTICLE.md │ │ ├── aliases.go │ │ ├── blocklist/ │ │ │ └── redis/ │ │ │ └── blocklist.go │ │ ├── blocklist.go │ │ ├── extractor.go │ │ ├── jwt.go │ │ ├── jwt_test.go │ │ ├── signer.go │ │ └── verifier.go │ ├── logger/ │ │ ├── config.go │ │ └── logger.go │ ├── methodoverride/ │ │ ├── methodoverride.go │ │ └── methodoverride_test.go │ ├── modrevision/ │ │ └── modrevision.go │ ├── monitor/ │ │ ├── expvar_uint64.go │ │ ├── monitor.go │ │ ├── stats.go │ │ └── view.go │ ├── pprof/ │ │ └── pprof.go │ ├── rate/ │ │ └── rate.go │ ├── recaptcha/ │ │ └── recaptcha.go │ ├── recover/ │ │ └── recover.go │ ├── requestid/ │ │ ├── requestid.go │ │ └── requestid_test.go │ └── rewrite/ │ ├── rewrite.go │ └── rewrite_test.go ├── mvc/ │ ├── aliases.go │ ├── controller.go │ ├── controller_handle_test.go │ ├── controller_method_parser.go │ ├── controller_method_result_test.go │ ├── controller_overlap_test.go │ ├── controller_test.go │ ├── grpc.go │ ├── mvc.go │ ├── reflect.go │ └── versioning.go ├── sessions/ │ ├── config.go │ ├── database.go │ ├── provider.go │ ├── session.go │ ├── sessiondb/ │ │ ├── badger/ │ │ │ └── database.go │ │ ├── boltdb/ │ │ │ └── database.go │ │ └── redis/ │ │ ├── database.go │ │ ├── driver.go │ │ └── driver_goredis.go │ ├── sessions.go │ ├── sessions_test.go │ └── transcoding.go ├── versioning/ │ ├── deprecation.go │ ├── deprecation_test.go │ ├── group.go │ ├── group_test.go │ ├── version.go │ └── version_test.go ├── view/ │ ├── README.md │ ├── ace.go │ ├── blocks.go │ ├── django.go │ ├── fs.go │ ├── handlebars.go │ ├── html.go │ ├── jet.go │ ├── pug.go │ └── view.go ├── websocket/ │ ├── aliases.go │ └── websocket.go └── x/ ├── client/ │ ├── client.go │ ├── client_test.go │ ├── error.go │ ├── handler_transport.go │ ├── option.go │ └── request_handler.go ├── errors/ │ ├── aliases.go │ ├── context_error_handler.go │ ├── errors.go │ ├── handlers.go │ ├── path_parameter_type_error_handler.go │ ├── validation/ │ │ ├── error.go │ │ ├── number.go │ │ ├── slice.go │ │ └── string.go │ └── validation_error.go ├── jsonx/ │ ├── day_time.go │ ├── day_time_test.go │ ├── duration.go │ ├── exampler.go │ ├── iso8601.go │ ├── iso8601_test.go │ ├── jsonx.go │ ├── kitchen_time.go │ ├── kitchen_time_test.go │ ├── season.go │ ├── simple_date.go │ ├── simple_date_test.go │ └── time_notation.go ├── mathx/ │ └── round.go ├── pagination/ │ └── pagination.go ├── reflex/ │ ├── error.go │ ├── func.go │ ├── reflex.go │ ├── struct.go │ ├── types.go │ └── zero.go ├── sqlx/ │ ├── sqlx.go │ ├── sqlx_test.go │ ├── struct_row.go │ └── util.go └── timex/ ├── weekday.go └── weekday_test.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .deepsource.toml ================================================ version = 1 test_patterns = ["**/*_test.go"] exclude_patterns = [ "_examples/**", "_benchmarks/**", ".github/**" ] [[analyzers]] name = "go" enabled = true [analyzers.meta] import_paths = ["github.com/kataras/iris/v12"] ================================================ FILE: .fossa.yml ================================================ version: 3 cli: server: https://app.fossa.com fetcher: git package: github.com/kataras/iris project: github.com/kataras/iris analyze: modules: - name: iris type: go target: . path: . ================================================ FILE: .gitattributes ================================================ *.go linguist-language=Go _examples/* linguist-documentation _benchmarks/* linguist-documentation go.sum linguist-generated # Set the default behavior, in case people don't have core.autocrlf set. # if from windows: # git config --global core.autocrlf true # if from unix: # git config --global core.autocrlf input # https://help.github.com/articles/dealing-with-line-endings/#per-repository-settings * text=auto # ignore perms # git config core.filemode false ================================================ FILE: .github/CODEOWNERS ================================================ # These owners will be the default owners for everything in the repo. * @kataras ================================================ FILE: .github/FUNDING.yml ================================================ # patreon: # Replace with a single Patreon username # open_collective: # Replace with a single Open Collective username # ko_fi: # Replace with a single Ko-fi username # tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel # custom: https://iris-go.com/donate # Replace with a single custom sponsorship URL github: kataras ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: "[BUG]" labels: type:bug assignees: kataras --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. [...] **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - OS: [e.g. ubuntu, windows] **iris.Version** - e.g. v12.2.5 or main Please make sure the bug is reproducible over the `main` branch: ```sh $ cd PROJECT $ go get -u github.com/kataras/iris/v12@main $ go run . ``` **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/custom.md ================================================ --- name: Custom issue template about: Other title: '' labels: '' assignees: '' --- Describe the issue you are facing or ask for help ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: "[FEATURE REQUEST]" labels: type:idea assignees: kataras --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/ISSUE_TEMPLATE.md ================================================ Examples for the Iris project can be found at . Documentation for the Iris project can be found at . Love iris? Please consider supporting the project: 👉 https://iris-go.com/donate Care to be part of a larger community? Fill our user experience form: 👉 https://goo.gl/forms/lnRbVgA6ICTkPyk02 ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ # We'd love to see more contributions Read how you can [contribute to the project](https://github.com/kataras/iris/blob/main/CONTRIBUTING.md). > Please attach an [issue](https://github.com/kataras/iris/issues) link which your PR solves otherwise your work may be rejected. ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "gomod" directory: "/" exclude-paths: - "_examples/" schedule: interval: "monthly" - package-ecosystem: "github-actions" directory: "/" schedule: interval: "monthly" ================================================ FILE: .github/scripts/setup_examples_test.bash ================================================ #!/usr/bin/env bash for f in ../../_examples/*; do if [ -d "$f" ]; then # Will not run if no directories are available go mod init go get -u github.com/kataras/iris/v12@main go mod download go run . fi done # git update-index --chmod=+x ./.github/scripts/setup_examples_test.bash ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: push: branches: [main] pull_request: branches: [main] permissions: contents: read jobs: test: name: Test runs-on: ubuntu-latest strategy: matrix: go_version: [1.25.x] steps: - name: Check out code into the Go module directory uses: actions/checkout@v6 - name: Set up Go 1.x uses: actions/setup-go@v6 with: go-version-file: './go.mod' check-latest: true - run: go version - name: Test run: go test -v ./... - name: Setup examples for testing run: ./.github/scripts/setup_examples_test.bash - name: Test examples continue-on-error: true working-directory: _examples run: go test -v -mod=mod -cover -race ./... ================================================ FILE: .github/workflows/codeql-analysis.yml ================================================ # For most projects, this workflow file will not need changing; you simply need # to commit it to your repository. # # You may wish to alter this file to override the set of languages analyzed, # or to provide custom queries or build logic. # # ******** NOTE ******** # We have attempted to detect the languages in your repository. Please check # the `language` matrix defined below to confirm you have the correct set of # supported CodeQL languages. # name: "CodeQL" on: push: branches: [ main ] pull_request: # The branches below must be a subset of the branches above branches: [ main ] schedule: - cron: '24 11 * * 6' jobs: analyze: name: Analyze runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: language: [ 'go'] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] # Learn more: # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed steps: - name: Checkout repository uses: actions/checkout@v6 - name: Set up Go 1.x uses: actions/setup-go@v6 with: go-version-file: './go.mod' check-latest: true - run: go version # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # queries: ./path/to/local/query, your-org/your-repo/queries@main # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild uses: github/codeql-action/autobuild@v3 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines # and modify them (or add more) to build your code if your project # uses a compiled language #- run: | # make bootstrap # make release - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 ================================================ FILE: .gitignore ================================================ .idea .vscode .directory coverage.out package-lock.json access.log node_modules issue-*/ internalcode-*/ /_examples/feature-*/ _examples/**/uploads/* _issues/** .DS_STORE ================================================ FILE: AUTHORS ================================================ # This is the official list of Iris authors for copyright # purposes. Gerasimos Maropoulos ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at kataras2006@hotmail.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing First of all read our [Code of Conduct](https://github.com/kataras/iris/blob/main/CODE_OF_CONDUCT.md). ## PR 1. Open a new [issue](https://github.com/kataras/iris/issues/new) * Write version of your local Iris. * Write version of your local Go programming language. * Describe your problem, what did you expect to see and what you see instead. * If it's a feature request, describe your idea as better as you can * optionally, navigate to the [chat](https://chat.iris-go.com) to push other members to participate and share their thoughts about your brilliant idea. 2. Fork the [repository](https://github.com/kataras/iris). 3. Make your changes. 4. Compare & Push the PR from [here](https://github.com/kataras/iris/compare). ## Translate We need your help with translations into your native language. Iris needs your help, please think about contributing to the translation of the [README](README.md) and https://iris-go.com, you will be rewarded. Instructions can be found at: https://github.com/kataras/iris/issues/796 ## Share ### Writing Write an article about Iris in https://medium.com, https://dev.to or if you're being a hackathon at https://hackernoon.com and send us the link on iris-go@outlook.com. ### Social networks If you're part of any social network, do a post(or tweet if twitter) about Iris and what you love about it, many examples can be found, the most recent one is [that](https://twitter.com/DorMoshe/status/1154486477247508480). ================================================ FILE: FAQ.md ================================================ # FAQ ## [![iris](https://img.shields.io/badge/iris-powered-2196f3.svg?style=for-the-badge)](https://github.com/kataras/iris) Add a `badge` to your open-source projects powered by [Iris](https://iris-go.com) by pasting the below code snippet to the project repo's README.md: ```md [![iris](https://img.shields.io/badge/iris-powered-2196f3.svg?style=for-the-badge)](https://github.com/kataras/iris) ``` ## Editors & IDEs Extensions ### Visual Studio Code > Please feel free to list your own Iris extension(s) here by [PR](https://github.com/kataras/iris/pulls) ## How to upgrade ```sh go get github.com/kataras/iris/v12@latest ``` Go version 1.20 and above is required. ## Learning More than 280 practical examples, tutorials and articles at: - https://www.iris-go.com/docs - https://www.iris-go.com/#ebookDonateForm - https://github.com/kataras/iris/tree/main/_examples - https://pkg.go.dev/github.com/kataras/iris/v12@main ## Active development mode Iris may have reached version 12, but we're not stopping there. We have many feature ideas on our board that we're anxious to add and other innovative web development solutions that we're planning to build into Iris. ## Can I find a job if I learn how to use Iris? Yes, not only because you will learn Golang in the same time, but there are some positions open for Iris-specific developers the time we speak. Go to our facebook page, like it and receive notifications about new job offers, we already have couple of them stay at the top of the page: https://www.facebook.com/iris.framework ## Do we have a Community chat? Yes, https://chat.iris-go.com ## How is the development of Iris economically supported? By people like you, who help us by donating small or large amounts of money. Help this project deliver awesome and unique features with the highest possible code quality by donating any amount via [PayPal or Stripe](https://iris-go.com/donate). Your name will be published [here](https://www.iris-go.com/#review) after your approval via e-mail. ================================================ FILE: HISTORY.md ================================================ # Changelog ### Looking for free and real-time support? https://github.com/kataras/iris/issues https://chat.iris-go.com ### Looking for previous versions? https://github.com/kataras/iris/releases ### Want to be hired? https://facebook.com/iris.framework ### Should I upgrade my Iris? Developers are not forced to upgrade if they don't really need it. Upgrade whenever you feel ready. **How to upgrade**: Open your command-line and execute this command: `go get github.com/kataras/iris/v12@latest` and `go mod tidy -compat=1.21`. # Next Changes apply to `main` branch. # Thu, 25 April 2024 | v12.2.11 Dear Iris Community, You might have noticed a recent lull in activity on the Iris repository. I want to assure you that this silence is not without reason. For the past **3-4 months**, I've been diligently working on the next major release of Iris. This upcoming version is poised to be a significant leap forward, fully embracing the **Generics** feature introduced in Go. We're not just stopping at Generics, though. Expect a suite of **new features**, **enhancements**, and **optimizations** that will elevate your development experience to new heights. My journey with Go spans over **8 years**, and with each year, my expertise and understanding of the language deepen. This accumulated knowledge is being poured into Iris, ensuring that the framework not only evolves with the language but also with the community's growing needs. Stay tuned for more updates, and thank you for your continued support and patience. The wait will be worth it. Warm regards,
Gerasimos (Makis) Maropoulos ### This is the last release for the version 12 family. - Security improvements and dependencies upgrade. - New `Application/Party.MiddlewareExists(handlerNameOrHandler)` method added, example: ```go package main import ( "fmt" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/x/errors" ) func main() { app := iris.New() app.UseRouter(errors.RecoveryHandler) if app.MiddlewareExists(errors.RecoveryHandler) { // <- HERE. fmt.Println("errors.RecoveryHandler exists") } // OR: // if app.MiddlewareExists("iris.errors.recover") { // fmt.Println("Iris.Errors.Recovery exists") // } app.Get("/", indexHandler) app.Listen(":8080") } func indexHandler(ctx iris.Context) { panic("an error here") ctx.HTML("

Index

") } ``` - New `x/errors.Intercept(func(ctx iris.Context, req *CreateRequest, resp *CreateResponse) error{ ... })` package-level function. ```go func main() { app := iris.New() // Create a new service and pass it to the handlers. service := new(myService) app.Post("/", errors.Intercept(responseHandler), errors.CreateHandler(service.Create)) // [...] } func responseHandler(ctx iris.Context, req *CreateRequest, resp *CreateResponse) error { fmt.Printf("intercept: request got: %+v\nresponse sent: %#+v\n", req, resp) return nil } ``` - Rename `x/errors/ContextValidator.ValidateContext(iris.Context) error` to `x/errors/RequestHandler.HandleRequest(iris.Context) error`. # Thu, 18 Jan 2024 | v12.2.10 - Simplify the `/core/host` subpackage and remove its `DeferFlow` and `RestoreFlow` methods. These methods are replaced with: `Supervisor.Configure(host.NonBlocking())` before `Serve` and ` Supervisor.Wait(context.Context) error` after `Serve`. - Fix internal `trimHandlerName` and other minor stuff. - New `iris.NonBlocking()` configuration option to run the server without blocking the main routine, `Application.Wait(context.Context) error` method can be used to block and wait for the server to be up and running. Example: ```go func main() { app := iris.New() app.Get("/", func(ctx iris.Context) { ctx.Writef("Hello, %s!", "World") }) app.Listen(":8080", iris.NonBlocking(), iris.WithoutServerError(iris.ErrServerClosed)) ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() if err := app.Wait(ctx); err != nil { log.Fatal(err) } // [Server is up and running now, you may continue with other functions below]. } ``` - Add `x/mathx.RoundToInteger` math helper function. # Wed, 10 Jan 2024 | v12.2.9 - Add `x/errors.RecoveryHandler` package-level function. - Add `x/errors.Validation` package-level function to add one or more validations for the request payload before a service call of the below methods. - Add `x/errors.Handler`, `CreateHandler`, `NoContentHandler`, `NoContentOrNotModifiedHandler` and `ListHandler` ready-to-use handlers for service method calls to Iris Handler. - Add `x/errors.List` package-level function to support `ListObjects(ctx context.Context, opts pagination.ListOptions, f Filter) ([]Object, int64, error)` type of service calls. - Simplify how validation errors on `/x/errors` package works. A new `x/errors/validation` sub-package added to make your life easier (using the powerful Generics feature). - Add `x/errors.OK`, `Create`, `NoContent` and `NoContentOrNotModified` package-level generic functions as custom service method caller helpers. Example can be found [here](_examples/routing/http-wire-errors/service/main.go). - Add `x/errors.ReadPayload`, `ReadQuery`, `ReadPaginationOptions`, `Handle`, `HandleCreate`, `HandleCreateResponse`, `HandleUpdate` and `HandleDelete` package-level functions as helpers for common actions. - Add `x/jsonx.GetSimpleDateRange(date, jsonx.WeekRange, time.Monday, time.Sunday)` which returns all dates between the given range and start/end weekday values for WeekRange. - Add `x/timex.GetMonthDays` and `x/timex.GetMonthEnd` functions. - Add `iris.CookieDomain` and `iris.CookieOverride` cookie options to handle [#2309](https://github.com/kataras/iris/issues/2309). - New `x/errors.ErrorCodeName.MapErrorFunc`, `MapErrors`, `Wrap` methods and `x/errors.HandleError` package-level function. # Sun, 05 Nov 2023 | v12.2.8 - A new way to customize the handler's parameter among with the `hero` and `mvc` packages. New `iris.NewContextWrapper` and `iris.NewContextPool` methods were added to wrap a handler (`.Handler`, `.Handlers`, `.HandlerReturnError`, `HandlerReturnDuration`, `Filter` and `FallbackViewFunc` methods) and use a custom context instead of the iris.Context directly. Example at: https://github.com/kataras/iris/tree/main/_examples/routing/custom-context. - The `cache` sub-package has an update, 4 years after: - Add support for custom storage on `cache` package, through the `Handler#Store` method. - Add support for custom expiration duration on `cache` package, trough the `Handler#MaxAge` method. - Improve the overral performance of the `cache` package. - The `cache.Handler` input and output arguments remain as it is. - The `cache.Cache` input argument changed from `time.Duration` to `func(iris.Context) time.Duration`. # Mon, 25 Sep 2023 | v12.2.7 Minor bug fixes and support of multiple `block` and `define` directives in multiple layouts and templates in the `Blocks` view engine. # Mon, 21 Aug 2023 | v12.2.5 - Add optional `Singleton() bool` method to controllers to mark them as singleton, will panic with a specific error if a controller expects dynamic dependencies. This behavior is idendical to the app-driven `app.EnsureStaticBindings()`. - Non-zero fields of a controller that are marked as ignored, with `ignore:"true"` field tag, they are not included in the dependencies at all now. - Re-add error log on context rich write (e.g. JSON) failures when the application is running under debug mode (with `app.Logger().SetLevel("debug")`) and there is no a registered context error handler at place. - `master` branch finally renamed to `main`. Don't worry GitHub will still navigate any `master` request to `main` automatically. Examples, Documentation and other Pages are refactored too. # Sat, 12 Aug 2023 | v12.2.4 - Add new `iris.WithDynamicHandler` option (`EnableDynamicHandler` setting) to work with `iris.Application.RefreshRouter` method. It allows to change the entire router while your server is up and running. Handles [issue #2167](https://github.com/kataras/iris/issues/2167). Example at [_examples/routing/route-state/main.go](_examples/routing/route-state/main.go). > We jumped v12.2.2 and v12.2.3. # Mon, 17 July 2023 | v12.2.1 - Add `mvc.Application.EnableStructDependents()` method to handle [#2158](https://github.com/kataras/iris/issues/2158). - Fix [iris-premium#17](https://github.com/kataras/iris-premium/issues/17). - Replace [russross/blackfriday](github.com/russross/blackfriday/v2) with [gomarkdown](https://github.com/gomarkdown/markdown) as requested at [#2098](https://github.com/kataras/iris/issues/2098). - Add `mvc.IgnoreEmbedded` option to handle [#2103](https://github.com/kataras/iris/issues/2103). Example Code: ```go func configure(m *mvc.Application) { m.Router.Use(cacheHandler) m.Handle(&exampleController{ timeFormat: "Mon, Jan 02 2006 15:04:05", }, mvc.IgnoreEmbedded /* BaseController.GetDoSomething will not be parsed at all */) } type BaseController struct { Ctx iris.Context } func (c *BaseController) GetDoSomething(i any) error { return nil } type exampleController struct { BaseController timeFormat string } func (c *exampleController) Get() string { now := time.Now().Format(c.timeFormat) return "last time executed without cache: " + now } ``` - Add `LoadKV` method on `Iris.Application.I18N` instance. It should be used when no locale files are available. It loads locales via pure Go Map (or database decoded values). - Remove [ace](https://github.com/eknkc/amber) template parser support, as it was discontinued by its author more than five years ago. # Sa, 11 March 2023 | v12.2.0 This release introduces new features and some breaking changes. The codebase for Dependency Injection, Internationalization and localization and more have been simplified a lot (fewer LOCs and easier to read and follow up). ## 24 Dec 2022 All new features have been tested in production and seem to work fine. Fixed all reported and reproducible bugs. The `v12.2.0-beta7` is the latest beta release of v12.2.0. Expect the final public and stable release of v12.2.0 shortly after February 2023. ## Fixes and Improvements - Add `iris.TrimParamFilePart` to handle cases like [#2024](https://github.com/kataras/iris/issues/2024) and improve the [_examples/routing/dynamic-path/main.go](_examples/routing/dynamic-path/main.go#L356) example to include that case as well. - **Breaking-change**: HTML template functions `yield`, `part`, `partial`, `partial_r` and `render` now accept (and require for some cases) a second argument of the binding data context too. Convert: `{{ yield }}` to `{{ yield . }}`, `{{ render "templates/mytemplate.html" }}` to `{{ render "templates/mytemplate.html" . }}`, `{{ partial "partials/mypartial.html" }}` to `{{ partial "partials/mypartial.html" . }}` and so on. - Add new `URLParamSeparator` to the configuration. Defaults to "," but can be set to an empty string to disable splitting query values on `Context.URLParamSlice` method. - [PR #1992](https://github.com/kataras/iris/pull/1992): Added support for third party packages on [httptest](https://github.com/kataras/iris/tree/main/httptest). An example using 3rd-party module named [Ginkgo](github.com/onsi/ginkgo) can be found [here](https://github.com/kataras/iris/blob/main/_examples/testing/ginkgotest). - Add `Context.Render` method for compatibility. - Support of embedded [locale files](https://github.com/kataras/iris/blob/main/_examples/i18n/template-embedded/main.go) using standard `embed.FS` with the new `LoadFS` function. - Support of direct embedded view engines (`HTML, Blocks, Django, Handlebars, Pug, Jet` and `Ace`) with `embed.FS` or `fs.FS` (in addition to `string` and `http.FileSystem` types). - Add support for `embed.FS` and `fs.FS` on `app.HandleDir`. - Add `iris.Patches()` package-level function to customize Iris Request Context REST (and more to come) behavior. - Minor fixes. - Enable setting a custom "go-redis" client through `SetClient` go redis driver method or `Client` struct field on sessions/database/redis driver as requested at [chat](https://chat.iris-go.com). - Ignore `"csrf.token"` form data key when missing on `ctx.ReadForm` by default as requested at [#1941](https://github.com/kataras/iris/issues/1941). - Fix [CVE-2020-5398](https://github.com/advisories/GHSA-8wx2-9q48-vm9r). - New `{x:weekday}` path parameter type, example code: ```go // 0 to 7 (leading zeros don't matter) or "Sunday" to "Monday" or "sunday" to "monday". // http://localhost:8080/schedule/monday or http://localhost:8080/schedule/Monday or // http://localhost:8080/schedule/1 or http://localhost:8080/schedule/0001. app.Get("/schedule/{day:weekday}", func(ctx iris.Context) { day, _ := ctx.Params().GetWeekday("day") ctx.Writef("Weekday requested was: %v\n", day) }) ``` - Make the `Context.JSON` method customizable by modifying the `context.WriteJSON` package-level function. - Add new `iris.NewGuide` which helps you build a simple and nice JSON API with services as dependencies and better design pattern. - Make `Context.Domain()` customizable by letting developers to modify the `Context.GetDomain` package-level function. - Remove Request Context-based Transaction feature as its usage can be replaced with just the Iris Context (as of go1.7+) and better [project](_examples/project) structure. - Fix [#1882](https://github.com/kataras/iris/issues/1882) - Fix [#1877](https://github.com/kataras/iris/issues/1877) - Fix [#1876](https://github.com/kataras/iris/issues/1876) - New `date` dynamic path parameter type. E.g. `/blog/{param:date}` matches to `"/blog/2022/04/21"`. - Add `iris.AllowQuerySemicolons` and `iris.WithoutServerError(iris.ErrURLQuerySemicolon)` to handle golang.org/issue/25192 as reported at: https://github.com/kataras/iris/issues/1875. - Add new `Application.SetContextErrorHandler` to globally customize the default behavior (status code 500 without body) on `JSON`, `JSONP`, `Protobuf`, `MsgPack`, `XML`, `YAML` and `Markdown` method call write errors instead of catching the error on each handler. - Add new [x/pagination](x/pagination/pagination.go) sub-package which supports generics code (go 1.18+). - Add new [middleware/modrevision](middleware/modrevision) middleware (example at [_examples/project/api/router.go]_examples/project/api/router.go). - Add `iris.BuildRevision` and `iris.BuildTime` to embrace the new go's 1.18 debug build information. - ~Add `Context.SetJSONOptions` to customize on a higher level the JSON options on `Context.JSON` calls.~ update: remains as it's, per JSON call. - Add new [auth](auth) sub-package which helps on any user type auth using JWT (access & refresh tokens) and a cookie (optional). - Add `Party.EnsureStaticBindings` which, if called, the MVC binder panics if a struct's input binding depends on the HTTP request data instead of a static dependency. This is useful to make sure your API crafted through `Party.PartyConfigure` depends only on struct values you already defined at `Party.RegisterDependency` == will never use reflection at serve-time (maximum performance). - Add a new [x/sqlx](/x/sqlx/) sub-package ([example](_examples/database/sqlx/main.go)). - Add a new [x/reflex](/x/reflex) sub-package. - Add `Context.ReadMultipartRelated` as requested at: [issues/#1787](https://github.com/kataras/iris/issues/1787). - Add `Container.DependencyMatcher` and `Dependency.Match` to implement the feature requested at [issues/#1842](https://github.com/kataras/iris/issues/1842). - Register [CORS middleware](middleware/cors) to the Application by default when `iris.Default()` is used instead of `iris.New()`. - Add [x/jsonx: DayTime](/x/jsonx/day_time.go) for JSON marshal and unmarshal of "15:04:05" (hour, minute, second). - Fix a bug of `WithoutBodyConsumptionOnUnmarshal` configurator and a minor dependency injection issue caused by the previous alpha version between 20 and 26 February of 2022. - New basic [cors middleware](middleware/cors). - New `httptest.NewServer` helper. - New [x/errors](x/errors) sub-package, helps with HTTP Wire Errors. Example can be found [here](_examples/routing/http-wire-errors/main.go). - New [x/timex](x/timex) sub-package, helps working with weekdays. - Minor improvements to the [JSON Kitchen Time](x/jsonx/kitchen_time.go). - A session database can now implement the `EndRequest(ctx *context.Context, session *Session)` method which will be fired at the end of the request-response lifecycle. - Improvements on JSON and ReadJSON when `Iris.Configuration.EnableOptimizations` is true. The request's Context is used whenever is necessary. - New [monitor](_examples/monitor/monitor-middleware/main.go) middleware. - New `RegisterRequestHandler` package-level and client methods to the new `x/client` package. Control or log the request-response lifecycle. - New `RateLimit` and `Debug` HTTP Client options to the new `x/client` package. - Push a security fix reported by [Kirill Efimov](https://github.com/kirill89) for older go runtimes. - New `Configuration.Timeout` and `Configuration.TimeoutMessage` fields. Use it to set HTTP timeouts. Note that your http server's (`Application.ConfigureHost`) Read/Write timeouts should be a bit higher than the `Configuration.Timeout` in order to give some time to http timeout handler to kick in and be able to send the `Configuration.TimeoutMessage` properly. - New `apps.OnApplicationRegistered` method which listens on new Iris applications hosted under the same binary. Use it on your `init` functions to configure Iris applications by any spot in your project's files. - `Context.JSON` respects any object implements the `easyjson.Marshaler` interface and renders the result using the [easyjon](https://github.com/mailru/easyjson)'s writer. **Set** the `Configuration.EnableProtoJSON` and `Configuration.EnableEasyJSON` to true in order to enable this feature. - minor: `Context` structure implements the standard go Context interface now (includes: Deadline, Done, Err and Value methods). Handlers can now just pass the `ctx iris.Context` as a shortcut of `ctx.Request().Context()` when needed. - New [x/jsonx](x/jsonx) sub-package for JSON type helpers. - New [x/mathx](x/mathx) sub-package for math related functions. - New [/x/client](x/client) HTTP Client sub-package. - New `email` builtin path parameter type. Example: ```go // +------------------------+ // | {param:email} | // +------------------------+ // Email + mx look up path parameter validation. Use it on production. // http://localhost:8080/user/kataras2006@hotmail.com -> OK // http://localhost:8080/user/b-c@invalid_domain -> NOT FOUND app.Get("/user/{user_email:email}", func(ctx iris.Context) { email := ctx.Params().Get("user_email") ctx.WriteString(email) }) // +------------------------+ // | {param:mail} | // +------------------------+ // Simple email path parameter validation. // http://localhost:8080/user/kataras2006@hotmail.com -> OK // http://localhost:8080/user/b-c@invalid_domainxxx1.com -> NOT FOUND app.Get("/user/{local_email:mail}", func(ctx iris.Context) { email := ctx.Params().Get("local_email") ctx.WriteString(email) }) ``` - New `iris.IsErrEmptyJSON(err) bool` which reports whether the given "err" is caused by a `Context.ReadJSON` call when the request body didn't start with { (or it was totally empty). Example Code: ```go func handler(ctx iris.Context) { var opts SearchOptions if err := ctx.ReadJSON(&opts); err != nil && !iris.IsErrEmptyJSON(err) { ctx.StopWithJSON(iris.StatusBadRequest, iris.Map{"message": "unable to parse body"}) return } // [...continue with default values of "opts" struct if the client didn't provide some] } ``` That means that the client can optionally set a JSON body. - New `APIContainer.EnableStrictMode(bool)` to disable automatic payload binding and panic on missing dependencies for exported struct'sfields or function's input parameters on MVC controller or hero function or PartyConfigurator. - New `Party.PartyConfigure(relativePath string, partyReg ...PartyConfigurator) Party` helper, registers a children Party like `Party` and `PartyFunc` but instead it accepts a structure value which may contain one or more of the dependencies registered by `RegisterDependency` or `ConfigureContainer().RegisterDependency` methods and fills the unset/zero exported struct's fields respectfully (useful when the api's dependencies amount are too much to pass on a function). - **New feature:** add the ability to set custom error handlers on path type parameters errors (existing or custom ones). Example Code: ```go app.Macros().Get("uuid").HandleError(func(ctx iris.Context, paramIndex int, err error) { ctx.StatusCode(iris.StatusBadRequest) param := ctx.Params().GetEntryAt(paramIndex) ctx.JSON(iris.Map{ "error": err.Error(), "message": "invalid path parameter", "parameter": param.Key, "value": param.ValueRaw, }) }) app.Get("/users/{id:uuid}", getUser) ``` - Improve the performance and fix `:int, :int8, :int16, :int32, :int64, :uint, :uint8, :uint16, :uint32, :uint64` path type parameters couldn't accept a positive number written with the plus symbol or with a leading zeroes, e.g. `+42` and `021`. - The `iris.WithEmptyFormError` option is respected on `context.ReadQuery` method too, as requested at [#1727](https://github.com/kataras/iris/issues/1727). [Example comments](https://github.com/kataras/iris/blob/main/_examples/request-body/read-query/main.go) were updated. - New `httptest.Strict` option setter to enable the `httpexpect.RequireReporter` instead of the default `httpexpect.AssetReporter. Use that to enable complete test failure on the first error. As requested at: [#1722](https://github.com/kataras/iris/issues/1722). - New `uuid` builtin path parameter type. Example: ```go // +------------------------+ // | {param:uuid} | // +------------------------+ // UUIDv4 (and v1) path parameter validation. // http://localhost:8080/user/bb4f33e4-dc08-40d8-9f2b-e8b2bb615c0e -> OK // http://localhost:8080/user/dsadsa-invalid-uuid -> NOT FOUND app.Get("/user/{id:uuid}", func(ctx iris.Context) { id := ctx.Params().Get("id") ctx.WriteString(id) }) ``` - New `Configuration.KeepAlive` and `iris.WithKeepAlive(time.Duration) Configurator` added as helpers to start the server using a tcp listener featured with keep-alive. - New `DirOptions.ShowHidden bool` is added by [@tuhao1020](https://github.com/tuhao1020) at [PR #1717](https://github.com/kataras/iris/pull/1717) to show or hide the hidden files when `ShowList` is set to true. - New `Context.ReadJSONStream` method and `JSONReader` options for `Context.ReadJSON` and `Context.ReadJSONStream`, see the [example](_examples/request-body/read-json-stream/main.go). - New `FallbackView` feature, per-party or per handler chain. Example can be found at: [_examples/view/fallback](_examples/view/fallback). ```go app.FallbackView(iris.FallbackViewFunc(func(ctx iris.Context, err iris.ErrViewNotExist) error { // err.Name is the previous template name. // err.IsLayout reports whether the failure came from the layout template. // err.Data is the template data provided to the previous View call. // [...custom logic e.g. ctx.View("fallback.html", err.Data)] return err })) ``` - New `versioning.Aliases` middleware and up to 80% faster version resolve. Example Code: ```go app := iris.New() api := app.Party("/api") api.Use(Aliases(map[string]string{ versioning.Empty: "1", // when no version was provided by the client. "beta": "4.0.0", "stage": "5.0.0-alpha" })) v1 := NewGroup(api, ">=1.0.0 <2.0.0") v1.Get/Post... v4 := NewGroup(api, ">=4.0.0 <5.0.0") v4.Get/Post... stage := NewGroup(api, "5.0.0-alpha") stage.Get/Post... ``` - New [Basic Authentication](https://github.com/kataras/iris/tree/main/middleware/basicauth) middleware. Its `Default` function has not changed, however, the rest, e.g. `New` contains breaking changes as the new middleware features new functionalities. - Add `iris.DirOptions.SPA bool` field to allow [Single Page Applications](https://github.com/kataras/iris/tree/main/_examples/file-server/single-page-application/basic/main.go) under a file server. - A generic User interface, see the `Context.SetUser/User` methods in the New Context Methods section for more. In-short, the basicauth middleware's stored user can now be retrieved through `Context.User()` which provides more information than the native `ctx.Request().BasicAuth()` method one. Third-party authentication middleware creators can benefit of these two methods, plus the Logout below. - A `Context.Logout` method is added, can be used to invalidate [basicauth](https://github.com/kataras/iris/blob/main/_examples/auth/basicauth/basic/main.go) or [jwt](https://github.com/kataras/iris/blob/main/_examples/auth/jwt/blocklist/main.go) client credentials. - Add the ability to [share functions](https://github.com/kataras/iris/tree/main/_examples/routing/writing-a-middleware/share-funcs) between handlers chain and add an [example](https://github.com/kataras/iris/tree/main/_examples/routing/writing-a-middleware/share-services) on sharing Go structures (aka services). - Add the new `Party.UseOnce` method to the `*Route` - Add a new `*Route.RemoveHandler(...any) int` and `Party.RemoveHandler(...any) Party` methods, delete a handler based on its name or the handler pc function. ```go func middleware(ctx iris.Context) { // [...] } func main() { app := iris.New() // Register the middleware to all matched routes. app.Use(middleware) // Handlers = middleware, other app.Get("/", index) // Handlers = other app.Get("/other", other).RemoveHandler(middleware) } ``` - Redis Driver is now based on the [go-redis](https://github.com/go-redis/redis/) module. Radix and redigo removed entirely. Sessions are now stored in hashes which fixes [issue #1610](https://github.com/kataras/iris/issues/1610). The only breaking change on default configuration is that the `redis.Config.Delim` option was removed. The redis sessions database driver is now defaults to the `&redis.GoRedisDriver{}`. End-developers can implement their own implementations too. The `Database#Close` is now automatically called on interrupt signals, no need to register it by yourself. - Add builtin support for **[i18n pluralization](https://github.com/kataras/iris/tree/main/_examples/i18n/plurals)**. Please check out the [following yaml locale example](https://github.com/kataras/iris/tree/main/_examples/i18n/plurals/locales/en-US/welcome.yml) to see an overview of the supported formats. - Fix [#1650](https://github.com/kataras/iris/issues/1650) - Fix [#1649](https://github.com/kataras/iris/issues/1649) - Fix [#1648](https://github.com/kataras/iris/issues/1648) - Fix [#1641](https://github.com/kataras/iris/issues/1641) - Add `Party.SetRoutesNoLog(disable bool) Party` to disable (the new) verbose logging of next routes. - Add `mvc.Application.SetControllersNoLog(disable bool) *mvc.Application` to disable (the new) verbose logging of next controllers. As requested at [#1630](https://github.com/kataras/iris/issues/1630). - Fix [#1621](https://github.com/kataras/iris/issues/1621) and add a new `cache.WithKey` to customize the cached entry key. - Add a `Response() *http.Response` to the Response Recorder. - Fix Response Recorder `Flush` when transfer-encoding is `chunked`. - Fix Response Recorder `Clone` concurrent access afterwards. - Add a `ParseTemplate` method on view engines to manually parse and add a template from a text as [requested](https://github.com/kataras/iris/issues/1617). [Examples](https://github.com/kataras/iris/tree/main/_examples/view/parse-template). - Full `http.FileSystem` interface support for all **view** engines as [requested](https://github.com/kataras/iris/issues/1575). The first argument of the functions(`HTML`, `Blocks`, `Pug`, `Ace`, `Jet`, `Django`, `Handlebars`) can now be either a directory of `string` type (like before) or a value which completes the `http.FileSystem` interface. The `.Binary` method of all view engines was removed: pass the go-bindata's latest version `AssetFile()` exported function as the first argument instead of string. - Add `Route.ExcludeSitemap() *Route` to exclude a route from sitemap as requested in [chat](https://chat.iris-go.com), also offline routes are excluded automatically now. - Improved tracing (with `app.Logger().SetLevel("debug")`) for routes. Screens: #### DBUG Routes (1) ![DBUG routes 1](https://iris-go.com/static/images/v12.2.0-dbug.png?v=0) #### DBUG Routes (2) ![DBUG routes 2](https://iris-go.com/static/images/v12.2.0-dbug2.png?v=0) #### DBUG Routes (3) ![DBUG routes with Controllers](https://iris-go.com/static/images/v12.2.0-dbug3.png?v=0) - Update the [pprof middleware](https://github.com/kataras/iris/tree/main/middleware/pprof). - New `Controller.HandleHTTPError(mvc.Code) ` optional Controller method to handle http errors as requested at: [MVC - More Elegent OnErrorCode registration?](https://github.com/kataras/iris/issues/1595). Example can be found [here](https://github.com/kataras/iris/tree/main/_examples/mvc/error-handler-http/main.go). ![MVC: HTTP Error Handler Method](https://user-images.githubusercontent.com/22900943/90948989-e04cd300-e44c-11ea-8c97-54d90fb0cbb6.png) - New [Rewrite Engine Middleware](https://github.com/kataras/iris/tree/main/middleware/rewrite). Set up redirection rules for path patterns using the syntax we all know. [Example Code](https://github.com/kataras/iris/tree/main/_examples/routing/rewrite). ```yml RedirectMatch: # REDIRECT_CODE_DIGITS | PATTERN_REGEX | TARGET_REPL # Redirects /seo/* to /* - 301 /seo/(.*) /$1 # Redirects /docs/v12* to /docs - 301 /docs/v12(.*) /docs # Redirects /old(.*) to / - 301 /old(.*) / # Redirects http or https://test.* to http or https://newtest.* - 301 ^(http|https)://test.(.*) $1://newtest.$2 # Handles /*.json or .xml as *?format=json or xml, # without redirect. See /users route. # When Code is 0 then it does not redirect the request, # instead it changes the request URL # and leaves a route handle the request. - 0 /(.*).(json|xml) /$1?format=$2 # Redirects root domain to www. # Creation of a www subdomain inside the Application is unnecessary, # all requests are handled by the root Application itself. PrimarySubdomain: www ``` - New `TraceRoute bool` on [middleware/logger](https://github.com/kataras/iris/tree/main/middleware/logger) middleware. Displays information about the executed route. Also marks the handlers executed. Screenshot: ![logger middleware: TraceRoute screenshot](https://iris-go.com/static/images/github/logger-trace-route.png) - Implement feature request [Log when I18n Translation Fails?](https://github.com/kataras/iris/issues/1593) by using the new `Application.I18n.DefaultMessageFunc` field **before** `I18n.Load`. [Example of usage](https://github.com/kataras/iris/blob/main/_examples/i18n/basic/main.go#L28-L50). - Fix [#1594](https://github.com/kataras/iris/issues/1594) and add a new `PathAfterHandler` which can be set to true to enable the old behavior (not recommended though). - New [apps](https://github.com/kataras/iris/tree/main/apps) subpackage. [Example of usage](https://github.com/kataras/iris/tree/main/_examples/routing/subdomains/redirect/multi-instances). ![apps image example](https://user-images.githubusercontent.com/22900943/90459288-8a54f400-e109-11ea-8dea-20631975c9fc.png) - Fix `AutoTLS` when used with `iris.TLSNoRedirect` [*](https://github.com/kataras/iris/issues/1577). The `AutoTLS` runner can be customized through the new `iris.AutoTLSNoRedirect` instead, read its go documentation. Example of having both TLS and non-TLS versions of the same application without conflicts with letsencrypt `./well-known` path: ![](https://iris-go.com/static/images/github/autotls-1.png) ```go package main import ( "net/http" "time" "github.com/kataras/iris/v12" ) func main() { app := iris.New() app.Logger().SetLevel("debug") app.Get("/", func(ctx iris.Context) { ctx.JSON(iris.Map{ "time": time.Now().Unix(), "tls": ctx.Request().TLS != nil, }) }) var fallbackServer = func(acme func(http.Handler) http.Handler) *http.Server { srv := &http.Server{Handler: acme(app)} go srv.ListenAndServe() return srv } app.Run(iris.AutoTLS(":443", "example.com", "mail@example.com", iris.AutoTLSNoRedirect(fallbackServer))) } ``` - `iris.Minify` middleware to minify responses based on their media/content-type. - `Context.OnCloseErr` and `Context.OnConnectionCloseErr` - to call a function of `func() error` instead of an `iris.Handler` when request is closed or manually canceled. - `Party.UseError(...Handler)` - to register handlers to run before any http errors (e.g. before `OnErrorCode/OnAnyErrorCode` or default error codes when no handler is responsible to handle a specific http status code). - `Party.UseRouter(...Handler) and Party.ResetRouterFilters()` - to register handlers before the main router, useful on handlers that should control whether the router itself should ran or not. Independently of the incoming request's method and path values. These handlers will be executed ALWAYS against ALL incoming matched requests. Example of use-case: CORS. - `*versioning.Group` type is a full `Party` now. - `Party.UseOnce` - either inserts a middleware, or on the basis of the middleware already existing, replace that existing middleware instead. - Ability to register a view engine per group of routes or for the current chain of handlers through `Party.RegisterView` and `Context.ViewEngine` respectfully. - Add [Blocks](_examples/view/template_blocks_0) template engine. - Add [Ace](_examples/view/template_ace_0) template parser to the view engine and other minor improvements. - Fix huge repo size of 55.7MB, which slows down the overall Iris installation experience. Now, go-get performs ~3 times faster. I 've managed it using the [bfg-repo-cleaner](https://github.com/rtyley/bfg-repo-cleaner) tool - an alternative to git-filter-branch command. Watch the small gif below to learn how: [![](https://media.giphy.com/media/U8560aiWTurW4iAOLn/giphy.gif)](https://media.giphy.com/media/U8560aiWTurW4iAOLn/giphy.gif) - [gRPC](https://grpc.io/) features: - New Router [Wrapper](middleware/grpc). - New MVC `.Handle(ctrl, mvc.GRPC{...})` option which allows to register gRPC services per-party (without the requirement of a full wrapper) and optionally strict access to gRPC clients only, see the [example here](_examples/mvc/grpc-compatible). - Add `Configuration.RemoteAddrHeadersForce bool` to force `Context.RemoteAddr() string` to return the first entry of request headers as a fallback instead of the `Request.RemoteAddr` one, as requested at: [1567#issuecomment-663972620](https://github.com/kataras/iris/issues/1567#issuecomment-663972620). - Fix [#1569#issuecomment-663739177](https://github.com/kataras/iris/issues/1569#issuecomment-663739177). - Fix [#1564](https://github.com/kataras/iris/issues/1564). - Fix [#1553](https://github.com/kataras/iris/issues/1553). - New `DirOptions.Cache` to cache assets in-memory among with their compressed contents (in order to be ready to served if client ask). Learn more about this feature by reading [all #1556 comments](https://github.com/kataras/iris/issues/1556#issuecomment-661057446). Usage: ```go var dirOpts = DirOptions{ // [...other options] Cache: DirCacheOptions{ Enable: true, // Don't compress files smaller than 300 bytes. CompressMinSize: 300, // Ignore compress already compressed file types // (some images and pdf). CompressIgnore: iris.MatchImagesAssets, // Gzip, deflate, br(brotli), snappy. Encodings: []string{"gzip", "deflate", "br", "snappy"}, // Log to the stdout the total reduced file size. Verbose: 1, }, } ``` - New `DirOptions.PushTargets` and `PushTargetsRegexp` to push index' assets to the client without additional requests. Inspirated by issue [#1562](https://github.com/kataras/iris/issues/1562). Example matching all `.js, .css and .ico` files (recursively): ```go var dirOpts = iris.DirOptions{ // [...other options] IndexName: "/index.html", PushTargetsRegexp: map[string]*regexp.Regexp{ "/": regexp.MustCompile("((.*).js|(.*).css|(.*).ico)$"), // OR: // "/": iris.MatchCommonAssets, }, Compress: true, } ``` - Update jet parser to v5.0.2, closes [#1551](https://github.com/kataras/iris/issues/1551). It contains two breaking changes by its author: - Relative paths on `extends, import, include...` tmpl functions, e.g. `{{extends "../layouts/application.jet"}}` instead of `layouts/application.jet` - the new [jet.Ranger](https://github.com/CloudyKit/jet/pull/165) interface now requires a `ProvidesIndex() bool` method too - Example has been [updated](https://github.com/kataras/iris/tree/main/_examples/view/template_jet_0) - Fix [#1552](https://github.com/kataras/iris/issues/1552). - Proper listing of root directories on `Party.HandleDir` when its `DirOptions.ShowList` was set to true. - Customize the file/directory listing page through views, see [example](https://github.com/kataras/iris/tree/main/_examples/file-server/file-server). - Socket Sharding as requested at [#1544](https://github.com/kataras/iris/issues/1544). New `iris.WithSocketSharding` Configurator and `SocketSharding bool` setting. - Versioned Controllers feature through the new `mvc.Version` option. See [_examples/mvc/versioned-controller](https://github.com/kataras/iris/blob/main/_examples/mvc/versioned-controller/main.go). - Fix [#1539](https://github.com/kataras/iris/issues/1539). - New [rollbar example](https://github.com/kataras/iris/blob/main/_examples/logging/rollbar/main.go). - New builtin [requestid](https://github.com/kataras/iris/tree/main/middleware/requestid) middleware. - New builtin [JWT](https://github.com/kataras/iris/tree/main/middleware/jwt) middleware based on the fastest JWT implementation; [kataras/jwt](https://github.com/kataras/jwt) featured with optional wire encryption to set claims with sensitive data when necessary. - New `iris.RouteOverlap` route registration rule. `Party.SetRegisterRule(iris.RouteOverlap)` to allow overlapping across multiple routes for the same request subdomain, method, path. See [1536#issuecomment-643719922](https://github.com/kataras/iris/issues/1536#issuecomment-643719922). This allows two or more **MVC Controllers** to listen on the same path based on one or more registered dependencies (see [_examples/mvc/authenticated-controller](https://github.com/kataras/iris/tree/main/_examples/mvc/authenticated-controller)). - `Context.ReadForm` now can return an `iris.ErrEmptyForm` instead of `nil` when the new `Configuration.FireEmptyFormError` is true (when `iris.WithEmptyFormError` is set) on missing form body to read from. - `Configuration.EnablePathIntelligence | iris.WithPathIntelligence` to enable path intelligence automatic path redirection on the most closest path (if any), [example]((https://github.com/kataras/iris/blob/main/_examples/routing/intelligence/main.go) - Enhanced cookie security and management through new `Context.AddCookieOptions` method and new cookie options (look on New Package-level functions section below), [securecookie](https://github.com/kataras/iris/tree/main/_examples/cookies/securecookie) example has been updated. - `Context.RemoveCookie` removes also the Request's specific cookie of the same request lifecycle when `iris.CookieAllowReclaim` is set to cookie options, [example](https://github.com/kataras/iris/tree/main/_examples/cookies/options). - `iris.TLS` can now accept certificates in form of raw `[]byte` contents too. - `iris.TLS` registers a secondary http server which redirects "http://" to their "https://" equivalent requests, unless the new `iris.TLSNoRedirect` host Configurator is provided on `iris.TLS`, e.g. `app.Run(iris.TLS("127.0.0.1:443", "mycert.cert", "mykey.key", iris.TLSNoRedirect))`. There is `iris.AutoTLSNoRedirect` option for `AutoTLS` too. - Fix an [issue](https://github.com/kataras/i18n/issues/1) about i18n loading from path which contains potential language code. - Server will not return neither log the `ErrServerClosed` error if `app.Shutdown` was called manually via interrupt signal(CTRL/CMD+C), note that if the server closed by any other reason the error will be fired as previously (unless `iris.WithoutServerError(iris.ErrServerClosed)`). - Finally, Log level's and Route debug information colorization is respected across outputs. Previously if the application used more than one output destination (e.g. a file through `app.Logger().AddOutput`) the color support was automatically disabled from all, including the terminal one, this problem is fixed now. Developers can now see colors in their terminals while log files are kept with clear text. - New `iris.WithLowercaseRouting` option which forces all routes' paths to be lowercase and converts request paths to their lowercase for matching. - New `app.Validator { Struct(any) error }` field and `app.Validate` method were added. The `app.Validator = ` can be used to integrate a 3rd-party package such as [go-playground/validator](https://github.com/go-playground/validator). If set-ed then Iris `Context`'s `ReadJSON`, `ReadXML`, `ReadMsgPack`, `ReadYAML`, `ReadForm`, `ReadQuery`, `ReadBody` methods will return the validation error on data validation failures. The [read-json-struct-validation](_examples/request-body/read-json-struct-validation) example was updated. - A result of can implement the new `hero.PreflightResult` interface which contains a single method of `Preflight(iris.Context) error`. If this method exists on a custom struct value which is returned from a handler then it will fire that `Preflight` first and if not errored then it will cotninue by sending the struct value as JSON(by-default) response body. - `ctx.JSON, JSONP, XML`: if `iris.WithOptimizations` is NOT passed on `app.Run/Listen` then the indentation defaults to `" "` (four spaces) and `" "` respectfully otherwise it is empty or the provided value. - Hero Handlers (and `app.ConfigureContainer().Handle`) do not have to require `iris.Context` just to call `ctx.Next()` anymore, this is done automatically now. - Improve Remote Address parsing as requested at: [#1453](https://github.com/kataras/iris/issues/1453). Add `Configuration.RemoteAddrPrivateSubnets` to exclude those addresses when fetched by `Configuration.RemoteAddrHeaders` through `context.RemoteAddr() string`. - Fix [#1487](https://github.com/kataras/iris/issues/1487). - Fix [#1473](https://github.com/kataras/iris/issues/1473). ## New Package-level Variables - `iris.DirListRichOptions` to pass on `iris.DirListRich` method. - `iris.DirListRich` to override the default look and feel if the `DirOptions.ShowList` was set to true, can be passed to `DirOptions.DirList` field. - `DirOptions.PushTargets` for http/2 push on index [*](https://github.com/kataras/iris/tree/main/_examples/file-server/http2push/main.go). - `iris.Compression` middleware to compress responses and decode compressed request data respectfully. - `iris.B, KB, MB, GB, TB, PB, EB` for byte units. - `TLSNoRedirect` to disable automatic "http://" to "https://" redirections (see below) - `CookieAllowReclaim`, `CookieAllowSubdomains`, `CookieSameSite`, `CookieSecure` and `CookieEncoding` to bring previously sessions-only features to all cookies in the request. ## New Context Methods - `Context.FormFiles(key string, before ...func(*Context, *multipart.FileHeader) bool) (files []multipart.File, headers []*multipart.FileHeader, err error)` method. - `Context.ReadURL(ptr any) error` shortcut of `ReadParams` and `ReadQuery`. Binds URL dynamic path parameters and URL query parameters to the given "ptr" pointer of a struct value. - `Context.SetUser(User)` and `Context.User() User` to store and retrieve an authenticated client. Read more [here](https://github.com/iris-contrib/middleware/issues/63). - `Context.SetLogoutFunc(fn any, persistenceArgs ...any)` and `Logout(args ...any) error` methods to allow different kind of auth middlewares to be able to set a "logout" a user/client feature with a single function, the route handler may not be aware of the implementation of the authentication used. - `Context.SetFunc(name string, fn any, persistenceArgs ...any)` and `Context.CallFunc(name string, args ...any) ([]reflect.Value, error)` to allow middlewares to share functions dynamically when the type of the function is not predictable, see the [example](https://github.com/kataras/iris/tree/main/_examples/routing/writing-a-middleware/share-funcs) for more. - `Context.TextYAML(any) error` same as `Context.YAML` but with set the Content-Type to `text/yaml` instead (Google Chrome renders it as text). - `Context.IsDebug() bool` reports whether the application is running under debug/development mode. It is a shortcut of Application.Logger().Level >= golog.DebugLevel. - `Context.IsRecovered() bool` reports whether the current request was recovered from the [recover middleware](https://github.com/kataras/iris/tree/main/middleware/recover). Also the `Context.GetErrPublic() (bool, error)`, `Context.SetErrPrivate(err error)` methods and `iris.ErrPrivate` interface have been introduced. - `Context.RecordRequestBody(bool)` same as the Application's `DisableBodyConsumptionOnUnmarshal` configuration field but registers per chain of handlers. It makes the request body readable more than once. - `Context.IsRecordingBody() bool` reports whether the request body can be readen multiple times. - `Context.ReadHeaders(ptr any) error` binds request headers to "ptr". [Example](https://github.com/kataras/iris/blob/main/_examples/request-body/read-headers/main.go). - `Context.ReadParams(ptr any) error` binds dynamic path parameters to "ptr". [Example](https://github.com/kataras/iris/blob/main/_examples/request-body/read-params/main.go). - `Context.SaveFormFile(fh *multipart.FileHeader, dest string) (int64, error)` previously unexported. Accepts a result file of `Context.FormFile` and saves it to the disk. - `Context.URLParamSlice(name string) []string` is a a shortcut of `ctx.Request().URL.Query()[name]`. Like `URLParam` but it returns all values as a string slice instead of a single string separated by commas. Note that it skips any empty values (e.g. https://iris-go.com?values=). - `Context.PostValueMany(name string) (string, error)` returns the post data of a given key. The returned value is a single string separated by commas on multiple values. It also reports whether the form was empty or when the "name" does not exist or whether the available values are empty. It strips any empty key-values from the slice before return. See `ErrEmptyForm`, `ErrNotFound` and `ErrEmptyFormField` respectfully. The `PostValueInt`, `PostValueInt64`, `PostValueFloat64` and `PostValueBool` now respect the above errors too (the `PostValues` method now returns a second output argument of `error` too, see breaking changes below). - `Context.URLParamsSorted() []memstore.StringEntry` returns a sorted (by key) slice of key-value entries of the URL Query parameters. - `Context.ViewEngine(ViewEngine)` to set a view engine on-fly for the current chain of handlers, responsible to render templates through `ctx.View`. [Example](_examples/view/context-view-engine). - `Context.SetErr(error)` and `Context.GetErr() error` helpers. - `Context.CompressWriter(bool) error` and `Context.CompressReader(bool) error`. - `Context.Clone() Context` returns a copy of the Context safe for concurrent access. - `Context.IsCanceled() bool` reports whether the request has been canceled by the client. - `Context.IsSSL() bool` reports whether the request is under HTTPS SSL (New `Configuration.SSLProxyHeaders` and `HostProxyHeaders` fields too). - `Context.CompressReader(enable bool)` method and `iris.CompressReader` middleware to enable future request read body calls to decompress data, [example](_examples/compression/main.go). - `Context.RegisterDependency(v any)` and `Context.UnregisterDependency(typ reflect.Type)` to register/remove struct dependencies on serve-time through a middleware. - `Context.SetID(id any)` and `Context.GetID() any` added to register a custom unique indetifier to the Context, if necessary. - `Context.Scheme() string` returns the full scheme of the request URL. - `Context.SubdomainFull() string` returns the full subdomain(s) part of the host (`host[0:rootLevelDomain]`). - `Context.Domain() string` returns the root level domain. - `Context.AddCookieOptions(...CookieOption)` adds options for `SetCookie`, `SetCookieKV, UpsertCookie` and `RemoveCookie` methods for the current request. - `Context.ClearCookieOptions()` clears any cookie options registered through `AddCookieOptions`. - `Context.SetLanguage(langCode string)` force-sets a language code from inside a middleare, similar to the `app.I18n.ExtractFunc` - `Context.ServeContentWithRate`, `ServeFileWithRate` and `SendFileWithRate` methods to throttle the "download" speed of the client - `Context.IsHTTP2() bool` reports whether the protocol version for incoming request was HTTP/2 - `Context.IsGRPC() bool` reports whether the request came from a gRPC client - `Context.UpsertCookie(*http.Cookie, cookieOptions ...context.CookieOption)` upserts a cookie, fixes [#1485](https://github.com/kataras/iris/issues/1485) too - `Context.StopWithStatus(int)` stops the handlers chain and writes the status code - `StopWithText(statusCode int, format string, args ...any)` stops the handlers chain, writes thre status code and a plain text message - `Context.StopWithError(int, error)` stops the handlers chain, writes thre status code and the error's message - `Context.StopWithJSON(int, any)` stops the handlers chain, writes the status code and sends a JSON response - `Context.StopWithProblem(int, iris.Problem)` stops the handlers, writes the status code and sends an `application/problem+json` response - `Context.Protobuf(proto.Message)` sends protobuf to the client (note that the `Context.JSON` is able to send protobuf as JSON) - `Context.MsgPack(any)` sends msgpack format data to the client - `Context.ReadProtobuf(ptr)` binds request body to a proto message - `Context.ReadJSONProtobuf(ptr, ...options)` binds JSON request body to a proto message - `Context.ReadMsgPack(ptr)` binds request body of a msgpack format to a struct - `Context.ReadBody(ptr)` binds the request body to the "ptr" depending on the request's Method and Content-Type - `Context.ReflectValue() []reflect.Value` stores and returns the `[]reflect.ValueOf(ctx)` - `Context.Controller() reflect.Value` returns the current MVC Controller value. ## MVC & Dependency Injection The new release contains a fresh new and awesome feature....**a function dependency can accept previous registered dependencies and update or return a new value of any type**. The new implementation is **faster** on both design and serve-time. The most common scenario from a route to handle is to: - accept one or more path parameters and request data, a payload - send back a response, a payload (JSON, XML,...) The new Iris Dependency Injection feature is about **33.2% faster** than its predecessor on the above case. This drops down even more the performance cost between native handlers and dynamic handlers with dependencies. This reason itself brings us, with safety and performance-wise, to the new `Party.ConfigureContainer(builder ...func(*iris.APIContainer)) *APIContainer` method which returns methods such as `Handle(method, relativePath string, handlersFn ...any) *Route` and `RegisterDependency`. Look how clean your codebase can be when using Iris': ```go package main import "github.com/kataras/iris/v12" type ( testInput struct { Email string `json:"email"` } testOutput struct { ID int `json:"id"` Name string `json:"name"` } ) func handler(id int, in testInput) testOutput { return testOutput{ ID: id, Name: in.Email, } } func main() { app := iris.New() app.ConfigureContainer(func(api *iris.APIContainer) { api.Post("/{id:int}", handler) }) app.Listen(":5000", iris.WithOptimizations) } ``` Your eyes don't lie you. You read well, no `ctx.ReadJSON(&v)` and `ctx.JSON(send)` neither `error` handling are presented. It is a huge relief but if you ever need, you still have the control over those, even errors from dependencies. Here is a quick list of the new Party.ConfigureContainer()'s fields and methods: ```go // Container holds the DI Container of this Party featured Dependency Injection. // Use it to manually convert functions or structs(controllers) to a Handler. Container *hero.Container ``` ```go // OnError adds an error handler for this Party's DI Hero Container and its handlers (or controllers). // The "errorHandler" handles any error may occurred and returned // during dependencies injection of the Party's hero handlers or from the handlers themselves. OnError(errorHandler func(iris.Context, error)) ``` ```go // RegisterDependency adds a dependency. // The value can be a single struct value or a function. // Follow the rules: // * {structValue} // * func(accepts ) returns or (, error) // * func(accepts iris.Context) returns or (, error) // // A Dependency can accept a previous registered dependency and return a new one or the same updated. // * func(accepts1 , accepts2 ) returns or (, error) or error // * func(acceptsPathParameter1 string, id uint64) returns or (, error) // // Usage: // // - RegisterDependency(loggerService{prefix: "dev"}) // - RegisterDependency(func(ctx iris.Context) User {...}) // - RegisterDependency(func(User) OtherResponse {...}) RegisterDependency(dependency any) // UseResultHandler adds a result handler to the Container. // A result handler can be used to inject the returned struct value // from a request handler or to replace the default renderer. UseResultHandler(handler func(next iris.ResultHandler) iris.ResultHandler) ```
ResultHandler ```go type ResultHandler func(ctx iris.Context, v any) error ```
```go // Use same as a common Party's "Use" but it accepts dynamic functions as its "handlersFn" input. Use(handlersFn ...any) // Done same as a common Party's but it accepts dynamic functions as its "handlersFn" input. Done(handlersFn ...any) ``` ```go // Handle same as a common Party's `Handle` but it accepts one or more "handlersFn" functions which each one of them // can accept any input arguments that match with the Party's registered Container's `Dependencies` and // any output result; like custom structs , string, []byte, int, error, // a combination of the above, hero.Result(hero.View | hero.Response) and more. // // It's common from a hero handler to not even need to accept a `Context`, for that reason, // the "handlersFn" will call `ctx.Next()` automatically when not called manually. // To stop the execution and not continue to the next "handlersFn" // the end-developer should output an error and return `iris.ErrStopExecution`. Handle(method, relativePath string, handlersFn ...any) *Route // Get registers a GET route, same as `Handle("GET", relativePath, handlersFn....)`. Get(relativePath string, handlersFn ...any) *Route // and so on... ``` Prior to this version the `iris.Context` was the only one dependency that has been automatically binded to the handler's input or a controller's fields and methods, read below to see what types are automatically binded: | Type | Maps To | |------|:---------| | [*mvc.Application](https://pkg.go.dev/github.com/kataras/iris/v12/mvc?tab=doc#Application) | Current MVC Application | | [iris.Context](https://pkg.go.dev/github.com/kataras/iris/v12/context?tab=doc#Context) | Current Iris Context | | [*sessions.Session](https://pkg.go.dev/github.com/kataras/iris/v12/sessions?tab=doc#Session) | Current Iris Session | | [context.Context](https://golang.org/pkg/context/#Context) | [ctx.Request().Context()](https://golang.org/pkg/net/http/#Request.Context) | | [*http.Request](https://golang.org/pkg/net/http/#Request) | `ctx.Request()` | | [http.ResponseWriter](https://golang.org/pkg/net/http/#ResponseWriter) | `ctx.ResponseWriter()` | | [http.Header](https://golang.org/pkg/net/http/#Header) | `ctx.Request().Header` | | [time.Time](https://golang.org/pkg/time/#Time) | `time.Now()` | | [*golog.Logger](https://pkg.go.dev/github.com/kataras/golog) | Iris Logger | | [net.IP](https://golang.org/pkg/net/#IP) | `net.ParseIP(ctx.RemoteAddr())` | | [mvc.Code](https://pkg.go.dev/github.com/kataras/iris/v12/mvc?tab=doc#Code) | `ctx.GetStatusCode() int` | | [mvc.Err](https://pkg.go.dev/github.com/kataras/iris/v12/mvc?tab=doc#Err) | `ctx.GetErr() error` | | [iris/context.User](https://pkg.go.dev/github.com/kataras/iris/v12/context?tab=doc#User) | `ctx.User()` | | `string`, | | | `int, int8, int16, int32, int64`, | | | `uint, uint8, uint16, uint32, uint64`, | | | `float, float32, float64`, | | | `bool`, | | | `slice` | [Path Parameter](https://github.com/kataras/iris/blob/main/_examples/routing/dynamic-path/main.go#L20) | | Struct | [Request Body](https://github.com/kataras/iris/tree/main/_examples/request-body) of `JSON`, `XML`, `YAML`, `Form`, `URL Query`, `Protobuf`, `MsgPack` | Here is a preview of what the new Hero handlers look like: ### Request & Response & Path Parameters **1.** Declare Go types for client's request body and a server's response. ```go type ( request struct { Firstname string `json:"firstname"` Lastname string `json:"lastname"` } response struct { ID uint64 `json:"id"` Message string `json:"message"` } ) ``` **2.** Create the route handler. Path parameters and request body are binded automatically. - **id uint64** binds to "id:uint64" - **input request** binds to client request data such as JSON ```go func updateUser(id uint64, input request) response { return response{ ID: id, Message: "User updated successfully", } } ``` **3.** Configure the container per group and register the route. ```go app.Party("/user").ConfigureContainer(container) func container(api *iris.APIContainer) { api.Put("/{id:uint64}", updateUser) } ``` **4.** Simulate a [client](https://curl.haxx.se/download.html) request which sends data to the server and displays the response. ```sh curl --request PUT -d '{"firstanme":"John","lastname":"Doe"}' http://localhost:8080/user/42 ``` ```json { "id": 42, "message": "User updated successfully" } ``` ### Custom Preflight Before we continue to the next section, register dependencies, you may want to learn how a response can be customized through the `iris.Context` right before sent to the client. The server will automatically execute the `Preflight(iris.Context) error` method of a function's output struct value right before send the response to the client. Take for example that you want to fire different HTTP status codes depending on the custom logic inside your handler and also modify the value(response body) itself before sent to the client. Your response type should contain a `Preflight` method like below. ```go type response struct { ID uint64 `json:"id,omitempty"` Message string `json:"message"` Code int `json:"code"` Timestamp int64 `json:"timestamp,omitempty"` } func (r *response) Preflight(ctx iris.Context) error { if r.ID > 0 { r.Timestamp = time.Now().Unix() } if r.Code > 0 { ctx.StatusCode(r.Code) } return nil } ``` Now, each handler that returns a `*response` value will call the `response.Preflight` method automatically. ```go func deleteUser(db *sql.DB, id uint64) *response { // [...custom logic] return &response{ Message: "User has been marked for deletion", Code: iris.StatusAccepted, } } ``` If you register the route and fire a request you should see an output like this, the timestamp is filled and the HTTP status code of the response that the client will receive is 202 (Status Accepted). ```json { "message": "User has been marked for deletion", "code": 202, "timestamp": 1583313026 } ``` ### Register Dependencies **1.** Import packages to interact with a database. The go-sqlite3 package is a database driver for [SQLite](https://www.sqlite.org/index.html). ```go import "database/sql" import _ "github.com/mattn/go-sqlite3" ``` **2.** Configure the container ([see above](#request--response--path-parameters)), register your dependencies. Handler expects an *sql.DB instance. ```go localDB, _ := sql.Open("sqlite3", "./foo.db") api.RegisterDependency(localDB) ``` **3.** Register a route to create a user. ```go api.Post("/{id:uint64}", createUser) ``` **4.** The create user Handler. The handler accepts a database and some client request data such as JSON, Protobuf, Form, URL Query and e.t.c. It Returns a response. ```go func createUser(db *sql.DB, user request) *response { // [custom logic using the db] userID, err := db.CreateUser(user) if err != nil { return &response{ Message: err.Error(), Code: iris.StatusInternalServerError, } } return &response{ ID: userID, Message: "User created", Code: iris.StatusCreated, } } ``` **5.** Simulate a [client](https://curl.haxx.se/download.html) to create a user. ```sh # JSON curl --request POST -d '{"firstname":"John","lastname":"Doe"}' \ --header 'Content-Type: application/json' \ http://localhost:8080/user ``` ```sh # Form (multipart) curl --request POST 'http://localhost:8080/users' \ --header 'Content-Type: multipart/form-data' \ --form 'firstname=John' \ --form 'lastname=Doe' ``` ```sh # Form (URL-encoded) curl --request POST 'http://localhost:8080/users' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'firstname=John' \ --data-urlencode 'lastname=Doe' ``` ```sh # URL Query curl --request POST 'http://localhost:8080/users?firstname=John&lastname=Doe' ``` Response: ```json { "id": 42, "message": "User created", "code": 201, "timestamp": 1583313026 } ``` ## Breaking Changes - The `versioning.NewMatcher` has been removed entirely in favor of `NewGroup`. Strict versions format on `versioning.NewGroup` is required. E.g. `"1"` is not valid anymore, you have to specify `"1.0.0"`. Example: `NewGroup(api, ">=1.0.0 <2.0.0")`. The [routing/versioning](_examples/routing/versioning) examples have been updated. - Now that `RegisterView` can be used to register different view engines per-Party, there is no need to support registering multiple engines under the same Party. The `app.RegisterView` now upserts the given Engine instead of append. You can now render templates **without file extension**, e.g. `index` instead of `index.ace`, both forms are valid now. - The `Context.ContentType` does not accept filenames to resolve the mime type anymore (caused issues with vendor-specific(vnd) MIME types). - The `Configuration.RemoteAddrPrivateSubnets.IPRange.Start and End` are now type of `string` instead of `net.IP`. The `WithRemoteAddrPrivateSubnet` option remains as it is, already accepts `string`s. - The `i18n#LoaderConfig.FuncMap template.FuncMap` field was replaced with `Funcs func(iris.Locale) template.FuncMap` in order to give current locale access to the template functions. A new `app.I18n.Loader` was introduced too, in order to make it easier for end-developers to customize the translation key values. - Request Logger's `Columns bool` field has been removed. Use the new [accesslog](https://github.com/kataras/iris/tree/main/_examples/logging/request-logger/accesslog/main.go) middleware instead. - The `.Binary` method of all view engines was removed: pass the go-bindata's latest version `AssetFile()` exported function as the first argument instead of string. All examples updated. - `ContextUploadFormFiles(destDirectory string, before ...func(*Context, *multipart.FileHeader) bool) (uploaded []*multipart.FileHeader, n int64, err error)` now returns the total files uploaded too (as its first parameter) and the "before" variadic option should return a boolean, if false then the specific file is skipped. - `Context.PostValues(name string) ([]string, error)` now returns a second output argument of `error` type too, which reports `ErrEmptyForm` or `ErrNotFound` or `ErrEmptyFormField`. The single post value getters now returns the **last value** if multiple was given instead of the first one (this allows clients to append values on flow updates). - `Party.GetReporter()` **removed**. The `Application.Build` returns the first error now and the API's errors are logged, this allows the server to run even if some of the routes are invalid but not fatal to the entire application (it was a request from a company). - `versioning.NewGroup(string)` now accepts a `Party` as its first input argument: `NewGroup(Party, string)`. - `versioning.RegisterGroups` is **removed** as it is no longer necessary. - `Configuration.RemoteAddrHeaders` from `map[string]bool` to `[]string`. If you used `With(out)RemoteAddrHeader` then you are ready to proceed without any code changes for that one. - `ctx.Gzip(boolean)` replaced with `ctx.CompressWriter(boolean) error`. - `ctx.GzipReader(boolean) error` replaced with `ctx.CompressReader(boolean) error`. - `iris.Gzip` and `iris.GzipReader` replaced with `iris.Compression` (middleware). - `ctx.ClientSupportsGzip() bool` replaced with `ctx.ClientSupportsEncoding("gzip", "br" ...) bool`. - `ctx.GzipResponseWriter()` is **removed**. - `Party.HandleDir/iris.FileServer` now accepts both `http.FileSystem` and `string` and returns a list of `[]*Route` (GET and HEAD) instead of GET only. You can write: both `app.HandleDir("/", iris.Dir("./assets"))` and `app.HandleDir("/", "./assets")` and `DirOptions.Asset, AssetNames, AssetInfo` removed, use `go-bindata -fs [..]` and `app.HandleDir("/", AssetFile())` instead. - `Context.OnClose` and `Context.OnCloseConnection` now both accept an `iris.Handler` instead of a simple `func()` as their callback. - `Context.StreamWriter(writer func(w io.Writer) bool)` changed to `StreamWriter(writer func(w io.Writer) error) error` and it's now the `Context.Request().Context().Done()` channel that is used to receive any close connection/manual cancel signals, instead of the deprecated `ResponseWriter().CloseNotify()` one. Same for the `Context.OnClose` and `Context.OnCloseConnection` methods. - Fixed handler's error response not be respected when response recorder was used instead of the common writer. Fixes [#1531](https://github.com/kataras/iris/issues/1531). It contains a **BREAKING CHANGE** of: the new `Configuration.ResetOnFireErrorCode` field should be set **to true** in order to behave as it used before this update (to reset the contents on recorder). - `Context.String()` (rarely used by end-developers) it does not return a unique string anymore, to achieve the old representation you must call the new `Context.SetID` method first. - `iris.CookieEncode` and `CookieDecode` are replaced with the `iris.CookieEncoding`. - `sessions#Config.Encode` and `Decode` are removed in favor of (the existing) `Encoding` field. - `versioning.GetVersion` now returns an empty string if version wasn't found. - Change the MIME type of `Javascript .js` and `JSONP` as the HTML specification now recommends to `"text/javascript"` instead of the obselete `"application/javascript"`. This change was pushed to the `Go` language itself as well. See . - Remove the last input argument of `enableGzipCompression` in `Context.ServeContent`, `ServeFile` methods. This was deprecated a few versions ago. A middleware (`app.Use(iris.CompressWriter)`) or a prior call to `Context.CompressWriter(true)` will enable compression. Also these two methods and `Context.SendFile` one now support `Content-Range` and `Accept-Ranges` correctly out of the box (`net/http` had a bug, which is now fixed). - `Context.ServeContent` no longer returns an error, see `ServeContentWithRate`, `ServeFileWithRate` and `SendFileWithRate` new methods too. - `route.Trace() string` changed to `route.Trace(w io.Writer)`, to achieve the same result just pass a `bytes.Buffer` - `var mvc.AutoBinding` removed as the default behavior now resolves such dependencies automatically (see [[FEATURE REQUEST] MVC serving gRPC-compatible controller](https://github.com/kataras/iris/issues/1449)). - `mvc#Application.SortByNumMethods()` removed as the default behavior now binds the "thinnest" empty `any` automatically (see [MVC: service injecting fails](https://github.com/kataras/iris/issues/1343)). - `mvc#BeforeActivation.Dependencies().Add` should be replaced with `mvc#BeforeActivation.Dependencies().Register` instead - **REMOVE** the `kataras/iris/v12/typescript` package in favor of the new [iris-cli](https://github.com/kataras/iris-cli). Also, the alm typescript online editor was removed as it is deprecated by its author, please consider using the [designtsx](https://designtsx.com/) instead. # Su, 16 February 2020 | v12.1.8 New Features: - [[FEATURE REQUEST] MVC serving gRPC-compatible controller](https://github.com/kataras/iris/issues/1449) Fixes: - [App can't find embedded pug template files by go-bindata](https://github.com/kataras/iris/issues/1450) New Examples: - [_examples/mvc/grpc-compatible](_examples/mvc/grpc-compatible) # Mo, 10 February 2020 | v12.1.7 Implement **new** `SetRegisterRule(iris.RouteOverride, RouteSkip, RouteError)` to resolve: https://github.com/kataras/iris/issues/1448 New Examples: - [_examples/routing/route-register-rule](_examples/routing/route-register-rule) # We, 05 February 2020 | v12.1.6 Fixes: - [jet.View - urlpath error](https://github.com/kataras/iris/issues/1438) - [Context.ServeFile send 'application/wasm' with a wrong extra field](https://github.com/kataras/iris/issues/1440) # Su, 02 February 2020 | v12.1.5 Various improvements and linting. # Su, 29 December 2019 | v12.1.4 Minor fix on serving embedded files. # We, 25 December 2019 | v12.1.3 Fix [[BUG] [iris.Default] RegisterView](https://github.com/kataras/iris/issues/1410) # Th, 19 December 2019 | v12.1.2 Fix [[BUG]Session works incorrectly when meets the multi-level TLDs](https://github.com/kataras/iris/issues/1407). # Mo, 16 December 2019 | v12.1.1 Add [Context.FindClosest(n int) []string](https://github.com/kataras/iris/blob/main/_examples/routing/intelligence/manual/main.go#L22) ```go app := iris.New() app.OnErrorCode(iris.StatusNotFound, notFound) ``` ```go func notFound(ctx iris.Context) { suggestPaths := ctx.FindClosest(3) if len(suggestPaths) == 0 { ctx.WriteString("404 not found") return } ctx.HTML("Did you mean?
    ") for _, s := range suggestPaths { ctx.HTML(`
  • %s
  • `, s, s) } ctx.HTML("
") } ``` ![](https://iris-go.com/static/images/iris-not-found-suggests.png) # Fr, 13 December 2019 | v12.1.0 ## Breaking Changes Minor as many of you don't even use them but, indeed, they need to be covered here. - Old i18n middleware(iris/middleware/i18n) was replaced by the [i18n](i18n) sub-package which lives as field at your application: `app.I18n.Load(globPathPattern string, languages ...string)` (see below) - Community-driven i18n middleware(iris-contrib/middleware/go-i18n) has a `NewLoader` function which returns a loader which can be passed at `app.I18n.Reset(loader i18n.Loader, languages ...string)` to change the locales parser - The Configuration's `TranslateFunctionContextKey` was replaced by `LocaleContextKey` which Context store's value (if i18n is used) returns the current Locale which contains the translate function, the language code, the language tag and the index position of it - The `context.Translate` method was replaced by `context.Tr` as a shortcut for the new `context.GetLocale().GetMessage(format, args...)` method and it matches the view's function `{{tr format args}}` too - If you used [Iris Django](https://github.com/kataras/iris/tree/main/_examples/view/template_django_0) view engine with `import _ github.com/flosch/pongo2-addons` you **must change** the import path to `_ github.com/iris-contrib/pongo2-addons` or add a [go mod replace](https://github.com/golang/go/wiki/Modules#when-should-i-use-the-replace-directive) to your `go.mod` file, e.g. `replace github.com/flosch/pongo2-addons => github.com/iris-contrib/pongo2-addons v0.0.1`. ## Fixes All known issues. 1. [#1395](https://github.com/kataras/iris/issues/1395) 2. [#1369](https://github.com/kataras/iris/issues/1369) 3. [#1399](https://github.com/kataras/iris/issues/1399) with PR [#1400](https://github.com/kataras/iris/pull/1400) 4. [#1401](https://github.com/kataras/iris/issues/1401) 5. [#1406](https://github.com/kataras/iris/issues/1406) 6. [neffos/#20](https://github.com/kataras/neffos/issues/20) 7. [pio/#5](https://github.com/kataras/pio/issues/5) ## New Features ### Internationalization and localization Support for i18n is now a **builtin feature** and is being respected across your entire application, per say [sitemap](https://github.com/kataras/iris/blob/main/_examples/routing/sitemap/main.go) and [views](https://github.com/kataras/iris/blob/main/_examples/i18n/basic/main.go#L50). ### Sitemaps Iris generates and serves one or more [sitemap.xml](https://www.sitemaps.org/protocol.html) for your static routes. Navigate through: https://github.com/kataras/iris/blob/main/_examples/routing/sitemap/main.go for more. ## New Examples 2. [_examples/i18n](_examples/i18n) 1. [_examples/sitemap](_examples/routing/sitemap) 3. [_examples/desktop/blink](_examples/desktop/blink) 4. [_examples/desktop/lorca](_examples/desktop/lorca) 5. [_examples/desktop/webview](_examples/desktop/webview) # Sa, 26 October 2019 | v12.0.0 - Add version suffix of the **import path**, learn why and see what people voted at [issue #1370](https://github.com/kataras/iris/issues/1370) ![](https://iris-go.com/static/images/vote-v12-version-suffix_26_oct_2019.png) - All errors are now compatible with go1.13 `errors.Is`, `errors.As` and `fmt.Errorf` and a new `core/errgroup` package created - Fix [#1383](https://github.com/kataras/iris/issues/1383) - Report whether system couldn't find the directory of view templates - Remove the `Party#GetReport` method, keep `Party#GetReporter` which is an `error` and an `errgroup.Group`. - Remove the router's deprecated methods such as StaticWeb and StaticEmbedded_XXX - The `Context#CheckIfModifiedSince` now returns an `context.ErrPreconditionFailed` type of error when client conditions are not met. Usage: `if errors.Is(err, context.ErrPreconditionFailed) { ... }` - Add `SourceFileName` and `SourceLineNumber` to the `Route`, reports the exact position of its registration inside your project's source code. - Fix a bug about the MVC package route binding, see [PR #1364](https://github.com/kataras/iris/pull/1364) - Add `mvc/Application#SortByNumMethods` as requested at [#1343](https://github.com/kataras/iris/issues/1343#issuecomment-524868164) - Add status code `103 Early Hints` - Fix performance of session.UpdateExpiration on 200 thousands+ keys with new radix as reported at [issue #1328](https://github.com/kataras/iris/issues/1328) - New redis session database configuration field: `Driver: redis.Redigo()` or `redis.Radix()`, see [updated examples](_examples/sessions/database/redis/) - Add Clusters support for redis:radix session database (`Driver: redis:Radix()`) as requested at [issue #1339](https://github.com/kataras/iris/issues/1339) - Create Iranian [README_FA](README_FA.md) translation with [PR #1360](https://github.com/kataras/iris/pull/1360) - Create Korean [README_KO](README_KO.md) translation with [PR #1356](https://github.com/kataras/iris/pull/1356) - Create Spanish [README_ES](README_ES.md) and [HISTORY_ES](HISTORY_ES.md) translations with [PR #1344](https://github.com/kataras/iris/pull/1344). The iris-contrib/middleare and examples are updated to use the new `github.com/kataras/iris/v12` import path. # Fr, 16 August 2019 | v11.2.8 - Set `Cookie.SameSite` to `Lax` when subdomains sessions share is enabled[*](https://github.com/kataras/iris/commit/6bbdd3db9139f9038641ce6f00f7b4bab6e62550) - Add and update all [experimental handlers](https://github.com/iris-contrib/middleware) - New `XMLMap` function which wraps a `map[string]any` and converts it to a valid xml content to render through `Context.XML` method - Add new `ProblemOptions.XML` and `RenderXML` fields to render the `Problem` as XML(application/problem+xml) instead of JSON("application/problem+json) and enrich the `Negotiate` to easily accept the `application/problem+xml` mime. Commit log: https://github.com/kataras/iris/compare/v11.2.7...v11.2.8 # Th, 15 August 2019 | v11.2.7 This minor version contains improvements on the Problem Details for HTTP APIs implemented on [v11.2.5](#mo-12-august-2019--v1125). - Fix https://github.com/kataras/iris/issues/1335#issuecomment-521319721 - Add `ProblemOptions` with `RetryAfter` as requested at: https://github.com/kataras/iris/issues/1335#issuecomment-521330994. - Add `iris.JSON` alias for `context#JSON` options type. [Example](https://github.com/kataras/iris/blob/45d7c6fedb5adaef22b9730592255f7bb375e809/_examples/routing/http-errors/main.go#L85) updated. References: - https://tools.ietf.org/html/rfc7231#section-7.1.3 - https://tools.ietf.org/html/rfc7807 Commit log: https://github.com/kataras/iris/compare/v11.2.6...v11.2.7 # We, 14 August 2019 | v11.2.6 Allow [handle more than one route with the same paths and parameter types but different macro validation functions](https://github.com/kataras/iris/issues/1058#issuecomment-521110639). ```go app.Get("/{alias:string regexp(^[a-z0-9]{1,10}\\.xml$)}", PanoXML) app.Get("/{alias:string regexp(^[a-z0-9]{1,10}$)}", Tour) ``` Commit log: https://github.com/kataras/iris/compare/v11.2.5...v11.2.6 # Mo, 12 August 2019 | v11.2.5 - [New Feature: Problem Details for HTTP APIs](https://github.com/kataras/iris/pull/1336) - [Add Context.AbsoluteURI](https://github.com/kataras/iris/pull/1336/files#diff-15cce7299aae8810bcab9b0bf9a2fdb1R2368) Commit log: https://github.com/kataras/iris/compare/v11.2.4...v11.2.5 # Fr, 09 August 2019 | v11.2.4 - Fixes [iris.Jet: no view engine found for '.jet' or '.html'](https://github.com/kataras/iris/issues/1327) - Fixes [ctx.ViewData not work with JetEngine](https://github.com/kataras/iris/issues/1330) - **New Feature**: [HTTP Method Override](https://github.com/kataras/iris/issues/1325) - Fixes [Poor performance of session.UpdateExpiration on 200 thousands+ keys with new radix lib](https://github.com/kataras/iris/issues/1328) by introducing the `sessions.Config.Driver` configuration field which defaults to `Redigo()` but can be set to `Radix()` too, future additions are welcomed. Commit log: https://github.com/kataras/iris/compare/v11.2.3...v11.2.4 # Tu, 30 July 2019 | v11.2.3 - [New Feature: Handle different parameter types in the same path](https://github.com/kataras/iris/issues/1315) - [New Feature: Content Negotiation](https://github.com/kataras/iris/issues/1319) - [Context.ReadYAML](https://github.com/kataras/iris/tree/main/_examples/request-body/read-yaml) - Fixes https://github.com/kataras/neffos/issues/1#issuecomment-515698536 # We, 24 July 2019 | v11.2.2 Sessions as middleware: ```go import "github.com/kataras/iris/v12/sessions" // [...] app := iris.New() sess := sessions.New(sessions.Config{...}) app.Get("/path", func(ctx iris.Context){ session := sessions.Get(ctx) // [work with session...] }) ``` - Add `Session.Len() int` to return the total number of stored values/entries. - Make `Context.HTML` and `Context.Text` to accept an optional, variadic, `args ...any` input arg(s) too. ## v11.1.1 - https://github.com/kataras/iris/issues/1298 - https://github.com/kataras/iris/issues/1207 # Tu, 23 July 2019 | v11.2.0 Read about the new release at: https://www.facebook.com/iris.framework/posts/3276606095684693 ================================================ FILE: LICENSE ================================================ BSD 3-Clause License Copyright (c) 2016-2026, Gerasimos (Makis) Maropoulos All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: NOTICE ================================================ ================================================================================ Third-Party Software for iris ================================================================================ The following 3rd-party software packages may be used by or distributed with iris. This document was automatically generated by FOSSA on 4 Oct 2020; any information relevant to third-party vendors listed below are collected using common, reasonable means. Revision ID: 5fc50a00491616d5cd0cbce3abd8b699838e25ca ----------------- ----------------- ------------------------------------------ Package Version Website ----------------- ----------------- ------------------------------------------ ace ea038f4770b6746 https://github.com/yosssi/ace c3f8f84f14fa60d 9fe1205b56 badger 536fed1846d0f4d https://github.com/dgraph-io/badger b9579bcff679761 4e134eadfa bbolt a8af23b57f672fe https://github.com/etcd-io/bbolt f05637de531bba5 aa00013364 markdown 2ced44d5b58482a https://github.com/gomarkdown/markdown 9b77d1abad4c3d3 4b190880fe bluemonday 0a75d7616912ab9 https://github.com/microcosm-cc/bluemonday beb9cc6f7283ec1 917c61b135 blocks 2782010d7127295 https://github.com/kataras/blocks d72dc16632c7c0c 01dfbf6ceb brotli c3da72aa01ed78f https://github.com/andybalholm/brotli 164593b9624fd91 d25082d2d2 closestmatch 1fbe626be92eb4c https://github.com/schollz/closestmatch 347d182cae9f8f0 0a046bf2f4 compress 4a2d40e4b07e5b3 https://github.com/klauspost/compress d333bc0569facd0 f2dbf4ef39 crypto 4b2356b1ed79e6b https://go.googlesource.com/crypto e3deca3737a3db3 d132d2847a easyjson 8ab5ff9cd8e4e43 https://github.com/mailru/easyjson 2e8b79f6c47d324 a31dd803cf securecookie e59506cc896acb7 https://github.com/gorilla/securecookie f7bf732d4fdf5e2 5f7ccd8983 semver 4487282d78122a2 https://github.com/blang/semver 45e413d7515e7c5 16b70c33fd golog f7561df84e64ab9 https://github.com/kataras/golog 212f021923ce4ff db5df5594d goreferrer ec9c9a553398739 https://github.com/Shopify/goreferrer f0dcf817e0ad5e0 1c4e7dcd08 httpexpect bfc40287c2c3ad4 https://github.com/iris-contrib/ cdf8c4f40e7908a httpexpect b137f9227d ini.v1 39bc4ddcb8b9d01 https://gopkg.in/ini.v1 00f7a040816380c cda878b94a jade 92f294662063510 https://github.com/iris-contrib/jade cac723decad0fba 2ebc878560 jet 305ebcf60d48fce https://github.com/CloudyKit/jet 5905bdf00159a0e 1029f1d962 msgpack 911bfe50493ebbc https://github.com/vmihailenco/msgpack b6e0af9e6f36451 255746ff46 minify 119ab8b676c60a6 https://github.com/tdewolff/minify 12b9cc824e6a84a 865191aabb neffos 2221a9afc8392b9 https://github.com/kataras/neffos a3e984473dd34f8 380ce80840 pio 2e3d576cc65913a https://github.com/kataras/pio dd6106f1ce02837 2c7e6d943c pongo2 f946812ec8d53b7 https://github.com/flosch/pongo2 24e4daeb888c95a b63c98b3c0 protobuf 6c66de79d66478d https://github.com/golang/protobuf 166c7ea05f5d2cc af016fbd6b raymond cd18a09da5ea49b https://github.com/mailgun/raymond b9a8cbec107a22a 9c0e792c21 go-redis 7125bf611e5d7d9 https://github.com/go-redis/redis bb4487dd6cb80d8 88bad92d23 schema 1f5dc3fa1ac5179 https://github.com/iris-contrib/schema 78c014cb1df9954 0fa5b17f7e sitemap f223711fe0c7a64 https://github.com/kataras/sitemap 2bb2d8f2645f0ed 386ba577bc structs 878a968ab225483 https://github.com/fatih/structs 62a09bdb3322f98 b00f470d46 toml 3012a1dbe2e4bd1 https://github.com/BurntSushi/toml 391d42b32f0577c b7bbc7f005 jwt 933b4a74659b074 https://github.com/kataras/jwt 00070920d0700b9 63fa545d6c uuid cb32006e483f2a2 https://github.com/google/uuid 3230e24209cf185 c65b477dbf ================================================ FILE: README.md ================================================ # Iris Web Framework [![build status](https://img.shields.io/github/actions/workflow/status/kataras/iris/ci.yml?branch=main&style=for-the-badge)](https://github.com/kataras/iris/actions/workflows/ci.yml) [![view examples](https://img.shields.io/badge/examples%20-285-a83adf.svg?style=for-the-badge&logo=go)](https://github.com/kataras/iris/tree/main/_examples) [![chat](https://img.shields.io/gitter/room/iris_go/community.svg?color=cc2b5e&logo=gitter&style=for-the-badge)](https://gitter.im/iris_go/community) [![donate](https://img.shields.io/badge/support-Iris-blue.svg?style=for-the-badge&logo=paypal)](https://iris-go.com/donate) 🎁 Iris is a fast, simple yet fully featured and very efficient web framework for Go - the perfect gift for developers this holiday season! ✨ It provides a beautifully expressive and easy to use foundation for your next website or API, wrapped with care and decorated with powerful features. 🌟 Learn what [others saying about Iris](https://www.iris-go.com/#review) and **[star](https://github.com/kataras/iris/stargazers)** this open-source project [![](https://iris-go.com/static/images/reviews.gif)](https://iris-go.com/testimonials/) [![Benchmarks: Jul 18, 2020 at 10:46am (UTC)](https://iris-go.com/static/images/benchmarks.svg)](https://github.com/kataras/server-benchmarks) ```go package main import "github.com/kataras/iris/v12" func main() { app := iris.New() app.Use(iris.Compression) app.Get("/", func(ctx iris.Context) { ctx.HTML("Happy New Year %s! 🎅", "World") }) app.Listen(":8080") } ``` As one [Go developer](https://twitter.com/dkuye/status/1532087942696554497) once said, **Iris got you covered all-round and standing strong over the years** ⭐ Some of the features Iris offers: * HTTP/2 (Push, even Embedded data) * Middleware (Accesslog, Basicauth, CORS, gRPC, Anti-Bot hCaptcha, JWT, MethodOverride, ModRevision, Monitor, PPROF, Ratelimit, Anti-Bot reCaptcha, Recovery, RequestID, Rewrite) * API Versioning * Model-View-Controller * Websockets * gRPC * Auto-HTTPS * Builtin support for ngrok to put your app on the internet, the fastest way * Unique Router with dynamic path as parameter with standard types like :uuid, :string, :int... and the ability to create your own * Compression * View Engines (HTML, Django, Handlebars, Pug/Jade and more) * Create your own File Server and host your own WebDAV server * Cache * Localization (i18n, sitemap) * Sessions * Rich Responses (HTML, Text, Markdown, XML, YAML, Binary, JSON, JSONP, Protocol Buffers, MessagePack, Content Negotiation, Streaming, Server-Sent Events and more) * Response Compression (gzip, deflate, brotli, snappy, s2) * Rich Requests (Bind URL Query, Headers, Form, Text, XML, YAML, Binary, JSON, Validation, Protocol Buffers, MessagePack and more) * Dependency Injection (MVC, Handlers, API Routers) * Testing Suite * And the most important... you get fast answers and support from the 1st day until now - that's six full years! ## 👑 Supporters With your help, we can improve Open Source web development for everyone!

getsentry github lensesio thepunterbot h4rdc0m draFWM gf3 trading-peter AlbinoGeek basilarchia sumjoe simpleittools xiaozhuai Remydeme celsosz linxcoder jnelle TechMaster janwebdev altafino jakoubek alekperos day0ng hengestone thomasfr code-chimp CetinBasoz International Juanses SometimesMage ansrivas boreevyuri brentwilson camilbinas ekobayong lexrus li3p madhu72 mosorize se77en tstangenberg vincent-li DavidShaw sascha11110 clichi2002 derReineke Sirisap22 primadi agoncecelia chrisliang12 zyu hobysmith pluja antonio-pedrazzini clacroix njeff3 ixalender mubariz-ahmed Cesar th31nitiate stgrosshh Didainius DmarshalTU IwateKyle Little-YangYang Major2828 MatejLach amritpal042 andrefiorot boomhut cshum dtrifonov gadokrisztian geordee guanting112 iantuan ichenhe rodrigoghm icibiri jewe11er jfloresremar jingtianfeng kilarusravankumar leandrobraga lfbos lpintes macropas marcmmx mark2b miguel-devs mihado mmckeen75 narven odas0r olaf-lexemo pitexplore pr123 rsousacode sankethpb wixregiga GeorgeFourikis saz59 shadowfiga siriushaha skurtz97 srinivasganti syrm tuhao1020 BlackHole1 L-M-Sherlock claudemuller keymanye wahyuief xuyan2018 xvalen xytis ElNovi IpastorSan KKP4 Lernakow ernestocolombo francisstephan pixelheresy rcapraro soiestad spkarason thanasolykos ukitzmann DanielKirkwood colinf simonproctor FernandoLangOFC Firdavs9512 Flammable-Duck Gepetdo Hongjian0619 JoeD Jude-X Kartoffelbot KevinZhouRafael KrishManohar Laotanling Longf99999 Lyansun MihaiPopescu1985 TBNilles ajanicij aprinslo1 Mohammed8960 NA Neulhan kyoukhana spazzymoto victorgrey ArishSultan ehayun kukaki oshirokazuhide t6tg 15189573255 AGPDev AnatolyUA AwsIT NguyenPhuoc Oka00 PaddyFrenchman RainerGevers Ramblestsad SamuelNeves Scorpio69t Serissa4000 TianJIANG Ubun1 WangYajun39 XinYoungCN YukinaMochizuki a112121788 acdias aeonsthorn agent3bood ajb-neodynamics-io alessandromarotta algobot76 algoflows angelaahhu anhxuanpham annieruci antoniejiao artman328 b2cbd baoch254 bastengao beytullahakyuz bjoroen blackHoleNgc1277 bunnycodego carlos-enginner centratelemedia chrismalek civicwar cnzhangquan cuong48d damiensy danlanxiaohei dextercai dfaugusto dkzhang dloprodu donam-givita dph0899 dvitale ec0629 edwindna2 ekiyooka ekofedriyanto eli-yip eljefedelrodeodeljefe fenriz07 ffelipelimao frenchmajesty gastropulgite geGao123 globalflea gloudx gnosthi gogoswift goten002 guanzi008 hdezoscar93 hieungm hieunmg homerious hzxd inyellowbus iuliancarnaru iysaleh jackptoke jackysywk jeff2go jeremiahyan joelywz kamolcu kana99 edsongley katsubushiken kattaprasanth keeio keval6706 khasanovrs kkdaypenny knavels kohakuhubo korowiov kostasvk lafayetteDan lbsubash leki75 lemuelroberto liheyuan lingyingtan linuxluigi lipatti maikelcoke marek-kuticka marman-hp mattbowen maxgozou maxgozzz mitas mizzlespot mkell43 mnievesco mo3lyana motogo mtrense mukunhao mulyawansentosa nasoma ngseiyu nikharsaxena nronzel odelanno onlysumitg xPoppa yesudeep ymonk yonson2 yshengliao ytxmobile98 yusong-offx zhenggangpku zou8944 SergeShin - BelmonduS Diewald cty4ka martinjanda evan hazmi-e205 jtgoral ky2s lauweliam ozfive paulcockrell paulxu21 pesquive petros9282 phil535 pitt134 poscard qiepeipei qiuzhanghua rapita rbondi relaera remopavithran rfunix rhernandez-itemsoft rikoriswandha risallaw robivictor rubiagatra rubyangxg rxrw saleebm sbenimeli sebyno seun-otosho shobhitsinghal77 solohiroshi su1gen sukiejosh suresh16671 svirmi terjelafton thiennguyen93 unixedia vadgun valsorym vguhesan vpiduri vrocadev vuhoanglam walter-wang martinlindhe mdamschen letmestudy michaelsmanley Curtman SridarDhandapani madrigaltenor opusmagna ShahramMebashar b4zz4r bobmcallan fangli galois-tnp mblandr midhubalan netbaalzovf oliverjosefzimmer peacememories talebisinan valkuere lfaynman ArturWierzbicki aaxx crashCoder derekslenk dochoaj evillgenius75 gog200921 mauricedcastro mwiater sj671 statik supersherm5 thejones CSRaghunandan ndimorle rosales-stephanie shyyawn vcruzato wangbl11 wofka72 geoshan juanxme nguyentamvinhlong yoru74 xsokev oleang michalsz pomland-94 tejzpr theantichris tuxaanand raphael-brand willypuzzle dmcbane malcolm-white-dti HieuLsw carlosmoran092 yangxianglong

## 📖 Learning Iris ### Installation The only requirement is the [Go Programming Language](https://go.dev/dl/). #### Create a new project ```sh $ mkdir myapp $ cd myapp $ go mod init myapp $ go get github.com/kataras/iris/v12@latest # or @v12.2.11 ```
Install on existing project ```sh $ cd myapp $ go get github.com/kataras/iris/v12@latest ``` **Run** ```sh $ go mod tidy -compat=1.23 # -compat="1.23" for windows. $ go run . ```
![](https://www.iris-go.com/images/gifs/install-create-iris.gif) Iris contains extensive and thorough **[documentation](https://www.iris-go.com/docs)** making it easy to get started with the framework. For a more detailed technical documentation you can head over to our [godocs](https://pkg.go.dev/github.com/kataras/iris/v12@v12.2.11). And for executable code you can always visit the [./_examples](_examples) repository's subdirectory. ### Do you like to read while traveling? Book cover [![follow author on twitter](https://img.shields.io/twitter/follow/makismaropoulos?color=3D8AA3&logoColor=3D8AA3&style=for-the-badge&logo=twitter)](https://twitter.com/intent/follow?screen_name=makismaropoulos) [![follow Iris web framework on twitter](https://img.shields.io/twitter/follow/iris_framework?color=ee7506&logoColor=ee7506&style=for-the-badge&logo=twitter)](https://twitter.com/intent/follow?screen_name=iris_framework) [![follow Iris web framework on facebook](https://img.shields.io/badge/Follow%20%40Iris.framework-569-2D88FF.svg?style=for-the-badge&logo=facebook)](https://www.facebook.com/iris.framework) You can [request](https://www.iris-go.com/#ebookDonateForm) a PDF and online access of the **Iris E-Book** (New Edition, **future v12.2.0+**) today and be participated in the development of Iris. ## 🙌 Contributing We'd love to see your contribution to the Iris Web Framework! For more information about contributing to the Iris project please check the [CONTRIBUTING.md](CONTRIBUTING.md) file. [List of all Contributors](https://github.com/kataras/iris/graphs/contributors) ## 🛡 Security Vulnerabilities If you discover a security vulnerability within Iris, please send an e-mail to [iris-go@outlook.com](mailto:iris-go@outlook.com). All security vulnerabilities will be promptly addressed. ## 📝 License This project is licensed under the [BSD 3-clause license](LICENSE), just like the Go project itself. The project name "Iris" was inspired by the Greek mythology. ================================================ FILE: README_AR.md ================================================ أعزائي أعضاء مجتمع Iris, قد تكون قد لاحظت تراجعًا مؤخرًا في مستوى النشاط على مستودع Iris، ولكن هذا الهدوء له غاية مقصودة. خلال **الأشهر الثمانية إلى التسعة الماضية،** كنت أعمل بجد على الإعداد للإصدار الرئيسي الجديد من Iris، والذي يعتمد بشكل كامل على **الأنواع العامة (Generics)**، إلى جانب تقديم مجموعة من **الميزات الجديدة،** و **التحسينات**، والابتكارات. هذا التطوير يستند إلى خبرتي الممتدة على مدى **ثماني سنوات** في لغة Go، لضمان أن يستمر تطور Iris بما يتماشى مع احتياجاتكم المتنامية. مع أطيب التحيات,
Gerasimos (Makis) Maropoulos

Iris Web Framework إطار العمل إريس

[![build status](https://img.shields.io/github/actions/workflow/status/kataras/iris/ci.yml?branch=main&style=for-the-badge)](https://github.com/kataras/iris/actions/workflows/ci.yml) [![view examples](https://img.shields.io/badge/examples%20-285-a83adf.svg?style=for-the-badge&logo=go)](https://github.com/kataras/iris/tree/main/_examples) [![chat](https://img.shields.io/gitter/room/iris_go/community.svg?color=cc2b5e&logo=gitter&style=for-the-badge)](https://gitter.im/iris_go/community) [![donate](https://img.shields.io/badge/support-Iris-blue.svg?style=for-the-badge&logo=paypal)](https://iris-go.com/donate)
Iris هو إطار عمل ويب سريع، بسيط، ومع ذلك غني بالميزات وفعّال للغاية بلغة Go. يوفّر أساسًا مرنًا، سهل الاستخدام، وذا تعبيرية رائعة لبناء موقعك الإلكتروني أو واجهة برمجة التطبيقات (API) القادمة. **تعرف** على [آراء اللآخرين حول Iris](https://www.iris-go.com/#review) و **[امنح المشروع نجمة](https://github.com/kataras/iris/stargazers)** لدعم إمكاناته كمشروع مفتوح المصدر.
[![](https://iris-go.com/static/images/reviews.gif)](https://iris-go.com/testimonials/) [![Benchmarks: Jul 18, 2020 at 10:46am (UTC)](https://iris-go.com/static/images/benchmarks.svg)](https://github.com/kataras/server-benchmarks) ```go package main import "github.com/kataras/iris/v12" func main() { app := iris.New() app.Use(iris.Compression) app.Get("/", func(ctx iris.Context) { ctx.HTML("Hello %s!", "World") }) app.Listen(":8080") } ``` كما قال أحد [مطوري Go](https://twitter.com/dkuye/status/1532087942696554497) ذات مرة: **Iris يغطي جميع الجوانب، وظل صامدًا وقويًا على مدار السنوات**. بعض الميزات التي يقدّمها Iris: *

دعم كامل لبروتوكول HTTP/2 (يشمل Push وبيانات مضمنة)

*

البرمجيات الوسيطة (Middleware): Accesslog، Basicauth، CORS، gRPC، Anti-Bot hCaptcha، JWT، MethodOverride، ModRevision، Monitor، PPROF، Ratelimit، Anti-Bot reCaptcha، Recovery، RequestID، Rewrite

*

إصدار واجهات برمجة التطبيقات (API Versioning)

*

نموذج-عرض-تحكم (Model-View-Controller)

*

دعم كامل لبروتوكول WebSockets

*

دعم gRPC

*

دعم تلقائي لHTTPS

*

دعم مدمج لـ ngrok لعرض تطبيقك على الإنترنت بأسرع طريقة

*

موجّه (Router) فريد يدعم المسارات الديناميكية مع أنواع قياسية مثل: :uuid، :string، :int، وإمكانية إنشاء أنواعك الخاصة

*

ضغط البيانات

*

محركات العرض: HTML، Django، Handlebars، Pug/Jade وغيرها

*

إنشاء خادم ملفات خاص بك واستضافة خادم WebDAV

*

التخزين المؤقت

*

التوطين (i18n, sitemap)

*

الجلسات

*

استجابات غنية تشمل: HTML، نص، Markdown، XML، YAML، ثنائي، JSON، JSONP، Protocol Buffers، MessagePack، Content Negotiation، البث المتدفق، وأحداث مرسلة من الخادم والمزيد

*

ضغط الاستجابات (gzip, deflate, brotli, snappy, s2)

*

طلبات غنية (Rich Requests) تدعم: ربط استعلامات URL، الرؤوس، النماذج، النصوص، XML، YAML، ثنائي، JSON، التحقق، Protocol Buffers، MessagePack والمزيد

*

الحقن التلقائي: لمعماريات MVC، المعالجات، وواجهات برمجة التطبيقات

*

مجموعة اختبارات

*

والأهم... تحصل على دعم سريع وإجابات فورية منذ اليوم الأول وحتى الآن، على مدار ست سنوات متواصلة!

##

👑 الداعمين

بدعمكم، سنتمكن معًا من الارتقاء بتطوير الويب مفتوح المصدر وجعله أفضل للجميع!

getsentry github lensesio thepunterbot h4rdc0m draFWM gf3 trading-peter AlbinoGeek basilarchia sumjoe simpleittools xiaozhuai Remydeme celsosz linxcoder jnelle TechMaster janwebdev altafino jakoubek alekperos day0ng hengestone thomasfr code-chimp CetinBasoz International Juanses SometimesMage ansrivas boreevyuri brentwilson camilbinas ekobayong lexrus li3p madhu72 mosorize se77en tstangenberg vincent-li DavidShaw sascha11110 clichi2002 derReineke Sirisap22 primadi agoncecelia chrisliang12 zyu hobysmith pluja antonio-pedrazzini clacroix njeff3 ixalender mubariz-ahmed Cesar th31nitiate stgrosshh Didainius DmarshalTU IwateKyle Little-YangYang Major2828 MatejLach amritpal042 andrefiorot boomhut cshum dtrifonov gadokrisztian geordee guanting112 iantuan ichenhe rodrigoghm icibiri jewe11er jfloresremar jingtianfeng kilarusravankumar leandrobraga lfbos lpintes macropas marcmmx mark2b miguel-devs mihado mmckeen75 narven odas0r olaf-lexemo pitexplore pr123 rsousacode sankethpb wixregiga GeorgeFourikis saz59 shadowfiga siriushaha skurtz97 srinivasganti syrm tuhao1020 BlackHole1 L-M-Sherlock claudemuller keymanye wahyuief xuyan2018 xvalen xytis ElNovi IpastorSan KKP4 Lernakow ernestocolombo francisstephan pixelheresy rcapraro soiestad spkarason thanasolykos ukitzmann DanielKirkwood colinf simonproctor FernandoLangOFC Firdavs9512 Flammable-Duck Gepetdo Hongjian0619 JoeD Jude-X Kartoffelbot KevinZhouRafael KrishManohar Laotanling Longf99999 Lyansun MihaiPopescu1985 TBNilles ajanicij aprinslo1 Mohammed8960 NA Neulhan kyoukhana spazzymoto victorgrey ArishSultan ehayun kukaki oshirokazuhide t6tg 15189573255 AGPDev AnatolyUA AwsIT NguyenPhuoc Oka00 PaddyFrenchman RainerGevers Ramblestsad SamuelNeves Scorpio69t Serissa4000 TianJIANG Ubun1 WangYajun39 XinYoungCN YukinaMochizuki a112121788 acdias aeonsthorn agent3bood ajb-neodynamics-io alessandromarotta algobot76 algoflows angelaahhu anhxuanpham annieruci antoniejiao artman328 b2cbd baoch254 bastengao beytullahakyuz bjoroen blackHoleNgc1277 bunnycodego carlos-enginner centratelemedia chrismalek civicwar cnzhangquan cuong48d damiensy danlanxiaohei dextercai dfaugusto dkzhang dloprodu donam-givita dph0899 dvitale ec0629 edwindna2 ekiyooka ekofedriyanto eli-yip eljefedelrodeodeljefe fenriz07 ffelipelimao frenchmajesty gastropulgite geGao123 globalflea gloudx gnosthi gogoswift goten002 guanzi008 hdezoscar93 hieungm hieunmg homerious hzxd inyellowbus iuliancarnaru iysaleh jackptoke jackysywk jeff2go jeremiahyan joelywz kamolcu kana99 edsongley katsubushiken kattaprasanth keeio keval6706 khasanovrs kkdaypenny knavels kohakuhubo korowiov kostasvk lafayetteDan lbsubash leki75 lemuelroberto liheyuan lingyingtan linuxluigi lipatti maikelcoke marek-kuticka marman-hp mattbowen maxgozou maxgozzz mitas mizzlespot mkell43 mnievesco mo3lyana motogo mtrense mukunhao mulyawansentosa nasoma ngseiyu nikharsaxena nronzel odelanno onlysumitg xPoppa yesudeep ymonk yonson2 yshengliao ytxmobile98 yusong-offx zhenggangpku zou8944 SergeShin - BelmonduS Diewald cty4ka martinjanda evan hazmi-e205 jtgoral ky2s lauweliam ozfive paulcockrell paulxu21 pesquive petros9282 phil535 pitt134 poscard qiepeipei qiuzhanghua rapita rbondi relaera remopavithran rfunix rhernandez-itemsoft rikoriswandha risallaw robivictor rubiagatra rubyangxg rxrw saleebm sbenimeli sebyno seun-otosho shobhitsinghal77 solohiroshi su1gen sukiejosh suresh16671 svirmi terjelafton thiennguyen93 unixedia vadgun valsorym vguhesan vpiduri vrocadev vuhoanglam walter-wang martinlindhe mdamschen letmestudy michaelsmanley Curtman SridarDhandapani madrigaltenor opusmagna ShahramMebashar b4zz4r bobmcallan fangli galois-tnp mblandr midhubalan netbaalzovf oliverjosefzimmer peacememories talebisinan valkuere lfaynman ArturWierzbicki aaxx crashCoder derekslenk dochoaj evillgenius75 gog200921 mauricedcastro mwiater sj671 statik supersherm5 thejones CSRaghunandan ndimorle rosales-stephanie shyyawn vcruzato wangbl11 wofka72 geoshan juanxme nguyentamvinhlong yoru74 xsokev oleang michalsz pomland-94 tejzpr theantichris tuxaanand raphael-brand willypuzzle dmcbane malcolm-white-dti HieuLsw carlosmoran092 yangxianglong

##

تعلّم Iris

###

التثبيت

المتطلب الوحيد هو [لغة البرمجة Go](https://go.dev/dl/). ####

إنشاء مشروع جديد

```sh $ mkdir myapp $ cd myapp $ go mod init myapp $ go get github.com/kataras/iris/v12@latest # or @v12.2.11 ```
التثبيت على مشروع قائم ```sh $ cd myapp $ go get github.com/kataras/iris/v12@latest ``` **تشغيل** ```sh $ go mod tidy -compat=1.23 # -compat="1.23" for windows. $ go run . ```
![](https://www.iris-go.com/images/gifs/install-create-iris.gif) يحتوي Iris على **[وثائق](https://www.iris-go.com/docs)** شاملة ومفصّلة تسهّل عليك البدء باستخدام الإطار. للاطلاع على توثيق تقني أكثر تفصيلاً، يمكنك زيارة [godocs](https://pkg.go.dev/github.com/kataras/iris/v12@v12.2.11). الخاص بنا، وللاطلاع على أمثلة قابلة للتنفيذ، يمكنك دائمًا زيارة المجلد الفرعي للمستودع [./_examples](_examples). ###

هل تحب القراءة أثناء السفر؟

Book cover [![follow author on twitter](https://img.shields.io/twitter/follow/makismaropoulos?color=3D8AA3&logoColor=3D8AA3&style=for-the-badge&logo=twitter)](https://twitter.com/intent/follow?screen_name=makismaropoulos) [![follow Iris web framework on twitter](https://img.shields.io/twitter/follow/iris_framework?color=ee7506&logoColor=ee7506&style=for-the-badge&logo=twitter)](https://twitter.com/intent/follow?screen_name=iris_framework) [![follow Iris web framework on facebook](https://img.shields.io/badge/Follow%20%40Iris.framework-569-2D88FF.svg?style=for-the-badge&logo=facebook)](https://www.facebook.com/iris.framework) يمكنك [طلب نسخة](https://www.iris-go.com/#ebookDonateForm) PDF أو الوصول إلى **النسخة الإلكترونية** من كتاب Iris (الإصدار الجديد **v12.2.0+**) اليوم والمساهمة في تطوير الإطار. ##

🙌 المساهمة

نسعد برؤية مساهمتك في تطوير إطار عمل Iris! لمزيد من التفاصيل حول كيفية المساهمة، يُرجى الاطلاع على ملف [CONTRIBUTING.md](CONTRIBUTING.md). [قائمة جميع المساهمين](https://github.com/kataras/iris/graphs/contributors) ##

🛡 الثغرات الأمنية

إذا اكتشفت ثغرة أمنية في Iris، يُرجى إرسال بريد إلكتروني إلى [iris-go@outlook.com](mailto:iris-go@outlook.com). سيتم التعامل مع جميع الثغرات الأمنية بسرعة. ##

📝 الرخصة

هذا المشروع مرخّص بموجب رخصة [BSD بثلاثة بنود](LICENSE), تمامًا كحال مشروع Go نفسه. اسم المشروع "Iris" مستوحى من الأساطير اليونانية. ================================================ FILE: README_ES.md ================================================ # Iris Web Framework [![build status](https://img.shields.io/github/actions/workflow/status/kataras/iris/ci.yml?branch=main&style=for-the-badge)](https://github.com/kataras/iris/actions/workflows/ci.yml) [![FOSSA Status](https://img.shields.io/badge/LICENSE%20SCAN-PASSING❤️-CD2956?style=for-the-badge&logo=fossa)](https://app.fossa.io/projects/git%2Bgithub.com%2Fkataras%2Firis?ref=badge_shield) [![view examples](https://img.shields.io/badge/learn%20by-examples-0C8EC5.svg?style=for-the-badge&logo=go)](https://github.com/kataras/iris/tree/main/_examples) [![chat](https://img.shields.io/gitter/room/iris_go/community.svg?color=7E18DD&logo=gitter&style=for-the-badge)](https://gitter.im/iris_go/community) Iris es un framework web rápido, simple pero con muchas funcionalidades y muy eficiente para Go. Proporciona una base bellamente expresiva y fácil de usar para su próximo sitio web o API. Descubra lo que [otros dicen sobre Iris](https://iris-go.com/testimonials/) y **siga** :star: este repositorio github. [![](https://media.giphy.com/media/j5WLmtvwn98VPrm7li/giphy.gif)](https://iris-go.com/testimonials/) [![Benchmarks: Apr 2, 2020 at 12:13pm (UTC)](https://iris-go.com/static/images/benchmarks.svg)](https://github.com/kataras/server-benchmarks) ## Aprende Iris
Inicio rapido ```sh # agrega el siguiente código en el archivo ejemplo.go $ cat ejemplo.go ``` ```go package main import "github.com/kataras/iris/v12" func main() { app := iris.Default() app.Get("/ping", func(ctx iris.Context) { ctx.JSON(iris.Map{ "message": "pong", }) }) app.Listen(":8080") } ``` ```sh # ejecuta ejemplo.go y # visita http://localhost:8080/ping en el navegador $ go run ejemplo.go ``` > El enrutamiento es impulsado por [muxie](https://github.com/kataras/muxie), el software basado en trie más potente y rápido escrito en Go.
Iris contiene un extenso y completo **[wiki](https://www.iris-go.com/#ebookDonateForm)** que facilita comenzar con el framework. Para obtener una documentación técnica más detallada, puede dirigirse a nuestros [godocs](https://pkg.go.dev/github.com/kataras/iris/v12@v12.2.11). Y para código ejecutable siempre puede visitar el subdirectorio del repositorio [\_examples](_examples/). ### ¿Te gusta leer mientras viajas? Book cover [![follow author](https://img.shields.io/twitter/follow/makismaropoulos.svg?style=for-the-badge)](https://twitter.com/intent/follow?screen_name=makismaropoulos) Puedes [solicitar](https://www.iris-go.com/#ebookDonateForm) una versión en PDF y acceso en línea del **E-Book** hoy y participar en el desarrollo de Iris. ## Contribuir ¡Nos encantaría ver su contribución al Framework Web Iris! Para obtener más información sobre cómo contribuir al proyecto Iris, consulte el archivo [CONTRIBUTING.md](CONTRIBUTING). [Lista de todos los contribuyentes](https://github.com/kataras/iris/graphs/contributors) ## Vulnerabilidades de seguridad Si descubres una vulnerabilidad de seguridad dentro de Iris, envíe un correo electrónico a [iris-go@outlook.com](mailto:iris-go@outlook.com). Todas las vulnerabilidades de seguridad serán tratadas de inmediato. ## Licencia El nombre del proyecto "Iris" se inspiró en la mitología griega. El Web Framework Iris es un software gratuito y de código abierto con licencia bajo la [Licencia BSD 3 cláusulas](LICENSE). ================================================ FILE: README_FA.md ================================================
## خبرها > این شاخه تحت توسعه است. برای رفتن به شاخه نسخه بعدی [v12.2.0](HISTORY.md#Next) یا اگر به دنبال یک انتشار پایدار هستید, به جای آن به شاخه [v12.1.8 branch](https://github.com/kataras/iris/tree/v12.1.8) مراجعه کنید. > ![](https://iris-go.com/static/images/cli.png) همین امروز برنامه رسمی [Iris Command Line Interface](https://github.com/kataras/iris-cli) را امتحان کنید. > با توجه به بالا بودن حجم کار، ممکن است در پاسخ به [سوالات](https://github.com/kataras/iris/issues) شما تاخیری وجود داشته باشد. # Iris Web Framework [![build status](https://img.shields.io/github/actions/workflow/status/kataras/iris/ci.yml?branch=main&style=for-the-badge)](https://github.com/kataras/iris/actions/workflows/ci.yml) [![FOSSA Status](https://img.shields.io/badge/LICENSE%20SCAN-PASSING❤️-CD2956?style=for-the-badge&logo=fossa)](https://app.fossa.io/projects/git%2Bgithub.com%2Fkataras%2Firis?ref=badge_shield) [![view examples](https://img.shields.io/badge/learn%20by-examples-0C8EC5.svg?style=for-the-badge&logo=go)](https://github.com/kataras/iris/tree/main/_examples) [![chat](https://img.shields.io/gitter/room/iris_go/community.svg?color=7E18DD&logo=gitter&style=for-the-badge)](https://gitter.im/iris_go/community) آیریس یک چارچوب وب پر سرعت ، ساده و در عین حال کاملاً برجسته و بسیار کارآمد برای Go است.
Simple Handler ```go package main import "github.com/kataras/iris/v12" type ( request struct { Firstname string `json:"firstname"` Lastname string `json:"lastname"` } response struct { ID uint64 `json:"id"` Message string `json:"message"` } ) func main() { app := iris.New() app.Handle("PUT", "/users/{id:uint64}", updateUser) app.Listen(":8080") } func updateUser(ctx iris.Context) { id, _ := ctx.Params().GetUint64("id") var req request if err := ctx.ReadJSON(&req); err != nil { ctx.StopWithError(iris.StatusBadRequest, err) return } resp := response{ ID: id, Message: req.Firstname + " updated successfully", } ctx.JSON(resp) } ``` > !برای اطلاعات بیشتر ، [مثال های مسیریابی](https://github.com/kataras/iris/blob/main/_examples/routing) را بخوانید
Handler with custom input and output arguments [![https://github.com/kataras/iris/blob/main/_examples/dependency-injection/basic/main.go](https://user-images.githubusercontent.com/22900943/105253731-b8db6d00-5b88-11eb-90c1-0c92a5581c86.png)](https://twitter.com/iris_framework/status/1234783655408668672) > اگر برایتان جالب بود [مثال های دیگری](https://github.com/kataras/iris/blob/main/_examples/dependency-injection) را مطالعه کنید
MVC ```go package main import ( "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/mvc" ) type ( request struct { Firstname string `json:"firstname"` Lastname string `json:"lastname"` } response struct { ID uint64 `json:"id"` Message string `json:"message"` } ) func main() { app := iris.New() mvc.Configure(app.Party("/users"), configureMVC) app.Listen(":8080") } func configureMVC(app *mvc.Application) { app.Handle(new(userController)) } type userController struct { // [...dependencies] } func (c *userController) PutBy(id uint64, req request) response { return response{ ID: id, Message: req.Firstname + " updated successfully", } } ``` اگر به دنبال مثال‌های بیشتری هستید می‌توانید در [اینجا](_examples/mvc) مطالعه کنید
> دیگران درباره آیریس چه می گویند و برای پشتیبانی از پتانسیل‌های این پروژه متن باز می‌توانید از آن حمایت کنید [![](https://iris-go.com/static/images/reviews.gif)](https://iris-go.com/testimonials/) [![Benchmarks: Jul 18, 2020 at 10:46am (UTC)](https://iris-go.com/static/images/benchmarks.svg)](https://github.com/kataras/server-benchmarks) ## 👑 حامیان با کمک شما, ما می‌توانیم توسعه وب متن باز را برای همه بهبود ببخشیم ! > کمک هایی که تا حالا دریافت شده است ! ## اموزش آیریس ### ساخت یک پروژه جدید
```sh $ mkdir myapp $ cd myapp $ go mod init myapp $ go get github.com/kataras/iris/v12@latest # or @v12.2.11 ```
نصب بر روی پروژه موجود
```sh $ cd myapp $ go get github.com/kataras/iris/v12@latest ```
نصب با پرونده go.mod
```txt module myapp go 1.20 require github.com/kataras/iris/v12 v12.2.0-beta4.0.20220920072528-ff81f370625a ``` ![](https://www.iris-go.com/images/gifs/install-create-iris.gif)
آیریس شامل مستندات گسترده و کاملی است که کار با چارچوب را آسان می کند. > [مستندات](https://www.iris-go.com/docs) برای اطلاعات بیشتر در مورد اسناد فنی می توانید به مستندات اصلی ما مراجعه کنید. > [مستندات اصلی](https://pkg.go.dev/github.com/kataras/iris/v12@main) ## دوست دارید در حین مسافرت کتاب بخوانید ? Book cover [![follow author on twitter](https://img.shields.io/twitter/follow/makismaropoulos?color=3D8AA3&logoColor=3D8AA3&style=for-the-badge&logo=twitter)](https://twitter.com/intent/follow?screen_name=makismaropoulos) [![follow Iris web framework on twitter](https://img.shields.io/twitter/follow/iris_framework?color=ee7506&logoColor=ee7506&style=for-the-badge&logo=twitter)](https://twitter.com/intent/follow?screen_name=iris_framework) [![follow Iris web framework on facebook](https://img.shields.io/badge/Follow%20%40Iris.framework-522-2D88FF.svg?style=for-the-badge&logo=facebook)](https://www.facebook.com/iris.framework) امروز می توانید از طریق کتاب الکترونیکی آیریس (نسخه جدید ، آینده v12.2.0 +) دسترسی PDF و دسترسی آنلاین داشته باشید و در توسعه آیریس شرکت کنید. ## 🙌 مشارکت ما خیلی دوست داریم شما سهمی در توسعه چارچوب آیریس داشته باشید! برای دریافت اطلاعات بیشتر در مورد مشارکت در پروژه آیریس لطفاً پرونده [CONTRIBUTING.md](CONTRIBUTING.md) را مطالعه کنید. [لیست همه شرکت کنندگان](https://github.com/kataras/iris/graphs/contributors) ## 🛡 آسیب‌پذیری‌های امنیتی اگر آسیب‌پذیری امنیتی در درون آیریس مشاهده کردید, لطفاً ایمیلی به [iris-go@outlook.com](mailto:iris-go@outlook.com) بفرستید. کلیه ضعف‌های امنیتی بلافاصله مورد توجه قرار خواهند گرفت. ## 📝 مجوز این پروژه تحت پروانه [BSD 3-clause license](LICENSE) مجوز دارد ، دقیقاً مانند پروژه Go. نام پروژه "آیریس" از اساطیر یونانی الهام‌گرفته شده است.
================================================ FILE: README_FR.md ================================================ # Iris Web Framework [![build status](https://img.shields.io/github/actions/workflow/status/kataras/iris/ci.yml?branch=main&style=for-the-badge)](https://github.com/kataras/iris/actions/workflows/ci.yml) [![FOSSA Status](https://img.shields.io/badge/LICENSE%20SCAN-PASSING❤️-CD2956?style=for-the-badge&logo=fossa)](https://app.fossa.io/projects/git%2Bgithub.com%2Fkataras%2Firis?ref=badge_shield) [![view examples](https://img.shields.io/badge/learn%20by-examples-0C8EC5.svg?style=for-the-badge&logo=go)](https://github.com/kataras/iris/tree/main/_examples) [![chat](https://img.shields.io/gitter/room/iris_go/community.svg?color=7E18DD&logo=gitter&style=for-the-badge)](https://gitter.im/iris_go/community) Iris est un framework open-source pour Go à la fois simple, rapide et pourvu de nombreuses fonctionnalités. Il fournit des moyens simples et élégants de construire les bases et fonctionnalités de votre site, application backend ou API Rest. Lisez [ce que les développeurs pensent d'Iris](https://iris-go.com/testimonials/) et si l'envie vous prend **[étoilez](https://github.com/kataras/iris/stargazers)** le projet pour faire monter son potentiel. [![](https://media.giphy.com/media/j5WLmtvwn98VPrm7li/giphy.gif)](https://iris-go.com/testimonials/) [![Benchmarks: Apr 2, 2020 at 12:13pm (UTC)](https://iris-go.com/static/images/benchmarks.svg)](https://github.com/kataras/server-benchmarks) ## 📖 Démarrer avec Iris
Un simple Hello World ```sh # https://www.iris-go.com/#ebookDonateForm $ go get github.com/kataras/iris/v12@latest # assume the following code in example.go file $ cat example.go ``` ```go package main import "github.com/kataras/iris/v12" func main() { app := iris.New() app.Get("/ping", func(ctx iris.Context) { ctx.JSON(iris.Map{ "message": "pong", }) }) app.Listen(":8080") // port d'écoute } ``` ```sh # compile et execute example.go $ go run example.go # maintenant visitez http://localhost:8080/ping ``` > Le routing est géré par [muxie](https://github.com/kataras/muxie), la librairie Go la plus rapide et complète.
Iris possède un **[wiki](https://www.iris-go.com/#ebookDonateForm)** complet et précis qui vous permettra d'implémenter ses fonctionnalités rapidement et facilement. Pour une documentation encore plus complète vous pouvez visiter notre [godocs](https://pkg.go.dev/github.com/kataras/iris/v12@v12.2.11) (en Anglais). Et vous trouverez du code executable dans le dossier [\_examples](_examples/). ### Vous préférez une version PDF? Book cover [![follow author](https://img.shields.io/twitter/follow/makismaropoulos.svg?style=for-the-badge)](https://twitter.com/intent/follow?screen_name=makismaropoulos) Vous pouvez [demander](https://www.iris-go.com/#ebookDonateForm) une version **E-Book** (en Anglais) de la documentation et contribuer au développement d'Iris. ## 🙌 Contribuer Toute contribution à Iris est la bienvenue ! Pour plus d'informations sur la contribution au projet référez-vous au fichier [CONTRIBUTING.md](CONTRIBUTING.md). [Liste des contributeurs](https://github.com/kataras/iris/graphs/contributors) ## 🛡 Sécurité et vulnérabilités Si vous trouvez une vulnérabilité dans Iris, envoyez un e-mail à [iris-go@outlook.com](mailto:iris-go@outlook.com). Toute vulnérabilité sera corrigée aussi rapidement que possible. ## 📝 Licence Le projet est sous licence [licence BSD 3](LICENSE), tout comme le langage Go lui même. Le nom "Iris" est inspiré de la mythologie Grecque. ================================================ FILE: README_GR.md ================================================ # Iris Web Framework [![build status](https://img.shields.io/github/actions/workflow/status/kataras/iris/ci.yml?branch=main&style=for-the-badge)](https://github.com/kataras/iris/actions/workflows/ci.yml) [![FOSSA Status](https://img.shields.io/badge/LICENSE%20SCAN-PASSING❤️-CD2956?style=for-the-badge&logo=fossa)](https://app.fossa.io/projects/git%2Bgithub.com%2Fkataras%2Firis?ref=badge_shield) [![view examples](https://img.shields.io/badge/learn%20by-examples-0C8EC5.svg?style=for-the-badge&logo=go)](https://github.com/kataras/iris/tree/main/_examples) [![chat](https://img.shields.io/gitter/room/iris_go/community.svg?color=7E18DD&logo=gitter&style=for-the-badge)](https://gitter.im/iris_go/community) Το Iris είναι ένα γρήγορο, απλό αλλά και πλήρως λειτουργικό και πολύ αποδοτικό web framework για τη Go γλώσσα προγραμματισμού. Παρέχει ένα εκφραστικό και εύχρηστο υπόβαθρο για την επόμενη ιστοσελίδα σας. Μάθετε τι [λένε οι άλλοι για το Iris](https://iris-go.com/testimonials/) και δώστε ένα **αστεράκι** στο GitHub. [![](https://media.giphy.com/media/j5WLmtvwn98VPrm7li/giphy.gif)](https://iris-go.com/testimonials/) [![Benchmarks: Apr 2, 2020 at 12:13pm (UTC)](https://iris-go.com/static/images/benchmarks.svg)](https://github.com/kataras/server-benchmarks) ## Μαθαίνοντας το Iris
Γρήγορο ξεκίνημα ```sh # υποθέτοντας ότι ο παρακάτω κώδικας # βρίσκεται στο example.go αρχείο # $ cat example.go ``` ```go package main import "github.com/kataras/iris/v12" func main() { app := iris.Default() app.Get("/ping", func(ctx iris.Context) { ctx.JSON(iris.Map{ "message": "pong", }) }) app.Listen(":8080") } ``` ```sh # τρέξτε το example.go και # επισκεφτείτε την σελίδα http://localhost:8080/ping # στο πρόγραμμα περιήγησης σας # $ go run example.go ``` > Η δρομολόγηση τροφοδοτείται από το [muxie](https://github.com/kataras/muxie), το πιο ισχυρό και ταχύτερο λογισμικό βασισμένο σε trie αλγόριθμο που γράφτηκε σε Go.
Το Iris περιέχει εκτενείς και λεπτομερείς **[book](https://www.iris-go.com/#ebookDonateForm)** καθιστώντας το εύκολο στην εκμάθηση. Για λεπτομερέστερη τεχνική τεκμηρίωση μπορείτε να κατευθυνθείτε προς τα [godocs](https://pkg.go.dev/github.com/kataras/iris/v12@v12.2.11) μας. Και για εκτελέσιμο κώδικα μπορείτε πάντα να επισκέπτεστε τα [παραδείγματα](_examples/). ### Σας αρέσει να διαβάζετε ενώ ταξιδεύετε; Μπορείτε να [ζητήσετε](https://www.iris-go.com/#ebookDonateForm) σήμερα την PDF έκδοση και την online πρόσβαση στο Ηλεκτρονικό μας **Βιβλίο(E-Book)** και να συμμετάσχετε στην ανάπτυξη του Iris. [![https://iris-go.com/static/images/iris-book-cover-sm.jpg](https://iris-go.com/static/images/iris-book-cover-sm.jpg)](https://www.iris-go.com/#ebookDonateForm) [![follow author](https://img.shields.io/twitter/follow/makismaropoulos.svg?style=for-the-badge)](https://twitter.com/intent/follow?screen_name=makismaropoulos) ## Συνεισφορά Θα θέλαμε να δούμε τη συμβολή σας στο Iris Web Framework! Για περισσότερες πληροφορίες σχετικά με το πως μπορείτε να συμβάλετε, δείτε το [CONTRIBUTING.md](CONTRIBUTING.md) αρχείο. [Κατάλογος όλων των συνεισφορών](https://github.com/kataras/iris/graphs/contributors). ## Αδυναμίες Ασφάλειας Εάν εντοπίσετε κάποια αδυναμία ασφαλείας του Iris, στείλτε ένα μήνυμα ηλεκτρονικού ταχυδρομείου στο [iris-go@outlook.com](mailto:iris-go@outlook.com). Όλες οι τυχών αδυναμίες ασφαλείας θα αντιμετωπιστούν άμεσα. ## Άδεια Χρήσης Το όνομα "Iris" εμπνεύστηκε από την ελληνική μυθολογία, συγκεκριμένα από την θεά Ίριδα. Το Iris Web Framework είναι δωρεάν λογισμικό ανοιχτού λογισμικού με άδεια χρήσης [3-Clause BSD](LICENSE). ================================================ FILE: README_JA.md ================================================ # Iris Web Framework [![build status](https://img.shields.io/github/actions/workflow/status/kataras/iris/ci.yml?branch=main&style=for-the-badge)](https://github.com/kataras/iris/actions/workflows/ci.yml) [![view examples](https://img.shields.io/badge/examples%20-285-a83adf.svg?style=for-the-badge&logo=go)](https://github.com/kataras/iris/tree/main/_examples) [![chat](https://img.shields.io/gitter/room/iris_go/community.svg?color=cc2b5e&logo=gitter&style=for-the-badge)](https://gitter.im/iris_go/community) [![donate](https://img.shields.io/badge/support-Iris-blue.svg?style=for-the-badge&logo=paypal)](https://iris-go.com/donate) Irisは、高速でシンプルでありながら、十分な機能を備えた、非常に効率的なGo用Webフレームワークです。 あなたの次のウェブサイトやAPIのために、美しく表現力豊かで使いやすい基盤を提供します。 [Irisについての他の人々の意見](https://www.iris-go.com/#review)を学び、このオープンソースプロジェクトに **[スターをつけて](https://github.com/kataras/iris/stargazers)** 、その可能性を応援しましょう。 [![](https://iris-go.com/static/images/reviews.gif)](https://iris-go.com/testimonials/) [![Benchmarks: Jul 18, 2020 at 10:46am (UTC)](https://iris-go.com/static/images/benchmarks.svg)](https://github.com/kataras/server-benchmarks) ```go package main import "github.com/kataras/iris/v12" func main() { app := iris.New() app.Use(iris.Compression) app.Get("/", func(ctx iris.Context) { ctx.HTML("Hello %s!", "World") }) app.Listen(":8080") } ``` ある[Go開発者](https://twitter.com/dkuye/status/1532087942696554497)が言ったように、 **Irisはあなたをあらゆる面でサポートし、長年にわたって力強さを保ち続けています** 。 Irisが提供する機能の一部: * HTTP/2 (Push, Embedded data) * Middleware (Accesslog, Basicauth, CORS, gRPC, Anti-Bot hCaptcha, JWT, MethodOverride, ModRevision, Monitor, PPROF, Ratelimit, Anti-Bot reCaptcha, Recovery, RequestID, Rewrite) * API バージョニング * Model-View-Controller * Websockets * gRPC * Auto-HTTPS * ngrokの組み込みサポートにより、最速の方法でアプリをインターネットに公開できる * :uuid、:string、:int のような標準的な型を持つダイナミック・パスをパラメータとするユニークなルーター * Compression * View Engines (HTML, Django, Handlebars, Pug/Jade and more) * 独自のファイルサーバーを作成し、WebDAVサーバーをホストする * Cache * Localization (i18n, sitemap) * Sessions * 豊富な Response (HTML, Text, Markdown, XML, YAML, Binary, JSON, JSONP, Protocol Buffers, MessagePack, Content Negotiation, Streaming, Server-Sent Events など) * Response Compression (gzip, deflate, brotli, snappy, s2) * 豊富な Requests (Bind URL Query, Headers, Form, Text, XML, YAML, Binary, JSON, Validation, Protocol Buffers, MessagePack など) * Dependency Injection (MVC, Handlers, API Routers) * Testing Suite * そして最も重要なのは、初日から現在に至るまで、つまり丸6年間、迅速な回答とサポートを受けられることです! ## 👑 サポーター 皆様のご協力により、オープンソース・ウェブ開発をより良いものにすることができます! ## 📖 Irisを学ぶ ### インストール 必要なのは [Goプログラミング言語](https://go.dev/dl/) だけです。 #### 新規プロジェクトの作成 ```sh $ mkdir myapp $ cd myapp $ go mod init myapp $ go get github.com/kataras/iris/v12@latest # or @v12.2.11 ```
既存のプロジェクトにインストールする場合 ```sh $ cd myapp $ go get github.com/kataras/iris/v12@latest ``` **実行** ```sh $ go mod tidy -compat=1.20 # -compat="1.20" for windows. $ go run . ```
![](https://www.iris-go.com/images/gifs/install-create-iris.gif) Iris には広範で詳細な **[ドキュメント](https://www.iris-go.com/docs)** が含まれているので、フレームワークを簡単に使い始めることができます。 より詳細な技術文書については [godocs](https://pkg.go.dev/github.com/kataras/iris/v12@main) をご覧ください。また、実行可能なコードについては、いつでもリポジトリのサブディレクトリ [./_examples](_examples) にアクセスできます。 ### 旅行中に本を読むのは好きですか? Book cover [![follow author on twitter](https://img.shields.io/twitter/follow/makismaropoulos?color=3D8AA3&logoColor=3D8AA3&style=for-the-badge&logo=twitter)](https://twitter.com/intent/follow?screen_name=makismaropoulos) [![follow Iris web framework on twitter](https://img.shields.io/twitter/follow/iris_framework?color=ee7506&logoColor=ee7506&style=for-the-badge&logo=twitter)](https://twitter.com/intent/follow?screen_name=iris_framework) [![follow Iris web framework on facebook](https://img.shields.io/badge/Follow%20%40Iris.framework-569-2D88FF.svg?style=for-the-badge&logo=facebook)](https://www.facebook.com/iris.framework) **Iris E-Book**(新版、**将来のv12.2.0+**)のPDFとオンライン・アクセスを今すぐ [リクエスト](https://www.iris-go.com/#ebookDonateForm) して、Irisの開発に参加してください。 ## 🙌 貢献する Irisウェブ・フレームワークへの貢献をお待ちしています!Iris プロジェクトへの貢献についての詳細は、 [CONTRIBUTING.md](CONTRIBUTING.md) ファイルをご覧ください。 [全貢献者のリスト](https://github.com/kataras/iris/graphs/contributors) ## 🛡 セキュリティの脆弱性 Iris にセキュリティ上の脆弱性を発見した場合は、 [iris-go@outlook.com](mailto:iris-go@outlook.com) にメールを送ってください。すべてのセキュリティ脆弱性は、速やかに対処されます。 ## 📝 ライセンス このプロジェクトのライセンスは、Goプロジェクトと同様、 [BSD 3-clause license](LICENSE) です。 プロジェクト名の "Iris" はギリシャ神話からインスピレーションを得たものです。 ================================================ FILE: README_KO.md ================================================ # Iris Web Framework [![build status](https://img.shields.io/github/actions/workflow/status/kataras/iris/ci.yml?branch=main&style=for-the-badge)](https://github.com/kataras/iris/actions/workflows/ci.yml) [![FOSSA Status](https://img.shields.io/badge/LICENSE%20SCAN-PASSING❤️-CD2956?style=for-the-badge&logo=fossa)](https://app.fossa.io/projects/git%2Bgithub.com%2Fkataras%2Firis?ref=badge_shield) [![view examples](https://img.shields.io/badge/learn%20by-examples-0C8EC5.svg?style=for-the-badge&logo=go)](https://github.com/kataras/iris/tree/main/_examples) [![chat](https://img.shields.io/gitter/room/iris_go/community.svg?color=7E18DD&logo=gitter&style=for-the-badge)](https://gitter.im/iris_go/community) Iris는 단순하고 빠르며 좋은 성능과 모든 기능을 갖춘 Go언어용 웹 프레임워크입니다. 당신의 웹사이트나 API를 위해서 아름답고 사용하기 쉬운 기반을 제공합니다. [여러 사람들의 의견](https://iris-go.com/testimonials/)을 둘러보세요. 그리고 이 github repository을 **star**하세요. [![](https://media.giphy.com/media/j5WLmtvwn98VPrm7li/giphy.gif)](https://iris-go.com/testimonials/) [![Benchmarks: Apr 2, 2020 at 12:13pm (UTC)](https://iris-go.com/static/images/benchmarks.svg)](https://github.com/kataras/server-benchmarks) ## Iris 배우기
일단 해보기 ```sh # 다음 코드를 example.go 화일에 입력하세요. $ cat example.go ``` ```go package main import "github.com/kataras/iris/v12" func main() { app := iris.Default() app.Get("/ping", func(ctx iris.Context) { ctx.JSON(iris.Map{ "message": "pong", }) }) app.Listen(":8080") } ``` ```sh # example.go 를 실행하고, # 웹브라우저에서 http://localhost:8080/ping 를 열어보세요. $ go run example.go ``` > 라우팅은 Go로 작성한 가장 강력하고 빠른 trie기반의 소프트웨어인 [muxie](https://github.com/kataras/muxie)로 처리합니다.
Iris는 광범위하고 꼼꼼한 **[wiki](https://www.iris-go.com/#ebookDonateForm)** 를 가지고 있기 때문에 쉽게 프레임워크를 시작할 수 있습니다. 더 자세한 기술문서를 보시려면 [godocs](https://pkg.go.dev/github.com/kataras/iris/v12@v12.2.11)를 방문하세요. 그리고 실행가능한 예제코드는 [\_examples](_examples/) 하위 디렉토리에 있습니다. ### 여행하면서 독서를 즐기세요? Book cover [![follow author](https://img.shields.io/twitter/follow/makismaropoulos.svg?style=for-the-badge)](https://twitter.com/intent/follow?screen_name=makismaropoulos) PDF 버전과 **E-Book** 에 대한 온라인 접근을 [요청](https://www.iris-go.com/#ebookDonateForm)하시고 Iris의 개발에 참가하실 수 있습니다. ## 기여하기 Iris 웹 프레임워크에 대한 여러분의 기여를 환영합니다! Iris 프로젝트에 기여하는 방법에 대한 자세한 내용은 [CONTRIBUTING.md](CONTRIBUTING.md) 파일을 참조하십시오. [기여자 리스트](https://github.com/kataras/iris/graphs/contributors) ## 보안 취약점 만약 Iris에서 보안 취약점을 발견하시면 [iris-go@outlook.com](mailto:iris-go@outlook.com) 로 메일을 보내주세요. 모든 보안 취약점은 즉 해결할 것입니다. ## 라이센스 이 프로젝트의 이름 "Iris"는 그리스 신화에서 영감을 받았습니다. Iris 웹 프레임워크는 [3-Clause BSD License](LICENSE)를 가지는 무료 오픈소스 소프트웨어입니다. ================================================ FILE: README_PT_BR.md ================================================ # Iris Web Framework [![build status](https://img.shields.io/github/actions/workflow/status/kataras/iris/ci.yml?branch=main&style=for-the-badge)](https://github.com/kataras/iris/actions/workflows/ci.yml) [![view examples](https://img.shields.io/badge/examples%20-270-a83adf.svg?style=for-the-badge&logo=go)](https://github.com/kataras/iris/tree/main/_examples) [![chat](https://img.shields.io/gitter/room/iris_go/community.svg?color=cc2b5e&logo=gitter&style=for-the-badge)](https://gitter.im/iris_go/community) [![donate](https://img.shields.io/badge/support-Iris-blue.svg?style=for-the-badge&logo=paypal)](https://iris-go.com/donate) Iris é um framework web rápido, simples, mas completo e muito eficiente para Go. Ele fornece uma base lindamente expressiva e fácil de usar para seu próximo site ou API. ```go package main import "github.com/kataras/iris/v12" func main() { app := iris.New() app.Use(iris.Compression) app.Get("/", func(ctx iris.Context) { ctx.HTML("Hello %s!", "World") }) app.Listen(":8080") } ``` Como um [Desenvolvedor Go](https://twitter.com/dkuye/status/1532087942696554497) disse uma vez, **Iris abrangeu tudo e se manteve forte ao longo dos anos**. Alguns dos recursos que o Iris Web Framework oferece: * HTTP/2 (Push, mesmo para dados incorporados) * Middleware (Accesslog, Basicauth, CORS, gRPC, Anti-Bot hCaptcha, JWT, MethodOverride, ModRevision, Monitor, PPROF, Ratelimit, Anti-Bot reCaptcha, Recovery, RequestID, Rewrite) * Versionamento de API * Model-View-Controller * Websockets * gRPC * Auto-HTTPS * Suporte integrado para ngrok para colocar seu aplicativo na internet da maneira mais rápida * Router único com caminho dinâmico como parametro com tipos padrões como :uuid, :string, :int... e a habilidade de criar o seu próprio router * Compressão * View Engines (HTML, Django, Handlebars, Pug/Jade e mais) * Cria seu próprio Servidor de Arquivo e hospeda seu próprio servidor WebDAV * Cache * Localização (i18n, sitemap) * Sessões * Respostas Ricas (HTML, Text, Markdown, XML, YAML, Binary, JSON, JSONP, Protocol Buffers, MessagePack, Content Negotiation, Streaming, Server-Sent Events e mais) * Compressão de resposta (gzip, deflate, brotli, snappy, s2) * Requisições Ricas (Bind URL Query, Headers, Form, Text, XML, YAML, Binary, JSON, Validation, Protocol Buffers, MessagePack e mais) * Injeção de dependência (MVC, Handlers, API Routers) * Suite de testes * E o mais importante... você obtém respostas rápidas e suporte desde o 1º dia até agora - são seis anos completos! Aprenda com [o que os outros falam sobre Iris](https://www.iris-go.com/#review) e **[marque com uma estrela](https://github.com/kataras/iris/stargazers)** esse projeto de código aberto para apoiar o seu potencial. [![](https://iris-go.com/static/images/reviews.gif)](https://iris-go.com/testimonials/) [![Benchmarks: Jul 18, 2020 at 10:46am (UTC)](https://iris-go.com/static/images/benchmarks.svg)](https://github.com/kataras/server-benchmarks) ## 👑 Apoiadores Com a sua ajuda, nós podemos melhorar o desenvolvimento web de Código Aberto para todos ! > [@github](https://github.com/github) agora está patrocinando você por $550,00 uma vez. > > Uma nota do seu novo patrocinador: > > Para comemorar o Mês do Mantenedor, queremos agradecer por tudo que você faz pela comunidade de código aberto. Confira nossa postagem no blog para saber mais sobre como o GitHub está investindo em mantenedores > Doações direto da [China](https://github.com/kataras/iris/issues/1870#issuecomment-1101418349) agora são aceitas! ## 📖 Aprenda sobre o Iris Web Framework ### Instalação O único requisito é a [Linguagem de programação Go](https://go.dev/dl/). #### Criar um novo projeto ```sh $ mkdir myapp $ cd myapp $ go mod init myapp $ go get github.com/kataras/iris/v12@latest # or @v12.2.11 ```
Instalar num projeto existente ```sh $ cd myapp $ go get github.com/kataras/iris/v12@latest ``` **Run** ```sh $ go mod tidy -compat=1.20 # -compat="1.20" for windows. $ go run . ```
![](https://www.iris-go.com/images/gifs/install-create-iris.gif) Iris contém extensa e completa **[documentação](https://www.iris-go.com/docs)**, o que torna fácil o começo com o framework. Para obter uma documentação técnica mais detalhada, você pode acessar nosso [godocs](https://pkg.go.dev/github.com/kataras/iris/v12@main). E para executar o código você sempre pode visitar os subdiretórios do diretório [./_examples](_examples). ### Você gosta de ler enquanto viaja ? Book cover [![follow author on twitter](https://img.shields.io/twitter/follow/makismaropoulos?color=3D8AA3&logoColor=3D8AA3&style=for-the-badge&logo=twitter)](https://twitter.com/intent/follow?screen_name=makismaropoulos) [![follow Iris web framework on twitter](https://img.shields.io/twitter/follow/iris_framework?color=ee7506&logoColor=ee7506&style=for-the-badge&logo=twitter)](https://twitter.com/intent/follow?screen_name=iris_framework) [![follow Iris web framework on facebook](https://img.shields.io/badge/Follow%20%40Iris.framework-569-2D88FF.svg?style=for-the-badge&logo=facebook)](https://www.facebook.com/iris.framework) Você pode [solicitar](https://www.iris-go.com/#ebookDonateForm) o acesso ao **Iris E-Book** de forma online e também no formato PDF (Nova edição, **future v12.2.0+**) hoje today e se antecipar no desenvolvimento do Iris. ## 🙌 Contribuidores Adoraríamos ver sua contribuição para o Iris Web Framework! Para mais informações sobre como contribuir com o projeto Iris, consulte o arquivo [CONTRIBUTING.md](CONTRIBUTING.md). [Lista de todos os Contribuidores](https://github.com/kataras/iris/graphs/contributors) ## 🛡Vulnerabilidades de segurança Se você descobrir alguma vulnerabilidade de segurança dentro do Iris, por favor, envie um email para [iris-go@outlook.com](mailto:iris-go@outlook.com). Todas as vulnerabilidades de segurança serão prontamente tratadas. ## 📝 Licença Este projeto está licenciado sob a [Licença BSD 3-clause](LICENSE), assim como o próprio projeto Go. O nome do projeto "Iris" foi inspirado pela mitologia Grega. ================================================ FILE: README_RU.md ================================================ # Iris Web Framework [![build status](https://img.shields.io/github/actions/workflow/status/kataras/iris/ci.yml?branch=main&style=for-the-badge)](https://github.com/kataras/iris/actions/workflows/ci.yml) [![FOSSA Status](https://img.shields.io/badge/LICENSE%20SCAN-PASSING❤️-CD2956?style=for-the-badge&logo=fossa)](https://app.fossa.io/projects/git%2Bgithub.com%2Fkataras%2Firis?ref=badge_shield) [![view examples](https://img.shields.io/badge/learn%20by-examples-0C8EC5.svg?style=for-the-badge&logo=go)](https://github.com/kataras/iris/tree/main/_examples) [![chat](https://img.shields.io/gitter/room/iris_go/community.svg?color=7E18DD&logo=gitter&style=for-the-badge)](https://gitter.im/iris_go/community) Iris — это быстрый, простой, но полнофункциональный и эффективный веб-фреймворк для Go. Он обеспечивает красивую, выразительную и простую в использовании основу для вашего следующего веб-сайта или API. Узнайте, что [говорят другие люди об Iris](https://iris-go.com/testimonials/) и поставьте **[звёздочку](https://github.com/kataras/iris/stargazers)** этому проекту с открытым исходным кодом, чтобы поддержать его потенциал. [![](https://media.giphy.com/media/j5WLmtvwn98VPrm7li/giphy.gif)](https://iris-go.com/testimonials/) [![Benchmarks: Apr 2, 2020 at 12:13pm (UTC)](https://iris-go.com/static/images/benchmarks.svg)](https://github.com/kataras/server-benchmarks) ## Изучение Iris
Быстрый старт ```sh # например, код в файле example.go будет таким: $ cat example.go ``` ```go package main import "github.com/kataras/iris/v12" func main() { app := iris.Default() app.Get("/ping", func(ctx iris.Context) { ctx.JSON(iris.Map{ "message": "pong", }) }) app.Listen(":8080") } ``` ```sh # запустите example.go и перейдите в браузер # по адресу http://localhost:8080/ping $ go run example.go ``` > Система роутинга запросов работает на [muxie](https://github.com/kataras/muxie), мощное и быстрое trie-based ПО, написанное на Go.
У Iris есть исчерпывающий и тщательный **[wiki](https://www.iris-go.com/#ebookDonateForm)**, который позволит вам быстрее начать работу с фреймворком. Для получения более подробной технической документации вы можете обратиться к нашему [godoc](https://pkg.go.dev/github.com/kataras/iris/v12@v12.2.11). А для живых примеров кода — вы всегда можете посетить [\_examples](_examples/) в поддиректории этого репозитория. ### Вы любите читать во время путешествий? Book cover Вы можете [запросить](https://www.iris-go.com/#ebookDonateForm) PDF версию и онлайн-доступ к **E-Book** сегодня и принять участие в разработке Iris. ## Содействие Мы будем рады видеть ваш вклад в веб-фреймворк Iris! Для получения дополнительной информации о содействии проекту Iris, пожалуйста, проверьте файл [CONTRIBUTING.md](CONTRIBUTING.md). [Список всех участников](https://github.com/kataras/iris/graphs/contributors) ## Уязвимость безопасности Если вы обнаружите уязвимость безопасности в Iris, отправьте электронное письмо по адресу [iris-go@outlook.com](mailto:iris-go@outlook.com). Все уязвимости безопасности будут оперативно устранены. ## Лицензия Название проекта «Iris» было вдохновлено греческой мифологией. Веб-фреймворк Iris — это ПО с открытым исходным кодом под лицензией [3-Clause BSD License](LICENSE). ## Накопление звёзд со временем [![Stargazers over time](https://starchart.cc/kataras/iris.svg)](https://starchart.cc/kataras/iris) ================================================ FILE: README_VN.md ================================================ # Iris Web Framework [![build status](https://img.shields.io/github/actions/workflow/status/kataras/iris/ci.yml?branch=main&style=for-the-badge)](https://github.com/kataras/iris/actions/workflows/ci.yml) [![view examples](https://img.shields.io/badge/examples%20-285-a83adf.svg?style=for-the-badge&logo=go)](https://github.com/kataras/iris/tree/main/_examples) [![chat](https://img.shields.io/gitter/room/iris_go/community.svg?color=cc2b5e&logo=gitter&style=for-the-badge)](https://gitter.im/iris_go/community) [![donate](https://img.shields.io/badge/support-Iris-blue.svg?style=for-the-badge&logo=paypal)](https://iris-go.com/donate) Iris là một khung web nhanh, đơn giản nhưng đầy đủ tính năng và rất hiệu quả dành cho Go. Nó cung cấp một nền tảng đẹp mắt và dễ sử dụng cho trang web hoặc API tiếp theo của bạn. Tìm hiểu xem [những người khác nói gì về Iris](https://www.iris-go.com/#review) và **[gắn sao](https://github.com/kataras/iris/stargazers)** dự án mã nguồn mở này để phát huy tiềm năng của nó. [![](https://iris-go.com/static/images/reviews.gif)](https://iris-go.com/testimonials/) [![Benchmarks: Jul 18, 2020 at 10:46am (UTC)](https://iris-go.com/static/images/benchmarks.svg)](https://github.com/kataras/server-benchmarks) ```go package main import "github.com/kataras/iris/v12" func main() { app := iris.New() app.Use(iris.Compression) app.Get("/", func(ctx iris.Context) { ctx.HTML("Xin chào %s!", "Thế Giới") }) app.Listen(":8080") } ``` Như một [nhà phát triển Go](https://twitter.com/dkuye/status/1532087942696554497) đã từng nói, **Iris giúp bạn bảo vệ toàn diện và đứng vững qua nhiều năm**. Một số tính năng Iris cung cấp: * HTTP/2 (Push, cả những Embedded data) * Middleware (Accesslog, Basicauth, CORS, gRPC, Anti-Bot hCaptcha, JWT, MethodOverride, ModRevision, Monitor, PPROF, Ratelimit, Anti-Bot reCaptcha, Recovery, RequestID, Rewrite) * API Versioning * Model-View-Controller * Websockets * gRPC * Auto-HTTPS * Tích hợp hỗ trợ ngrok để đưa ứng dụng của bạn lên internet một cách nhanh nhất * Unique Router với đường dẫn động làm tham số với các loại tiêu chuẩn như :uuid, :string, :int... và khả năng tạo của riêng bạn * Compression * View Engines (HTML, Django, Handlebars, Pug/Jade and more) * Tạo Máy chủ tệp của riêng bạn và lưu trữ máy chủ WebDAV của riêng bạn * Cache * Localization (i18n, sitemap) * Sessions * Rich Responses (HTML, Text, Markdown, XML, YAML, Binary, JSON, JSONP, Protocol Buffers, MessagePack, Content Negotiation, Streaming, Server-Sent Events and more) * Response Compression (gzip, deflate, brotli, snappy, s2) * Rich Requests (Bind URL Query, Headers, Form, Text, XML, YAML, Binary, JSON, Validation, Protocol Buffers, MessagePack and more) * Dependency Injection (MVC, Handlers, API Routers) * Testing Suite * Và điều quan trọng nhất... bạn nhận được câu trả lời và hỗ trợ nhanh chóng từ ngày đầu tiên cho đến bây giờ - đó là sáu năm đầy đủ! ## 👑 Người ủng hộ Với sự giúp đỡ của bạn, chúng tôi có thể cải thiện việc phát triển web Nguồn mở cho mọi người! ## 📖 Học Iris ### Cài đặt Yêu cầu duy nhất là [Ngôn ngữ lập trình Go](https://go.dev/dl/). #### Tạo một dự án mới ```sh $ mkdir myapp $ cd myapp $ go mod init myapp $ go get github.com/kataras/iris/v12@latest # or @v12.2.11 ```
Cài đặt trên dự án hiện có ```sh $ cd myapp $ go get github.com/kataras/iris/v12@latest ``` **Run** ```sh $ go mod tidy -compat=1.20 # -compat="1.20" for windows. $ go run . ```
![](https://www.iris-go.com/images/gifs/install-create-iris.gif) Iris chứa **[tài liệu](https://www.iris-go.com/docs)** phong phú và kỹ lưỡng giúp bạn dễ dàng bắt đầu với khung. Để có tài liệu kỹ thuật chi tiết hơn, bạn có thể truy cập [godocs](https://pkg.go.dev/github.com/kataras/iris/v12@main) của chúng tôi. Và đối với mã thực thi, bạn luôn có thể truy cập thư mục con của kho lưu trữ [./_examples](_examples). ### Bạn có thích đọc khi đi du lịch không? Book cover [![follow author on twitter](https://img.shields.io/twitter/follow/makismaropoulos?color=3D8AA3&logoColor=3D8AA3&style=for-the-badge&logo=twitter)](https://twitter.com/intent/follow?screen_name=makismaropoulos) [![follow Iris web framework on twitter](https://img.shields.io/twitter/follow/iris_framework?color=ee7506&logoColor=ee7506&style=for-the-badge&logo=twitter)](https://twitter.com/intent/follow?screen_name=iris_framework) [![follow Iris web framework on facebook](https://img.shields.io/badge/Follow%20%40Iris.framework-569-2D88FF.svg?style=for-the-badge&logo=facebook)](https://www.facebook.com/iris.framework) Bạn có thể [yêu cầu](https://www.iris-go.com/#ebookDonateForm) PDF và truy cập trực tuyến **Sách điện tử Iris** (Phiên bản mới, **tương lai v12.2.0+**) hôm nay và được tham gia vào sự phát triển của Iris. ## 🙌 Đóng góp Chúng tôi muốn thấy sự đóng góp của bạn cho Iris Web Framework! Để biết thêm thông tin về việc đóng góp cho dự án Iris, vui lòng kiểm tra tệp [CONTRIBUTING.md](CONTRIBUTING.md). [Danh sách những người đóng góp](https://github.com/kataras/iris/graphs/contributors) ## 🛡 Lỗ hổng bảo mật Nếu bạn phát hiện ra lỗ hổng bảo mật trong Iris, vui lòng gửi e-mail tới [iris-go@outlook.com](mailto:iris-go@outlook.com). Tất cả các lỗ hổng bảo mật sẽ được giải quyết kịp thời. ## 📝 Giấy phép Dự án này được cấp phép theo [BSD 3-clause license](LICENSE), giống như chính dự án Go. Tên dự án "Iris" được lấy cảm hứng từ thần thoại Hy Lạp. ================================================ FILE: README_ZH.md ================================================ # Iris Web Framework [Traditional Chinese (zht / zh-Hant)](README_ZH_HANT.md) [Simplified Chinese (zhs / zh-Hans)](README_ZH_HANS.md) ================================================ FILE: README_ZH_HANS.md ================================================ [![黑人的命也是命](https://iris-go.com/static/images/blacklivesmatter_banner.png)](https://support.eji.org/give/153413/#!/donation/checkout) > 这是一个**开发中的版本**。敬请关注即将发布的版本 [v12.2.0](HISTORY.md#Next)。如果想使用稳定版本,请查看 [v12.1.8 分支](https://github.com/kataras/iris/tree/v12.1.8) 。 > > ![](https://iris-go.com/static/images/cli.png) 立即尝试官方的[Iris命令行工具](https://github.com/kataras/iris-cli)! # Iris Web Framework [![build status](https://img.shields.io/github/actions/workflow/status/kataras/iris/ci.yml?branch=main&style=for-the-badge)](https://github.com/kataras/iris/actions/workflows/ci.yml) [![view examples](https://img.shields.io/badge/examples%20-253-a83adf.svg?style=for-the-badge&logo=go)](https://github.com/kataras/iris/tree/main/_examples) [![chat](https://img.shields.io/gitter/room/iris_go/community.svg?color=cc2b5e&logo=gitter&style=for-the-badge)](https://gitter.im/iris_go/community) [![donate](https://img.shields.io/badge/support-Iris-blue.svg?style=for-the-badge&logo=paypal)](https://iris-go.com/donate) Iris 是基于 Go 编写的一个快速,简单但功能齐全且非常高效的 Web 框架。 它为您的下一个网站或 API 提供了一个非常富有表现力且易于使用的基础。 看看 [其他人如何评价 Iris](https://iris-go.com/testimonials/),同时欢迎各位为此开源项目点亮 **[star](https://github.com/kataras/iris/stargazers)**。 [![](https://iris-go.com/static/images/reviews.gif)](https://iris-go.com/testimonials/) [![Benchmarks: Jul 18, 2020 at 10:46am (UTC)](https://iris-go.com/static/images/benchmarks.svg)](https://github.com/kataras/server-benchmarks) ## 📖 开始学习 Iris ```sh # 安装Iris:https://www.iris-go.com/#ebookDonateForm $ go get github.com/kataras/iris/v12@latest # 假设main.go文件中已存在以下代码 $ cat main.go ``` ```go package main import "github.com/kataras/iris/v12" func main() { app := iris.New() booksAPI := app.Party("/books") { booksAPI.Use(iris.Compression) // GET: http://localhost:8080/books booksAPI.Get("/", list) // POST: http://localhost:8080/books booksAPI.Post("/", create) } app.Listen(":8080") } // Book example. type Book struct { Title string `json:"title"` } func list(ctx iris.Context) { books := []Book{ {"Mastering Concurrency in Go"}, {"Go Design Patterns"}, {"Black Hat Go"}, } ctx.JSON(books) // 提示: 在服务器优先级和客户端请求中进行响应协商, // 以此来代替 ctx.JSON: // ctx.Negotiation().JSON().MsgPack().Protobuf() // ctx.Negotiate(books) } func create(ctx iris.Context) { var b Book err := ctx.ReadJSON(&b) // 提示: 使用 ctx.ReadBody(&b) 代替,来绑定所有类型的入参 if err != nil { ctx.StopWithProblem(iris.StatusBadRequest, iris.NewProblem(). Title("Book creation failure").DetailErr(err)) // 提示: 如果仅有纯文本(plain text)错误响应, // 可使用 ctx.StopWithError(code, err) return } println("Received Book: " + b.Title) ctx.StatusCode(iris.StatusCreated) } ``` 同样地,在**MVC**中 : ```go import "github.com/kataras/iris/v12/mvc" ``` ```go m := mvc.New(booksAPI) m.Handle(new(BookController)) ``` ```go type BookController struct { /* dependencies */ } // GET: http://localhost:8080/books func (c *BookController) Get() []Book { return []Book{ {"Mastering Concurrency in Go"}, {"Go Design Patterns"}, {"Black Hat Go"}, } } // POST: http://localhost:8080/books func (c *BookController) Post(b Book) int { println("Received Book: " + b.Title) return iris.StatusCreated } ``` **启动** 您的 Iris web 服务: ```sh $ go run main.go > Now listening on: http://localhost:8080 > Application started. Press CTRL+C to shut down. ``` Books **列表查询** : ```sh $ curl --header 'Accept-Encoding:gzip' http://localhost:8080/books [ { "title": "Mastering Concurrency in Go" }, { "title": "Go Design Patterns" }, { "title": "Black Hat Go" } ] ``` **创建** 新的Book: ```sh $ curl -i -X POST \ --header 'Content-Encoding:gzip' \ --header 'Content-Type:application/json' \ --data "{\"title\":\"Writing An Interpreter In Go\"}" \ http://localhost:8080/books > HTTP/1.1 201 Created ``` 这是**错误**响应所展示的样子: ```sh $ curl -X POST --data "{\"title\" \"not valid one\"}" \ http://localhost:8080/books > HTTP/1.1 400 Bad Request { "status": 400, "title": "Book creation failure" "detail": "invalid character '\"' after object key", } ``` [![run in the browser](https://img.shields.io/badge/Run-in%20the%20Browser-348798.svg?style=for-the-badge&logo=repl.it)](https://replit.com/@kataras/Iris-Hello-World-v1220?v=1) Iris 有完整且详尽的 **[使用文档](https://www.iris-go.com/#ebookDonateForm)** ,让您可以轻松地使用此框架。 要了解更详细的技术文档,请访问我们的 [godocs](https://pkg.go.dev/github.com/kataras/iris/v12@main)。如果想要寻找代码示例,您可以到仓库的 [./_examples](_examples) 子目录下获取。 ### 你喜欢在旅行时阅读吗? Book cover [![follow Iris web framework on twitter](https://img.shields.io/twitter/follow/iris_framework?color=ee7506&logoColor=ee7506&style=for-the-badge)](https://twitter.com/intent/follow?screen_name=iris_framework) 您可以[获取](https://www.iris-go.com/#ebookDonateForm)PDF版本或在线访问**电子图书**,并参与到Iris的开发中。 ## 🙌 贡献 我们欢迎您为Iris框架做出贡献!想要知道如何为Iris项目做贡献,请查看[CONTRIBUTING.md](CONTRIBUTING.md)。 [贡献者名单](https://github.com/kataras/iris/graphs/contributors) ## 🛡 安全漏洞 如果您发现在 Iris 存在安全漏洞,请发送电子邮件至 [iris-go@outlook.com](mailto:iris-go@outlook.com)。所有安全漏洞将会得到及时解决。 ## 📝 开源协议(License) 就像Go语言的协议一样,此项目也采用 [BSD 3-clause license](LICENSE)。 项目名称 "Iris" 的灵感来自于希腊神话。 ================================================ FILE: README_ZH_HANT.md ================================================ # Iris Web 框架 [![組建狀態](https://img.shields.io/github/actions/workflow/status/kataras/iris/ci.yml?branch=main&style=for-the-badge)](https://github.com/kataras/iris/actions/workflows/ci.yml) [![查看範例](https://img.shields.io/badge/examples%20-285-a83adf.svg?style=for-the-badge&logo=go)](https://github.com/kataras/iris/tree/main/_examples) [![聊天室](https://img.shields.io/gitter/room/iris_go/community.svg?color=cc2b5e&logo=gitter&style=for-the-badge)](https://gitter.im/iris_go/community) [![捐助](https://img.shields.io/badge/support-Iris-blue.svg?style=for-the-badge&logo=paypal)](https://iris-go.com/donate) Iris 是款不僅迅速、簡捷,並且功能完善、高效率的 Go 語言 Web 框架。**與 Go 生態系統中其它人提供的免費軟體套件不同,這個軟體保證終身主動維護。** > 想要取得接下來 **v12.2.0** 穩定版本(正在逐步推進 (2023🎅))的新消息,請收藏 🌟 並關注 👀 這個儲存庫! Iris 能為你的下一個網站或 API,立下漂亮、富有表達性,且易於使用的基礎。 ```go package main import "github.com/kataras/iris/v12" func main() { app := iris.New() app.Use(iris.Compression) app.Get("/", func(ctx iris.Context) { ctx.HTML("哈囉,%s!", "世界") }) app.Listen(":8080") } ``` 據一位 [Go 開發者](https://twitter.com/dkuye/status/1532087942696554497) 所言,**Iris 能向您提供全方位的服務,並地位多年來屹立不搖**。 Iris 提供了至少這些功能: - HTTP/2 (Push, 甚至是 Embedded 資料) - 中介模組(存取日誌、基礎認證、CORS、gRPC、防機器人 hCaptcha、JWT、方法覆寫、模組版本顯示、監控、PPROF、速率限制、防機器人 reCaptcha、panic 救援、請求識別碼、重寫請求) - API 分版 (versioning) - MVC (Model-View-Controller) 模式 - Websocket - gRPC - 自動啟用 HTTPS - 內建 ngrok 支援,讓您可以把 app 以最快速的方式推上網際網路 - 包含動態路徑、具唯一性的路由,支援如 :uuid、:string、:int 等等的標準類型,並且可以自己建立 - 壓縮功能 - 檢視 (View) 算繪引擎 (HTML、Django、Handlebars、Pug/Jade 等等) - 建立自己的檔案伺服器,並寄存您自己的 WebDAV 伺服器 - 快取 - 本地化 (i18n、sitemap) - 連線階段管理 - 豐富的回應格式(HTML、純文字、Markdown、XML、YAML、二進位、JSON、JSONP、Protocol Buffers、MessagePack、(HTTP) 內容協商、串流、Server-Sent Events 等) - 回應壓縮功能(gzip、deflate、brotli、snappy、s2) - 豐富的請求方式(綁定 URL 查詢、標頭、文字、XML、YAML、二進位、JSON、資料驗證、Protocol Buffers、MessagePack 等) - 依賴注入(MVC、處理常式 (handler)、API 路由) - 測試套件 - 最重要的是…… 從發行第一天到現在(已經整整六年),解答與支援一直都十分迅速! 看看別人 [是如何評價 Iris 的](https://www.iris-go.com/#review),並且 **[給這個開放原始碼專案一顆小星星](https://github.com/kataras/iris/stargazers)**,支持專案的潛力。 [![](https://iris-go.com/static/images/reviews.gif)](https://iris-go.com/testimonials/) [![Benchmarks: Jul 18, 2020 at 10:46am (UTC)](https://iris-go.com/static/images/benchmarks.svg)](https://github.com/kataras/server-benchmarks) ## 👑 支援者 你的一臂之力,能夠為大家帶來更好的開放原始碼 Web 開發體驗! > [@github](https://github.com/github) is now sponsoring you for $550.00 one time. > > A note from your new sponsor: > > To celebrate Maintainer Month we want to thank you for all you do for the open source community. Check out our blog post to learn more about how GitHub is investing in maintainers. https://github.blog/2022-06-24-thank-you-to-our-maintainers/ > 現已支援來自 [中國](https://github.com/kataras/iris/issues/1870#issuecomment-1101418349) 的捐款! ## 📖 學習 Iris ### 安裝 只要先安裝好 [Go 程式語言](https://go.dev/dl/) 即可。 #### 建立新專案 ```sh $ mkdir myapp $ cd myapp $ go mod init myapp $ go get github.com/kataras/iris/v12@latest # 或 @v12.2.11 ```
在現有專案安裝 ```sh $ cd myapp $ go get github.com/kataras/iris/v12@latest ``` **執行** ```sh $ go mod tidy -compat=1.20 # Windows 的話,請試試 -compat="1.20" $ go run . ```
![](https://www.iris-go.com/images/gifs/install-create-iris.gif) Iris 包含極其豐富且透徹的 **[文件](https://www.iris-go.com/docs)**,讓框架的入門觸手可及。 如需更為詳細的技術性文件,您可以前往我們的 [godocs](https://pkg.go.dev/github.com/kataras/iris/v12@main)。如果要可以直接執行的程式碼,可以到 [./\_examples](_examples) 儲存庫的子目錄參閱。 ### 想一邊旅行、一邊閱讀嗎? Book cover [![在 Twitter 上追蹤作者](https://img.shields.io/twitter/follow/makismaropoulos?color=3D8AA3&logoColor=3D8AA3&style=for-the-badge&logo=twitter)](https://twitter.com/intent/follow?screen_name=makismaropoulos) [![在 Twitter 上追蹤 Iris Web 框架](https://img.shields.io/twitter/follow/iris_framework?color=ee7506&logoColor=ee7506&style=for-the-badge&logo=twitter)](https://twitter.com/intent/follow?screen_name=iris_framework) [![在 Facebook 上追蹤 Iris Web 框架](https://img.shields.io/badge/Follow%20%40Iris.framework-569-2D88FF.svg?style=for-the-badge&logo=facebook)](https://www.facebook.com/iris.framework) 您現在可以 [請求索取](https://www.iris-go.com/#ebookDonateForm) **Iris 電子書**(新版,**針對未來版本 v12.2.0+**) 的 PDF 和線上閱讀存取權限,並共同參與 Iris 的開發。 ## 🙌 貢獻 我們殷切期盼你對 Iris Web 框架的貢獻!有關貢獻 Iris 專案的更多資訊,請參閱 [CONTRIBUTING.md](CONTRIBUTING.md) 檔案。 [所有貢獻者名單](https://github.com/kataras/iris/graphs/contributors) ## 🛡 安全性漏洞 如果你發現 Iris 中有安全性漏洞,請寄一封電子郵件至 [iris-go@outlook.com](mailto:iris-go@outlook.com)。我們會儘速解決所有安全性漏洞。 ## 📝 授權條款 本專案和 Go 語言相同,皆採 [BSD 3-clause 授權條款](LICENSE) 授權。 專案的名稱「Iris」取材自希臘神話。 ================================================ FILE: SECURITY.md ================================================ # Security Policy ## Supported Versions We are focusing on fixing issues and security vulnerabilities to the latest and greatest version of Iris. Iris users SHOULD always update their backend code to the latest version of Iris API, there is a complete [HISTORY](https://github.com/kataras/iris/blob/main/HISTORY.md) file which acts as a code migration assistant. ## Reporting a Vulnerability Please report (suspected) security vulnerabilities to **[iris-go@outlook.com](mailto:iris-go@outlook.com)**. You will receive a response from us within 3-4 working days. If the issue is confirmed, we will release a new minor version as soon as possible depending on complexity but historically within a few hours or days. ================================================ FILE: VERSION ================================================ v12.2.0:https://github.com/kataras/iris/tree/v12.2.0 ================================================ FILE: _benchmarks/README.md ================================================ # Benchmarks - [HTTP/2 Benchmarks](https://github.com/kataras/server-benchmarks#benchmarks) - [View Engine Benchmarks](./view) ================================================ FILE: _benchmarks/view/README.md ================================================ # View Engine Benchmarks Benchmark between all 7 supported template parsers. Ace and Pug parsers minifies the template before render. So, to have a fair benchmark, we must make sure that the byte amount of the total response body is exactly the same across all. Therefore, all other template files are minified too. ![Benchmarks Chart Graph](chart.png) > Last updated: Mar 17, 2023 at 10:31am (UTC) ## System | | | |----|:---| | Processor | 12th Gen Intel(R) Core(TM) i7-12700H | | RAM | 15.68 GB | | OS | Microsoft Windows 11 Pro | | [Bombardier](https://github.com/codesenberg/bombardier) | v1.2.4 | | [Go](https://golang.org) | go1.20.2 | | [Node.js](https://nodejs.org/) | v19.5.0 | ## Terminology **Name** is the name of the framework(or router) used under a particular test. **Reqs/sec** is the avg number of total requests could be processed per second (the higher the better). **Latency** is the amount of time it takes from when a request is made by the client to the time it takes for the response to get back to that client (the smaller the better). **Throughput** is the rate of production or the rate at which data are transferred (the higher the better, it depends from response length (body + headers). **Time To Complete** is the total time (in seconds) the test completed (the smaller the better). ## Results ### Test:Template Layout, Partial and Data 📖 Fires 1000000 requests with 125 concurrent clients. It receives HTML response. The server handler sets some template **data** and renders a template file which consists of a **layout** and a **partial** footer. | Name | Language | Reqs/sec | Latency | Throughput | Time To Complete | |------|:---------|:---------|:--------|:-----------|:-----------------| | [Jet](./jet) | Go |248957 |500.81us |88.26MB |4.02s | | [Blocks](./blocks) | Go |238854 |521.76us |84.74MB |4.19s | | [Pug](./pug) | Go |238153 |523.74us |85.07MB |4.20s | | [Django](./django) | Go |224448 |555.40us |79.61MB |4.46s | | [Handlebars](./handlebars) | Go |197267 |631.99us |69.96MB |5.07s | | [Ace](./ace) | Go |157415 |792.53us |55.83MB |6.35s | | [HTML](./html) | Go |120811 |1.03ms |42.82MB |8.29s | ## How to Run ```sh $ go install github.com/kataras/server-benchmarks@master $ go install github.com/codesenberg/bombardier@master $ server-benchmarks --wait-run=3s -o ./results ``` ================================================ FILE: _benchmarks/view/ace/main.go ================================================ package main import ( "fmt" "github.com/kataras/iris/v12" ) func main() { app := iris.New() // By default Ace engine minifies the template before render. app.RegisterView(iris.Ace("./views", ".ace").SetIndent("")) app.Get("/", index) app.Listen(":8080") } func index(ctx iris.Context) { data := iris.Map{ "Title": "Page Title", "FooterText": "Footer contents", "Message": "Main contents", } ctx.ViewLayout("layouts/main") if err := ctx.View("index", data); err != nil { ctx.HTML(fmt.Sprintf("

%s

", err.Error())) return } } ================================================ FILE: _benchmarks/view/ace/views/index.ace ================================================ h1 Index Body h3 Message: {{.Message}} ================================================ FILE: _benchmarks/view/ace/views/layouts/main.ace ================================================ = doctype html html head title {{.Title}} body {{ yield . }} footer = include partials/footer.ace . ================================================ FILE: _benchmarks/view/ace/views/partials/footer.ace ================================================ h3 Footer Partial h4 {{.FooterText}} ================================================ FILE: _benchmarks/view/blocks/main.go ================================================ package main import ( "fmt" "github.com/kataras/iris/v12" ) func main() { app := iris.New() app.RegisterView(iris.Blocks("./views", ".html")) // Note, in Blocks engine, layouts // are used by their base names, the // blocks.LayoutDir(layoutDir) defaults to "./layouts". app.Get("/", index) app.Listen(":8080") } func index(ctx iris.Context) { data := iris.Map{ "Title": "Page Title", "FooterText": "Footer contents", "Message": "Main contents", } ctx.ViewLayout("main") if err := ctx.View("index", data); err != nil { ctx.HTML(fmt.Sprintf("

%s

", err.Error())) return } } ================================================ FILE: _benchmarks/view/blocks/views/index.html ================================================

Index Body

Message: {{.Message}}

================================================ FILE: _benchmarks/view/blocks/views/layouts/main.html ================================================ {{.Title}}{{ template "content" . }}
{{ partial "partials/footer" . }}
================================================ FILE: _benchmarks/view/blocks/views/partials/footer.html ================================================

Footer Partial

{{.FooterText}}

================================================ FILE: _benchmarks/view/django/main.go ================================================ package main import ( "fmt" "github.com/kataras/iris/v12" ) func main() { app := iris.New() app.RegisterView(iris.Django("./views", ".html")) app.Get("/", index) app.Listen(":8080") } func index(ctx iris.Context) { data := iris.Map{ "Title": "Page Title", "FooterText": "Footer contents", "Message": "Main contents", } // On Django this is ignored: ctx.ViewLayout("layouts/main") // Layouts are only rendered from inside the index page itself // using the "extends" keyword. if err := ctx.View("index", data); err != nil { ctx.HTML(fmt.Sprintf("

%s

", err.Error())) return } } ================================================ FILE: _benchmarks/view/django/views/index.html ================================================ {% extends "layouts/main.html" %}{% block content %}

Index Body

Message: {{Message}}

{% endblock %} ================================================ FILE: _benchmarks/view/django/views/layouts/main.html ================================================ {{Title}}{% block content %} {% endblock %}
{% include "../partials/footer.html" %}
================================================ FILE: _benchmarks/view/django/views/partials/footer.html ================================================

Footer Partial

{{FooterText}}

================================================ FILE: _benchmarks/view/handlebars/main.go ================================================ package main import ( "fmt" "github.com/kataras/iris/v12" ) func main() { app := iris.New() app.RegisterView(iris.Handlebars("./views", ".html")) app.Get("/", index) app.Listen(":8080") } func index(ctx iris.Context) { data := iris.Map{ "Title": "Page Title", "FooterText": "Footer contents", "Message": "Main contents", } ctx.ViewLayout("layouts/main") if err := ctx.View("index", data); err != nil { ctx.HTML(fmt.Sprintf("

%s

", err.Error())) return } } ================================================ FILE: _benchmarks/view/handlebars/views/index.html ================================================

Index Body

Message: {{Message}}

================================================ FILE: _benchmarks/view/handlebars/views/layouts/main.html ================================================ {{Title}}{{ yield . }}
{{ render "partials/footer.html" .}}
================================================ FILE: _benchmarks/view/handlebars/views/partials/footer.html ================================================

Footer Partial

{{FooterText}}

================================================ FILE: _benchmarks/view/html/main.go ================================================ package main import ( "fmt" "github.com/kataras/iris/v12" ) func main() { app := iris.New() app.RegisterView(iris.HTML("./views", ".html")) app.Get("/", index) app.Listen(":8080") } func index(ctx iris.Context) { data := iris.Map{ "Title": "Page Title", "FooterText": "Footer contents", "Message": "Main contents", } ctx.ViewLayout("layouts/main") if err := ctx.View("index", data); err != nil { ctx.HTML(fmt.Sprintf("

%s

", err.Error())) return } } ================================================ FILE: _benchmarks/view/html/views/index.html ================================================

Index Body

Message: {{.Message}}

================================================ FILE: _benchmarks/view/html/views/layouts/main.html ================================================ {{.Title}}{{ yield . }}
{{ render "partials/footer.html" . }}
================================================ FILE: _benchmarks/view/html/views/partials/footer.html ================================================

Footer Partial

{{.FooterText}}

================================================ FILE: _benchmarks/view/jet/main.go ================================================ package main import ( "fmt" "github.com/kataras/iris/v12" ) func main() { app := iris.New() app.RegisterView(iris.Jet("./views", ".jet")) app.Get("/", index) app.Listen(":8080") } func index(ctx iris.Context) { data := iris.Map{ "Title": "Page Title", "FooterText": "Footer contents", "Message": "Main contents", } // On Jet this is ignored: ctx.ViewLayout("layouts/main") // Layouts are only rendered from inside the index page itself // using the "extends" keyword. if err := ctx.View("index", data); err != nil { ctx.HTML(fmt.Sprintf("

%s

", err.Error())) return } } ================================================ FILE: _benchmarks/view/jet/views/index.jet ================================================ {{ extends "../layouts/main.jet" }}{{ block documentBody() }}

Index Body

Message: {{.Message}}

{{ end }} ================================================ FILE: _benchmarks/view/jet/views/layouts/main.jet ================================================ {{.Title}}{{ yield documentBody() }}
{{ include "../partials/footer.jet" . }}
================================================ FILE: _benchmarks/view/jet/views/partials/footer.jet ================================================

Footer Partial

{{.FooterText}}

================================================ FILE: _benchmarks/view/pug/main.go ================================================ package main import ( "fmt" "github.com/kataras/iris/v12" ) func main() { app := iris.New() // Ace engine minifies the template before render. app.RegisterView(iris.Pug("./views", ".pug")) app.Get("/", index) app.Listen(":8080") } func index(ctx iris.Context) { data := iris.Map{ "Title": "Page Title", "FooterText": "Footer contents", "Message": "Main contents", } // On Pug this is ignored: ctx.ViewLayout("layouts/main") // Layouts are only rendered from inside the index page itself // using the "extends" keyword. if err := ctx.View("index", data); err != nil { ctx.HTML(fmt.Sprintf("

%s

", err.Error())) return } } ================================================ FILE: _benchmarks/view/pug/views/index.pug ================================================ extends layouts/main.pug block content h1 Index Body h3 Message: {{.Message}} ================================================ FILE: _benchmarks/view/pug/views/layouts/main.pug ================================================ doctype html html head title {{.Title}} body block content footer include ../partials/footer.pug ================================================ FILE: _benchmarks/view/pug/views/partials/footer.pug ================================================ h3 Footer Partial h4 {{.FooterText}} ================================================ FILE: _benchmarks/view/tests.yml ================================================ - Name: Template Layout, Partial and Data Description: > Fires {{.NumberOfRequests}} requests with {{.NumberOfConnections}} concurrent clients. It receives HTML response. The server handler sets some template **data** and renders a template file which consists of a **layout** and a **partial** footer. NumberOfRequests: 1000000 NumberOfConnections: 125 Method: GET URL: http://localhost:8080 Envs: - Name: Ace Dir: ./ace - Name: Blocks Dir: ./blocks - Name: Django Dir: ./django - Name: Handlebars Dir: ./handlebars - Name: HTML Dir: ./html - Name: Jet Dir: ./jet - Name: Pug Dir: ./pug ================================================ FILE: _examples/README.md ================================================ # Table of Contents 🎁 **Welcome to the Iris Examples** Unwrap the magic of Iris with these festive examples, each one a present waiting to help you build amazing applications ✨ * [Serverless](https://github.com/iris-contrib/gateway#netlify) * [REST API for Apache Kafka](kafka-api) * [URL Shortener](url-shortener) * [Dropzone.js](dropzonejs) * [Caddy](caddy) * [Bootstrapper](bootstrapper) * [Project Structure](project) :fire: * Monitor * [Simple Process Monitor (includes UI)](monitor/monitor-middleware/main.go) **NEW** * [Heap, MSpan/MCache, Size Classes, Objects, Goroutines, GC/CPU fraction (includes UI)](monitor/statsviz/main.go) **NEW** * Database * [MySQL, Groupcache & Docker](database/mysql) * [MongoDB](database/mongodb) * [Sqlx](database/orm/sqlx/main.go) * [Gorm](database/orm/gorm/main.go) * [Reform](database/orm/reform/main.go) * [x/sqlx](database/sqlx/main.go) **NEW** * GraphQL * [GraphQL: schema-first](graphql/schema-first) **NEW** * HTTP Server * [HOST:PORT](http-server/listen-addr/main.go) * [Public Test Domain](http-server/listen-addr-public/main.go) * [UNIX socket file](http-server/listen-unix/main.go) * [TLS](http-server/listen-tls/main.go) * [Letsencrypt (Automatic Certifications)](http-server/listen-letsencrypt/main.go) * [Socket Sharding (SO_REUSEPORT)](http-server/socket-sharding/main.go) * [Graceful Shutdown](http-server/graceful-shutdown/default-notifier/main.go) * [Notify on shutdown](http-server/notify-on-shutdown/main.go) * Custom TCP Listener * [Common net.Listener](http-server/custom-listener/main.go) * Custom HTTP Server * [Pass a custom Server](http-server/custom-httpserver/easy-way/main.go) * [Use Iris as a single http.Handler](http-server/custom-httpserver/std-way/main.go) * [Multi Instances](http-server/custom-httpserver/multi/main.go) * [HTTP/3 Quic](http-server/http3-quic) * [H2C](http-server/h2c/main.go) **NEW** * [Timeout](http-server/timeout/main.go) * HTTP Client * [Weather Client](http-client/weatherapi) * Configuration * [Functional](configuration/functional/main.go) * [Configuration Struct](configuration/from-configuration-structure/main.go) * [Using Viper](configuration/viper) * [Import from YAML](configuration/from-yaml-file/main.go) * [Share Configuration across instances](configuration/from-yaml-file/shared-configuration/main.go) * [Import from TOML](configuration/from-toml-file/main.go) * [Multi Environment Configuration](configuration/multi-environments) **NEW** * Routing * [Custom Context](routing/custom-context/main.go) **HOT/NEW** * [Party Controller](routing/party-controller) **NEW** * [Overview](routing/overview/main.go) * [Basic](routing/basic/main.go) * [Custom HTTP Errors](routing/http-errors/main.go) * [HTTP Wire Errors](routing/http-wire-errors/main.go) **NEW** * [Service and Validation](routing/http-wire-errors/service/main.go) **NEW** * [Not Found - Intelligence](routing/intelligence/main.go) * [Not Found - Suggest Closest Paths](routing/intelligence/manual/main.go) * [Dynamic Path](routing/dynamic-path/main.go) * [At-username](routing/dynamic-path/at-username/main.go) * [Root Wildcard](routing/dynamic-path/root-wildcard/main.go) * [Implement a Parameter Type](routing/macros/main.go) * [Same Path Pattern but Func](routing/dynamic-path/same-pattern-different-func/main.go) * Middleware * [Per Route](routing/writing-a-middleware/per-route/main.go) * [Globally](routing/writing-a-middleware/globally/main.go) * [Remove a Handler](routing/remove-handler/main.go) * Share Values * [Share Services](routing/writing-a-middleware/share-services/main.go) * [Share Functions](routing/writing-a-middleware/share-funcs/main.go) * [Handlers Execution Rule](routing/route-handlers-execution-rules/main.go) * [Route Register Rule](routing/route-register-rule/main.go) * Convert net/http Handlers * [From func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc)](convert-handlers/negroni-like/main.go) * [From http.Handler or http.HandlerFunc](convert-handlers/nethttp/main.go) * [From func(http.Handler) http.Handler](convert-handlers/wrapper/main.go) * [Convert by your own: sentry/raven middleware](convert-handlers/real-usecase-raven/writing-middleware/main.go) * [Rewrite Middleware](routing/rewrite/main.go) * [Route State](routing/route-state/main.go) * [Remove Route](routing/remove-route/main.go) * [Reverse Routing](routing/reverse/main.go) * [Router Wrapper](routing/custom-wrapper/main.go) * [Custom Router](routing/custom-router/main.go) * Subdomains * [Single](routing/subdomains/single/main.go) * [Multi](routing/subdomains/multi/main.go) * [Wildcard](routing/subdomains/wildcard/main.go) * [WWW](routing/subdomains/www/main.go) * [WWW Method](routing/subdomains/www/www-method/main.go) * [Redirection](routing/subdomains/redirect/main.go) * [Multi Instances](routing/subdomains/redirect/multi-instances/main.go) * [HTTP Errors View](routing/subdomains/http-errors-view/main.go) * [HTTP Method Override](https://github.com/kataras/iris/blob/main/middleware/methodoverride/methodoverride_test.go) * [API Versioning](routing/versioning/main.go) * [Sitemap](routing/sitemap/main.go) * Logging * [Application File Logger](logging/file-logger/main.go) * [Application JSON Logger](logging/json-logger/main.go) * [Rollbar](logging/rollbar/main.go) * AccessLog * [Log Requests to a JSON File](logging/request-logger/accesslog-simple/main.go) * [Using Log Rotation and more](logging/request-logger/accesslog) * [Custom Fields and Template](logging/request-logger/accesslog-template/main.go) * [Listen and render Logs to a Client](logging/request-logger/accesslog-broker/main.go) * [The CSV Formatter](logging/request-logger/accesslog-csv/main.go) * [Create your own Formatter](logging/request-logger/accesslog-formatter/main.go) * [Root and Proxy AccessLog instances](logging/request-logger/accesslog-proxy/main.go) * [Slack integration example](logging/request-logger/accesslog-slack/main.go) * API Documentation * [Swagger](https://github.com/iris-contrib/swagger/tree/master/_examples/basic) * Testing * [Testing with httptest](testing/httptest/main_test.go) * [Testing with ginkgo](testing/ginkgotest) * [Recovery](recover/main.go) * [Panic and custom Error Handler with Compression](recover/panic-and-custom-error-handler-with-compression/main.go) * [Profiling](pprof/main.go) * File Server * [File Server](file-server/file-server/main.go) * [HTTP/2 Push Targets](file-server/http2push/main.go) * [HTTP/2 Push Targets (Embedded)](file-server/http2push-embedded/main.go) * [HTTP/2 Push Targets (Gzipped Embedded)](file-server/http2push-embedded-gzipped/main.go) * [Favicon](file-server/favicon/main.go) * [Basic](file-server/basic/main.go) * [Embedding Files Into App Executable File](file-server/embedding-files-into-app/main.go) * [Embedding Files Into App Executable File (Bindata)](file-server/embedding-files-into-app-bindata/main.go) * [Embedding Gzipped Files Into App Executable File (Bindata)](file-server/embedding-gzipped-files-into-app-bindata/main.go) * [Send Files (rate limiter included)](file-server/send-files/main.go) * Single Page Applications * [Vue Router](file-server/spa-vue-router) * [Basic SPA](file-server/single-page-application/basic/main.go) * [Embedded Single Page Application and `iris.PrefixDir`](file-server/single-page-application/embedded-single-page-application/main.go) * [Embedded Single Page Application with other routes](file-server/single-page-application/embedded-single-page-application-with-other-routes/main.go) * [Upload File](file-server/upload-file/main.go) * [Upload Multiple Files](file-server/upload-files/main.go) * [WebDAV](file-server/webdav/main.go) * View * [Overview](view/overview/main.go) * [Layout](view/layout) * [Ace](view/layout/ace) * [Blocks](view/layout/blocks) * [Django](view/layout/django) * [Handlebars](view/layout/handlebars) * [HTML](view/layout/html) * [Jet](view/layout/jet) * [Pug](view/layout/pug) * [Basic](view/template_html_0/main.go) * [A simple Layout](view/template_html_1/main.go) * [Layouts: `yield` and `render` tmpl funcs](view/template_html_2/main.go) * The `urlpath` template func * [HTML](view/template_html_3/main.go) * [Django](view/template_django_1/main.go) * [The `url` template func](view/template_html_4/main.go) * [Inject Data Between Handlers](view/context-view-data/main.go) * [Inject Engine Between Handlers](view/context-view-engine/main.go) * [Embedding Templates Into App Executable File](view/embedding-templates-into-app/main.go) * [Embedding Templates Into App Executable File (Bindata)](view/embedding-templates-into-app-bindata/main.go) * [Write to a custom `io.Writer`](view/write-to) * Parse a Template from Text * [HTML, Pug and Ace](view/parse-parse/main.go) * [Django](view/parse-parse/django/main.go) * [Jet](view/parse-parse/jet/main.go) * [Handlebars](view/parse-parse/handlebars/main.go) * [Blocks](view/template_blocks_0) * [Blocks Embedded](view/template_blocks_1_embedded) * [Pug: `Actions`](view/template_pug_0) * [Pug: `Includes`](view/template_pug_1) * [Pug Embedded`](view/template_pug_2_embedded) * [Ace](view/template_ace_0) * [Django](view/template_django_0) * [Jet](view/template_jet_0) * [Jet Embedded](view/template_jet_1_embedded) * [Jet 'urlpath' tmpl func](view/template_jet_2) * [Jet Template Funcs from Struct](view/template_jet_3) * [Handlebars](view/template_handlebars_0) * Third-Parties * [Render `valyala/quicktemplate` templates](view/quicktemplate) * [Render `shiyanhui/hero` templates](view/herotemplate) * [Render `a-h/templ` templates](view/templ) **NEW** * [Request ID](https://github.com/kataras/iris/blob/main/middleware/requestid/requestid_test.go) * [Request Rate Limit](request-ratelimit/main.go) * [Request Referrer](request-referrer/main.go) * [Webassembly](webassembly/main.go) * Request Body * [Bind JSON](request-body/read-json/main.go) * * [JSON Stream and disable unknown fields](request-body/read-json-stream/main.go) * * [Struct Validation](request-body/read-json-struct-validation/main.go) * [Bind XML](request-body/read-xml/main.go) * [Bind MsgPack](request-body/read-msgpack/main.go) * [Bind YAML](request-body/read-yaml/main.go) * [Bind Form](request-body/read-form/main.go) * [Checkboxes](request-body/read-form/checkboxes/main.go) * [Bind Query](request-body/read-query/main.go) * [Bind Params](request-body/read-params/main.go) * [Bind URL](request-body/read-url/main.go) * [Bind Headers](request-body/read-headers/main.go) * [Bind Body](request-body/read-body/main.go) * [Add Converter](request-body/form-query-headers-params-decoder/main.go) * [Bind Custom per type](request-body/read-custom-per-type/main.go) * [Bind Custom via Unmarshaler](request-body/read-custom-via-unmarshaler/main.go) * [Bind Many times](request-body/read-many/main.go) * Response Writer * [Content Negotiation](response-writer/content-negotiation) * [Text, Markdown, YAML, HTML, JSON, JSONP, Msgpack, XML and Binary](response-writer/write-rest/main.go) * [Third-party JSON Encoder](response-writer/json-third-party/main.go) * [Protocol Buffers](response-writer/protobuf/main.go) * [HTTP/2 Server Push](response-writer/http2push/main.go) * [Stream Writer](response-writer/stream-writer/main.go) * [Server-Sent Events](response-writer/sse/main.go) * [SSE 3rd-party (r3labs/sse)](response-writer/sse-third-party/main.go) * [SSE 3rd-party (alexandrevicenzi/go-sse)](response-writer/sse-third-party-2/main.go) * Cache * [Simple](response-writer/cache/simple/main.go) * [Client-Side (304)](response-writer/cache/client-side/main.go) * Compression * [Server-Side](compression/main.go) * [Client-Side](compression/client/main.go) * [Client-Side (using Iris)](compress/client-using-iris/main.go) * Localization and Internationalization * [Basic](i18n/basic) * [Ttemplates and Functions](i18n/template) * [Ttemplates and Functions (Embedded)](i18n/template-embedded) * [Pluralization and Variables](i18n/plurals) * Authentication, Authorization & Bot Detection * [Recommended: Auth package and Single-Sign-On](auth/auth) **NEW (GO 1.18 Generics required)** * Basic Authentication * [Basic](auth/basicauth/basic) * [Load from a slice of Users](auth/basicauth/users_list) * [Load from a file & encrypted passwords](auth/basicauth/users_file_bcrypt) * [Fetch & validate a User from a Database (MySQL)](auth/basicauth/database) * [CORS](auth/cors) * JSON Web Tokens * [Basic](auth/jwt/basic/main.go) * [Middleware](auth/jwt/midleware/main.go) * [Blocklist](auth/jwt/blocklist/main.go) * [Refresh Token](auth/jwt/refresh-token/main.go) * [Tutorial](auth/jwt/tutorial) * [JWT (community edition)](https://github.com/iris-contrib/middleware/tree/v12/jwt/_example/main.go) * [OAUth2](auth/goth/main.go) * [Manage Permissions](auth/permissions/main.go) * [Google reCAPTCHA](auth/recaptcha/main.go) * [hCaptcha](auth/hcaptcha/main.go) * Cookies * [Basic](cookies/basic/main.go) * [Options](cookies/options/main.go) * [Encode/Decode (with `securecookie`)](cookies/securecookie/main.go) * Sessions * [Overview: Config](sessions/overview/main.go) * [Overview: Routes](sessions/overview/example/example.go) * [Basic](sessions/basic/main.go) * [Secure Cookie](sessions/securecookie/main.go) * [Flash Messages](sessions/flash-messages/main.go) * [Databases](sessions/database) * [Badger](sessions/database/badger/main.go) * [BoltDB](sessions/database/boltdb/main.go) * [Redis](sessions/database/redis/main.go) * [View Data](sessions/viewdata) * Websocket * [Gorilla FileWatch (3rd-party)](websocket/gorilla-filewatch/main.go) * [Basic](websocket/basic) * [Server](websocket/basic/server.go) * [Go Client](websocket/basic/go-client/client.go) * [Browser Client](websocket/basic/browser/index.html) * [Browser NPM Client (browserify)](websocket/basic/browserify/app.js) * [Native Messages](websocket/native-messages/main.go) * [TLS](websocket/secure/README.md) * [Online Visitors](websocket/online-visitors/main.go) * Dependency Injection * [Overview (Movies Service)](ependency-injection/overview/main.go) * [Basic](dependency-injection/basic/main.go) * [Middleware](dependency-injection/basic/middleware/main.go) * [Sessions](dependency-injection/sessions/main.go) * [Smart Contract](dependency-injection/smart-contract/main.go) * [JWT](dependency-injection/jwt/main.go) * [JWT (iris-contrib)](dependency-injection/jwt/contrib/main.go) * [Register Dependency from Context](dependency-injection/context-register-dependency/main.go) * MVC * [Overview](mvc/overview) * [Repository and Service layers](mvc/repository) * [Hello world](mvc/hello-world/main.go) * [Basic](mvc/basic/main.go) * [Wildcard](mvc/basic/wildcard/main.go) * [Default request values](mvc/request-default-values/main.go) * [Singleton](mvc/singleton) * [Regexp](mvc/regexp/main.go) * [Session Controller](mvc/session-controller/main.go) * [Authenticated Controller](mvc/authenticated-controller/main.go) * [Versioned Controller](mvc/versioned-controller/main.go) * [Websocket Controller](mvc/websocket) * [Websocket + Authentication (Single-Sign-On)](mvc/websocket-auth) **NEW (GO 1.18 Generics required)** * [Register Middleware](mvc/middleware) * [gRPC](mvc/grpc-compatible) * [gRPC Bidirectional Stream](mvc/grpc-compatible-bidirectional-stream) * [Login (Repository and Service layers)](mvc/login) * [Login (Single Responsibility)](mvc/login-mvc-single-responsibility) * [Vue.js Todo App](mvc/vuejs-todo-mvc) * [HTTP Error Handler](mvc/error-handler-http) * [Error Handler](mvc/error-handler) * [Handle errors using mvc.Result](mvc/error-handler-custom-result) * [Handle errors using PreflightResult](mvc/error-handler-preflight) * [Handle errors by hijacking the result](mvc/error-handler-hijack) * Desktop Applications * [The blink package](desktop/blink) * [The lorca package](desktop/lorca) * [The webview package](desktop/webview) * Middlewares [(Community)](https://github.com/iris-contrib/middleware) ================================================ FILE: _examples/README_ZH_HANT.md ================================================ # 章節目錄 - [無伺服器 (Serverless)](https://github.com/iris-contrib/gateway#netlify) - [適用 Apache Kafka 的 REST API](kafka-api) - [縮網址服務](url-shortener) - [Dropzone.js](dropzonejs) - [Caddy](caddy) - [初始化工具](bootstrapper) - [專案結構](project) :fire: - 監控 - [簡易處理程序監控工具 (含 UI)](monitor/monitor-middleware/main.go) **新範例** - [堆積、MSpan/MCache、Size Classes、物件、Goroutines、GC/CPU 分數 (含 UI)](monitor/statsviz/main.go) **新範例** - 資料庫 - [MySQL、Groupcache 和 Docker](database/mysql) - [MongoDB](database/mongodb) - [Sqlx](database/orm/sqlx/main.go) - [Gorm](database/orm/gorm/main.go) - [Reform](database/orm/reform/main.go) - [x/sqlx](database/sqlx/main.go) **新範例** - HTTP 伺服器 - [主機:連線埠](http-server/listen-addr/main.go) - [公開測試網域](http-server/listen-addr-public/main.go) - [UNIX 通訊端 (socket) 檔案](http-server/listen-unix/main.go) - [TLS](http-server/listen-tls/main.go) - [Let's Encrypt(自動發證)](http-server/listen-letsencrypt/main.go) - [通訊端切分 (SO_REUSEPORT)](http-server/socket-sharding/main.go) - [優雅關閉伺服器](http-server/graceful-shutdown/default-notifier/main.go) - [關閉伺服器時通知](http-server/notify-on-shutdown/main.go) - 自訂 TCP 監聽 - [通用 net.Listener](http-server/custom-listener/main.go) - 自訂 HTTP 伺服器 - [傳入自訂 Server 實體](http-server/custom-httpserver/easy-way/main.go) - [將 Iris 用作單一 http.Handler](http-server/custom-httpserver/std-way/main.go) - [多實體](http-server/custom-httpserver/multi/main.go) - [HTTP/3 QUIC](http-server/http3-quic) - [H2C](http-server/h2c/main.go) **新範例** - [延時 (timeout)](http-server/timeout/main.go) - HTTP 用戶端 - [天氣用戶端](http-client/weatherapi) - 設定 - [Functional](configuration/functional/main.go) - [Configuration Struct](configuration/from-configuration-structure/main.go) - [Using Viper](configuration/viper) - [Import from YAML](configuration/from-yaml-file/main.go) - [Share Configuration across instances](configuration/from-yaml-file/shared-configuration/main.go) - [Import from TOML](configuration/from-toml-file/main.go) - [Multi Environment Configuration](configuration/multi-environments) **新範例** - 路由 - [Party Controller](routing/party-controller) **新範例** - [Overview](routing/overview/main.go) - [Basic](routing/basic/main.go) - [Custom HTTP Errors](routing/http-errors/main.go) - [HTTP Wire Errors](routing/http-wire-errors/main.go) **新範例** - [Not Found - Intelligence](routing/intelligence/main.go) - [Not Found - Suggest Closest Paths](routing/intelligence/manual/main.go) - [Dynamic Path](routing/dynamic-path/main.go) - [At-username](routing/dynamic-path/at-username/main.go) - [Root Wildcard](routing/dynamic-path/root-wildcard/main.go) - [Implement a Parameter Type](routing/macros/main.go) - [Same Path Pattern but Func](routing/dynamic-path/same-pattern-different-func/main.go) - Middleware - [Per Route](routing/writing-a-middleware/per-route/main.go) - [Globally](routing/writing-a-middleware/globally/main.go) - [Remove a Handler](routing/remove-handler/main.go) - Share Values - [Share Services](routing/writing-a-middleware/share-services/main.go) - [Share Functions](routing/writing-a-middleware/share-funcs/main.go) - [Handlers Execution Rule](routing/route-handlers-execution-rules/main.go) - [Route Register Rule](routing/route-register-rule/main.go) - Convert net/http Handlers - [From func(w http.ResponseWriter, r \*http.Request, next http.HandlerFunc)](convert-handlers/negroni-like/main.go) - [From http.Handler or http.HandlerFunc](convert-handlers/nethttp/main.go) - [From func(http.Handler) http.Handler](convert-handlers/wrapper/main.go) - [Convert by your own: sentry/raven middleware](convert-handlers/real-usecase-raven/writing-middleware/main.go) - [Rewrite Middleware](routing/rewrite/main.go) - [Route State](routing/route-state/main.go) - [Remove Route](routing/remove-route/main.go) - [Reverse Routing](routing/reverse/main.go) - [Router Wrapper](routing/custom-wrapper/main.go) - [Custom Router](routing/custom-router/main.go) - Subdomains - [Single](routing/subdomains/single/main.go) - [Multi](routing/subdomains/multi/main.go) - [Wildcard](routing/subdomains/wildcard/main.go) - [WWW](routing/subdomains/www/main.go) - [WWW Method](routing/subdomains/www/www-method/main.go) - [Redirection](routing/subdomains/redirect/main.go) - [Multi Instances](routing/subdomains/redirect/multi-instances/main.go) - [HTTP Errors View](routing/subdomains/http-errors-view/main.go) - [HTTP Method Override](https://github.com/kataras/iris/blob/main/middleware/methodoverride/methodoverride_test.go) - [API Versioning](routing/versioning/main.go) - [Sitemap](routing/sitemap/main.go) - 日誌 - [Application File Logger](logging/file-logger/main.go) - [Application JSON Logger](logging/json-logger/main.go) - [Rollbar](logging/rollbar/main.go) - AccessLog - [Log Requests to a JSON File](logging/request-logger/accesslog-simple/main.go) - [Using Log Rotation and more](logging/request-logger/accesslog) - [Custom Fields and Template](logging/request-logger/accesslog-template/main.go) - [Listen and render Logs to a Client](logging/request-logger/accesslog-broker/main.go) - [The CSV Formatter](logging/request-logger/accesslog-csv/main.go) - [Create your own Formatter](logging/request-logger/accesslog-formatter/main.go) - [Root and Proxy AccessLog instances](logging/request-logger/accesslog-proxy/main.go) - [Slack integration example](logging/request-logger/accesslog-slack/main.go) - API 文件 - [Swagger](https://github.com/iris-contrib/swagger/tree/master/_examples/basic) - 測試 - [Testing with httptest](testing/httptest/main_test.go) - [Testing with ginkgo](testing/ginkgotest) - [救援錯誤](recover/main.go) - [Panic and custom Error Handler with Compression](recover/panic-and-custom-error-handler-with-compression/main.go) - [效能分析 (Profiling)](pprof/main.go) - 檔案伺服器 - [檔案伺服器](file-server/file-server/main.go) - [HTTP/2 Push Targets](file-server/http2push/main.go) - [HTTP/2 Push Targets (Embedded)](file-server/http2push-embedded/main.go) - [HTTP/2 Push Targets (Gzipped Embedded)](file-server/http2push-embedded-gzipped/main.go) - [Favicon](file-server/favicon/main.go) - [Basic](file-server/basic/main.go) - [Embedding Files Into App Executable File](file-server/embedding-files-into-app/main.go) - [Embedding Files Into App Executable File (Bindata)](file-server/embedding-files-into-app-bindata/main.go) - [Embedding Gzipped Files Into App Executable File (Bindata)](file-server/embedding-gzipped-files-into-app-bindata/main.go) - [Send Files (rate limiter included)](file-server/send-files/main.go) - 單頁面應用程式 - [Vue Router](file-server/spa-vue-router) - [Basic SPA](file-server/single-page-application/basic/main.go) - [Embedded Single Page Application and `iris.PrefixDir`](file-server/single-page-application/embedded-single-page-application/main.go) - [Embedded Single Page Application with other routes](file-server/single-page-application/embedded-single-page-application-with-other-routes/main.go) - [Upload File](file-server/upload-file/main.go) - [Upload Multiple Files](file-server/upload-files/main.go) - [WebDAV](file-server/webdav/main.go) - 檢視 - [概覽](view/overview/main.go) - [排版引擎](view/layout) - [Ace](view/layout/ace) - [Blocks](view/layout/blocks) - [Django](view/layout/django) - [Handlebars](view/layout/handlebars) - [HTML](view/layout/html) - [Jet](view/layout/jet) - [Pug](view/layout/pug) - [基礎](view/template_html_0/main.go) - [A simple Layout](view/template_html_1/main.go) - [Layouts: `yield` and `render` tmpl funcs](view/template_html_2/main.go) - `urlpath` 樣板函式 - [HTML](view/template_html_3/main.go) - [Django](view/template_django_1/main.go) - [`url` 樣板函式](view/template_html_4/main.go) - [Inject Data Between Handlers](view/context-view-data/main.go) - [Inject Engine Between Handlers](view/context-view-engine/main.go) - [Embedding Templates Into App Executable File](view/embedding-templates-into-app/main.go) - [Embedding Templates Into App Executable File (Bindata)](view/embedding-templates-into-app-bindata/main.go) - [Write to a custom `io.Writer`](view/write-to) - 從文字解析樣板 - [HTML, Pug and Ace](view/parse-parse/main.go) - [Django](view/parse-parse/django/main.go) - [Jet](view/parse-parse/jet/main.go) - [Handlebars](view/parse-parse/handlebars/main.go) - [Blocks](view/template_blocks_0) - [Blocks Embedded](view/template_blocks_1_embedded) - [Pug: `Actions`](view/template_pug_0) - [Pug: `Includes`](view/template_pug_1) - [Pug Embedded`](view/template_pug_2_embedded) - [Ace](view/template_ace_0) - [Django](view/template_django_0) - [Jet](view/template_jet_0) - [Jet Embedded](view/template_jet_1_embedded) - [Jet 'urlpath' tmpl func](view/template_jet_2) - [Jet Template Funcs from Struct](view/template_jet_3) - [Handlebars](view/template_handlebars_0) - 第三方引擎 - [Render `valyala/quicktemplate` templates](view/quicktemplate) - [Render `shiyanhui/hero` templates](view/herotemplate) - [請求 ID](https://github.com/kataras/iris/blob/main/middleware/requestid/requestid_test.go) - [請求速率限制](request-ratelimit/main.go) - [請求 Referrer](request-referrer/main.go) - [Webassembly](webassembly/main.go) - 請求本文 - [綁定 JSON](request-body/read-json/main.go) - [JSON Stream and disable unknown fields](request-body/read-json-stream/main.go) - [Struct Validation](request-body/read-json-struct-validation/main.go) - [Bind XML](request-body/read-xml/main.go) - [Bind MsgPack](request-body/read-msgpack/main.go) - [Bind YAML](request-body/read-yaml/main.go) - [Bind Form](request-body/read-form/main.go) - [Checkboxes](request-body/read-form/checkboxes/main.go) - [Bind Query](request-body/read-query/main.go) - [Bind Params](request-body/read-params/main.go) - [Bind URL](request-body/read-url/main.go) - [Bind Headers](request-body/read-headers/main.go) - [Bind Body](request-body/read-body/main.go) - [Add Converter](request-body/form-query-headers-params-decoder/main.go) - [Bind Custom per type](request-body/read-custom-per-type/main.go) - [Bind Custom via Unmarshaler](request-body/read-custom-via-unmarshaler/main.go) - [Bind Many times](request-body/read-many/main.go) - 請求寫入器 - [Content Negotiation](response-writer/content-negotiation) - [Text, Markdown, YAML, HTML, JSON, JSONP, Msgpack, XML and Binary](response-writer/write-rest/main.go) - [Third-party JSON Encoder](response-writer/json-third-party/main.go) - [Protocol Buffers](response-writer/protobuf/main.go) - [HTTP/2 Server Push](response-writer/http2push/main.go) - [Stream Writer](response-writer/stream-writer/main.go) - [Server-Sent Events](response-writer/sse/main.go) - [SSE 3rd-party (r3labs/sse)](response-writer/sse-third-party/main.go) - [SSE 3rd-party (alexandrevicenzi/go-sse)](response-writer/sse-third-party-2/main.go) - 快取 - [Simple](response-writer/cache/simple/main.go) - [Client-Side (304)](response-writer/cache/client-side/main.go) - 壓縮 - [Server-Side](compression/main.go) - [Client-Side](compression/client/main.go) - [Client-Side (using Iris)](compress/client-using-iris/main.go) - 本地化與國際化 - [基礎](i18n/basic) - [樣板與函式](i18n/template) - [樣板與函式 (嵌入式)](i18n/template-embedded) - [複數形與變數](i18n/plurals) - 認證、授權與機器人偵測 - [推薦:Auth 套件與單點登入](auth/auth) **新範例(需要 GO 1.18 的泛型功能)** - 基礎認證 - [Basic](auth/basicauth/basic) - [Load from a slice of Users](auth/basicauth/users_list) - [Load from a file & encrypted passwords](auth/basicauth/users_file_bcrypt) - [Fetch & validate a User from a Database (MySQL)](auth/basicauth/database) - [CORS](auth/cors) - JSON Web Tokens - [Basic](auth/jwt/basic/main.go) - [Middleware](auth/jwt/midleware/main.go) - [Blocklist](auth/jwt/blocklist/main.go) - [Refresh Token](auth/jwt/refresh-token/main.go) - [Tutorial](auth/jwt/tutorial) - [JWT (community edition)](https://github.com/iris-contrib/middleware/tree/v12/jwt/_example/main.go) - [OAUth2](auth/goth/main.go) - [Manage Permissions](auth/permissions/main.go) - [Google reCAPTCHA](auth/recaptcha/main.go) - [hCaptcha](auth/hcaptcha/main.go) - Cookies - [Basic](cookies/basic/main.go) - [Options](cookies/options/main.go) - [Encode/Decode (with `securecookie`)](cookies/securecookie/main.go) - 連線階段 - [概觀:組態設定](sessions/overview/main.go) - [概觀:路由](sessions/overview/example/example.go) - [Basic](sessions/basic/main.go) - [Secure Cookie](sessions/securecookie/main.go) - [Flash Messages](sessions/flash-messages/main.go) - [Databases](sessions/database) - [Badger](sessions/database/badger/main.go) - [BoltDB](sessions/database/boltdb/main.go) - [Redis](sessions/database/redis/main.go) - [View Data](sessions/viewdata) - Websocket - [Gorilla FileWatch (3rd-party)](websocket/gorilla-filewatch/main.go) - [Basic](websocket/basic) - [Server](websocket/basic/server.go) - [Go Client](websocket/basic/go-client/client.go) - [Browser Client](websocket/basic/browser/index.html) - [Browser NPM Client (browserify)](websocket/basic/browserify/app.js) - [Native Messages](websocket/native-messages/main.go) - [TLS](websocket/secure/README.md) - [Online Visitors](websocket/online-visitors/main.go) - 依賴注入 - [概觀 (電影服務)](ependency-injection/overview/main.go) - [Basic](dependency-injection/basic/main.go) - [Middleware](dependency-injection/basic/middleware/main.go) - [Sessions](dependency-injection/sessions/main.go) - [Smart Contract](dependency-injection/smart-contract/main.go) - [JWT](dependency-injection/jwt/main.go) - [JWT (iris-contrib)](dependency-injection/jwt/contrib/main.go) - [Register Dependency from Context](dependency-injection/context-register-dependency/main.go) - MVC - [Overview](mvc/overview) - [Repository and Service layers](mvc/repository) - [Hello world](mvc/hello-world/main.go) - [Basic](mvc/basic/main.go) - [Wildcard](mvc/basic/wildcard/main.go) - [Default request values](mvc/request-default-values/main.go) - [Singleton](mvc/singleton) - [Regexp](mvc/regexp/main.go) - [Session Controller](mvc/session-controller/main.go) - [Authenticated Controller](mvc/authenticated-controller/main.go) - [Versioned Controller](mvc/versioned-controller/main.go) - [Websocket Controller](mvc/websocket) - [Websocket + Authentication (Single-Sign-On)](mvc/websocket-auth) **新範例(需要 GO 1.18 的泛型功能)** - [Register Middleware](mvc/middleware) - [gRPC](mvc/grpc-compatible) - [gRPC Bidirectional Stream](mvc/grpc-compatible-bidirectional-stream) - [Login (Repository and Service layers)](mvc/login) - [Login (Single Responsibility)](mvc/login-mvc-single-responsibility) - [Vue.js Todo App](mvc/vuejs-todo-mvc) - [HTTP Error Handler](mvc/error-handler-http) - [Error Handler](mvc/error-handler) - [Handle errors using mvc.Result](mvc/error-handler-custom-result) - [Handle errors using PreflightResult](mvc/error-handler-preflight) - [Handle errors by hijacking the result](mvc/error-handler-hijack) - 桌面應用程式 - [blink 套件](desktop/blink) - [lorca 套件](desktop/lorca) - [webview 套件](desktop/webview) - 中介模組 [(社群)](https://github.com/iris-contrib/middleware) ================================================ FILE: _examples/apidoc/swagger/README.md ================================================ # Swagger 2.0 Visit https://github.com/iris-contrib/swagger instead. ================================================ FILE: _examples/auth/auth/README.md ================================================ # Auth Package (+ Single Sign On) ```sh $ go run . ``` 1. GET/POST: http://localhost:8080/signin 2. GET: http://localhost:8080/member 3. GET: http://localhost:8080/owner 4. POST: http://localhost:8080/refresh 5. GET: http://localhost:8080/signout 6. GET: http://localhost:8080/signout-all ================================================ FILE: _examples/auth/auth/auth.yml ================================================ Headers: # required. - "Authorization" - "X-Authorization" Cookie: # optional. Name: "iris_auth_cookie" Secure: false Hash: "D*G-KaPdSgUkXp2s5v8y/B?E(H+MbQeThWmYq3t6w9z$C&F)J@NcRfUjXn2r4u7x" # length of 64 characters (512-bit). Block: "VkYp3s6v9y$B&E)H@McQfTjWmZq4t7w!" # length of 32 characters (256-bit). Keys: - ID: IRIS_AUTH_ACCESS # required. Alg: EdDSA MaxAge: 2h # 2 hours lifetime for access tokens. Private: |+ -----BEGIN PRIVATE KEY----- MC4CAQAwBQYDK2VwBCIEIFdZWoDdFny5SMnP9Fyfr8bafi/B527EVZh8JJjDTIFO -----END PRIVATE KEY----- Public: |+ -----BEGIN PUBLIC KEY----- MCowBQYDK2VwAyEAzpgjKSr9E032DX+foiOxq1QDsbzjLxagTN+yVpGWZB4= -----END PUBLIC KEY----- - ID: IRIS_AUTH_REFRESH # optional. Good practise to have it though. Alg: EdDSA # 1 month lifetime for refresh tokens, # after that period the user has to signin again. MaxAge: 720h Private: |+ -----BEGIN PRIVATE KEY----- MC4CAQAwBQYDK2VwBCIEIHJ1aoIjA2sRp5eqGjGR3/UMucrHbBdBv9p8uwfzZ1KZ -----END PRIVATE KEY----- Public: |+ -----BEGIN PUBLIC KEY----- MCowBQYDK2VwAyEAsKKAr+kDtfAqwG7cZdoEAfh9jHt9W8qi9ur5AA1KQAQ= -----END PUBLIC KEY----- # Example of setting a binary form of the encryption key for refresh tokens, # it could be a "string" as well. EncryptionKey: !!binary stSNLTu91YyihPxzeEOXKwGVMG00CjcC/68G8nMgmqA= ================================================ FILE: _examples/auth/auth/main.go ================================================ //go:build go1.18 // +build go1.18 package main import ( "fmt" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/auth" ) func allowRole(role AccessRole) auth.VerifyUserFunc[User] { return func(u User) error { if !u.Role.Allow(role) { return fmt.Errorf("invalid role") } return nil } } const configFilename = "./auth.yml" func main() { app := iris.New() app.RegisterView(iris.Blocks(iris.Dir("./views"), ".html"). LayoutDir("layouts"). Layout("main")) /* // Easiest 1-liner way, load from configuration and initialize a new auth instance: s := auth.MustLoad[User]("./auth.yml") // Bind a configuration from file: var c auth.Configuration c.BindFile("./auth.yml") s, err := auth.New[User](c) // OR create new programmatically configuration: config := auth.Configuration{ ...fields } s, err := auth.New[User](config) // OR generate a new configuration: config := auth.MustGenerateConfiguration() s, err := auth.New[User](config) // OR generate a new config and save it if cannot open the config file. if _, err := os.Stat(configFilename); err != nil { generatedConfig := auth.MustGenerateConfiguration() configContents, err := generatedConfig.ToYAML() if err != nil { panic(err) } err = os.WriteFile(configFilename, configContents, 0600) if err != nil { panic(err) } } */ // 1. Load configuration from a file. authConfig, err := auth.LoadConfiguration(configFilename) if err != nil { panic(err) } // 2. Initialize a new auth instance for "User" claims (generics: go1.18 +). s, err := auth.New[User](authConfig) if err != nil { panic(err) } // 3. Add a custom provider, in our case is just a memory-based one. s.AddProvider(NewProvider()) // 3.1. Optionally set a custom error handler. // s.SetErrorHandler(new(auth.DefaultErrorHandler)) app.Get("/signin", renderSigninForm) // 4. generate token pairs. app.Post("/signin", s.SigninHandler) // 5. refresh token pairs. app.Post("/refresh", s.RefreshHandler) // 6. calls the provider's InvalidateToken method. app.Get("/signout", s.SignoutHandler) // 7. calls the provider's InvalidateTokens method. app.Get("/signout-all", s.SignoutAllHandler) // 8.1. allow access for users with "Member" role. app.Get("/member", s.VerifyHandler(allowRole(Member)), renderMemberPage(s)) // 8.2. allow access for users with "Owner" role. app.Get("/owner", s.VerifyHandler(allowRole(Owner)), renderOwnerPage(s)) /* Subdomain user verify: app.Subdomain("owner", s.VerifyHandler(allowRole(Owner))).Get("/", renderOwnerPage(s)) */ app.Listen(":8080", iris.WithOptimizations) // Setup HTTPS/TLS for production instead. /* Test subdomain user verify, one way is ingrok, add the below to the arguments above: , iris.WithConfiguration(iris.Configuration{ EnableOptmizations: true, Tunneling: iris.TunnelingConfiguration{ AuthToken: "YOUR_AUTH_TOKEN", Region: "us", Tunnels: []tunnel.Tunnel{ { Name: "Iris Auth (Test)", Addr: ":8080", Hostname: "YOUR_DOMAIN", }, { Name: "Iris Auth (Test Subdomain)", Addr: ":8080", Hostname: "owner.YOUR_DOMAIN", }, }, }, })*/ } func renderSigninForm(ctx iris.Context) { if err := ctx.View("signin", iris.Map{"Title": "Signin Page"}); err != nil { ctx.HTML(fmt.Sprintf("

%s

", err.Error())) return } } func renderMemberPage(s *auth.Auth[User]) iris.Handler { return func(ctx iris.Context) { user := s.GetUser(ctx) ctx.Writef("Hello member: %s\n", user.Email) } } func renderOwnerPage(s *auth.Auth[User]) iris.Handler { return func(ctx iris.Context) { user := s.GetUser(ctx) ctx.Writef("Hello owner: %s\n", user.Email) } } ================================================ FILE: _examples/auth/auth/user.go ================================================ //go:build go1.18 // +build go1.18 package main type AccessRole uint16 func (r AccessRole) Is(v AccessRole) bool { return r&v != 0 } func (r AccessRole) Allow(v AccessRole) bool { return r&v >= v } const ( InvalidAccessRole AccessRole = 1 << iota Read Write Delete Owner = Read | Write | Delete Member = Read | Write ) type User struct { ID string `json:"id"` Email string `json:"email"` Role AccessRole `json:"role"` } func (u User) GetID() string { return u.ID } ================================================ FILE: _examples/auth/auth/user_provider.go ================================================ //go:build go1.18 // +build go1.18 package main import ( "context" "fmt" "sync" "time" "github.com/kataras/iris/v12/auth" ) type Provider struct { dataset []User invalidated map[string]struct{} // key = token. Entry is blocked. invalidatedAll map[string]int64 // key = user id, value = timestamp. Issued before is consider invalid. mu sync.RWMutex } func NewProvider() *Provider { return &Provider{ dataset: []User{ { ID: "id-1", Email: "kataras2006@hotmail.com", Role: Owner, }, { ID: "id-2", Email: "example@example.com", Role: Member, }, }, invalidated: make(map[string]struct{}), invalidatedAll: make(map[string]int64), } } func (p *Provider) Signin(ctx context.Context, username, password string) (User, error) { // fired on SigninHandler. // your database... for _, user := range p.dataset { if user.Email == username { return user, nil } } return User{}, fmt.Errorf("user not found") } func (p *Provider) ValidateToken(ctx context.Context, standardClaims auth.StandardClaims, u User) error { // fired on VerifyHandler. // your database and checks of blocked tokens... // check for specific token ids. p.mu.RLock() _, tokenBlocked := p.invalidated[standardClaims.ID] if !tokenBlocked { // this will disallow refresh tokens with origin jwt token id as the blocked access token as well. if standardClaims.OriginID != "" { _, tokenBlocked = p.invalidated[standardClaims.OriginID] } } p.mu.RUnlock() if tokenBlocked { return fmt.Errorf("token was invalidated") } // // check all tokens issuet before the "InvalidateToken" method was fired for this user. p.mu.RLock() ts, oldUserBlocked := p.invalidatedAll[u.ID] p.mu.RUnlock() if oldUserBlocked && standardClaims.IssuedAt <= ts { return fmt.Errorf("token was invalidated") } // return nil // else valid. } func (p *Provider) InvalidateToken(ctx context.Context, standardClaims auth.StandardClaims, u User) error { // fired on SignoutHandler. // invalidate this specific token. p.mu.Lock() p.invalidated[standardClaims.ID] = struct{}{} p.mu.Unlock() return nil } func (p *Provider) InvalidateTokens(ctx context.Context, u User) error { // fired on SignoutAllHandler. // invalidate all previous tokens came from "u". p.mu.Lock() p.invalidatedAll[u.ID] = time.Now().Unix() p.mu.Unlock() return nil } ================================================ FILE: _examples/auth/auth/views/layouts/main.html ================================================ {{ if .Title }}{{ .Title }}{{ else }}Default Main Title{{ end }}
{{ template "content" . }}
{{ partial "partials/footer" . }}
================================================ FILE: _examples/auth/auth/views/partials/footer.html ================================================ Iris Web Framework © 2022 ================================================ FILE: _examples/auth/auth/views/signin.html ================================================ ================================================ FILE: _examples/auth/basicauth/basic/main.go ================================================ package main import ( "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/x/errors" "github.com/kataras/iris/v12/middleware/basicauth" ) func newApp() *iris.Application { app := iris.New() /* opts := basicauth.Options{ Realm: "Authorization Required", MaxAge: 30 * time.Minute, GC: basicauth.GC{ Every: 2 * time.Hour, }, Allow: basicauth.AllowUsers(map[string]string{ "myusername": "mypassword", "mySecondusername": "mySecondpassword", }), MaxTries: 2, } auth := basicauth.New(opts) OR simply: */ auth := basicauth.Default(map[string]string{ "myusername": "mypassword", "mySecondusername": "mySecondpassword", }) // To the next routes of a party (group of routes): /* app.Use(auth) */ // For global effect, including not founds: /* app.UseRouter(auth) */ // For global effect, excluding http errors such as not founds: /* app.UseGlobal(auth) or app.Use(auth) before any route registered. */ // For single/per routes: /* app.Get("/mysecret", auth, h) */ app.Get("/", func(ctx iris.Context) { ctx.Redirect("/admin") }) // to party needAuth := app.Party("/admin", auth) { //http://localhost:8080/admin needAuth.Get("/", handler) // http://localhost:8080/admin/profile needAuth.Get("/profile", handler) // http://localhost:8080/admin/settings needAuth.Get("/settings", handler) needAuth.Get("/logout", logout) } return app } func main() { app := newApp() // open http://localhost:8080/admin app.Listen(":8080") } func handler(ctx iris.Context) { // user := ctx.User().(*myUserType) // or ctx.User().GetRaw().(*myUserType) // ctx.Writef("%s %s:%s", ctx.Path(), user.Username, user.Password) // OR if you don't have registered custom User structs: username, password, _ := ctx.Request().BasicAuth() ctx.Writef("%s %s:%s", ctx.Path(), username, password) } func logout(ctx iris.Context) { // fires 401, invalidates the basic auth, // logout through javascript and ajax is a better solution though. err := ctx.Logout() if err != nil { errors.Internal.Err(ctx, err) return } } ================================================ FILE: _examples/auth/basicauth/basic/main_test.go ================================================ package main import ( "testing" "github.com/kataras/iris/v12/httptest" ) func TestBasicAuth(t *testing.T) { app := newApp() e := httptest.New(t, app) // redirects to /admin without basic auth e.GET("/").Expect().Status(httptest.StatusUnauthorized) // without basic auth e.GET("/admin").Expect().Status(httptest.StatusUnauthorized) // with valid basic auth e.GET("/admin").WithBasicAuth("myusername", "mypassword").Expect(). Status(httptest.StatusOK).Body().IsEqual("/admin myusername:mypassword") e.GET("/admin/profile").WithBasicAuth("myusername", "mypassword").Expect(). Status(httptest.StatusOK).Body().IsEqual("/admin/profile myusername:mypassword") e.GET("/admin/settings").WithBasicAuth("myusername", "mypassword").Expect(). Status(httptest.StatusOK).Body().IsEqual("/admin/settings myusername:mypassword") // with invalid basic auth e.GET("/admin/settings").WithBasicAuth("invalidusername", "invalidpassword"). Expect().Status(httptest.StatusUnauthorized) } ================================================ FILE: _examples/auth/basicauth/database/Dockerfile ================================================ # docker build -t myapp . # docker run --rm -it -p 8080:8080 myapp:latest FROM golang:latest AS builder RUN apt-get update ENV GO111MODULE=on \ CGO_ENABLED=0 \ GOOS=linux \ GOARCH=amd64 WORKDIR /go/src/app COPY go.mod . RUN go mod download # cache step COPY . . RUN go install FROM scratch COPY --from=builder /go/bin/myapp . ENTRYPOINT ["./myapp"] ================================================ FILE: _examples/auth/basicauth/database/README.md ================================================ # BasicAuth + MySQL & Docker Example ## ⚡ Get Started Download the folder. ### Install (Docker) Install [Docker](https://www.docker.com/) and execute the command below ```sh $ docker-compose up --build ``` ### Install (Manually) Run `go build -mod=mod` or `go run -mod=mod main.go` and read below. #### MySQL Environment variables: ```sh MYSQL_USER=user_myapp MYSQL_PASSWORD=dbpassword MYSQL_HOST=localhost MYSQL_DATABASE=myapp ``` Download the schema from [migration/db.sql](migration/db.sql) and execute it against your MySQL server instance. ```sh username: admin password: admin ``` ```sh username: iris password: iris_password ``` The example does not contain code to add a user to the database, as this is out of the scope of this middleware. More features can be implemented by end-developers. ================================================ FILE: _examples/auth/basicauth/database/docker-compose.yml ================================================ version: '3.1' services: db: image: mysql command: --default-authentication-plugin=mysql_native_password environment: MYSQL_ROOT_PASSWORD: dbpassword MYSQL_DATABASE: myapp MYSQL_USER: user_myapp MYSQL_PASSWORD: dbpassword tty: true volumes: - ./migration:/docker-entrypoint-initdb.d app: build: . ports: - 8080:8080 environment: PORT: 8080 MYSQL_USER: user_myapp MYSQL_PASSWORD: dbpassword MYSQL_DATABASE: myapp MYSQL_HOST: db restart: on-failure healthcheck: test: ["CMD", "curl", "-f", "tcp://db:3306"] interval: 30s timeout: 10s retries: 10 depends_on: - db ================================================ FILE: _examples/auth/basicauth/database/go.mod ================================================ module myapp go 1.25 require ( github.com/go-sql-driver/mysql v1.9.3 github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd ) require ( filippo.io/edwards25519 v1.1.0 // indirect github.com/BurntSushi/toml v1.6.0 // indirect github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect github.com/CloudyKit/jet/v6 v6.3.1 // indirect github.com/Joker/jade v1.1.3 // indirect github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 // indirect github.com/andybalholm/brotli v1.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/fatih/structs v1.1.0 // indirect github.com/flosch/pongo2/v4 v4.0.2 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/iris-contrib/schema v0.0.6 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/kataras/blocks v0.0.8 // indirect github.com/kataras/golog v0.1.12 // indirect github.com/kataras/pio v0.0.13 // indirect github.com/kataras/sitemap v0.0.6 // indirect github.com/kataras/tunnel v0.0.4 // indirect github.com/klauspost/compress v1.18.2 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mailgun/raymond/v2 v2.0.48 // indirect github.com/mailru/easyjson v0.9.1 // indirect github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/schollz/closestmatch v2.1.0+incompatible // indirect github.com/sirupsen/logrus v1.8.1 // indirect github.com/stretchr/testify v1.11.1 // indirect github.com/tdewolff/minify/v2 v2.24.8 // indirect github.com/tdewolff/parse/v2 v2.8.5 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/yosssi/ace v0.0.5 // indirect golang.org/x/crypto v0.47.0 // indirect golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect golang.org/x/net v0.49.0 // indirect golang.org/x/sys v0.40.0 // indirect golang.org/x/text v0.33.0 // indirect golang.org/x/time v0.14.0 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: _examples/auth/basicauth/database/go.sum ================================================ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= github.com/CloudyKit/jet/v6 v6.3.1 h1:6IAo5Cx21xrHVaR8zzXN5gJatKV/wO7Nf6bfCnCSbUw= github.com/CloudyKit/jet/v6 v6.3.1/go.mod h1:lf8ksdNsxZt7/yH/3n4vJQWA9RUq4wpaHtArHhGVMOw= github.com/Joker/hpp v1.0.0 h1:65+iuJYdRXv/XyN62C1uEmmOx3432rNG/rKlX6V7Kkc= github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= github.com/Joker/jade v1.1.3 h1:Qbeh12Vq6BxURXT1qZBRHsDxeURB8ztcL6f3EXSGeHk= github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKduM= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 h1:dG7/gLroJGht/jSQtHiLvT48Hxn+crbmvyItZC8cWOs= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05/go.mod h1:NYezi6wtnJtBm5btoprXc5SvAdqH0XTXWnUup0MptAI= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/flosch/pongo2/v4 v4.0.2 h1:gv+5Pe3vaSVmiJvh/BZa82b7/00YUGm0PIyVVLop0Hw= github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+BWPhwZD8= github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a h1:l7A0loSszR5zHd/qK53ZIHMO8b3bBSmENnQ6eKnUT0A= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/iris-contrib/httpexpect/v2 v2.15.2 h1:T9THsdP1woyAqKHwjkEsbCnMefsAFvk8iJJKokcJ3Go= github.com/iris-contrib/httpexpect/v2 v2.15.2/go.mod h1:JLDgIqnFy5loDSUv1OA2j0mb6p/rDhiCqigP22Uq9xE= github.com/iris-contrib/schema v0.0.6 h1:CPSBLyx2e91H2yJzPuhGuifVRnZBBJ3pCOMbOvPZaTw= github.com/iris-contrib/schema v0.0.6/go.mod h1:iYszG0IOsuIsfzjymw1kMzTL8YQcCWlm65f3wX8J5iA= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kataras/blocks v0.0.8 h1:MrpVhoFTCR2v1iOOfGng5VJSILKeZZI+7NGfxEh3SUM= github.com/kataras/blocks v0.0.8/go.mod h1:9Jm5zx6BB+06NwA+OhTbHW1xkMOYxahnqTN5DveZ2Yg= github.com/kataras/golog v0.1.12 h1:Bu7I/G4ilJlbfzjmU39O9N+2uO1pBcMK045fzZ4ytNg= github.com/kataras/golog v0.1.12/go.mod h1:wrGSbOiBqbQSQznleVNX4epWM8rl9SJ/rmEacl0yqy4= github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd h1:oMsQgqKACbUdlaIWnN+EZzzAnHkw90hE/y/R0BSij2U= github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd/go.mod h1:yucjtDl9Vn3OctWM1fi0MuM9OckemC7kEreFa9QtPF8= github.com/kataras/pio v0.0.13 h1:x0rXVX0fviDTXOOLOmr4MUxOabu1InVSTu5itF8CXCM= github.com/kataras/pio v0.0.13/go.mod h1:k3HNuSw+eJ8Pm2lA4lRhg3DiCjVgHlP8hmXApSej3oM= github.com/kataras/sitemap v0.0.6 h1:w71CRMMKYMJh6LR2wTgnk5hSgjVNB9KL60n5e2KHvLY= github.com/kataras/sitemap v0.0.6/go.mod h1:dW4dOCNs896OR1HmG+dMLdT7JjDk7mYBzoIRwuj5jA4= github.com/kataras/tunnel v0.0.4 h1:sCAqWuJV7nPzGrlb0os3j49lk2JhILT0rID38NHNLpA= github.com/kataras/tunnel v0.0.4/go.mod h1:9FkU4LaeifdMWqZu7o20ojmW4B7hdhv2CMLwfnHGpYw= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw= github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18= github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo= github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk= github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tdewolff/minify/v2 v2.24.8 h1:58/VjsbevI4d5FGV0ZSuBrHMSSkH4MCH0sIz/eKIauE= github.com/tdewolff/minify/v2 v2.24.8/go.mod h1:0Ukj0CRpo/sW/nd8uZ4ccXaV1rEVIWA3dj8U7+Shhfw= github.com/tdewolff/parse/v2 v2.8.5 h1:ZmBiA/8Do5Rpk7bDye0jbbDUpXXbCdc3iah4VeUvwYU= github.com/tdewolff/parse/v2 v2.8.5/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo= github.com/tdewolff/test v1.0.11 h1:FdLbwQVHxqG16SlkGveC0JVyrJN62COWTRyUFzfbtBE= github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA= github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0= github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs= moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHcE= ================================================ FILE: _examples/auth/basicauth/database/main.go ================================================ package main // Look README.md import ( "context" "database/sql" "fmt" "os" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/middleware/basicauth" _ "github.com/go-sql-driver/mysql" // lint: mysql driver. ) // User is just an example structure of a user, // it MUST contain a Username and Password exported fields // or/and complete the basicauth.User interface. type User struct { ID int64 `db:"id" json:"id"` Username string `db:"username" json:"username"` Password string `db:"password" json:"password"` Email string `db:"email" json:"email"` } // GetUsername returns the Username field. func (u User) GetUsername() string { return u.Username } // GetPassword returns the Password field. func (u User) GetPassword() string { return u.Password } func main() { dsn := fmt.Sprintf("%s:%s@tcp(%s:3306)/%s?parseTime=true&charset=utf8mb4&collation=utf8mb4_unicode_ci", getenv("MYSQL_USER", "user_myapp"), getenv("MYSQL_PASSWORD", "dbpassword"), getenv("MYSQL_HOST", "localhost"), getenv("MYSQL_DATABASE", "myapp"), ) db, err := connect(dsn) if err != nil { panic(err) } // Validate a user from database. allowFunc := func(ctx iris.Context, username, password string) (any, bool) { user, err := db.getUserByUsernameAndPassword(context.Background(), username, password) return user, err == nil } opts := basicauth.Options{ Realm: basicauth.DefaultRealm, ErrorHandler: basicauth.DefaultErrorHandler, Allow: allowFunc, } auth := basicauth.New(opts) app := iris.New() app.Use(auth) app.Get("/", index) app.Listen(":8080") } func index(ctx iris.Context) { user, _ := ctx.User().GetRaw() // user is a type of main.User ctx.JSON(user) } func getenv(key string, def string) string { v := os.Getenv(key) if v == "" { return def } return v } type database struct { *sql.DB } func connect(dsn string) (*database, error) { conn, err := sql.Open("mysql", dsn) if err != nil { return nil, err } err = conn.Ping() if err != nil { conn.Close() return nil, err } return &database{conn}, nil } func (db *database) getUserByUsernameAndPassword(ctx context.Context, username, password string) (User, error) { query := fmt.Sprintf("SELECT * FROM %s WHERE %s = ? AND %s = ? LIMIT 1", "users", "username", "password") rows, err := db.QueryContext(ctx, query, username, password) if err != nil { return User{}, err } defer rows.Close() if !rows.Next() { return User{}, sql.ErrNoRows } var user User err = rows.Scan(&user.ID, &user.Username, &user.Password, &user.Email) return user, err } ================================================ FILE: _examples/auth/basicauth/database/migration/db.sql ================================================ CREATE DATABASE IF NOT EXISTS myapp DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE myapp; SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; DROP TABLE IF EXISTS users; CREATE TABLE users ( id int(11) NOT NULL AUTO_INCREMENT, username varchar(255) NOT NULL, password varchar(255) NOT NULL, email varchar(255) NOT NULL, PRIMARY KEY (id) ); INSERT INTO users (username,password,email) VALUES ('admin', 'admin', 'kataras2006@hotmail.com'), ("iris", 'iris_password', 'iris-go@outlook.com'); SET FOREIGN_KEY_CHECKS = 1; ================================================ FILE: _examples/auth/basicauth/users_file_bcrypt/main.go ================================================ package main import ( "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/middleware/basicauth" ) func main() { auth := basicauth.Load("users.yml", basicauth.BCRYPT) /* Same as: opts := basicauth.Options{ Realm: basicauth.DefaultRealm, Allow: basicauth.AllowUsersFile("users.yml", basicauth.BCRYPT), } auth := basicauth.New(opts) */ app := iris.New() app.Use(auth) app.Get("/", index) // kataras:kataras_pass // makis:makis_pass app.Listen(":8080") } func index(ctx iris.Context) { user := ctx.User() ctx.JSON(user) } ================================================ FILE: _examples/auth/basicauth/users_file_bcrypt/users.yml ================================================ # The file cannot be modified during the serve time. # To support real-time users changes please use the Options.Allow instead, # (see the database example for that). # # Again, the username and password (or capitalized) fields are required, # the rest are optional, depending on your application needs. - username: kataras password: $2a$10$Irg8k8HWkDlvL0YDBKLCYee6j6zzIFTplJcvZYKA.B8/clHPZn2Ey # encrypted of kataras_pass age: 27 role: admin - username: makis password: $2a$10$3GXzp3J5GhHThGisbpvpZuftbmzPivDMo94XPnkTnDe7254x7sJ3O # encrypted of makis_pass ================================================ FILE: _examples/auth/basicauth/users_list/main.go ================================================ package main import ( "time" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/middleware/basicauth" ) // User is just an example structure of a user, // it MUST contain a Username and Password exported fields // or complete the basicauth.User interface. type User struct { Username string `json:"username"` Password string `json:"password"` Roles []string `json:"roles"` } var users = []User{ {"admin", "admin", []string{"admin"}}, {"kataras", "kataras_pass", []string{"manager", "author"}}, {"george", "george_pass", []string{"member"}}, {"john", "john_pass", []string{}}, } func main() { opts := basicauth.Options{ Realm: basicauth.DefaultRealm, // Defaults to 0, no expiration. // Prompt for new credentials on a client's request // made after 10 minutes the user has logged in: MaxAge: 10 * time.Minute, // Clear any expired users from the memory every one hour, // note that the user's expiration time will be // reseted on the next valid request (when Allow passed). GC: basicauth.GC{ Every: 2 * time.Hour, }, // The users can be a slice of custom users structure // or a map[string]string (username:password) // or []map[string]any with username and passwords required fields, // read the godocs for more. Allow: basicauth.AllowUsers(users), } auth := basicauth.New(opts) // OR: basicauth.Default(users) app := iris.New() app.Use(auth) app.Get("/", index) app.Listen(":8080") } func index(ctx iris.Context) { user, _ := ctx.User().GetRaw() ctx.JSON(user) } ================================================ FILE: _examples/auth/cors/main.go ================================================ // Package main integrates the "rs/cors" net/http middleware into Iris. // That cors third-party middleware cannot be registered through `iris.FromStd` // as a common middleware because it should be injected before the Iris Router itself, // it allows/dissallows HTTP Methods too. // // This is just an example you can use to run something, based on custom logic, // before the Iris Router itself. // // In the "routing/custom-wrapper" example // we learn how we can acquire and release an Iris context to fire an Iris Handler // based on custom logic, before the Iris Router itself. In that example // we will fire a net/http handler (the "rs/cors" handler one) instead. // // https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS package main import ( "github.com/kataras/iris/v12" "github.com/rs/cors" ) func main() { app := iris.New() c := cors.New(cors.Options{ AllowedOrigins: []string{"*"}, AllowCredentials: true, // Enable Debugging for testing, consider disabling in production Debug: true, }) // app.WrapRouter(func(w http.ResponseWriter, r *http.Request, router http.HandlerFunc) { // [custom logic...] // if shouldFireNetHTTPHandler { // ...ServeHTTP(w,r) // return // } // router(w,r) // }) // In our case, the cors package has a ServeHTTP // of the same form of app.WrapRouter's accept input argument, // so we can just do: app.WrapRouter(c.ServeHTTP) // Serve ./public/index.html, main.js. app.HandleDir("/", iris.Dir("./public")) // Register routes here... app.Get("/data", listData) // http://localhost:8080 and click the "fetch data" button. app.Listen(":8080") } type item struct { Title string `json:"title"` } func listData(ctx iris.Context) { ctx.JSON([]item{ {"Item 1"}, {"Item 2"}, {"Item 3"}, }) } ================================================ FILE: _examples/auth/cors/public/index.html ================================================ Iris Cors Example
================================================ FILE: _examples/auth/cors/public/main.js ================================================ // https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch async function doRequest(method = 'GET', url = '', data = {}) { // Default options are marked with * const request = { method: method, // *GET, POST, PUT, DELETE, etc. mode: 'cors', // no-cors, *cors, same-origin cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached credentials: 'same-origin', // include, *same-origin, omit redirect: 'follow', // manual, *follow, error referrerPolicy: 'no-referrer', // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url }; if (data !== undefined && method !== 'GET' && method !== 'HEAD') { request.headers = { 'Content-Type': 'application/json' // 'Content-Type': 'application/x-www-form-urlencoded', }; // body data type must match "Content-Type" header. request.body = JSON.stringify(data); } const response = await fetch(url, request); return response.json(); // parses JSON response into native JavaScript objects. } const ul = document.getElementById("list"); function fetchData() { console.log("sending request...") doRequest('GET', '/data').then(data => { data.forEach(item => { var li = document.createElement("li"); li.appendChild(document.createTextNode(item.title)); ul.appendChild(li); }); console.log(data); // JSON data parsed by `response.json()` call. }); } document.getElementById("fetchBtn").onclick = fetchData; ================================================ FILE: _examples/auth/goth/main.go ================================================ package main // Any OAuth2 (even the pure golang/x/net/oauth2) package // can be used with iris but at this example we will see the markbates' goth: // // $ go get github.com/markbates/goth/... // // This OAuth2 example works with sessions, so we will need // to attach a session manager. // Optionally: for even more secure session values, // developers can use any third-party package to add a custom cookie encoder/decoder. // At this example we will use the gorilla's securecookie: // // $ go get github.com/gorilla/securecookie // Example of securecookie can be found at "sessions/securecookie" example folder. // Notes: // The whole example is converted by markbates/goth/example/main.go. // It's tested with my own TWITTER application and it worked, even for localhost. // I guess that everything else works as expected, all bugs reported by goth library's community // are fixed in the time I wrote that example, have fun! import ( "errors" "fmt" "os" "sort" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/sessions" "github.com/gorilla/securecookie" // optionally, used for session's encoder/decoder "github.com/markbates/goth" "github.com/markbates/goth/providers/amazon" "github.com/markbates/goth/providers/auth0" "github.com/markbates/goth/providers/bitbucket" "github.com/markbates/goth/providers/box" "github.com/markbates/goth/providers/dailymotion" "github.com/markbates/goth/providers/deezer" "github.com/markbates/goth/providers/digitalocean" "github.com/markbates/goth/providers/discord" "github.com/markbates/goth/providers/dropbox" "github.com/markbates/goth/providers/facebook" "github.com/markbates/goth/providers/fitbit" "github.com/markbates/goth/providers/github" "github.com/markbates/goth/providers/gitlab" "github.com/markbates/goth/providers/gplus" "github.com/markbates/goth/providers/heroku" "github.com/markbates/goth/providers/instagram" "github.com/markbates/goth/providers/intercom" "github.com/markbates/goth/providers/lastfm" "github.com/markbates/goth/providers/linkedin" "github.com/markbates/goth/providers/meetup" "github.com/markbates/goth/providers/onedrive" "github.com/markbates/goth/providers/openidConnect" "github.com/markbates/goth/providers/paypal" "github.com/markbates/goth/providers/salesforce" "github.com/markbates/goth/providers/slack" "github.com/markbates/goth/providers/soundcloud" "github.com/markbates/goth/providers/spotify" "github.com/markbates/goth/providers/steam" "github.com/markbates/goth/providers/stripe" "github.com/markbates/goth/providers/twitch" "github.com/markbates/goth/providers/twitter" "github.com/markbates/goth/providers/uber" "github.com/markbates/goth/providers/wepay" "github.com/markbates/goth/providers/xero" "github.com/markbates/goth/providers/yahoo" "github.com/markbates/goth/providers/yammer" ) var sessionsManager *sessions.Sessions func init() { // attach a session manager cookieName := "mycustomsessionid" hashKey := securecookie.GenerateRandomKey(64) blockKey := securecookie.GenerateRandomKey(32) secureCookie := securecookie.New(hashKey, blockKey) sessionsManager = sessions.New(sessions.Config{ Cookie: cookieName, Encoding: secureCookie, AllowReclaim: true, }) } // These are some function helpers that you may use if you want // GetProviderName is a function used to get the name of a provider // for a given request. By default, this provider is fetched from // the URL query string. If you provide it in a different way, // assign your own function to this variable that returns the provider // name for your request. var GetProviderName = func(ctx iris.Context) (string, error) { // try to get it from the url param "provider" if p := ctx.URLParam("provider"); p != "" { return p, nil } // try to get it from the url PATH parameter "{provider} or :provider or {provider:string} or {provider:alphabetical}" if p := ctx.Params().Get("provider"); p != "" { return p, nil } // try to get it from context's per-request storage if p := ctx.Values().GetString("provider"); p != "" { return p, nil } // if not found then return an empty string with the corresponding error return "", errors.New("you must select a provider") } /* BeginAuthHandler is a convenience handler for starting the authentication process. It expects to be able to get the name of the provider from the query parameters as either "provider" or ":provider". BeginAuthHandler will redirect the user to the appropriate authentication end-point for the requested provider. See https://github.com/markbates/goth/examples/main.go to see this in action. */ func BeginAuthHandler(ctx iris.Context) { url, err := GetAuthURL(ctx) if err != nil { ctx.StopWithError(iris.StatusBadRequest, err) return } ctx.Redirect(url, iris.StatusTemporaryRedirect) } /* GetAuthURL starts the authentication process with the requested provided. It will return a URL that should be used to send users to. It expects to be able to get the name of the provider from the query parameters as either "provider" or ":provider" or from the context's value of "provider" key. I would recommend using the BeginAuthHandler instead of doing all of these steps yourself, but that's entirely up to you. */ func GetAuthURL(ctx iris.Context) (string, error) { providerName, err := GetProviderName(ctx) if err != nil { return "", err } provider, err := goth.GetProvider(providerName) if err != nil { return "", err } sess, err := provider.BeginAuth(SetState(ctx)) if err != nil { return "", err } url, err := sess.GetAuthURL() if err != nil { return "", err } session := sessionsManager.Start(ctx) session.Set(providerName, sess.Marshal()) return url, nil } // SetState sets the state string associated with the given request. // If no state string is associated with the request, one will be generated. // This state is sent to the provider and can be retrieved during the // callback. var SetState = func(ctx iris.Context) string { state := ctx.URLParam("state") if len(state) > 0 { return state } return "state" } // GetState gets the state returned by the provider during the callback. // This is used to prevent CSRF attacks, see // http://tools.ietf.org/html/rfc6749#section-10.12 var GetState = func(ctx iris.Context) string { return ctx.URLParam("state") } /* CompleteUserAuth does what it says on the tin. It completes the authentication process and fetches all of the basic information about the user from the provider. It expects to be able to get the name of the provider from the query parameters as either "provider" or "{provider}" path parameter. See https://github.com/markbates/goth/examples/main.go to see this in action. */ var CompleteUserAuth = func(ctx iris.Context) (goth.User, error) { providerName, err := GetProviderName(ctx) if err != nil { return goth.User{}, err } provider, err := goth.GetProvider(providerName) if err != nil { return goth.User{}, err } session := sessionsManager.Start(ctx) value := session.GetString(providerName) if value == "" { return goth.User{}, errors.New("session value for " + providerName + " not found") } sess, err := provider.UnmarshalSession(value) if err != nil { return goth.User{}, err } user, err := provider.FetchUser(sess) if err == nil { // user can be found with existing session data return user, err } // get new token and retry fetch _, err = sess.Authorize(provider, ctx.Request().URL.Query()) if err != nil { return goth.User{}, err } session.Set(providerName, sess.Marshal()) return provider.FetchUser(sess) } // Logout invalidates a user session. func Logout(ctx iris.Context) error { providerName, err := GetProviderName(ctx) if err != nil { return err } session := sessionsManager.Start(ctx) session.Delete(providerName) return nil } // End of the "some function helpers". func main() { goth.UseProviders( twitter.New(os.Getenv("TWITTER_KEY"), os.Getenv("TWITTER_SECRET"), "http://localhost:3000/auth/twitter/callback"), // If you'd like to use authenticate instead of authorize in Twitter provider, use this instead. // twitter.NewAuthenticate(os.Getenv("TWITTER_KEY"), os.Getenv("TWITTER_SECRET"), "http://localhost:3000/auth/twitter/callback"), facebook.New(os.Getenv("FACEBOOK_KEY"), os.Getenv("FACEBOOK_SECRET"), "http://localhost:3000/auth/facebook/callback"), fitbit.New(os.Getenv("FITBIT_KEY"), os.Getenv("FITBIT_SECRET"), "http://localhost:3000/auth/fitbit/callback"), gplus.New(os.Getenv("GPLUS_KEY"), os.Getenv("GPLUS_SECRET"), "http://localhost:3000/auth/gplus/callback"), github.New(os.Getenv("GITHUB_KEY"), os.Getenv("GITHUB_SECRET"), "http://localhost:3000/auth/github/callback"), spotify.New(os.Getenv("SPOTIFY_KEY"), os.Getenv("SPOTIFY_SECRET"), "http://localhost:3000/auth/spotify/callback"), linkedin.New(os.Getenv("LINKEDIN_KEY"), os.Getenv("LINKEDIN_SECRET"), "http://localhost:3000/auth/linkedin/callback"), lastfm.New(os.Getenv("LASTFM_KEY"), os.Getenv("LASTFM_SECRET"), "http://localhost:3000/auth/lastfm/callback"), twitch.New(os.Getenv("TWITCH_KEY"), os.Getenv("TWITCH_SECRET"), "http://localhost:3000/auth/twitch/callback"), dropbox.New(os.Getenv("DROPBOX_KEY"), os.Getenv("DROPBOX_SECRET"), "http://localhost:3000/auth/dropbox/callback"), digitalocean.New(os.Getenv("DIGITALOCEAN_KEY"), os.Getenv("DIGITALOCEAN_SECRET"), "http://localhost:3000/auth/digitalocean/callback", "read"), bitbucket.New(os.Getenv("BITBUCKET_KEY"), os.Getenv("BITBUCKET_SECRET"), "http://localhost:3000/auth/bitbucket/callback"), instagram.New(os.Getenv("INSTAGRAM_KEY"), os.Getenv("INSTAGRAM_SECRET"), "http://localhost:3000/auth/instagram/callback"), intercom.New(os.Getenv("INTERCOM_KEY"), os.Getenv("INTERCOM_SECRET"), "http://localhost:3000/auth/intercom/callback"), box.New(os.Getenv("BOX_KEY"), os.Getenv("BOX_SECRET"), "http://localhost:3000/auth/box/callback"), salesforce.New(os.Getenv("SALESFORCE_KEY"), os.Getenv("SALESFORCE_SECRET"), "http://localhost:3000/auth/salesforce/callback"), amazon.New(os.Getenv("AMAZON_KEY"), os.Getenv("AMAZON_SECRET"), "http://localhost:3000/auth/amazon/callback"), yammer.New(os.Getenv("YAMMER_KEY"), os.Getenv("YAMMER_SECRET"), "http://localhost:3000/auth/yammer/callback"), onedrive.New(os.Getenv("ONEDRIVE_KEY"), os.Getenv("ONEDRIVE_SECRET"), "http://localhost:3000/auth/onedrive/callback"), // Pointed localhost.com to http://localhost:3000/auth/yahoo/callback through proxy as yahoo // does not allow to put custom ports in redirection uri yahoo.New(os.Getenv("YAHOO_KEY"), os.Getenv("YAHOO_SECRET"), "http://localhost.com"), slack.New(os.Getenv("SLACK_KEY"), os.Getenv("SLACK_SECRET"), "http://localhost:3000/auth/slack/callback"), stripe.New(os.Getenv("STRIPE_KEY"), os.Getenv("STRIPE_SECRET"), "http://localhost:3000/auth/stripe/callback"), wepay.New(os.Getenv("WEPAY_KEY"), os.Getenv("WEPAY_SECRET"), "http://localhost:3000/auth/wepay/callback", "view_user"), // By default paypal production auth urls will be used, please set PAYPAL_ENV=sandbox as environment variable for testing // in sandbox environment paypal.New(os.Getenv("PAYPAL_KEY"), os.Getenv("PAYPAL_SECRET"), "http://localhost:3000/auth/paypal/callback"), steam.New(os.Getenv("STEAM_KEY"), "http://localhost:3000/auth/steam/callback"), heroku.New(os.Getenv("HEROKU_KEY"), os.Getenv("HEROKU_SECRET"), "http://localhost:3000/auth/heroku/callback"), uber.New(os.Getenv("UBER_KEY"), os.Getenv("UBER_SECRET"), "http://localhost:3000/auth/uber/callback"), soundcloud.New(os.Getenv("SOUNDCLOUD_KEY"), os.Getenv("SOUNDCLOUD_SECRET"), "http://localhost:3000/auth/soundcloud/callback"), gitlab.New(os.Getenv("GITLAB_KEY"), os.Getenv("GITLAB_SECRET"), "http://localhost:3000/auth/gitlab/callback"), dailymotion.New(os.Getenv("DAILYMOTION_KEY"), os.Getenv("DAILYMOTION_SECRET"), "http://localhost:3000/auth/dailymotion/callback", "email"), deezer.New(os.Getenv("DEEZER_KEY"), os.Getenv("DEEZER_SECRET"), "http://localhost:3000/auth/deezer/callback", "email"), discord.New(os.Getenv("DISCORD_KEY"), os.Getenv("DISCORD_SECRET"), "http://localhost:3000/auth/discord/callback", discord.ScopeIdentify, discord.ScopeEmail), meetup.New(os.Getenv("MEETUP_KEY"), os.Getenv("MEETUP_SECRET"), "http://localhost:3000/auth/meetup/callback"), // Auth0 allocates domain per customer, a domain must be provided for auth0 to work auth0.New(os.Getenv("AUTH0_KEY"), os.Getenv("AUTH0_SECRET"), "http://localhost:3000/auth/auth0/callback", os.Getenv("AUTH0_DOMAIN")), xero.New(os.Getenv("XERO_KEY"), os.Getenv("XERO_SECRET"), "http://localhost:3000/auth/xero/callback"), ) // OpenID Connect is based on OpenID Connect Auto Discovery URL (https://openid.net/specs/openid-connect-discovery-1_0-17.html) // because the OpenID Connect provider initialize it self in the New(), it can return an error which should be handled or ignored // ignore the error for now openidConnect, _ := openidConnect.New(os.Getenv("OPENID_CONNECT_KEY"), os.Getenv("OPENID_CONNECT_SECRET"), "http://localhost:3000/auth/openid-connect/callback", os.Getenv("OPENID_CONNECT_DISCOVERY_URL")) if openidConnect != nil { goth.UseProviders(openidConnect) } m := map[string]string{ "amazon": "Amazon", "bitbucket": "Bitbucket", "box": "Box", "dailymotion": "Dailymotion", "deezer": "Deezer", "digitalocean": "Digital Ocean", "discord": "Discord", "dropbox": "Dropbox", "facebook": "Facebook", "fitbit": "Fitbit", "github": "Github", "gitlab": "Gitlab", "soundcloud": "SoundCloud", "spotify": "Spotify", "steam": "Steam", "stripe": "Stripe", "twitch": "Twitch", "uber": "Uber", "wepay": "Wepay", "yahoo": "Yahoo", "yammer": "Yammer", "gplus": "Google Plus", "heroku": "Heroku", "instagram": "Instagram", "intercom": "Intercom", "lastfm": "Last FM", "linkedin": "Linkedin", "onedrive": "Onedrive", "paypal": "Paypal", "twitter": "Twitter", "salesforce": "Salesforce", "slack": "Slack", "meetup": "Meetup.com", "auth0": "Auth0", "openid-connect": "OpenID Connect", "xero": "Xero", } keys := make([]string, 0, len(m)) for k := range m { keys = append(keys, k) } sort.Strings(keys) providerIndex := &ProviderIndex{Providers: keys, ProvidersMap: m} // create our app, // set a view // set sessions // and setup the router for the showcase app := iris.New() app.Logger().SetLevel("debug") // attach and build our templates app.RegisterView(iris.HTML("./templates", ".html")) // start of the router app.Get("/auth/{provider}/callback", func(ctx iris.Context) { user, err := CompleteUserAuth(ctx) if err != nil { ctx.StopWithError(iris.StatusInternalServerError, err) return } // Between handlers (user as root .): // ctx.ViewData("", user) // // Between handlers (user as .user variable): // ctx.ViewData("user", user) // ---- // Directly (user as root): // ctx.View("user.html", user) // // Directly (user as .user variable): if err := ctx.View("user.html", iris.Map{ "user": user, }); err != nil { ctx.HTML(fmt.Sprintf("

%s

", err.Error())) return } }) app.Get("/logout/{provider}", func(ctx iris.Context) { Logout(ctx) ctx.Redirect("/", iris.StatusTemporaryRedirect) }) app.Get("/auth/{provider}", func(ctx iris.Context) { // try to get the user without re-authenticating gothUser, err := CompleteUserAuth(ctx) if err != nil { BeginAuthHandler(ctx) return } if err := ctx.View("user.html", gothUser); err != nil { ctx.HTML(fmt.Sprintf("

%s

", err.Error())) return } }) app.Get("/", func(ctx iris.Context) { if err := ctx.View("index.html", providerIndex); err != nil { ctx.HTML(fmt.Sprintf("

%s

", err.Error())) return } }) // http://localhost:3000 app.Listen("localhost:3000") } type ProviderIndex struct { Providers []string ProvidersMap map[string]string } ================================================ FILE: _examples/auth/goth/templates/index.html ================================================ {{range $key,$value:=.Providers}}

Log in with {{index $.ProvidersMap $value}}

{{end}} ================================================ FILE: _examples/auth/goth/templates/user.html ================================================

logout

Name: {{.Name}} [{{.LastName}}, {{.FirstName}}]

Email: {{.Email}}

NickName: {{.NickName}}

Location: {{.Location}}

AvatarURL: {{.AvatarURL}}

Description: {{.Description}}

UserID: {{.UserID}}

AccessToken: {{.AccessToken}}

ExpiresAt: {{.ExpiresAt}}

RefreshToken: {{.RefreshToken}}


Iterate all properties

{{range $key, $value := .RawData}} {{ $key }} => {{ $value }}
{{end}} ================================================ FILE: _examples/auth/hcaptcha/hosts ================================================ # https://docs.hcaptcha.com/#localdev # Add to the end of your hosts file, e.g. on windows: C:/windows/system32/drivers/etc/hosts 127.0.0.1 yourdomain.com ================================================ FILE: _examples/auth/hcaptcha/main.go ================================================ package main import ( "fmt" "os" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/middleware/hcaptcha" ) // Get the following values from: https://dashboard.hcaptcha.com // Also, check: https://docs.hcaptcha.com/#localdev to test on local environment. var ( siteKey = os.Getenv("HCAPTCHA-SITE-KEY") secretKey = os.Getenv("HCAPTCHA-SECRET-KEY") ) func main() { app := iris.New() app.RegisterView(iris.HTML("./templates", ".html")) hCaptcha := hcaptcha.New(secretKey) app.Get("/register", registerForm) app.Post("/register", hCaptcha, register) // See `hcaptcha.SiteVerify` for manual validation too. app.Logger().Infof("SiteKey = %s\tSecretKey = %s", siteKey, secretKey) // GET: http://yourdomain.com/register app.Listen(":80") } func register(ctx iris.Context) { hcaptchaResp, ok := hcaptcha.Get(ctx) if !ok { ctx.StatusCode(iris.StatusUnauthorized) ctx.WriteString("Are you a bot?") return } ctx.Writef("Register action here...action was asked by a Human.\nResponse value is: %#+v", hcaptchaResp) } func registerForm(ctx iris.Context) { ctx.ViewData("SiteKey", siteKey) if err := ctx.View("register_form.html"); err != nil { ctx.HTML(fmt.Sprintf("

%s

", err.Error())) return } } ================================================ FILE: _examples/auth/hcaptcha/templates/register_form.html ================================================ hCaptcha Demo

================================================ FILE: _examples/auth/jwt/basic/main.go ================================================ package main import ( "time" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/middleware/jwt" ) /* Documentation: https://github.com/kataras/jwt#table-of-contents */ // Replace with your own key and keep them secret. // The "signatureSharedKey" is used for the HMAC(HS256) signature algorithm. var signatureSharedKey = []byte("sercrethatmaycontainch@r32length") func main() { app := iris.New() app.Get("/", generateToken) app.Get("/protected", protected) app.Listen(":8080") } type fooClaims struct { Foo string `json:"foo"` } func generateToken(ctx iris.Context) { claims := fooClaims{ Foo: "bar", } // Sign and generate compact form token. token, err := jwt.Sign(jwt.HS256, signatureSharedKey, claims, jwt.MaxAge(10*time.Minute)) if err != nil { ctx.StopWithStatus(iris.StatusInternalServerError) return } tokenString := string(token) // or jwt.BytesToString ctx.HTML(`Token: ` + tokenString + `

/protected?token=` + tokenString + ``) } func protected(ctx iris.Context) { // Extract the token, e.g. cookie, Authorization: Bearer $token // or URL query. token := ctx.URLParam("token") // Verify the token. verifiedToken, err := jwt.Verify(jwt.HS256, signatureSharedKey, []byte(token)) if err != nil { ctx.StopWithStatus(iris.StatusUnauthorized) return } ctx.Writef("This is an authenticated request.\n\n") // Decode the custom claims. var claims fooClaims verifiedToken.Claims(&claims) // Just an example on how you can retrieve all the standard claims (set by jwt.MaxAge, "exp"). standardClaims := jwt.GetVerifiedToken(ctx).StandardClaims expiresAtString := standardClaims.ExpiresAt().Format(ctx.Application().ConfigurationReadOnly().GetTimeFormat()) timeLeft := standardClaims.Timeleft() ctx.Writef("foo=%s\nexpires at: %s\ntime left: %s\n", claims.Foo, expiresAtString, timeLeft) } ================================================ FILE: _examples/auth/jwt/blocklist/main.go ================================================ package main import ( "time" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/middleware/jwt" "github.com/kataras/iris/v12/middleware/jwt/blocklist/redis" // Optionally to set token identifier. "github.com/google/uuid" ) var ( signatureSharedKey = []byte("sercrethatmaycontainch@r32length") signer = jwt.NewSigner(jwt.HS256, signatureSharedKey, 15*time.Minute) verifier = jwt.NewVerifier(jwt.HS256, signatureSharedKey) ) type userClaims struct { Username string `json:"username"` } func main() { app := iris.New() // IMPORTANT // // To use the in-memory blocklist just: // verifier.WithDefaultBlocklist() // To use a persistence blocklist, e.g. redis, // start your redis-server and: blocklist := redis.NewBlocklist() // To configure single client or a cluster one: // blocklist.ClientOptions.Addr = "127.0.0.1:6379" // blocklist.ClusterOptions.Addrs = []string{...} // To set a prefix for jwt ids: // blocklist.Prefix = "myapp-" // // To manually connect and check its error before continue: // err := blocklist.Connect() // By default the verifier will try to connect, if failed then it will throw http error. // // And then register it: verifier.Blocklist = blocklist verifyMiddleware := verifier.Verify(func() any { return new(userClaims) }) app.Get("/", authenticate) protectedAPI := app.Party("/protected", verifyMiddleware) protectedAPI.Get("/", protected) protectedAPI.Get("/logout", logout) // http://localhost:8080 // http://localhost:8080/protected?token=$token // http://localhost:8080/logout?token=$token // http://localhost:8080/protected?token=$token (401) app.Listen(":8080") } func authenticate(ctx iris.Context) { claims := userClaims{ Username: "kataras", } // Generate JWT ID. random, err := uuid.NewRandom() if err != nil { ctx.StopWithError(iris.StatusInternalServerError, err) return } id := random.String() // Set the ID with the jwt.ID. token, err := signer.Sign(claims, jwt.ID(id)) if err != nil { ctx.StopWithError(iris.StatusInternalServerError, err) return } ctx.Write(token) } func protected(ctx iris.Context) { claims := jwt.Get(ctx).(*userClaims) // To the standard claims, e.g. the generated ID: // jwt.GetVerifiedToken(ctx).StandardClaims.ID ctx.WriteString(claims.Username) } func logout(ctx iris.Context) { ctx.Logout() ctx.Redirect("/", iris.StatusTemporaryRedirect) } ================================================ FILE: _examples/auth/jwt/middleware/main.go ================================================ package main import ( "time" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/middleware/jwt" ) var ( sigKey = []byte("signature_hmac_secret_shared_key") // encKey = []byte("GCM_AES_256_secret_shared_key_32") ) type fooClaims struct { Foo string `json:"foo"` } /* In this example you will learn the essentials of the Iris builtin JWT middleware based on the github.com/kataras/jwt package. */ func main() { app := iris.New() signer := jwt.NewSigner(jwt.HS256, sigKey, 10*time.Minute) // Enable payload encryption with: // signer.WithEncryption(encKey, nil) app.Get("/", generateToken(signer)) verifier := jwt.NewVerifier(jwt.HS256, sigKey) // Enable server-side token block feature (even before its expiration time): verifier.WithDefaultBlocklist() // Enable payload decryption with: // verifier.WithDecryption(encKey, nil) verifyMiddleware := verifier.Verify(func() any { return new(fooClaims) }) protectedAPI := app.Party("/protected") // Register the verify middleware to allow access only to authorized clients. protectedAPI.Use(verifyMiddleware) // ^ or UseRouter(verifyMiddleware) to disallow unauthorized http error handlers too. protectedAPI.Get("/", protected) // Invalidate the token through server-side, even if it's not expired yet. protectedAPI.Get("/logout", logout) // http://localhost:8080 // http://localhost:8080/protected?token=$token (or Authorization: Bearer $token) // http://localhost:8080/protected/logout?token=$token // http://localhost:8080/protected?token=$token (401) app.Listen(":8080") } func generateToken(signer *jwt.Signer) iris.Handler { return func(ctx iris.Context) { claims := fooClaims{Foo: "bar"} token, err := signer.Sign(claims) if err != nil { ctx.StopWithStatus(iris.StatusInternalServerError) return } ctx.Write(token) } } func protected(ctx iris.Context) { // Get the verified and decoded claims. claims := jwt.Get(ctx).(*fooClaims) // Optionally, get token information if you want to work with them. // Just an example on how you can retrieve all the standard claims (set by signer's max age, "exp"). standardClaims := jwt.GetVerifiedToken(ctx).StandardClaims expiresAtString := standardClaims.ExpiresAt().Format(ctx.Application().ConfigurationReadOnly().GetTimeFormat()) timeLeft := standardClaims.Timeleft() ctx.Writef("foo=%s\nexpires at: %s\ntime left: %s\n", claims.Foo, expiresAtString, timeLeft) } func logout(ctx iris.Context) { err := ctx.Logout() if err != nil { ctx.WriteString(err.Error()) } else { ctx.Writef("token invalidated, a new token is required to access the protected API") } } ================================================ FILE: _examples/auth/jwt/refresh-token/main.go ================================================ package main import ( "fmt" "time" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/middleware/jwt" ) const ( accessTokenMaxAge = 10 * time.Minute refreshTokenMaxAge = time.Hour ) var ( privateKey, publicKey = jwt.MustLoadRSA("rsa_private_key.pem", "rsa_public_key.pem") signer = jwt.NewSigner(jwt.RS256, privateKey, accessTokenMaxAge) verifier = jwt.NewVerifier(jwt.RS256, publicKey) ) // UserClaims a custom access claims structure. type UserClaims struct { ID string `json:"user_id"` // Do: `json:"username,required"` to have this field required // or see the Validate method below instead. Username string `json:"username"` } // GetID implements the partial context user's ID interface. // Note that if claims were a map then the claims value converted to UserClaims // and no need to implement any method. // // This is useful when multiple auth methods are used (e.g. basic auth, jwt) // but they all share a couple of methods. func (u *UserClaims) GetID() string { return u.ID } // GetUsername implements the partial context user's Username interface. func (u *UserClaims) GetUsername() string { return u.Username } // Validate completes the middleware's custom ClaimsValidator. // It will not accept a token which its claims missing the username field // (useful to not accept refresh tokens generated by the same algorithm). func (u *UserClaims) Validate() error { if u.Username == "" { return fmt.Errorf("username field is missing") } return nil } // For refresh token, we will just use the jwt.Claims // structure which contains the standard JWT fields. func main() { app := iris.New() app.OnErrorCode(iris.StatusUnauthorized, handleUnauthorized) app.Get("/authenticate", generateTokenPair) app.Get("/refresh", refreshToken) protectedAPI := app.Party("/protected") { verifyMiddleware := verifier.Verify(func() any { return new(UserClaims) }) protectedAPI.Use(verifyMiddleware) protectedAPI.Get("/", func(ctx iris.Context) { // Access the claims through: jwt.Get: // claims := jwt.Get(ctx).(*UserClaims) // ctx.Writef("Username: %s\n", claims.Username) // // OR through context's user (if at least one method was implement by our UserClaims): user := ctx.User() id, _ := user.GetID() username, _ := user.GetUsername() ctx.Writef("ID: %s\nUsername: %s\n", id, username) }) } // http://localhost:8080/protected (401) // http://localhost:8080/authenticate (200) (response JSON {access_token, refresh_token}) // http://localhost:8080/protected?token={access_token} (200) // http://localhost:8080/protected?token={refresh_token} (401) // http://localhost:8080/refresh?refresh_token={refresh_token} // OR http://localhost:8080/refresh (request JSON{refresh_token = {refresh_token}}) (200) (response JSON {access_token, refresh_token}) // http://localhost:8080/refresh?refresh_token={access_token} (401) app.Listen(":8080") } func generateTokenPair(ctx iris.Context) { // Simulate a user... userID := "53afcf05-38a3-43c3-82af-8bbbe0e4a149" // Map the current user with the refresh token, // so we make sure, on refresh route, that this refresh token owns // to that user before re-generate. refreshClaims := jwt.Claims{Subject: userID} accessClaims := UserClaims{ ID: userID, Username: "kataras", } // Generates a Token Pair, long-live for refresh tokens, e.g. 1 hour. // First argument is the access claims, // second argument is the refresh claims, // third argument is the refresh max age. tokenPair, err := signer.NewTokenPair(accessClaims, refreshClaims, refreshTokenMaxAge) if err != nil { ctx.Application().Logger().Errorf("token pair: %v", err) ctx.StopWithStatus(iris.StatusInternalServerError) return } // Send the generated token pair to the client. // The tokenPair looks like: {"access_token": $token, "refresh_token": $token} ctx.JSON(tokenPair) } // There are various methods of refresh token, depending on the application requirements. // In this example we will accept a refresh token only, we will verify only a refresh token // and we re-generate a whole new pair. An alternative would be to accept a token pair // of both access and refresh tokens, verify the refresh, verify the access with a Leeway time // and check if its going to expire soon, then generate a single access token. func refreshToken(ctx iris.Context) { // Assuming you have access to the current user, e.g. sessions. // // Simulate a database call against our jwt subject // to make sure that this refresh token is a pair generated by this user. // * Note: You can remove the ExpectSubject and do this validation later on by yourself. currentUserID := "53afcf05-38a3-43c3-82af-8bbbe0e4a149" // Get the refresh token from ?refresh_token=$token OR // the request body's JSON{"refresh_token": "$token"}. refreshToken := []byte(ctx.URLParam("refresh_token")) if len(refreshToken) == 0 { // You can read the whole body with ctx.GetBody/ReadBody too. var tokenPair jwt.TokenPair if err := ctx.ReadJSON(&tokenPair); err != nil { ctx.StopWithError(iris.StatusBadRequest, err) return } refreshToken = tokenPair.RefreshToken } // Verify the refresh token, which its subject MUST match the "currentUserID". _, err := verifier.VerifyToken(refreshToken, jwt.Expected{Subject: currentUserID}) if err != nil { ctx.Application().Logger().Errorf("verify refresh token: %v", err) ctx.StatusCode(iris.StatusUnauthorized) return } /* Custom validation checks can be performed after Verify calls too: currentUserID := "53afcf05-38a3-43c3-82af-8bbbe0e4a149" userID := verifiedToken.StandardClaims.Subject if userID != currentUserID { ctx.StopWithStatus(iris.StatusUnauthorized) return } */ // All OK, re-generate the new pair and send to client, // we could only generate an access token as well. generateTokenPair(ctx) } func handleUnauthorized(ctx iris.Context) { if err := ctx.GetErr(); err != nil { ctx.Application().Logger().Errorf("unauthorized: %v", err) } ctx.WriteString("Unauthorized") } ================================================ FILE: _examples/auth/jwt/refresh-token/rsa_private_key.pem ================================================ -----BEGIN PRIVATE KEY----- MIIEowIBAAKCAQEArwO0q8WbBvrplz3lTQjsWu66HC7M3mVAjmjLq8Wj/ipqVtiJ MrUL9t/0q9PNO/KX9u+HayFNYM4TnYkXVZX3M5E31W8fPPy74D/XpqFwrwT7bAEw pT51JJyxkoBAyOh08lmR2EYvpGF7qErra7qbkk4LGFbhoFCXdMLXguT4rPymkzFH dQrmGYOBS+v9imSuJddCZpXyv6Ko7AKB4mhzg4RC5RJZO5GEHVUrSMHxZB0syF8c U+28iL8A7SlGKTNZPZiHmCQVRqA6WlllL/YV/t6p24kaNZBUp9JGbAzOeKuVUv2u vfNKwB/aBwnFKauM9I6RmC4bnI1nGHjETlNNWwIDAQABAoIBAHBPKHmybTGlgpET nzo4J7SSzcuYHM/6mdrJVSn9wqcwAN2KR0DK/cqHHTPGz0VRAEPuojAVRtqAZAYM G3VIr0HgRrwoextf9BCL549+uhkWUWGVwenIktPT2f/xXaGPyrxazkTDhX8vL3Nn 4HtZXMweWPBdkJyYGxlKj5Hn7czTpG3VKpvpHeFlY4caF+FT2as1jcQ1MjPnGslH Ss+sYPBp/70w2T114Z4wlR4OryI1LeuFeje9obrn0HAmJd0ZKYM21awp/YWJ/y8J wIH6XQ4AGR9iTRhuffK1XRM/Iec3K/YhOn4PtKdT7OsIujAKY7A9WcqSFif+/E1g jom3eMECgYEAw5Zdqt2uZ19FuDlDTW4Kw8Z2NyXgWp33LkAXG1mJw7bqDhfPeB1c xTPs4i4RubGuDusygxZ3GgJAO7tLGzNQfWNoi03mM7Q/BJGkA9VZr+U28zsSRQOQ +J9xNsdgUMP1js7X/NNM2bxTC8zy9wEsWr9JwNo1C7uHTE9WXAumBI8CgYEA5RKV niSbyko36W3Vi0ZnGBrRhy0Eiq85V2mhWzHN+txcv+8aISow2wioTUzrpR0aVZ4j v9+siJENlALVzdUFihy0lPxHqLJT746Cixz95WRTLkdHeNllV0DMfOph2x3j1Hjd 3PgTv+jqb6npY0/2Vb2pp4t/zVikGaObsAalSHUCgYBne8B1bjMfqI3n6gxNBIMX kILtrNGmwFuPEgPnyZkVf0sZR8nSwJ5cDJwyE7P3LyZr6E9igllj3nsD35Xef2j/ 3r/qrL2275BEJ5bDHHgGk91eFgwVjcx/b0TkedrhAL2E4LXwpA/OSFEcNkT7IZjJ Ltqj+hAE9CSi4HtN2i/tywKBgBotKn28zzSpkIQTMgDNVcCSZ/kbctZqOZI8lty1 70TIY6znJMQ/bv/ImHrk3FSs47J+9LTbWXrtoHCWdlokCpMCvrv7rDCh2Cea0F4X PQg2k67JJGix5vu2guePXQlN/Bfui+PRUWhvtEJ4VxwrKgoYN0fXEA6mH3JymLrf t4l1AoGBALk4o9swGjw7MnByYJmOidlJ0p9Wj1BWWJJYoYX2VfjIuvZj6BNxkEb0 aVmYRC+40e9L1rOyrlyaO/TiQaIPE4ljVs/AmMKGz8sIcVfwdyERH3nDrXxvlAav lSvfKoYM3J+5c63CDuU45gztpmavNerzCczqYTLOEMx1eCLHOQlx -----END PRIVATE KEY----- ================================================ FILE: _examples/auth/jwt/refresh-token/rsa_public_key.pem ================================================ -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArwO0q8WbBvrplz3lTQjs Wu66HC7M3mVAjmjLq8Wj/ipqVtiJMrUL9t/0q9PNO/KX9u+HayFNYM4TnYkXVZX3 M5E31W8fPPy74D/XpqFwrwT7bAEwpT51JJyxkoBAyOh08lmR2EYvpGF7qErra7qb kk4LGFbhoFCXdMLXguT4rPymkzFHdQrmGYOBS+v9imSuJddCZpXyv6Ko7AKB4mhz g4RC5RJZO5GEHVUrSMHxZB0syF8cU+28iL8A7SlGKTNZPZiHmCQVRqA6WlllL/YV /t6p24kaNZBUp9JGbAzOeKuVUv2uvfNKwB/aBwnFKauM9I6RmC4bnI1nGHjETlNN WwIDAQAB -----END PUBLIC KEY----- ================================================ FILE: _examples/auth/jwt/tutorial/README.md ================================================ # Iris JWT Tutorial This example show how to use JWT with domain-driven design pattern with Iris. There is also a simple Go client which describes how you can use Go to authorize a user and use the server's API. ## Run the server ```sh $ go run main.go ``` ## Authenticate, get the token ```sh $ curl --location --request POST 'http://localhost:8080/signin' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'username=admin' \ --data-urlencode 'password=admin' > $token ``` ## Get all TODOs for this User ```sh $ curl --location --request GET 'http://localhost:8080/todos' \ --header 'Authorization: Bearer $token' > $todos ``` ## Get a specific User's TODO ```sh $ curl --location --request GET 'http://localhost:8080/todos/$id' \ --header 'Authorization: Bearer $token' > $todo ``` ## Get all TODOs for all Users (admin role) ```sh $ curl --location --request GET 'http://localhost:8080/admin/todos' \ --header 'Authorization: Bearer $token' > $todos ``` ## Create a new TODO ```sh $ curl --location --request POST 'http://localhost:8080/todos' \ --header 'Authorization: Bearer $token' \ --header 'Content-Type: application/json' \ --data-raw '{ "title": "test titlte", "body": "test body" }' > Status Created > $todo ``` ================================================ FILE: _examples/auth/jwt/tutorial/api/auth.go ================================================ package api import ( "fmt" "os" "time" "myapp/domain/model" "myapp/domain/repository" "myapp/util" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/middleware/jwt" ) const defaultSecretKey = "sercrethatmaycontainch@r$32chars" func getSecretKey() string { secret := os.Getenv(util.AppName + "_SECRET") if secret == "" { return defaultSecretKey } return secret } // UserClaims represents the user token claims. type UserClaims struct { UserID string `json:"user_id"` Roles []model.Role `json:"roles"` } // Validate implements the custom struct claims validator, // this is totally optionally and maybe unnecessary but good to know how. func (u *UserClaims) Validate() error { if u.UserID == "" { return fmt.Errorf("%w: %s", jwt.ErrMissingKey, "user_id") } return nil } // Verify allows only authorized clients. func Verify() iris.Handler { secret := getSecretKey() verifier := jwt.NewVerifier(jwt.HS256, []byte(secret), jwt.Expected{Issuer: util.AppName}) verifier.Extractors = []jwt.TokenExtractor{jwt.FromHeader} // extract token only from Authorization: Bearer $token return verifier.Verify(func() any { return new(UserClaims) }) } // AllowAdmin allows only authorized clients with "admin" access role. // Should be registered after Verify. func AllowAdmin(ctx iris.Context) { if !IsAdmin(ctx) { ctx.StopWithText(iris.StatusForbidden, "admin access required") return } ctx.Next() } // SignIn accepts the user form data and returns a token to authorize a client. func SignIn(repo repository.UserRepository) iris.Handler { secret := getSecretKey() signer := jwt.NewSigner(jwt.HS256, []byte(secret), 15*time.Minute) return func(ctx iris.Context) { /* type LoginForm struct { Username string `form:"username"` Password string `form:"password"` } and ctx.ReadForm OR use the ctx.FormValue(s) method. */ var ( username = ctx.FormValue("username") password = ctx.FormValue("password") ) user, ok := repo.GetByUsernameAndPassword(username, password) if !ok { ctx.StopWithText(iris.StatusBadRequest, "wrong username or password") return } claims := UserClaims{ UserID: user.ID, Roles: user.Roles, } // Optionally, generate a JWT ID. jti, err := util.GenerateUUID() if err != nil { ctx.StopWithError(iris.StatusInternalServerError, err) return } token, err := signer.Sign(claims, jwt.Claims{ ID: jti, Issuer: util.AppName, }) if err != nil { ctx.StopWithError(iris.StatusInternalServerError, err) return } ctx.Write(token) } } // SignOut invalidates a user from server-side using the jwt Blocklist. func SignOut(ctx iris.Context) { ctx.Logout() // this is automatically binded to a function which invalidates the current request token by the JWT Verifier above. } // GetClaims returns the current authorized client claims. func GetClaims(ctx iris.Context) *UserClaims { claims := jwt.Get(ctx).(*UserClaims) return claims } // GetUserID returns the current authorized client's user id extracted from claims. func GetUserID(ctx iris.Context) string { return GetClaims(ctx).UserID } // IsAdmin reports whether the current client has admin access. func IsAdmin(ctx iris.Context) bool { for _, role := range GetClaims(ctx).Roles { if role == model.Admin { return true } } return false } ================================================ FILE: _examples/auth/jwt/tutorial/api/router.go ================================================ package api import ( "myapp/domain/repository" "github.com/kataras/iris/v12" ) // NewRouter accepts some dependencies // and returns a function which returns the routes on the given Iris Party (group of routes). func NewRouter(userRepo repository.UserRepository, todoRepo repository.TodoRepository) func(iris.Party) { return func(router iris.Party) { router.Post("/signin", SignIn(userRepo)) router.Use(Verify()) // protect the next routes with JWT. router.Post("/todos", CreateTodo(todoRepo)) router.Get("/todos", ListTodos(todoRepo)) router.Get("/todos/{id}", GetTodo(todoRepo)) router.Get("/admin/todos", AllowAdmin, ListAllTodos(todoRepo)) } } ================================================ FILE: _examples/auth/jwt/tutorial/api/todo.go ================================================ package api import ( "errors" "myapp/domain/repository" "github.com/kataras/iris/v12" ) // TodoRequest represents a Todo HTTP request. type TodoRequest struct { Title string `json:"title" form:"title" url:"title"` Body string `json:"body" form:"body" url:"body"` } // CreateTodo handles the creation of a Todo entry. func CreateTodo(repo repository.TodoRepository) iris.Handler { return func(ctx iris.Context) { var req TodoRequest err := ctx.ReadBody(&req) // will bind the "req" to a JSON, form or url query request data. if err != nil { ctx.StopWithError(iris.StatusBadRequest, err) return } userID := GetUserID(ctx) todo, err := repo.Create(userID, req.Title, req.Body) if err != nil { ctx.StopWithError(iris.StatusInternalServerError, err) return } ctx.StatusCode(iris.StatusCreated) ctx.JSON(todo) } } // GetTodo lists all users todos. // Parameter: {id}. func GetTodo(repo repository.TodoRepository) iris.Handler { return func(ctx iris.Context) { id := ctx.Params().Get("id") userID := GetUserID(ctx) todo, err := repo.GetByID(id) if err != nil { code := iris.StatusInternalServerError if errors.Is(err, repository.ErrNotFound) { code = iris.StatusNotFound } ctx.StopWithError(code, err) return } if !IsAdmin(ctx) { // admin can access any user's todos. if todo.UserID != userID { ctx.StopWithStatus(iris.StatusForbidden) return } } ctx.JSON(todo) } } // ListTodos lists todos of the current user. func ListTodos(repo repository.TodoRepository) iris.Handler { return func(ctx iris.Context) { userID := GetUserID(ctx) todos, err := repo.GetAllByUser(userID) if err != nil { ctx.StopWithError(iris.StatusInternalServerError, err) return } // if len(todos) == 0 { // ctx.StopWithError(iris.StatusNotFound, fmt.Errorf("no entries found")) // return // } // Or let the client decide what to do on empty list. ctx.JSON(todos) } } // ListAllTodos lists all users todos. // Access: admin. // Middleware: AllowAdmin. func ListAllTodos(repo repository.TodoRepository) iris.Handler { return func(ctx iris.Context) { todos, err := repo.GetAll() if err != nil { ctx.StopWithError(iris.StatusInternalServerError, err) return } ctx.JSON(todos) } } /* Leave as exercise: use filtering instead... // ListTodosByUser lists all todos by a specific user. // Access: admin. // Middleware: AllowAdmin. // Parameter: {id}. func ListTodosByUser(repo repository.TodoRepository) iris.Handler { return func(ctx iris.Context) { userID := ctx.Params().Get("id") todos, err := repo.GetAllByUser(userID) if err != nil { ctx.StopWithError(iris.StatusInternalServerError, err) return } ctx.JSON(todos) } } */ ================================================ FILE: _examples/auth/jwt/tutorial/domain/model/role.go ================================================ package model // Role represents a role. type Role string const ( // Admin represents the Admin access role. Admin Role = "admin" ) ================================================ FILE: _examples/auth/jwt/tutorial/domain/model/todo.go ================================================ package model // Todo represents the Todo model. type Todo struct { ID string `json:"id"` UserID string `json:"user_id"` Title string `json:"title"` Body string `json:"body"` CreatedAt int64 `json:"created_at"` // unix seconds. } ================================================ FILE: _examples/auth/jwt/tutorial/domain/model/user.go ================================================ package model // User represents our User model. type User struct { ID string `json:"id"` Username string `json:"username"` HashedPassword []byte `json:"-"` Roles []Role `json:"roles"` } ================================================ FILE: _examples/auth/jwt/tutorial/domain/repository/samples.go ================================================ package repository import ( "fmt" "myapp/domain/model" ) // GenerateSamples generates data samples. func GenerateSamples(userRepo UserRepository, todoRepo TodoRepository) error { // Create users. for _, username := range []string{"vasiliki", "george", "kwstas"} { // My grandmother. // My young brother. // My youngest brother. password := fmt.Sprintf("%s_pass", username) if _, err := userRepo.Create(username, password); err != nil { return err } } // Create a user with admin role. if _, err := userRepo.Create("admin", "admin", model.Admin); err != nil { return err } // Create two todos per user. users, err := userRepo.GetAll() if err != nil { return err } for i, u := range users { for j := 0; j < 2; j++ { title := fmt.Sprintf("%s todo %d:%d title", u.Username, i, j) body := fmt.Sprintf("%s todo %d:%d body", u.Username, i, j) _, err := todoRepo.Create(u.ID, title, body) if err != nil { return err } } } return nil } ================================================ FILE: _examples/auth/jwt/tutorial/domain/repository/todo_repository.go ================================================ package repository import ( "errors" "sync" "myapp/domain/model" "myapp/util" ) // ErrNotFound indicates that an entry was not found. // Usage: errors.Is(err, ErrNotFound) var ErrNotFound = errors.New("not found") // TodoRepository is responsible for Todo CRUD operations, // however, for the sake of the example we only implement the Create and Read ones. type TodoRepository interface { Create(userID, title, body string) (model.Todo, error) GetByID(id string) (model.Todo, error) GetAll() ([]model.Todo, error) GetAllByUser(userID string) ([]model.Todo, error) } var ( _ TodoRepository = (*memoryTodoRepository)(nil) ) type memoryTodoRepository struct { todos []model.Todo // map[string]model.Todo mu sync.RWMutex } // NewMemoryTodoRepository returns the default in-memory todo repository. func NewMemoryTodoRepository() TodoRepository { r := new(memoryTodoRepository) return r } func (r *memoryTodoRepository) Create(userID, title, body string) (model.Todo, error) { id, err := util.GenerateUUID() if err != nil { return model.Todo{}, err } todo := model.Todo{ ID: id, UserID: userID, Title: title, Body: body, CreatedAt: util.Now().Unix(), } r.mu.Lock() r.todos = append(r.todos, todo) r.mu.Unlock() return todo, nil } func (r *memoryTodoRepository) GetByID(id string) (model.Todo, error) { r.mu.RLock() defer r.mu.RUnlock() for _, todo := range r.todos { if todo.ID == id { return todo, nil } } return model.Todo{}, ErrNotFound } func (r *memoryTodoRepository) GetAll() ([]model.Todo, error) { r.mu.RLock() tmp := make([]model.Todo, len(r.todos)) copy(tmp, r.todos) r.mu.RUnlock() return tmp, nil } func (r *memoryTodoRepository) GetAllByUser(userID string) ([]model.Todo, error) { // initialize a slice, so we don't have "null" at empty response. todos := make([]model.Todo, 0) r.mu.RLock() for _, todo := range r.todos { if todo.UserID == userID { todos = append(todos, todo) } } r.mu.RUnlock() return todos, nil } ================================================ FILE: _examples/auth/jwt/tutorial/domain/repository/user_repository.go ================================================ package repository import ( "sync" "myapp/domain/model" "myapp/util" ) // UserRepository is responsible for User CRUD operations, // however, for the sake of the example we only implement the Read one. type UserRepository interface { Create(username, password string, roles ...model.Role) (model.User, error) // GetByUsernameAndPassword should return a User based on the given input. GetByUsernameAndPassword(username, password string) (model.User, bool) GetAll() ([]model.User, error) } var ( _ UserRepository = (*memoryUserRepository)(nil) ) type memoryUserRepository struct { // Users represents a user database. // For the sake of the tutorial we use a simple slice of users. users []model.User mu sync.RWMutex } // NewMemoryUserRepository returns the default in-memory user repository. func NewMemoryUserRepository() UserRepository { r := new(memoryUserRepository) return r } func (r *memoryUserRepository) Create(username, password string, roles ...model.Role) (model.User, error) { id, err := util.GenerateUUID() if err != nil { return model.User{}, err } hashedPassword, err := util.GeneratePassword(password) if err != nil { return model.User{}, err } user := model.User{ ID: id, Username: username, HashedPassword: hashedPassword, Roles: roles, } r.mu.Lock() r.users = append(r.users, user) r.mu.Unlock() return user, nil } // GetByUsernameAndPassword returns a user from the storage based on the given "username" and "password". func (r *memoryUserRepository) GetByUsernameAndPassword(username, password string) (model.User, bool) { for _, u := range r.users { // our example uses a static slice. if u.Username == username { // we compare the user input and the stored hashed password. ok := util.ValidatePassword(password, u.HashedPassword) if ok { return u, true } } } return model.User{}, false } func (r *memoryUserRepository) GetAll() ([]model.User, error) { r.mu.RLock() tmp := make([]model.User, len(r.users)) copy(tmp, r.users) r.mu.RUnlock() return tmp, nil } ================================================ FILE: _examples/auth/jwt/tutorial/go-client/README.md ================================================ # Go Client ```sh $ go run . ``` ```sh 2020/11/04 21:08:40 Access Token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiYTAwYzI3ZDEtYjVhYS00NjU0LWFmMTYtYjExNzNkZTY1NjI5Iiwicm9sZXMiOlsiYWRtaW4iXSwiaWF0IjoxNjA0NTE2OTIwLCJleHAiOjE2MDQ1MTc4MjAsImp0aSI6IjYzNmVmMDc0LTE2MzktNGJhZi1hNGNiLTQ4ZDM4NGMxMzliYSIsImlzcyI6Im15YXBwIn0.T9B0zG0AHShO5JfQgrMQBlToH33KHgp8nLMPFpN6QmM" 2020/11/04 21:08:40 Todo Created: model.Todo{ID:"cfa38d7a-c556-4301-ae1f-fb90f705071c", UserID:"a00c27d1-b5aa-4654-af16-b1173de65629", Title:"test todo title", Body:"test todo body contents", CreatedAt:1604516920} ``` ================================================ FILE: _examples/auth/jwt/tutorial/go-client/client.go ================================================ package main import ( "bytes" "encoding/json" "fmt" "io" "net/http" "net/url" "strconv" "strings" ) // Client is the default http client instance used by the following methods. var Client = http.DefaultClient // RequestOption is a function which can be used to modify // a request instance before Do. type RequestOption func(*http.Request) error // WithAccessToken sets the given "token" to the authorization request header. func WithAccessToken(token []byte) RequestOption { bearer := "Bearer " + string(token) return func(req *http.Request) error { req.Header.Add("Authorization", bearer) return nil } } // WithContentType sets the content-type request header. func WithContentType(cType string) RequestOption { return func(req *http.Request) error { req.Header.Set("Content-Type", cType) return nil } } // WithContentLength sets the content-length request header. func WithContentLength(length int) RequestOption { return func(req *http.Request) error { req.Header.Set("Content-Length", strconv.Itoa(length)) return nil } } // Do fires a request to the server. func Do(method, url string, body io.Reader, opts ...RequestOption) (*http.Response, error) { req, err := http.NewRequest(method, url, body) if err != nil { return nil, err } for _, opt := range opts { if err = opt(req); err != nil { return nil, err } } return Client.Do(req) } // JSON fires a request with "v" as client json data. func JSON(method, url string, v any, opts ...RequestOption) (*http.Response, error) { buf := new(bytes.Buffer) err := json.NewEncoder(buf).Encode(v) if err != nil { return nil, err } opts = append(opts, WithContentType("application/json; charset=utf-8")) return Do(method, url, buf, opts...) } // Form fires a request with "formData" as client form data. func Form(method, url string, formData url.Values, opts ...RequestOption) (*http.Response, error) { encoded := formData.Encode() body := strings.NewReader(encoded) opts = append([]RequestOption{ WithContentType("application/x-www-form-urlencoded"), WithContentLength(len(encoded)), }, opts...) return Do(method, url, body, opts...) } // BindResponse binds a response body to the "dest" pointer and closes the body. func BindResponse(resp *http.Response, dest any) error { contentType := resp.Header.Get("Content-Type") if idx := strings.IndexRune(contentType, ';'); idx > 0 { contentType = contentType[0:idx] } switch contentType { case "application/json": defer resp.Body.Close() return json.NewDecoder(resp.Body).Decode(dest) default: return fmt.Errorf("unsupported content type: %s", contentType) } } // RawResponse simply returns the raw response body. func RawResponse(resp *http.Response) ([]byte, error) { defer resp.Body.Close() return io.ReadAll(resp.Body) } ================================================ FILE: _examples/auth/jwt/tutorial/go-client/main.go ================================================ package main import ( "fmt" "log" "net/http" "net/url" "myapp/api" "myapp/domain/model" ) const base = "http://localhost:8080" func main() { accessToken, err := authenticate("admin", "admin") if err != nil { log.Fatal(err) } log.Printf("Access Token:\n%q", accessToken) todo, err := createTodo(accessToken, "test todo title", "test todo body contents") if err != nil { log.Fatal(err) } log.Printf("Todo Created:\n%#+v", todo) } func authenticate(username, password string) ([]byte, error) { endpoint := base + "/signin" data := make(url.Values) data.Set("username", username) data.Set("password", password) resp, err := Form(http.MethodPost, endpoint, data) if err != nil { return nil, err } accessToken, err := RawResponse(resp) return accessToken, err } func createTodo(accessToken []byte, title, body string) (model.Todo, error) { var todo model.Todo endpoint := base + "/todos" req := api.TodoRequest{ Title: title, Body: body, } resp, err := JSON(http.MethodPost, endpoint, req, WithAccessToken(accessToken)) if err != nil { return todo, err } if resp.StatusCode != http.StatusCreated { rawData, _ := RawResponse(resp) return todo, fmt.Errorf("failed to create a todo: %s", string(rawData)) } err = BindResponse(resp, &todo) return todo, err } ================================================ FILE: _examples/auth/jwt/tutorial/go.mod ================================================ module myapp go 1.25 require ( github.com/google/uuid v1.6.0 github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd golang.org/x/crypto v0.47.0 ) require ( github.com/BurntSushi/toml v1.6.0 // indirect github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect github.com/CloudyKit/jet/v6 v6.3.1 // indirect github.com/Joker/jade v1.1.3 // indirect github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 // indirect github.com/andybalholm/brotli v1.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/fatih/structs v1.1.0 // indirect github.com/flosch/pongo2/v4 v4.0.2 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a // indirect github.com/gorilla/css v1.0.1 // indirect github.com/iris-contrib/schema v0.0.6 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/kataras/blocks v0.0.8 // indirect github.com/kataras/golog v0.1.12 // indirect github.com/kataras/jwt v0.1.17 // indirect github.com/kataras/pio v0.0.13 // indirect github.com/kataras/sitemap v0.0.6 // indirect github.com/kataras/tunnel v0.0.4 // indirect github.com/klauspost/compress v1.18.2 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mailgun/raymond/v2 v2.0.48 // indirect github.com/mailru/easyjson v0.9.1 // indirect github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/schollz/closestmatch v2.1.0+incompatible // indirect github.com/sirupsen/logrus v1.8.1 // indirect github.com/stretchr/testify v1.11.1 // indirect github.com/tdewolff/minify/v2 v2.24.8 // indirect github.com/tdewolff/parse/v2 v2.8.5 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/yosssi/ace v0.0.5 // indirect golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect golang.org/x/net v0.49.0 // indirect golang.org/x/sys v0.40.0 // indirect golang.org/x/text v0.33.0 // indirect golang.org/x/time v0.14.0 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: _examples/auth/jwt/tutorial/go.sum ================================================ github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= github.com/CloudyKit/jet/v6 v6.3.1 h1:6IAo5Cx21xrHVaR8zzXN5gJatKV/wO7Nf6bfCnCSbUw= github.com/CloudyKit/jet/v6 v6.3.1/go.mod h1:lf8ksdNsxZt7/yH/3n4vJQWA9RUq4wpaHtArHhGVMOw= github.com/Joker/hpp v1.0.0 h1:65+iuJYdRXv/XyN62C1uEmmOx3432rNG/rKlX6V7Kkc= github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= github.com/Joker/jade v1.1.3 h1:Qbeh12Vq6BxURXT1qZBRHsDxeURB8ztcL6f3EXSGeHk= github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKduM= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 h1:dG7/gLroJGht/jSQtHiLvT48Hxn+crbmvyItZC8cWOs= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05/go.mod h1:NYezi6wtnJtBm5btoprXc5SvAdqH0XTXWnUup0MptAI= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/flosch/pongo2/v4 v4.0.2 h1:gv+5Pe3vaSVmiJvh/BZa82b7/00YUGm0PIyVVLop0Hw= github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+BWPhwZD8= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a h1:l7A0loSszR5zHd/qK53ZIHMO8b3bBSmENnQ6eKnUT0A= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/iris-contrib/httpexpect/v2 v2.15.2 h1:T9THsdP1woyAqKHwjkEsbCnMefsAFvk8iJJKokcJ3Go= github.com/iris-contrib/httpexpect/v2 v2.15.2/go.mod h1:JLDgIqnFy5loDSUv1OA2j0mb6p/rDhiCqigP22Uq9xE= github.com/iris-contrib/schema v0.0.6 h1:CPSBLyx2e91H2yJzPuhGuifVRnZBBJ3pCOMbOvPZaTw= github.com/iris-contrib/schema v0.0.6/go.mod h1:iYszG0IOsuIsfzjymw1kMzTL8YQcCWlm65f3wX8J5iA= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kataras/blocks v0.0.8 h1:MrpVhoFTCR2v1iOOfGng5VJSILKeZZI+7NGfxEh3SUM= github.com/kataras/blocks v0.0.8/go.mod h1:9Jm5zx6BB+06NwA+OhTbHW1xkMOYxahnqTN5DveZ2Yg= github.com/kataras/golog v0.1.12 h1:Bu7I/G4ilJlbfzjmU39O9N+2uO1pBcMK045fzZ4ytNg= github.com/kataras/golog v0.1.12/go.mod h1:wrGSbOiBqbQSQznleVNX4epWM8rl9SJ/rmEacl0yqy4= github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd h1:oMsQgqKACbUdlaIWnN+EZzzAnHkw90hE/y/R0BSij2U= github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd/go.mod h1:yucjtDl9Vn3OctWM1fi0MuM9OckemC7kEreFa9QtPF8= github.com/kataras/jwt v0.1.17 h1:dYjemzcdYqA4ylwq9/56MslCr/pNOyVUZ2bl3hYNHgc= github.com/kataras/jwt v0.1.17/go.mod h1:HUnU5HDBCDanVF8zrPVSE2VK8HicospKefZDD4DzOKU= github.com/kataras/pio v0.0.13 h1:x0rXVX0fviDTXOOLOmr4MUxOabu1InVSTu5itF8CXCM= github.com/kataras/pio v0.0.13/go.mod h1:k3HNuSw+eJ8Pm2lA4lRhg3DiCjVgHlP8hmXApSej3oM= github.com/kataras/sitemap v0.0.6 h1:w71CRMMKYMJh6LR2wTgnk5hSgjVNB9KL60n5e2KHvLY= github.com/kataras/sitemap v0.0.6/go.mod h1:dW4dOCNs896OR1HmG+dMLdT7JjDk7mYBzoIRwuj5jA4= github.com/kataras/tunnel v0.0.4 h1:sCAqWuJV7nPzGrlb0os3j49lk2JhILT0rID38NHNLpA= github.com/kataras/tunnel v0.0.4/go.mod h1:9FkU4LaeifdMWqZu7o20ojmW4B7hdhv2CMLwfnHGpYw= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw= github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18= github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo= github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk= github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tdewolff/minify/v2 v2.24.8 h1:58/VjsbevI4d5FGV0ZSuBrHMSSkH4MCH0sIz/eKIauE= github.com/tdewolff/minify/v2 v2.24.8/go.mod h1:0Ukj0CRpo/sW/nd8uZ4ccXaV1rEVIWA3dj8U7+Shhfw= github.com/tdewolff/parse/v2 v2.8.5 h1:ZmBiA/8Do5Rpk7bDye0jbbDUpXXbCdc3iah4VeUvwYU= github.com/tdewolff/parse/v2 v2.8.5/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo= github.com/tdewolff/test v1.0.11 h1:FdLbwQVHxqG16SlkGveC0JVyrJN62COWTRyUFzfbtBE= github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA= github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0= github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs= moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHcE= ================================================ FILE: _examples/auth/jwt/tutorial/main.go ================================================ package main import ( "myapp/api" "myapp/domain/repository" "github.com/kataras/iris/v12" ) var ( userRepo = repository.NewMemoryUserRepository() todoRepo = repository.NewMemoryTodoRepository() ) func main() { if err := repository.GenerateSamples(userRepo, todoRepo); err != nil { panic(err) } app := iris.New() app.PartyFunc("/", api.NewRouter(userRepo, todoRepo)) // POST http://localhost:8080/signin (Form: username, password) // GET http://localhost:8080/todos // GET http://localhost:8080/todos/{id} // POST http://localhost:8080/todos (JSON, Form or URL: title, body) // GET http://localhost:8080/admin/todos app.Listen(":8080") } ================================================ FILE: _examples/auth/jwt/tutorial/util/app.go ================================================ package util // Constants for the application. const ( Version = "0.0.1" AppName = "myapp" ) ================================================ FILE: _examples/auth/jwt/tutorial/util/clock.go ================================================ package util import "time" // Now is the default current time for the whole application. // Can be modified for testing or custom timezone. var Now = time.Now ================================================ FILE: _examples/auth/jwt/tutorial/util/password.go ================================================ package util import "golang.org/x/crypto/bcrypt" // MustGeneratePassword same as GeneratePassword but panics on errors. func MustGeneratePassword(userPassword string) []byte { hashed, err := GeneratePassword(userPassword) if err != nil { panic(err) } return hashed } // GeneratePassword will generate a hashed password for us based on the // user's input. func GeneratePassword(userPassword string) ([]byte, error) { return bcrypt.GenerateFromPassword([]byte(userPassword), bcrypt.DefaultCost) } // ValidatePassword will check if passwords are matched. func ValidatePassword(userPassword string, hashed []byte) bool { err := bcrypt.CompareHashAndPassword(hashed, []byte(userPassword)) return err == nil } ================================================ FILE: _examples/auth/jwt/tutorial/util/uuid.go ================================================ package util import "github.com/google/uuid" // MustGenerateUUID returns a new v4 UUID or panics. func MustGenerateUUID() string { id, err := GenerateUUID() if err != nil { panic(err) } return id } // GenerateUUID returns a new v4 UUID. func GenerateUUID() (string, error) { id, err := uuid.NewRandom() if err != nil { return "", err } return id.String(), nil } ================================================ FILE: _examples/auth/permissions/main.go ================================================ package main import ( "fmt" "log" "strings" "github.com/kataras/iris/v12" permissions "github.com/xyproto/permissionbolt" // * PostgreSQL support: // permissions "github.com/xyproto/pstore" and // perm, err := permissions.New(...) // // * MariaDB/MySQL support: // permissions "github.com/xyproto/permissionsql" and // perm, err := permissions.New/NewWithDSN(...) // * Redis support: // permissions "github.com/xyproto/permissions2" // perm, err := permissions.New2() // * Bolt support (this one): // permissions "github.com/xyproto/permissionbolt" and // perm, err := permissions.New(...) ) func main() { app := iris.New() app.Logger().SetLevel("debug") // New permissions middleware. perm, err := permissions.New() if err != nil { log.Fatalln(err) } // Blank slate, no default permissions // perm.Clear() // Set up a middleware handler for Iris, with a custom "permission denied" message. permissionHandler := func(ctx iris.Context) { // Check if the user has the right admin/user rights if perm.Rejected(ctx.ResponseWriter(), ctx.Request()) { // Deny the request, don't call other middleware handlers ctx.StopWithText(iris.StatusForbidden, "Permission denied!") return } // Call the next middleware handler ctx.Next() } // Register the permissions middleware app.Use(permissionHandler) // Get the userstate, used in the handlers below userstate := perm.UserState() app.Get("/", func(ctx iris.Context) { msg := "" msg += fmt.Sprintf("Has user bob: %v\n", userstate.HasUser("bob")) msg += fmt.Sprintf("Logged in on server: %v\n", userstate.IsLoggedIn("bob")) msg += fmt.Sprintf("Is confirmed: %v\n", userstate.IsConfirmed("bob")) msg += fmt.Sprintf("Username stored in cookies (or blank): %v\n", userstate.Username(ctx.Request())) msg += fmt.Sprintf("Current user is logged in, has a valid cookie and *user rights*: %v\n", userstate.UserRights(ctx.Request())) msg += fmt.Sprintf("Current user is logged in, has a valid cookie and *admin rights*: %v\n", userstate.AdminRights(ctx.Request())) msg += fmt.Sprintln("\nTry: /register, /confirm, /remove, /login, /logout, /makeadmin, /clear, /data and /admin") ctx.WriteString(msg) }) app.Get("/register", func(ctx iris.Context) { userstate.AddUser("bob", "hunter1", "bob@zombo.com") ctx.Writef("User bob was created: %v\n", userstate.HasUser("bob")) }) app.Get("/confirm", func(ctx iris.Context) { userstate.MarkConfirmed("bob") ctx.Writef("User bob was confirmed: %v\n", userstate.IsConfirmed("bob")) }) app.Get("/remove", func(ctx iris.Context) { userstate.RemoveUser("bob") ctx.Writef("User bob was removed: %v\n", !userstate.HasUser("bob")) }) app.Get("/login", func(ctx iris.Context) { // Headers will be written, for storing a cookie userstate.Login(ctx.ResponseWriter(), "bob") ctx.Writef("bob is now logged in: %v\n", userstate.IsLoggedIn("bob")) }) app.Get("/logout", func(ctx iris.Context) { userstate.Logout("bob") ctx.Writef("bob is now logged out: %v\n", !userstate.IsLoggedIn("bob")) }) app.Get("/makeadmin", func(ctx iris.Context) { userstate.SetAdminStatus("bob") ctx.Writef("bob is now administrator: %v\n", userstate.IsAdmin("bob")) }) app.Get("/clear", func(ctx iris.Context) { userstate.ClearCookie(ctx.ResponseWriter()) ctx.WriteString("Clearing cookie") }) app.Get("/data", func(ctx iris.Context) { ctx.WriteString("user page that only logged in users must see!") }) app.Get("/admin", func(ctx iris.Context) { ctx.WriteString("super secret information that only logged in administrators must see!\n\n") if usernames, err := userstate.AllUsernames(); err == nil { ctx.Writef("list of all users: %s", strings.Join(usernames, ", ")) } }) // Serve app.Listen(":8080") } ================================================ FILE: _examples/auth/recaptcha/custom_form/main.go ================================================ package main import ( "fmt" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/middleware/recaptcha" ) // keys should be obtained by https://www.google.com/recaptcha const ( recaptchaPublic = "6Lf3WywUAAAAAKNfAm5DP2J5ahqedtZdHTYaKkJ6" recaptchaSecret = "6Lf3WywUAAAAAJpArb8nW_LCL_PuPuokmEABFfgw" ) func main() { app := iris.New() r := recaptcha.New(recaptchaSecret) app.Get("/comment", showRecaptchaForm) // pass the middleware before the main handler or use the `recaptcha.SiteVerify`. app.Post("/comment", r, postComment) app.Listen(":8080") } var htmlForm = `
` func showRecaptchaForm(ctx iris.Context) { contents := fmt.Sprintf(htmlForm, recaptchaPublic) ctx.HTML(contents) } func postComment(ctx iris.Context) { // [...] ctx.JSON(iris.Map{"success": true}) } ================================================ FILE: _examples/auth/recaptcha/main.go ================================================ package main import ( "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/middleware/recaptcha" ) // keys should be obtained by https://www.google.com/recaptcha const ( recaptchaPublic = "" recaptchaSecret = "" ) func showRecaptchaForm(ctx iris.Context, path string) { ctx.HTML(recaptcha.GetFormHTML(recaptchaPublic, path)) } func main() { app := iris.New() // On both Get and Post on this example, so you can easly // use a single route to show a form and the main subject if recaptcha's validation result succeed. app.HandleMany("GET POST", "/", func(ctx iris.Context) { if ctx.Method() == iris.MethodGet { showRecaptchaForm(ctx, "/") return } result := recaptcha.SiteVerify(ctx, recaptchaSecret) if !result.Success { /* redirect here if u want or do nothing */ ctx.HTML(" failed please try again ") return } ctx.Writef("succeed.") }) app.Listen(":8080") } ================================================ FILE: _examples/bootstrapper/bootstrap/bootstrapper.go ================================================ package bootstrap import ( "fmt" "time" "github.com/gorilla/securecookie" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/middleware/logger" "github.com/kataras/iris/v12/middleware/recover" "github.com/kataras/iris/v12/sessions" "github.com/kataras/iris/v12/websocket" ) type Configurator func(*Bootstrapper) type Bootstrapper struct { *iris.Application AppName string AppOwner string AppSpawnDate time.Time Sessions *sessions.Sessions } // New returns a new Bootstrapper. func New(appName, appOwner string, cfgs ...Configurator) *Bootstrapper { b := &Bootstrapper{ AppName: appName, AppOwner: appOwner, AppSpawnDate: time.Now(), Application: iris.New(), } for _, cfg := range cfgs { cfg(b) } return b } // SetupViews loads the templates. func (b *Bootstrapper) SetupViews(viewsDir string) { b.RegisterView(iris.HTML(viewsDir, ".html").Layout("shared/layout.html")) } // SetupSessions initializes the sessions, optionally. func (b *Bootstrapper) SetupSessions(expires time.Duration, cookieHashKey, cookieBlockKey []byte) { b.Sessions = sessions.New(sessions.Config{ Cookie: "SECRET_SESS_COOKIE_" + b.AppName, Expires: expires, Encoding: securecookie.New(cookieHashKey, cookieBlockKey), }) } // SetupWebsockets prepares the websocket server. func (b *Bootstrapper) SetupWebsockets(endpoint string, handler websocket.ConnHandler) { ws := websocket.New(websocket.DefaultGorillaUpgrader, handler) b.Get(endpoint, websocket.Handler(ws)) } // SetupErrorHandlers prepares the http error handlers // `(context.StatusCodeNotSuccessful`, which defaults to >=400 (but you can change it). func (b *Bootstrapper) SetupErrorHandlers() { b.OnAnyErrorCode(func(ctx iris.Context) { err := iris.Map{ "app": b.AppName, "status": ctx.GetStatusCode(), "message": ctx.Values().GetString("message"), } if jsonOutput := ctx.URLParamExists("json"); jsonOutput { ctx.JSON(err) return } ctx.ViewData("Err", err) ctx.ViewData("Title", "Error") if err := ctx.View("shared/error.html"); err != nil { ctx.HTML(fmt.Sprintf("

%s

", err.Error())) return } }) } const ( // StaticAssets is the root directory for public assets like images, css, js. StaticAssets = "./public/" // Favicon is the relative 9to the "StaticAssets") favicon path for our app. Favicon = "favicon.ico" ) // Configure accepts configurations and runs them inside the Bootstraper's context. func (b *Bootstrapper) Configure(cs ...Configurator) { for _, c := range cs { c(b) } } // Bootstrap prepares our application. // // Returns itself. func (b *Bootstrapper) Bootstrap() *Bootstrapper { b.SetupViews("./views") b.SetupSessions(24*time.Hour, []byte("the-big-and-secret-fash-key-here"), []byte("lot-secret-of-characters-big-too"), ) b.SetupErrorHandlers() // static files b.Favicon(StaticAssets + Favicon) b.HandleDir("/public", iris.Dir(StaticAssets)) // middleware, after static files b.Use(recover.New()) b.Use(logger.New()) return b } // Listen starts the http server with the specified "addr". func (b *Bootstrapper) Listen(addr string, cfgs ...iris.Configurator) { b.Run(iris.Addr(addr), cfgs...) } ================================================ FILE: _examples/bootstrapper/go.mod ================================================ module github.com/kataras/iris/v12/_examples/bootstrapper go 1.25 require ( github.com/gorilla/securecookie v1.1.2 github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd ) require ( github.com/BurntSushi/toml v1.6.0 // indirect github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect github.com/CloudyKit/jet/v6 v6.3.1 // indirect github.com/Joker/jade v1.1.3 // indirect github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 // indirect github.com/ajg/form v1.5.1 // indirect github.com/andybalholm/brotli v1.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fatih/color v1.15.0 // indirect github.com/fatih/structs v1.1.0 // indirect github.com/flosch/pongo2/v4 v4.0.2 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/ws v1.4.0 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/imkira/go-interpol v1.1.0 // indirect github.com/iris-contrib/httpexpect/v2 v2.15.2 // indirect github.com/iris-contrib/schema v0.0.6 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/kataras/blocks v0.0.8 // indirect github.com/kataras/golog v0.1.12 // indirect github.com/kataras/neffos v0.0.24 // indirect github.com/kataras/pio v0.0.13 // indirect github.com/kataras/sitemap v0.0.6 // indirect github.com/kataras/tunnel v0.0.4 // indirect github.com/klauspost/compress v1.18.2 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mailgun/raymond/v2 v2.0.48 // indirect github.com/mailru/easyjson v0.9.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect github.com/mediocregopher/radix/v3 v3.8.1 // indirect github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/nats-io/nats.go v1.40.1 // indirect github.com/nats-io/nkeys v0.4.9 // indirect github.com/nats-io/nuid v1.0.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sanity-io/litter v1.5.5 // indirect github.com/schollz/closestmatch v2.1.0+incompatible // indirect github.com/sergi/go-diff v1.0.0 // indirect github.com/sirupsen/logrus v1.8.1 // indirect github.com/stretchr/testify v1.11.1 // indirect github.com/tdewolff/minify/v2 v2.24.8 // indirect github.com/tdewolff/parse/v2 v2.8.5 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 // indirect github.com/yosssi/ace v0.0.5 // indirect github.com/yudai/gojsondiff v1.0.0 // indirect github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect golang.org/x/crypto v0.47.0 // indirect golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect golang.org/x/net v0.49.0 // indirect golang.org/x/sys v0.40.0 // indirect golang.org/x/text v0.33.0 // indirect golang.org/x/time v0.14.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect moul.io/http2curl/v2 v2.3.0 // indirect ) ================================================ FILE: _examples/bootstrapper/go.sum ================================================ github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= github.com/CloudyKit/jet/v6 v6.3.1 h1:6IAo5Cx21xrHVaR8zzXN5gJatKV/wO7Nf6bfCnCSbUw= github.com/CloudyKit/jet/v6 v6.3.1/go.mod h1:lf8ksdNsxZt7/yH/3n4vJQWA9RUq4wpaHtArHhGVMOw= github.com/Joker/hpp v1.0.0 h1:65+iuJYdRXv/XyN62C1uEmmOx3432rNG/rKlX6V7Kkc= github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= github.com/Joker/jade v1.1.3 h1:Qbeh12Vq6BxURXT1qZBRHsDxeURB8ztcL6f3EXSGeHk= github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKduM= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 h1:dG7/gLroJGht/jSQtHiLvT48Hxn+crbmvyItZC8cWOs= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05/go.mod h1:NYezi6wtnJtBm5btoprXc5SvAdqH0XTXWnUup0MptAI= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/flosch/pongo2/v4 v4.0.2 h1:gv+5Pe3vaSVmiJvh/BZa82b7/00YUGm0PIyVVLop0Hw= github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+BWPhwZD8= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a h1:l7A0loSszR5zHd/qK53ZIHMO8b3bBSmENnQ6eKnUT0A= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/iris-contrib/httpexpect/v2 v2.15.2 h1:T9THsdP1woyAqKHwjkEsbCnMefsAFvk8iJJKokcJ3Go= github.com/iris-contrib/httpexpect/v2 v2.15.2/go.mod h1:JLDgIqnFy5loDSUv1OA2j0mb6p/rDhiCqigP22Uq9xE= github.com/iris-contrib/schema v0.0.6 h1:CPSBLyx2e91H2yJzPuhGuifVRnZBBJ3pCOMbOvPZaTw= github.com/iris-contrib/schema v0.0.6/go.mod h1:iYszG0IOsuIsfzjymw1kMzTL8YQcCWlm65f3wX8J5iA= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kataras/blocks v0.0.8 h1:MrpVhoFTCR2v1iOOfGng5VJSILKeZZI+7NGfxEh3SUM= github.com/kataras/blocks v0.0.8/go.mod h1:9Jm5zx6BB+06NwA+OhTbHW1xkMOYxahnqTN5DveZ2Yg= github.com/kataras/golog v0.1.12 h1:Bu7I/G4ilJlbfzjmU39O9N+2uO1pBcMK045fzZ4ytNg= github.com/kataras/golog v0.1.12/go.mod h1:wrGSbOiBqbQSQznleVNX4epWM8rl9SJ/rmEacl0yqy4= github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd h1:oMsQgqKACbUdlaIWnN+EZzzAnHkw90hE/y/R0BSij2U= github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd/go.mod h1:yucjtDl9Vn3OctWM1fi0MuM9OckemC7kEreFa9QtPF8= github.com/kataras/neffos v0.0.24 h1:S3lHqJopCfXN285VdlbGeOj+Id83u4xdQKToa+w1vW0= github.com/kataras/neffos v0.0.24/go.mod h1:/3K9zQ0yEC5/xUiSQx46ToWa3xneGfUo/nMit/F5g+U= github.com/kataras/pio v0.0.13 h1:x0rXVX0fviDTXOOLOmr4MUxOabu1InVSTu5itF8CXCM= github.com/kataras/pio v0.0.13/go.mod h1:k3HNuSw+eJ8Pm2lA4lRhg3DiCjVgHlP8hmXApSej3oM= github.com/kataras/sitemap v0.0.6 h1:w71CRMMKYMJh6LR2wTgnk5hSgjVNB9KL60n5e2KHvLY= github.com/kataras/sitemap v0.0.6/go.mod h1:dW4dOCNs896OR1HmG+dMLdT7JjDk7mYBzoIRwuj5jA4= github.com/kataras/tunnel v0.0.4 h1:sCAqWuJV7nPzGrlb0os3j49lk2JhILT0rID38NHNLpA= github.com/kataras/tunnel v0.0.4/go.mod h1:9FkU4LaeifdMWqZu7o20ojmW4B7hdhv2CMLwfnHGpYw= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw= github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18= github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mediocregopher/radix/v3 v3.8.1 h1:rOkHflVuulFKlwsLY01/M2cM2tWCjDoETcMqKbAWu1M= github.com/mediocregopher/radix/v3 v3.8.1/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/nats-io/nats.go v1.40.1 h1:MLjDkdsbGUeCMKFyCFoLnNn/HDTqcgVa3EQm+pMNDPk= github.com/nats-io/nats.go v1.40.1/go.mod h1:wV73x0FSI/orHPSYoyMeJB+KajMDoWyXmFaRrrYaaTo= github.com/nats-io/nkeys v0.4.9 h1:qe9Faq2Gxwi6RZnZMXfmGMZkg3afLLOtrU+gDZJ35b0= github.com/nats-io/nkeys v0.4.9/go.mod h1:jcMqs+FLG+W5YO36OX6wFIFcmpdAns+w1Wm6D3I/evE= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7/go.mod h1:zO8QMzTeZd5cpnIkz/Gn6iK0jDfGicM1nynOkkPIl28= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo= github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk= github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8= github.com/tdewolff/minify/v2 v2.24.8 h1:58/VjsbevI4d5FGV0ZSuBrHMSSkH4MCH0sIz/eKIauE= github.com/tdewolff/minify/v2 v2.24.8/go.mod h1:0Ukj0CRpo/sW/nd8uZ4ccXaV1rEVIWA3dj8U7+Shhfw= github.com/tdewolff/parse/v2 v2.8.5 h1:ZmBiA/8Do5Rpk7bDye0jbbDUpXXbCdc3iah4VeUvwYU= github.com/tdewolff/parse/v2 v2.8.5/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo= github.com/tdewolff/test v1.0.11 h1:FdLbwQVHxqG16SlkGveC0JVyrJN62COWTRyUFzfbtBE= github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA= github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0= github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= github.com/yudai/pp v2.0.1+incompatible h1:Q4//iY4pNF6yPLZIigmvcl7k/bPgrcTPIFIcmawg5bI= github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs= moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHcE= ================================================ FILE: _examples/bootstrapper/main.go ================================================ package main import ( "github.com/kataras/iris/v12/_examples/bootstrapper/bootstrap" "github.com/kataras/iris/v12/_examples/bootstrapper/middleware/identity" "github.com/kataras/iris/v12/_examples/bootstrapper/routes" ) func newApp() *bootstrap.Bootstrapper { app := bootstrap.New("Awesome App", "kataras2006@hotmail.com") app.Bootstrap() app.Configure(identity.Configure, routes.Configure) return app } func main() { app := newApp() app.Listen(":8080") } ================================================ FILE: _examples/bootstrapper/main_test.go ================================================ package main import ( "testing" "github.com/kataras/iris/v12/httptest" ) // go test -v func TestApp(t *testing.T) { app := newApp() e := httptest.New(t, app.Application) // test our routes e.GET("/").Expect().Status(httptest.StatusOK) e.GET("/follower/42").Expect().Status(httptest.StatusOK). Body().IsEqual("from /follower/{id:int64} with ID: 42") e.GET("/following/52").Expect().Status(httptest.StatusOK). Body().IsEqual("from /following/{id:int64} with ID: 52") e.GET("/like/64").Expect().Status(httptest.StatusOK). Body().IsEqual("from /like/{id:int64} with ID: 64") // test not found e.GET("/notfound").Expect().Status(httptest.StatusNotFound) expectedErr := map[string]any{ "app": app.AppName, "status": httptest.StatusNotFound, "message": "", } e.GET("/anotfoundwithjson").WithQuery("json", nil). Expect().Status(httptest.StatusNotFound).JSON().IsEqual(expectedErr) } ================================================ FILE: _examples/bootstrapper/middleware/identity/identity.go ================================================ package identity import ( "time" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/_examples/bootstrapper/bootstrap" ) // New returns a new handler which adds some headers and view data // describing the application, i.e the owner, the startup time. func New(b *bootstrap.Bootstrapper) iris.Handler { return func(ctx iris.Context) { // response headers ctx.Header("App-Name", b.AppName) ctx.Header("App-Owner", b.AppOwner) ctx.Header("App-Since", time.Since(b.AppSpawnDate).String()) ctx.Header("Server", "Iris: https://iris-go.com") // view data if ctx.View or c.Tmpl = "$page.html" will be called next. ctx.ViewData("AppName", b.AppName) ctx.ViewData("AppOwner", b.AppOwner) ctx.Next() } } // Configure creates a new identity middleware and registers that to the app. func Configure(b *bootstrap.Bootstrapper) { h := New(b) b.UseGlobal(h) } ================================================ FILE: _examples/bootstrapper/routes/follower.go ================================================ package routes import ( "github.com/kataras/iris/v12" ) // GetFollowerHandler handles the GET: /follower/{id} func GetFollowerHandler(ctx iris.Context) { id, _ := ctx.Params().GetInt64("id") ctx.Writef("from "+ctx.GetCurrentRoute().Path()+" with ID: %d", id) } ================================================ FILE: _examples/bootstrapper/routes/following.go ================================================ package routes import ( "github.com/kataras/iris/v12" ) // GetFollowingHandler handles the GET: /following/{id} func GetFollowingHandler(ctx iris.Context) { id, _ := ctx.Params().GetInt64("id") ctx.Writef("from "+ctx.GetCurrentRoute().Path()+" with ID: %d", id) } ================================================ FILE: _examples/bootstrapper/routes/index.go ================================================ package routes import ( "fmt" "github.com/kataras/iris/v12" ) // GetIndexHandler handles the GET: / func GetIndexHandler(ctx iris.Context) { ctx.ViewData("Title", "Index Page") if err := ctx.View("index.html"); err != nil { ctx.HTML(fmt.Sprintf("

%s

", err.Error())) return } } ================================================ FILE: _examples/bootstrapper/routes/like.go ================================================ package routes import ( "github.com/kataras/iris/v12" ) // GetLikeHandler handles the GET: /like/{id} func GetLikeHandler(ctx iris.Context) { id, _ := ctx.Params().GetInt64("id") ctx.Writef("from "+ctx.GetCurrentRoute().Path()+" with ID: %d", id) } ================================================ FILE: _examples/bootstrapper/routes/routes.go ================================================ package routes import ( "github.com/kataras/iris/v12/_examples/bootstrapper/bootstrap" ) // Configure registers the necessary routes to the app. func Configure(b *bootstrap.Bootstrapper) { b.Get("/", GetIndexHandler) b.Get("/follower/{id:int64}", GetFollowerHandler) b.Get("/following/{id:int64}", GetFollowingHandler) b.Get("/like/{id:int64}", GetLikeHandler) } ================================================ FILE: _examples/bootstrapper/views/index.html ================================================

Welcome!!

================================================ FILE: _examples/bootstrapper/views/shared/error.html ================================================ 

Error.

An error occurred while processing your request.

{{.Err.status}}

{{.Err.message}}

================================================ FILE: _examples/bootstrapper/views/shared/layout.html ================================================  {{.Title}} - {{.AppName}}
{{ yield . }}

© 2017 - {{.AppOwner}}

================================================ FILE: _examples/caddy/Caddyfile ================================================ example.com { header / Server "Iris" proxy / example.com:9091 # localhost:9091 } api.example.com { header / Server "Iris" proxy / api.example.com:9092 # localhost:9092 } ================================================ FILE: _examples/caddy/README.md ================================================ # Caddy loves Iris The `Caddyfile` shows how you can use caddy to listen on ports 80 & 443 and sit in front of iris webserver(s) that serving on a different port (9091 and 9092 in this case; see Caddyfile). ## Running our two web servers 1. Go to `$GOPATH/src/github.com/kataras/iris/_examples/caddy/server1` 2. Open a terminal window and execute `go run main.go` 3. Go to `$GOPATH/src/github.com/kataras/iris/_examples/caddy/server2` 4. Open a new terminal window and execute `go run main.go` ## Caddy installation 1. Download caddy: https://caddyserver.com/download 2. Extract its contents where the `Caddyfile` is located, the `$GOPATH/src/github.com/kataras/iris/_examples/caddy` in this case 3. Open, read and modify the `Caddyfile` to see by yourself how easy it is to configure the servers 4. Run `caddy` directly or open a terminal window and execute `caddy` 5. Go to `https://example.com` and `https://api.example.com/user/42` ## Notes Iris has the `app.Run(iris.AutoTLS(":443", "example.com", "mail@example.com"))` which does the exactly same thing but caddy is a great tool that helps you when you run multiple web servers from one host machine, i.e iris, apache, tomcat. ================================================ FILE: _examples/caddy/server1/main.go ================================================ package main import ( "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/mvc" ) func main() { app := iris.New() templates := iris.HTML("./views", ".html").Layout("shared/layout.html") app.RegisterView(templates) mvc.New(app).Handle(new(Controller)) // http://localhost:9091 app.Listen(":9091") } // Layout contains all the binding properties for the shared/layout.html type Layout struct { Title string } // Controller is our example controller, request-scoped, each request has its own instance. type Controller struct { Layout Layout } // BeginRequest is the first method fired when client requests from this Controller's root path. func (c *Controller) BeginRequest(ctx iris.Context) { c.Layout.Title = "Home Page" } // EndRequest is the last method fired. // It's here just to complete the BaseController // in order to be tell iris to call the `BeginRequest` before the main method. func (c *Controller) EndRequest(ctx iris.Context) {} // Get handles GET http://localhost:9091 func (c *Controller) Get() mvc.View { return mvc.View{ Name: "index.html", Data: iris.Map{ "Layout": c.Layout, "Message": "Welcome to my website!", }, } } ================================================ FILE: _examples/caddy/server1/views/index.html ================================================
{{.Message}}
================================================ FILE: _examples/caddy/server1/views/shared/layout.html ================================================ {{.Layout.Title}} {{ yield . }} ================================================ FILE: _examples/caddy/server2/main.go ================================================ package main import ( "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/mvc" ) type postValue func(string) string func main() { app := iris.New() mvc.New(app.Party("/user")).Register( func(ctx iris.Context) postValue { return ctx.PostValue }).Handle(new(UserController)) // GET http://localhost:9092/user // GET http://localhost:9092/user/42 // POST http://localhost:9092/user // PUT http://localhost:9092/user/42 // DELETE http://localhost:9092/user/42 // GET http://localhost:9092/user/followers/42 app.Listen(":9092") } // UserController is our user example controller. type UserController struct{} // Get handles GET /user func (c *UserController) Get() string { return "Select all users" } // User is our test User model, nothing tremendous here. type User struct{ ID int64 } // GetBy handles GET /user/42, equal to .Get("/user/{id:int64}") func (c *UserController) GetBy(id int64) User { // Select User by ID == $id. return User{id} } // Post handles POST /user func (c *UserController) Post(post postValue) string { username := post("username") return "Create by user with username: " + username } // PutBy handles PUT /user/42 func (c *UserController) PutBy(id int) string { // Update user by ID == $id return "User updated" } // DeleteBy handles DELETE /user/42 func (c *UserController) DeleteBy(id int) bool { // Delete user by ID == %id // // when boolean then true = iris.StatusOK, false = iris.StatusNotFound return true } // GetFollowersBy handles GET /user/followers/42 func (c *UserController) GetFollowersBy(id int) []User { // Select all followers by user ID == $id return []User{ /* ... */ } } ================================================ FILE: _examples/compression/client/main.go ================================================ package main import ( "bytes" "compress/gzip" "encoding/json" "fmt" "io" "net/http" ) var client = http.DefaultClient const baseURL = "http://localhost:8080" func main() { fmt.Printf("Running client example on: %s\n", baseURL) getExample() postExample() } func getExample() { endpoint := baseURL + "/" req, err := http.NewRequest(http.MethodGet, endpoint, nil) if err != nil { panic(err) } // Required to receive server's compressed data. req.Header.Set("Accept-Encoding", "gzip") resp, err := client.Do(req) if err != nil { panic(err) } defer resp.Body.Close() // decompress server's compressed reply. r, err := gzip.NewReader(resp.Body) if err != nil { panic(err) } defer r.Close() body, err := io.ReadAll(r) if err != nil { panic(err) } fmt.Printf("Received from server: %s", string(body)) } type payload struct { Username string `json:"username"` } func postExample() { buf := new(bytes.Buffer) // Compress client's data. w := gzip.NewWriter(buf) b, err := json.Marshal(payload{Username: "Edward"}) if err != nil { panic(err) } w.Write(b) w.Close() endpoint := baseURL + "/" req, err := http.NewRequest(http.MethodPost, endpoint, buf) if err != nil { panic(err) } req.Header.Set("Content-Type", "application/json") // Required to send gzip compressed data to the server. req.Header.Set("Content-Encoding", "gzip") // Required to receive server's compressed data. req.Header.Set("Accept-Encoding", "gzip") resp, err := client.Do(req) if err != nil { panic(err) } defer resp.Body.Close() // Decompress server's compressed reply. r, err := gzip.NewReader(resp.Body) if err != nil { panic(err) } defer r.Close() body, err := io.ReadAll(r) if err != nil { panic(err) } fmt.Printf("Server replied with: %s", string(body)) } ================================================ FILE: _examples/compression/client-using-iris/main.go ================================================ package main import ( "bytes" "encoding/json" "fmt" "io" "net/http" "github.com/kataras/iris/v12/context" ) const baseURL = "http://localhost:8080" // Available options: // - "gzip", // - "deflate", // - "br" (for brotli), // - "snappy" and // - "s2" const encoding = context.BROTLI var client = http.DefaultClient func main() { fmt.Printf("Running client example on: %s\n", baseURL) getExample() postExample() } func getExample() { endpoint := baseURL + "/" req, err := http.NewRequest(http.MethodGet, endpoint, nil) if err != nil { panic(err) } // Required to receive server's compressed data. req.Header.Set("Accept-Encoding", encoding) resp, err := client.Do(req) if err != nil { panic(err) } defer resp.Body.Close() // decompress server's compressed reply. cr, err := context.NewCompressReader(resp.Body, encoding) if err != nil { panic(err) } defer cr.Close() body, err := io.ReadAll(cr) if err != nil { panic(err) } fmt.Printf("Received from server: %s", string(body)) } type payload struct { Username string `json:"username"` } func postExample() { buf := new(bytes.Buffer) // Compress client's data. cw, err := context.NewCompressWriter(buf, encoding, -1) if err != nil { panic(err) } json.NewEncoder(cw).Encode(payload{Username: "Edward"}) // `Close` or `Flush` required before `NewRequest` call. cw.Close() endpoint := baseURL + "/" req, err := http.NewRequest(http.MethodPost, endpoint, buf) if err != nil { panic(err) } req.Header.Set("Content-Type", "application/json") // Required to send gzip compressed data to the server. req.Header.Set("Content-Encoding", encoding) // Required to receive server's compressed data. req.Header.Set("Accept-Encoding", encoding) resp, err := client.Do(req) if err != nil { panic(err) } defer resp.Body.Close() // Decompress server's compressed reply. cr, err := context.NewCompressReader(resp.Body, encoding) if err != nil { panic(err) } defer cr.Close() body, err := io.ReadAll(cr) if err != nil { panic(err) } fmt.Printf("Server replied with: %s", string(body)) } ================================================ FILE: _examples/compression/main.go ================================================ package main import "github.com/kataras/iris/v12" func main() { app := newApp() app.Logger().SetLevel("debug") app.Listen(":8080") } func newApp() *iris.Application { app := iris.New() // HERE and you are ready to GO: app.Use(iris.Compression) app.Get("/", send) app.Post("/", receive) return app } type payload struct { Username string `json:"username"` } func send(ctx iris.Context) { ctx.JSON(payload{ Username: "Makis", }) } func receive(ctx iris.Context) { var p payload if err := ctx.ReadJSON(&p); err != nil { ctx.Application().Logger().Debugf("ReadJSON: %v", err) } ctx.WriteString(p.Username) } /* Manually: func enableCompression(ctx iris.Context) { // Enable writing using compression (deflate, gzip, brotli, snappy, s2): err := ctx.CompressWriter(true) if err != nil { ctx.Application().Logger().Debugf("writer: %v", err) // if you REQUIRE server to SEND compressed data then `return` here. // return } // Enable reading and binding request's compressed data: err = ctx.CompressReader(true) if err != nil && // on GET we don't expect writing with gzip from client ctx.Method() != iris.MethodGet { ctx.Application().Logger().Debugf("reader: %v", err) // if you REQUIRE server to RECEIVE only // compressed data then `return` here. // return } ctx.Next() } */ ================================================ FILE: _examples/compression/main_test.go ================================================ package main import ( "encoding/json" "reflect" "strings" "testing" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/httptest" ) func TestCompression(t *testing.T) { app := newApp() e := httptest.New(t, app) var expectedReply = payload{Username: "Makis"} testBody(t, e.GET("/"), expectedReply) } func TestCompressionAfterRecorder(t *testing.T) { var expectedReply = payload{Username: "Makis"} app := iris.New() app.Use(func(ctx iris.Context) { ctx.Record() ctx.Next() }) app.Use(iris.Compression) app.Get("/", func(ctx iris.Context) { ctx.JSON(expectedReply) }) e := httptest.New(t, app) testBody(t, e.GET("/"), expectedReply) } func TestCompressionBeforeRecorder(t *testing.T) { var expectedReply = payload{Username: "Makis"} app := iris.New() app.Use(iris.Compression) app.Use(func(ctx iris.Context) { ctx.Record() ctx.Next() }) app.Get("/", func(ctx iris.Context) { ctx.JSON(expectedReply) }) e := httptest.New(t, app) testBody(t, e.GET("/"), expectedReply) } func testBody(t *testing.T, req *httptest.Request, expectedReply any) { t.Helper() body := req.WithHeader(context.AcceptEncodingHeaderKey, context.GZIP).Expect(). Status(httptest.StatusOK). ContentEncoding(context.GZIP). ContentType(context.ContentJSONHeaderValue).Body().Raw() // Note that .Expect() consumes the response body // and stores it to unexported "contents" field // therefore, we retrieve it as string and put it to a new buffer. r := strings.NewReader(body) cr, err := context.NewCompressReader(r, context.GZIP) if err != nil { t.Fatal(err) } defer cr.Close() var got payload if err = json.NewDecoder(cr).Decode(&got); err != nil { t.Fatal(err) } if !reflect.DeepEqual(expectedReply, got) { t.Fatalf("expected %#+v but got %#+v", expectedReply, got) } } ================================================ FILE: _examples/configuration/from-configuration-structure/main.go ================================================ package main import ( "github.com/kataras/iris/v12" ) func main() { app := iris.New() app.Get("/", func(ctx iris.Context) { ctx.HTML("Hello!") }) // [...] // Good when you want to modify the whole configuration. app.Listen(":8080", iris.WithConfiguration(iris.Configuration{ // default configuration: DisableStartupLog: false, DisableInterruptHandler: false, DisablePathCorrection: false, EnablePathEscape: false, FireMethodNotAllowed: false, DisableBodyConsumptionOnUnmarshal: false, DisableAutoFireStatusCode: false, TimeFormat: "Mon, 02 Jan 2006 15:04:05 GMT", Charset: "utf-8", })) // or before Run: // app.Configure(iris.WithConfiguration(iris.Configuration{...})) } ================================================ FILE: _examples/configuration/from-toml-file/configs/iris.tml ================================================ DisablePathCorrection = false EnablePathEscape = false FireMethodNotAllowed = true DisableBodyConsumptionOnUnmarshal = false TimeFormat = "Mon, 01 Jan 2006 15:04:05 GMT" Charset = "utf-8" RemoteAddrHeaders = ["X-Real-Ip", "X-Forwarded-For", "CF-Connecting-IP"] [Other] MyServerName = "iris" ================================================ FILE: _examples/configuration/from-toml-file/main.go ================================================ package main import ( "github.com/kataras/iris/v12" ) func main() { app := iris.New() app.Get("/", func(ctx iris.Context) { ctx.HTML("Hello!") }) // [...] // Good when you have two configurations, one for development and a different one for production use. app.Listen(":8080", iris.WithConfiguration(iris.TOML("./configs/iris.tml"))) // or before run: // app.Configure(iris.WithConfiguration(iris.TOML("./configs/iris.tml"))) // app.Listen(":8080") } ================================================ FILE: _examples/configuration/from-yaml-file/configs/iris.yml ================================================ DisablePathCorrection: false EnablePathEscape: false FireMethodNotAllowed: true DisableBodyConsumptionOnUnmarshal: true TimeFormat: Mon, 01 Jan 2006 15:04:05 GMT Charset: UTF-8 SSLProxyHeaders: X-Forwarded-Proto: https HostProxyHeaders: X-Host: true RemoteAddrHeaders: - X-Real-Ip - X-Forwarded-For - CF-Connecting-IP Other: Addr: :8080 ================================================ FILE: _examples/configuration/from-yaml-file/main.go ================================================ package main import ( "github.com/kataras/iris/v12" ) func main() { app := iris.New() app.Get("/", func(ctx iris.Context) { ctx.HTML("Hello!") }) // [...] // Good when you have two configurations, one for development and a different one for production use. // If iris.YAML's input string argument is "~" then it loads the configuration from the home directory // and can be shared between many iris instances. cfg := iris.YAML("./configs/iris.yml") addr := cfg.Other["Addr"].(string) app.Listen(addr, iris.WithConfiguration(cfg)) // or before run: // app.Configure(iris.WithConfiguration(iris.YAML("./configs/iris.yml"))) // app.Listen(":8080") } ================================================ FILE: _examples/configuration/from-yaml-file/shared-configuration/main.go ================================================ package main import ( "github.com/kataras/iris/v12" ) func main() { app := iris.New() app.Get("/", func(ctx iris.Context) { ctx.HTML("Hello!") }) // [...] // Good when you share configuration between multiple iris instances. // This configuration file lives in your $HOME/iris.yml for unix hosts // or %HOMEDRIVE%+%HOMEPATH%/iris.yml for windows hosts, and you can modify it. app.Listen(":8080", iris.WithGlobalConfiguration) // or before run: // app.Configure(iris.WithGlobalConfiguration) // app.Listen(":8080") } ================================================ FILE: _examples/configuration/functional/main.go ================================================ package main import ( "github.com/kataras/iris/v12" ) func main() { app := iris.New() app.Get("/", func(ctx iris.Context) { ctx.HTML("Hello!") }) // [...] // Good when you want to change some of the configuration's field. // Prefix: "With", code editors will help you navigate through all // configuration options without even a glitch to the documentation. app.Listen(":8080", iris.WithoutStartupLog, iris.WithCharset("utf-8")) // or before run: // app.Configure(iris.WithoutStartupLog, iris.WithCharset("utf-8")) // app.Listen(":8080") } ================================================ FILE: _examples/configuration/multi-environments/README.md ================================================ # Environment-based Configuration ## Run production server ```sh $ go run main.go --config=server.yml ``` ## Run development server ```sh $ go run main.go --config=server.dev.yml ``` ================================================ FILE: _examples/configuration/multi-environments/api/configuration.go ================================================ package api import ( "os" "github.com/kataras/iris/v12" "gopkg.in/yaml.v3" ) type Configuration struct { Host string `yaml:"Host"` Port int `yaml:"Port"` EnableCompression bool `yaml:"EnableCompression"` AllowOrigin string `yaml:"AllowOrigin"` // Iris specific configuration. Iris iris.Configuration `yaml:"Iris"` } // BindFile binds the yaml file's contents to this Configuration. func (c *Configuration) BindFile(filename string) error { contents, err := os.ReadFile(filename) if err != nil { return err } return yaml.Unmarshal(contents, c) } ================================================ FILE: _examples/configuration/multi-environments/api/server.go ================================================ package api import ( "context" "fmt" "time" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/middleware/cors" "github.com/kataras/iris/v12/x/errors" ) type Server struct { *iris.Application config Configuration closers []func() // See "AddCloser" method. } // NewServer returns a new server instance. // See its "Start" method. func NewServer(congig Configuration) *Server { app := iris.New() app.Configure(iris.WithConfiguration(congig.Iris)) s := &Server{ config: congig, Application: app, } return s } func (s *Server) Build() error { if err := s.Application.Build(); err != nil { return err } // Register your 3rd-party drivers. // if err := s.registerDatabase(); err != nil { // return err // }s return s.configureRouter() } func (s *Server) configureRouter() error { s.SetContextErrorHandler(errors.DefaultContextErrorHandler) s.Macros().SetErrorHandler(errors.DefaultPathParameterTypeErrorHandler) s.registerMiddlewares() s.registerRoutes() return nil } func (s *Server) registerMiddlewares() { s.UseRouter(cors.New().AllowOrigin(s.config.AllowOrigin).Handler()) s.UseRouter(func(ctx iris.Context) { ctx.Header("Server", "Iris") ctx.Header("X-Developer", "kataras") ctx.Next() }) if s.config.EnableCompression { s.Use(iris.Compression) // .Use instead of .UseRouter to let it run only on registered routes and not on 404 errors and e.t.c. } } func (s *Server) registerRoutes() { // register your routes... s.Get("/health", func(ctx iris.Context) { ctx.WriteString("OK") }) } // AddCloser adds one or more function that should be called on // manual server shutdown or OS interrupt signals. func (s *Server) AddCloser(closers ...func()) { for _, closer := range closers { if closer == nil { continue } // Terminate any opened connections on OS interrupt signals. iris.RegisterOnInterrupt(closer) } s.closers = append(s.closers, closers...) } // Shutdown gracefully terminates the HTTP server and calls the closers afterwards. func (s *Server) Shutdown() error { ctx, cancelCtx := context.WithTimeout(context.Background(), 5*time.Second) err := s.Application.Shutdown(ctx) cancelCtx() for _, closer := range s.closers { if closer == nil { continue } closer() } return err } // Start starts the http server based on the config's host and port. func (s *Server) Listen() error { if err := s.Build(); err != nil { return err } addr := fmt.Sprintf("%s:%d", s.config.Host, s.config.Port) return s.Application.Listen(addr) } ================================================ FILE: _examples/configuration/multi-environments/cmd/root_command.go ================================================ package cmd import ( "github.com/kataras/my-iris-app/api" "github.com/spf13/cobra" ) const defaultConfigFilename = "server.dev.yml" var ( serverConfig api.Configuration ) // New returns a new root command. // Usage: // $ my-iris-app --config=server.yml func New() *cobra.Command { configFile := defaultConfigFilename rootCmd := &cobra.Command{ Use: "my-iris-app", Short: "My Command Line App", Long: "The root command will start the HTTP server.", Version: "v0.0.1", SilenceErrors: true, SilenceUsage: true, TraverseChildren: true, SuggestionsMinimumDistance: 1, PersistentPreRunE: func(cmd *cobra.Command, args []string) error { return serverConfig.BindFile(configFile) }, RunE: func(cmd *cobra.Command, args []string) error { return startServer() }, } // Shared flags. flags := rootCmd.PersistentFlags() flags.StringVar(&configFile, "config", configFile, "--config=server.yml a filepath which contains the YAML config format") // Subcommands here. return rootCmd } func startServer() error { srv := api.NewServer(serverConfig) return srv.Listen() } ================================================ FILE: _examples/configuration/multi-environments/go.mod ================================================ module github.com/kataras/my-iris-app go 1.25 require ( github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd github.com/spf13/cobra v1.10.2 gopkg.in/yaml.v3 v3.0.1 ) require ( github.com/BurntSushi/toml v1.6.0 // indirect github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect github.com/CloudyKit/jet/v6 v6.3.1 // indirect github.com/Joker/jade v1.1.3 // indirect github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 // indirect github.com/andybalholm/brotli v1.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/fatih/structs v1.1.0 // indirect github.com/flosch/pongo2/v4 v4.0.2 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/iris-contrib/schema v0.0.6 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/kataras/blocks v0.0.8 // indirect github.com/kataras/golog v0.1.12 // indirect github.com/kataras/pio v0.0.13 // indirect github.com/kataras/sitemap v0.0.6 // indirect github.com/kataras/tunnel v0.0.4 // indirect github.com/klauspost/compress v1.18.2 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mailgun/raymond/v2 v2.0.48 // indirect github.com/mailru/easyjson v0.9.1 // indirect github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/schollz/closestmatch v2.1.0+incompatible // indirect github.com/sirupsen/logrus v1.8.1 // indirect github.com/spf13/pflag v1.0.9 // indirect github.com/stretchr/testify v1.11.1 // indirect github.com/tdewolff/minify/v2 v2.24.8 // indirect github.com/tdewolff/parse/v2 v2.8.5 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/yosssi/ace v0.0.5 // indirect golang.org/x/crypto v0.47.0 // indirect golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect golang.org/x/net v0.49.0 // indirect golang.org/x/sys v0.40.0 // indirect golang.org/x/text v0.33.0 // indirect golang.org/x/time v0.14.0 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/ini.v1 v1.67.0 // indirect ) ================================================ FILE: _examples/configuration/multi-environments/go.sum ================================================ github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= github.com/CloudyKit/jet/v6 v6.3.1 h1:6IAo5Cx21xrHVaR8zzXN5gJatKV/wO7Nf6bfCnCSbUw= github.com/CloudyKit/jet/v6 v6.3.1/go.mod h1:lf8ksdNsxZt7/yH/3n4vJQWA9RUq4wpaHtArHhGVMOw= github.com/Joker/hpp v1.0.0 h1:65+iuJYdRXv/XyN62C1uEmmOx3432rNG/rKlX6V7Kkc= github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= github.com/Joker/jade v1.1.3 h1:Qbeh12Vq6BxURXT1qZBRHsDxeURB8ztcL6f3EXSGeHk= github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKduM= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 h1:dG7/gLroJGht/jSQtHiLvT48Hxn+crbmvyItZC8cWOs= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05/go.mod h1:NYezi6wtnJtBm5btoprXc5SvAdqH0XTXWnUup0MptAI= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/flosch/pongo2/v4 v4.0.2 h1:gv+5Pe3vaSVmiJvh/BZa82b7/00YUGm0PIyVVLop0Hw= github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+BWPhwZD8= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a h1:l7A0loSszR5zHd/qK53ZIHMO8b3bBSmENnQ6eKnUT0A= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/iris-contrib/httpexpect/v2 v2.15.2 h1:T9THsdP1woyAqKHwjkEsbCnMefsAFvk8iJJKokcJ3Go= github.com/iris-contrib/httpexpect/v2 v2.15.2/go.mod h1:JLDgIqnFy5loDSUv1OA2j0mb6p/rDhiCqigP22Uq9xE= github.com/iris-contrib/schema v0.0.6 h1:CPSBLyx2e91H2yJzPuhGuifVRnZBBJ3pCOMbOvPZaTw= github.com/iris-contrib/schema v0.0.6/go.mod h1:iYszG0IOsuIsfzjymw1kMzTL8YQcCWlm65f3wX8J5iA= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kataras/blocks v0.0.8 h1:MrpVhoFTCR2v1iOOfGng5VJSILKeZZI+7NGfxEh3SUM= github.com/kataras/blocks v0.0.8/go.mod h1:9Jm5zx6BB+06NwA+OhTbHW1xkMOYxahnqTN5DveZ2Yg= github.com/kataras/golog v0.1.12 h1:Bu7I/G4ilJlbfzjmU39O9N+2uO1pBcMK045fzZ4ytNg= github.com/kataras/golog v0.1.12/go.mod h1:wrGSbOiBqbQSQznleVNX4epWM8rl9SJ/rmEacl0yqy4= github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd h1:oMsQgqKACbUdlaIWnN+EZzzAnHkw90hE/y/R0BSij2U= github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd/go.mod h1:yucjtDl9Vn3OctWM1fi0MuM9OckemC7kEreFa9QtPF8= github.com/kataras/pio v0.0.13 h1:x0rXVX0fviDTXOOLOmr4MUxOabu1InVSTu5itF8CXCM= github.com/kataras/pio v0.0.13/go.mod h1:k3HNuSw+eJ8Pm2lA4lRhg3DiCjVgHlP8hmXApSej3oM= github.com/kataras/sitemap v0.0.6 h1:w71CRMMKYMJh6LR2wTgnk5hSgjVNB9KL60n5e2KHvLY= github.com/kataras/sitemap v0.0.6/go.mod h1:dW4dOCNs896OR1HmG+dMLdT7JjDk7mYBzoIRwuj5jA4= github.com/kataras/tunnel v0.0.4 h1:sCAqWuJV7nPzGrlb0os3j49lk2JhILT0rID38NHNLpA= github.com/kataras/tunnel v0.0.4/go.mod h1:9FkU4LaeifdMWqZu7o20ojmW4B7hdhv2CMLwfnHGpYw= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw= github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18= github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo= github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk= github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tdewolff/minify/v2 v2.24.8 h1:58/VjsbevI4d5FGV0ZSuBrHMSSkH4MCH0sIz/eKIauE= github.com/tdewolff/minify/v2 v2.24.8/go.mod h1:0Ukj0CRpo/sW/nd8uZ4ccXaV1rEVIWA3dj8U7+Shhfw= github.com/tdewolff/parse/v2 v2.8.5 h1:ZmBiA/8Do5Rpk7bDye0jbbDUpXXbCdc3iah4VeUvwYU= github.com/tdewolff/parse/v2 v2.8.5/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo= github.com/tdewolff/test v1.0.11 h1:FdLbwQVHxqG16SlkGveC0JVyrJN62COWTRyUFzfbtBE= github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA= github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0= github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs= moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHcE= ================================================ FILE: _examples/configuration/multi-environments/main.go ================================================ package main import ( "fmt" "os" "github.com/kataras/my-iris-app/cmd" ) func main() { app := cmd.New() if err := app.Execute(); err != nil { fmt.Println(err) os.Exit(1) } } ================================================ FILE: _examples/configuration/multi-environments/server.dev.yml ================================================ ServerName: "my-iris-app" Host: 0.0.0.0 Port: 8080 EnableCompression: false AllowOrigin: "*" # Your development environment's # database connection configuration here... #### # Iris configuration. Iris: LogLevel: info EnableOptimizations: true RemoteAddrHeaders: - "X-Real-Ip" - "X-Forwarded-For" - "CF-Connecting-IP" - "True-Client-Ip" - "X-Appengine-Remote-Addr" ================================================ FILE: _examples/configuration/multi-environments/server.yml ================================================ Host: 0.0.0.0 Port: 80 EnableCompression: true AllowOrigin: "*" # Your production's database connection configuration here... #### # Iris configuration. Iris: LogLevel: info EnableOptimizations: true RemoteAddrHeaders: - "X-Real-Ip" - "X-Forwarded-For" - "CF-Connecting-IP" - "True-Client-Ip" - "X-Appengine-Remote-Addr" ================================================ FILE: _examples/configuration/viper/config/config.go ================================================ package config import ( "fmt" "github.com/kataras/iris/v12" // Viper is a third-party package: // go get github.com/spf13/viper "github.com/spf13/viper" ) func init() { loadConfiguration() } // C is the configuration of the app. var C = struct { Iris iris.Configuration Addr struct { Internal struct { IP string Plain int Secure int } } Locale struct { Pattern string Default string Supported []string } }{ Iris: iris.DefaultConfiguration(), // other default values... } func loadConfiguration() { viper.SetConfigName("config") // name of config file (without extension) viper.SetConfigType("yaml") // REQUIRED if the config file does not have the extension in the name viper.AddConfigPath("/etc/app/") // path to look for the config file in viper.AddConfigPath("$HOME/.app") // call multiple times to add many search paths viper.AddConfigPath(".") // optionally look for config in the working directory err := viper.ReadInConfig() // Find and read the config file if err := viper.ReadInConfig(); err != nil { if _, ok := err.(viper.ConfigFileNotFoundError); ok { } else { panic(fmt.Errorf("load configuration: %w", err)) } } err = viper.Unmarshal(&C) if err != nil { panic(fmt.Errorf("load configuration: unmarshal: %w", err)) } } ================================================ FILE: _examples/configuration/viper/config.yml ================================================ Addr: Internal: IP: 127.0.0.1 Plain: 8080 Secure: 443 Locale: Pattern: "./locales/*/*.ini" Default: "en-US" Supported: - "en-US" - "el-GR" Iris: LogLevel: debug SocketSharding: true EnableOptimizations: true DisableStartupLog: false FireMethodNotAllowed: true ForceLowercaseRouting: true EnablePathIntelligence: true Charset: "utf-8" TimeFormat: "2006-01-02 15:04:05" DisableBodyConsumptionOnUnmarshal: true FireEmptyFormError: true PostMaxMemory: 67108864 RemoteAddrHeaders: - "X-Real-Ip" - "X-Forwarded-For" - "CF-Connecting-IP" - "True-Client-Ip" IgnoreServerErrors: - "http: Server closed" # Tunneling: # WebInterface: "http://127.0.0.1:4040" # AuthToken: "" # Tunnels: # - Name: "My awesome App" # Addr: "localhost:8080" # - Name: "My Second awesome App" # Addr: "localhost:9090" RemoteAddrPrivateSubnets: - Start: "192.168.0.0" End: "192.168.255.255" - Start: "198.18.0.0" End: "198.19.255.255" SSLProxyHeaders: X-Forwarded-Proto: "https" HostProxyHeaders: X-Host: true Other: ServerName: "My awesome Iris web server" ================================================ FILE: _examples/configuration/viper/go.mod ================================================ module app go 1.25 require ( github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd github.com/spf13/viper v1.21.0 ) require ( github.com/BurntSushi/toml v1.6.0 // indirect github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect github.com/CloudyKit/jet/v6 v6.3.1 // indirect github.com/Joker/jade v1.1.3 // indirect github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 // indirect github.com/andybalholm/brotli v1.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/fatih/structs v1.1.0 // indirect github.com/flosch/pongo2/v4 v4.0.2 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/iris-contrib/schema v0.0.6 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/kataras/blocks v0.0.8 // indirect github.com/kataras/golog v0.1.12 // indirect github.com/kataras/pio v0.0.13 // indirect github.com/kataras/sitemap v0.0.6 // indirect github.com/kataras/tunnel v0.0.4 // indirect github.com/klauspost/compress v1.18.2 // indirect github.com/mailgun/raymond/v2 v2.0.48 // indirect github.com/mailru/easyjson v0.9.1 // indirect github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect github.com/schollz/closestmatch v2.1.0+incompatible // indirect github.com/sirupsen/logrus v1.8.1 // indirect github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/tdewolff/minify/v2 v2.24.8 // indirect github.com/tdewolff/parse/v2 v2.8.5 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/yosssi/ace v0.0.5 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.47.0 // indirect golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect golang.org/x/net v0.49.0 // indirect golang.org/x/sys v0.40.0 // indirect golang.org/x/text v0.33.0 // indirect golang.org/x/time v0.14.0 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: _examples/configuration/viper/go.sum ================================================ github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= github.com/CloudyKit/jet/v6 v6.3.1 h1:6IAo5Cx21xrHVaR8zzXN5gJatKV/wO7Nf6bfCnCSbUw= github.com/CloudyKit/jet/v6 v6.3.1/go.mod h1:lf8ksdNsxZt7/yH/3n4vJQWA9RUq4wpaHtArHhGVMOw= github.com/Joker/hpp v1.0.0 h1:65+iuJYdRXv/XyN62C1uEmmOx3432rNG/rKlX6V7Kkc= github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= github.com/Joker/jade v1.1.3 h1:Qbeh12Vq6BxURXT1qZBRHsDxeURB8ztcL6f3EXSGeHk= github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKduM= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 h1:dG7/gLroJGht/jSQtHiLvT48Hxn+crbmvyItZC8cWOs= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05/go.mod h1:NYezi6wtnJtBm5btoprXc5SvAdqH0XTXWnUup0MptAI= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/flosch/pongo2/v4 v4.0.2 h1:gv+5Pe3vaSVmiJvh/BZa82b7/00YUGm0PIyVVLop0Hw= github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+BWPhwZD8= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a h1:l7A0loSszR5zHd/qK53ZIHMO8b3bBSmENnQ6eKnUT0A= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/iris-contrib/httpexpect/v2 v2.15.2 h1:T9THsdP1woyAqKHwjkEsbCnMefsAFvk8iJJKokcJ3Go= github.com/iris-contrib/httpexpect/v2 v2.15.2/go.mod h1:JLDgIqnFy5loDSUv1OA2j0mb6p/rDhiCqigP22Uq9xE= github.com/iris-contrib/schema v0.0.6 h1:CPSBLyx2e91H2yJzPuhGuifVRnZBBJ3pCOMbOvPZaTw= github.com/iris-contrib/schema v0.0.6/go.mod h1:iYszG0IOsuIsfzjymw1kMzTL8YQcCWlm65f3wX8J5iA= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kataras/blocks v0.0.8 h1:MrpVhoFTCR2v1iOOfGng5VJSILKeZZI+7NGfxEh3SUM= github.com/kataras/blocks v0.0.8/go.mod h1:9Jm5zx6BB+06NwA+OhTbHW1xkMOYxahnqTN5DveZ2Yg= github.com/kataras/golog v0.1.12 h1:Bu7I/G4ilJlbfzjmU39O9N+2uO1pBcMK045fzZ4ytNg= github.com/kataras/golog v0.1.12/go.mod h1:wrGSbOiBqbQSQznleVNX4epWM8rl9SJ/rmEacl0yqy4= github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd h1:oMsQgqKACbUdlaIWnN+EZzzAnHkw90hE/y/R0BSij2U= github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd/go.mod h1:yucjtDl9Vn3OctWM1fi0MuM9OckemC7kEreFa9QtPF8= github.com/kataras/pio v0.0.13 h1:x0rXVX0fviDTXOOLOmr4MUxOabu1InVSTu5itF8CXCM= github.com/kataras/pio v0.0.13/go.mod h1:k3HNuSw+eJ8Pm2lA4lRhg3DiCjVgHlP8hmXApSej3oM= github.com/kataras/sitemap v0.0.6 h1:w71CRMMKYMJh6LR2wTgnk5hSgjVNB9KL60n5e2KHvLY= github.com/kataras/sitemap v0.0.6/go.mod h1:dW4dOCNs896OR1HmG+dMLdT7JjDk7mYBzoIRwuj5jA4= github.com/kataras/tunnel v0.0.4 h1:sCAqWuJV7nPzGrlb0os3j49lk2JhILT0rID38NHNLpA= github.com/kataras/tunnel v0.0.4/go.mod h1:9FkU4LaeifdMWqZu7o20ojmW4B7hdhv2CMLwfnHGpYw= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw= github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18= github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo= github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk= github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/tdewolff/minify/v2 v2.24.8 h1:58/VjsbevI4d5FGV0ZSuBrHMSSkH4MCH0sIz/eKIauE= github.com/tdewolff/minify/v2 v2.24.8/go.mod h1:0Ukj0CRpo/sW/nd8uZ4ccXaV1rEVIWA3dj8U7+Shhfw= github.com/tdewolff/parse/v2 v2.8.5 h1:ZmBiA/8Do5Rpk7bDye0jbbDUpXXbCdc3iah4VeUvwYU= github.com/tdewolff/parse/v2 v2.8.5/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo= github.com/tdewolff/test v1.0.11 h1:FdLbwQVHxqG16SlkGveC0JVyrJN62COWTRyUFzfbtBE= github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA= github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0= github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs= moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHcE= ================================================ FILE: _examples/configuration/viper/main.go ================================================ package main import ( "fmt" "app/config" "github.com/kataras/iris/v12" ) func main() { app := iris.New() app.Get("/", func(ctx iris.Context) { ctx.TextYAML(config.C) }) addr := fmt.Sprintf("%s:%d", config.C.Addr.Internal.IP, config.C.Addr.Internal.Plain) app.Listen(addr, iris.WithConfiguration(config.C.Iris)) } ================================================ FILE: _examples/convert-handlers/negroni-like/main.go ================================================ package main import ( "net/http" "github.com/kataras/iris/v12" ) func main() { app := iris.New() irisMiddleware := iris.FromStd(negronilikeTestMiddleware) app.Use(irisMiddleware) // Method GET: http://localhost:8080/ app.Get("/", func(ctx iris.Context) { ctx.HTML("

Home

") // this will print an error, // this route's handler will never be executed because the middleware's criteria not passed. }) // Method GET: http://localhost:8080/ok app.Get("/ok", func(ctx iris.Context) { ctx.Writef("Hello world!") // this will print "OK. Hello world!". }) // http://localhost:8080 // http://localhost:8080/ok app.Listen(":8080") } func negronilikeTestMiddleware(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { if r.URL.Path == "/ok" && r.Method == "GET" { w.Write([]byte("OK. ")) next(w, r) // go to the next route's handler return } // else print an error and do not forward to the route's handler. w.WriteHeader(iris.StatusBadRequest) w.Write([]byte("Bad request")) } ================================================ FILE: _examples/convert-handlers/nethttp/main.go ================================================ package main import ( "net/http" "github.com/kataras/iris/v12" ) func main() { app := iris.New() irisMiddleware := iris.FromStd(nativeTestMiddleware) app.Use(irisMiddleware) // Method GET: http://localhost:8080/ app.Get("/", func(ctx iris.Context) { ctx.HTML("Home") }) // Method GET: http://localhost:8080/ok app.Get("/ok", func(ctx iris.Context) { ctx.HTML("Hello world!") }) // http://localhost:8080 // http://localhost:8080/ok app.Listen(":8080") } func nativeTestMiddleware(w http.ResponseWriter, r *http.Request) { println("Request path: " + r.URL.Path) } ================================================ FILE: _examples/convert-handlers/nethttp/wrapper/main.go ================================================ package main import ( "context" "net/http" "github.com/kataras/iris/v12" ) func main() { app := iris.New() httpThirdPartyWrapper := StandardWrapper(Options{ Message: "test_value", }) // This case app.WrapRouter(func(w http.ResponseWriter, r *http.Request, router http.HandlerFunc) { httpThirdPartyWrapper(router).ServeHTTP(w, r) // If was func(http.HandlerFunc) http.HandlerFunc: // httpThirdPartyWrapper(router.ServeHTTP).ServeHTTP(w, r) }) app.Get("/", index) app.Listen(":8080") } func index(ctx iris.Context) { ctx.Writef("Message: %s\n", ctx.Value(msgContextKey)) } type Options struct { Message string } type contextKey uint8 var ( msgContextKey contextKey = 1 ) func StandardWrapper(opts Options) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // ... req := r.WithContext(context.WithValue(r.Context(), msgContextKey, opts.Message)) next.ServeHTTP(w, req) }) } } ================================================ FILE: _examples/convert-handlers/real-usecase-raven/wrapping-the-router/main.go ================================================ package main import ( "errors" "fmt" "net/http" "runtime/debug" "github.com/kataras/iris/v12" "github.com/getsentry/raven-go" ) // https://docs.sentry.io/clients/go/integrations/http/ func init() { raven.SetDSN("https://:@sentry.io/") } func main() { app := iris.New() app.Get("/", func(ctx iris.Context) { ctx.Writef("Hi") }) // Example for WrapRouter is already here: // https://github.com/kataras/iris/blob/main/_examples/routing/custom-wrapper/main.go#L53 app.WrapRouter(func(w http.ResponseWriter, r *http.Request, irisRouter http.HandlerFunc) { // Exactly the same source code: // https://github.com/getsentry/raven-go/blob/379f8d0a68ca237cf8893a1cdfd4f574125e2c51/http.go#L70 defer func() { if rval := recover(); rval != nil { debug.PrintStack() rvalStr := fmt.Sprint(rval) packet := raven.NewPacket(rvalStr, raven.NewException(errors.New(rvalStr), raven.NewStacktrace(2, 3, nil)), raven.NewHttp(r)) raven.Capture(packet, nil) w.WriteHeader(http.StatusInternalServerError) } }() irisRouter(w, r) }) app.Listen(":8080") } ================================================ FILE: _examples/convert-handlers/real-usecase-raven/writing-middleware/main.go ================================================ package main import ( "errors" "fmt" "runtime/debug" "github.com/kataras/iris/v12" "github.com/getsentry/raven-go" ) // At this example you will see how to convert any net/http middleware // that has the form of `(HandlerFunc) HandlerFunc`. // If the `raven.RecoveryHandler` had the form of // `(http.HandlerFunc)` or `(http.HandlerFunc, next http.HandlerFunc)` // you could just use the `irisMiddleware := iris.FromStd(nativeHandler)` // but it doesn't, however as you already know Iris can work with net/http directly // because of the `ctx.ResponseWriter()` and `ctx.Request()` are the original // http.ResponseWriter and *http.Request. // (this one is a big advantage, as a result you can use Iris for ever :)). // // The source code of the native middleware does not change at all. // https://github.com/getsentry/raven-go/blob/379f8d0a68ca237cf8893a1cdfd4f574125e2c51/http.go#L70 // The only addition is the Line 18 and Line 39 (instead of handler(w,r)) // and you have a new iris middleware ready to use! func irisRavenMiddleware(ctx iris.Context) { w, r := ctx.ResponseWriter(), ctx.Request() defer func() { if rval := recover(); rval != nil { debug.PrintStack() rvalStr := fmt.Sprint(rval) packet := raven.NewPacket(rvalStr, raven.NewException(errors.New(rvalStr), raven.NewStacktrace(2, 3, nil)), raven.NewHttp(r)) raven.Capture(packet, nil) w.WriteHeader(iris.StatusInternalServerError) } }() ctx.Next() } // https://docs.sentry.io/clients/go/integrations/http/ func init() { raven.SetDSN("https://:@sentry.io/") } func main() { app := iris.New() app.Use(irisRavenMiddleware) app.Get("/", func(ctx iris.Context) { ctx.Writef("Hi") }) app.Listen(":8080") } ================================================ FILE: _examples/cookies/basic/main.go ================================================ package main import "github.com/kataras/iris/v12" func newApp() *iris.Application { app := iris.New() // Set A Cookie. app.Get("/cookies/{name}/{value}", func(ctx iris.Context) { name := ctx.Params().Get("name") value := ctx.Params().Get("value") ctx.SetCookieKV(name, value) // <-- // Alternatively: ctx.SetCookie(&http.Cookie{...}) // // If you want to set custom the path: // ctx.SetCookieKV(name, value, iris.CookiePath("/custom/path/cookie/will/be/stored")) // // If you want to be visible only to current request path: // (note that client should be responsible for that if server sent an empty cookie's path, all browsers are compatible) // ctx.SetCookieKV(name, value, iris.CookieCleanPath /* or iris.CookiePath("") */) // More: // iris.CookieExpires(time.Duration) // iris.CookieHTTPOnly(false) ctx.Writef("cookie added: %s = %s", name, value) }) // Retrieve A Cookie. app.Get("/cookies/{name}", func(ctx iris.Context) { name := ctx.Params().Get("name") value := ctx.GetCookie(name) // <-- // If you want more than the value then: // cookie, err := ctx.Request().Cookie(name) // if err != nil { // handle error. // } ctx.WriteString(value) }) // Delete A Cookie. app.Delete("/cookies/{name}", func(ctx iris.Context) { name := ctx.Params().Get("name") ctx.RemoveCookie(name) // <-- // If you want to set custom the path: // ctx.SetCookieKV(name, value, iris.CookiePath("/custom/path/cookie/will/be/stored")) ctx.Writef("cookie %s removed", name) }) return app } func main() { app := newApp() // GET: http://localhost:8080/cookies/my_name/my_value // GET: http://localhost:8080/cookies/my_name // DELETE: http://localhost:8080/cookies/my_name app.Listen(":8080") } ================================================ FILE: _examples/cookies/basic/main_test.go ================================================ package main import ( "fmt" "testing" "github.com/kataras/iris/v12/httptest" ) func TestCookiesBasic(t *testing.T) { app := newApp() e := httptest.New(t, app, httptest.URL("http://example.com")) cookieName, cookieValue := "my_cookie_name", "my_cookie_value" // Test set a Cookie. t1 := e.GET(fmt.Sprintf("/cookies/%s/%s", cookieName, cookieValue)).Expect().Status(httptest.StatusOK) t1.Cookie(cookieName).Value().Equal(cookieValue) // validate cookie's existence, it should be there now. t1.Body().Contains(cookieValue) // Test retrieve a Cookie. t2 := e.GET(fmt.Sprintf("/cookies/%s", cookieName)).Expect().Status(httptest.StatusOK) t2.Body().IsEqual(cookieValue) // Test remove a Cookie. t3 := e.DELETE(fmt.Sprintf("/cookies/%s", cookieName)).Expect().Status(httptest.StatusOK) t3.Body().Contains(cookieName) t4 := e.GET(fmt.Sprintf("/cookies/%s", cookieName)).Expect().Status(httptest.StatusOK) t4.Cookies().Empty() t4.Body().IsEmpty() } ================================================ FILE: _examples/cookies/options/main.go ================================================ package main import ( "github.com/kataras/iris/v12" ) func main() { app := newApp() // http://localhost:8080/set/name1/value1 // http://localhost:8080/get/name1 // http://localhost:8080/remove/name1 app.Listen(":8080", iris.WithLogLevel("debug")) } func newApp() *iris.Application { app := iris.New() app.Use(withCookieOptions) app.Get("/set/{name}/{value}", setCookie) app.Get("/get/{name}", getCookie) app.Get("/remove/{name}", removeCookie) return app } func withCookieOptions(ctx iris.Context) { // Register cookie options for request-lifecycle. // To register per cookie, just add the CookieOption // on the last variadic input argument of // SetCookie, SetCookieKV, UpsertCookie, RemoveCookie // and GetCookie Context methods. // // * CookieAllowReclaim // * CookieAllowSubdomains // * CookieSecure // * CookieHTTPOnly // * CookieSameSite // * CookiePath // * CookieCleanPath // * CookieExpires // * CookieEncoding ctx.AddCookieOptions(iris.CookieAllowReclaim()) // ctx.AddCookieOptions(iris.CookieSecure) // OR for a specific cookie: // ctx.SetCookieKV("cookie_name", "cookie_value", iris.CookieScure) // OR by passing a a &http.Cookie: // ctx.SetCookie(&http.Cookie{ // Name: "cookie_name", // Value: "cookie_value", // Secure: true, // }) ctx.Next() } func setCookie(ctx iris.Context) { name := ctx.Params().Get("name") value := ctx.Params().Get("value") ctx.SetCookieKV(name, value) // By-default net/http does not remove or set the Cookie on the Request object. // // With the `CookieAllowReclaim` option, whenever you set or remove a cookie // it will be also reflected in the Request object immediately (of the same request lifecycle) // therefore, any of the next handlers in the chain are not holding the old value. valueIsAvailableInRequestObject := ctx.GetCookie(name) ctx.Writef("cookie %s=%s", name, valueIsAvailableInRequestObject) } func getCookie(ctx iris.Context) { name := ctx.Params().Get("name") value := ctx.GetCookie(name) ctx.WriteString(value) } func removeCookie(ctx iris.Context) { name := ctx.Params().Get("name") ctx.RemoveCookie(name) removedFromRequestObject := ctx.GetCookie(name) // CookieAllowReclaim feature. ctx.Writef("cookie %s removed, value should be empty=%s", name, removedFromRequestObject) } ================================================ FILE: _examples/cookies/options/main_test.go ================================================ package main import ( "fmt" "testing" "github.com/kataras/iris/v12/httptest" ) func TestCookieOptions(t *testing.T) { app := newApp() e := httptest.New(t, app, httptest.URL("http://example.com")) cookieName, cookieValue := "my_cookie_name", "my_cookie_value" // Test set a Cookie. t1 := e.GET(fmt.Sprintf("/set/%s/%s", cookieName, cookieValue)).Expect().Status(httptest.StatusOK) t1.Cookie(cookieName).Value().Equal(cookieValue) t1.Body().Contains(fmt.Sprintf("%s=%s", cookieName, cookieValue)) // Test retrieve a Cookie. t2 := e.GET(fmt.Sprintf("/get/%s", cookieName)).Expect().Status(httptest.StatusOK) t2.Body().IsEqual(cookieValue) // Test remove a Cookie. t3 := e.GET(fmt.Sprintf("/remove/%s", cookieName)).Expect().Status(httptest.StatusOK) t3.Body().Contains(fmt.Sprintf("cookie %s removed, value should be empty=%s", cookieName, "")) t4 := e.GET(fmt.Sprintf("/get/%s", cookieName)).Expect().Status(httptest.StatusOK) t4.Cookies().Empty() t4.Body().IsEmpty() } ================================================ FILE: _examples/cookies/securecookie/main.go ================================================ package main // developers can use any library to add a custom cookie encoder/decoder. // At this example we use the gorilla's securecookie package: // $ go get github.com/gorilla/securecookie // $ go run main.go import ( "github.com/kataras/iris/v12" "github.com/gorilla/securecookie" ) func main() { app := newApp() // http://localhost:8080/cookies/name/value // http://localhost:8080/cookies/name // http://localhost:8080/cookies/remove/name app.Listen(":8080") } func newApp() *iris.Application { app := iris.New() r := app.Party("/cookies") { r.Use(useSecureCookies()) // Set A Cookie. r.Get("/{name}/{value}", func(ctx iris.Context) { name := ctx.Params().Get("name") value := ctx.Params().Get("value") ctx.SetCookieKV(name, value) ctx.Writef("cookie added: %s = %s", name, value) }) // Retrieve A Cookie. r.Get("/{name}", func(ctx iris.Context) { name := ctx.Params().Get("name") value := ctx.GetCookie(name) ctx.WriteString(value) }) r.Get("/remove/{name}", func(ctx iris.Context) { name := ctx.Params().Get("name") ctx.RemoveCookie(name) ctx.Writef("cookie %s removed", name) }) } return app } func useSecureCookies() iris.Handler { var ( hashKey = securecookie.GenerateRandomKey(64) blockKey = securecookie.GenerateRandomKey(32) s = securecookie.New(hashKey, blockKey) ) return func(ctx iris.Context) { ctx.AddCookieOptions(iris.CookieEncoding(s)) ctx.Next() } } ================================================ FILE: _examples/cookies/securecookie/main_test.go ================================================ package main import ( "fmt" "testing" "github.com/kataras/iris/v12/httptest" ) func TestSecureCookie(t *testing.T) { app := newApp() e := httptest.New(t, app, httptest.URL("http://example.com")) cookieName, cookieValue := "my_cookie_name", "my_cookie_value" // Test set a Cookie. t1 := e.GET(fmt.Sprintf("/cookies/%s/%s", cookieName, cookieValue)).Expect().Status(httptest.StatusOK) // note that this will not work because it doesn't always returns the same value: // cookieValueEncoded, _ := sc.Encode(cookieName, cookieValue) t1.Cookie(cookieName).Value().NotEqual(cookieValue) // validate cookie's existence and value is not on its raw form. t1.Body().Contains(cookieValue) // Test retrieve a Cookie. t2 := e.GET(fmt.Sprintf("/cookies/%s", cookieName)).Expect().Status(httptest.StatusOK) t2.Body().IsEqual(cookieValue) // Test remove a Cookie. t3 := e.GET(fmt.Sprintf("/cookies/remove/%s", cookieName)).Expect().Status(httptest.StatusOK) t3.Body().Contains(cookieName) t4 := e.GET(fmt.Sprintf("/cookies/%s", cookieName)).Expect().Status(httptest.StatusOK) t4.Cookies().Empty() t4.Body().IsEmpty() } ================================================ FILE: _examples/database/mongodb/Dockerfile ================================================ # docker build -t myapp . # docker run --rm -it -p 8080:8080 myapp:latest FROM golang:latest AS builder RUN apt-get update ENV GO111MODULE=on \ CGO_ENABLED=0 \ GOOS=linux \ GOARCH=amd64 WORKDIR /go/src/app COPY go.mod . RUN go mod download COPY . . RUN go install FROM scratch COPY --from=builder /go/bin/myapp . ENTRYPOINT ["./myapp"] ================================================ FILE: _examples/database/mongodb/README.md ================================================ # Build RESTful API with the official MongoDB Go Driver and Iris Article is coming soon, follow and stay tuned - - Read [the fully functional example](main.go). ## Run ### Docker Install [Docker](https://www.docker.com/) and execute the command below ```sh $ docker-compose up ``` ### Manually ```sh # .env file contents PORT=8080 DSN=mongodb://localhost:27017 ``` ```sh $ go run main.go > 2019/01/28 05:17:59 Loading environment variables from file: .env > 2019/01/28 05:17:59 ◽ Port=8080 > 2019/01/28 05:17:59 ◽ DSN=mongodb://localhost:27017 > Now listening on: http://localhost:8080 ``` ```sh GET : http://localhost:8080/api/store/movies POST : http://localhost:8080/api/store/movies GET : http://localhost:8080/api/store/movies/{id} PUT : http://localhost:8080/api/store/movies/{id} DELETE : http://localhost:8080/api/store/movies/{id} ``` ## Screens ### Add a Movie ![](0_create_movie.png) ### Update a Movie ![](1_update_movie.png) ### Get all Movies ![](2_get_all_movies.png) ### Get a Movie by its ID ![](3_get_movie.png) ### Delete a Movie by its ID ![](4_delete_movie.png) ================================================ FILE: _examples/database/mongodb/api/store/movie.go ================================================ package storeapi import ( "myapp/httputil" "myapp/store" "github.com/kataras/iris/v12" ) type MovieHandler struct { service store.MovieService } func NewMovieHandler(service store.MovieService) *MovieHandler { return &MovieHandler{service: service} } func (h *MovieHandler) GetAll(ctx iris.Context) { movies, err := h.service.GetAll(nil) if err != nil { httputil.InternalServerErrorJSON(ctx, err, "Server was unable to retrieve all movies") return } if movies == nil { // will return "null" if empty, with this "trick" we return "[]" json. movies = make([]store.Movie, 0) } ctx.JSON(movies) } func (h *MovieHandler) Get(ctx iris.Context) { id := ctx.Params().Get("id") m, err := h.service.GetByID(nil, id) if err != nil { if err == store.ErrNotFound { ctx.NotFound() } else { httputil.InternalServerErrorJSON(ctx, err, "Server was unable to retrieve movie [%s]", id) } return } ctx.JSON(m) } func (h *MovieHandler) Add(ctx iris.Context) { m := new(store.Movie) err := ctx.ReadJSON(m) if err != nil { httputil.FailJSON(ctx, iris.StatusBadRequest, err, "Malformed request payload") return } err = h.service.Create(nil, m) if err != nil { httputil.InternalServerErrorJSON(ctx, err, "Server was unable to create a movie") return } ctx.StatusCode(iris.StatusCreated) ctx.JSON(m) } func (h *MovieHandler) Update(ctx iris.Context) { id := ctx.Params().Get("id") var m store.Movie err := ctx.ReadJSON(&m) if err != nil { httputil.FailJSON(ctx, iris.StatusBadRequest, err, "Malformed request payload") return } err = h.service.Update(nil, id, m) if err != nil { if err == store.ErrNotFound { ctx.NotFound() return } httputil.InternalServerErrorJSON(ctx, err, "Server was unable to update movie [%s]", id) return } } func (h *MovieHandler) Delete(ctx iris.Context) { id := ctx.Params().Get("id") err := h.service.Delete(nil, id) if err != nil { if err == store.ErrNotFound { ctx.NotFound() return } httputil.InternalServerErrorJSON(ctx, err, "Server was unable to delete movie [%s]", id) return } } ================================================ FILE: _examples/database/mongodb/docker-compose.yml ================================================ version: "3.1" services: app: build: . environment: Port: 8080 DSN: db:27017 ports: - 8080:8080 depends_on: - db db: image: mongo environment: MONGO_INITDB_DATABASE: store ports: - 27017:27017 ================================================ FILE: _examples/database/mongodb/env/env.go ================================================ package env import ( "fmt" "log" "os" "path/filepath" "strings" "github.com/joho/godotenv" ) var ( // Port is the PORT environment variable or 8080 if missing. // Used to open the tcp listener for our web server. Port string // DSN is the DSN environment variable or mongodb://localhost:27017 if missing. // Used to connect to the mongodb. DSN string ) func parse() { Port = getDefault("PORT", "8080") DSN = getDefault("DSN", "mongodb://localhost:27017") log.Printf("• Port=%s\n", Port) log.Printf("• DSN=%s\n", DSN) } // Load loads environment variables that are being used across the whole app. // Loading from file(s), i.e .env or dev.env // // Example of a 'dev.env': // PORT=8080 // DSN=mongodb://localhost:27017 // // After `Load` the callers can get an environment variable via `os.Getenv`. func Load(envFileName string) { if args := os.Args; len(args) > 1 && args[1] == "help" { fmt.Fprintln(os.Stderr, "https://github.com/kataras/iris/blob/main/_examples/database/mongodb/README.md") os.Exit(-1) } // If more than one filename passed with comma separated then load from all // of these, a env file can be a partial too. envFiles := strings.Split(envFileName, ",") for _, envFile := range envFiles { if filepath.Ext(envFile) == "" { envFile += ".env" } if fileExists(envFile) { log.Printf("Loading environment variables from file: %s\n", envFile) if err := godotenv.Load(envFile); err != nil { panic(fmt.Sprintf("error loading environment variables from [%s]: %v", envFile, err)) } } } // envMap, _ := godotenv.Read(envFiles...) // for k, v := range envMap { // log.Printf("◽ %s=%s\n", k, v) // } parse() } func getDefault(key string, def string) string { value := os.Getenv(key) if value == "" { os.Setenv(key, def) value = def } return value } func fileExists(filename string) bool { info, err := os.Stat(filename) if os.IsNotExist(err) { return false } return !info.IsDir() } ================================================ FILE: _examples/database/mongodb/go.mod ================================================ module myapp go 1.25 require ( github.com/joho/godotenv v1.5.1 github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd go.mongodb.org/mongo-driver v1.17.6 ) require ( github.com/BurntSushi/toml v1.6.0 // indirect github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect github.com/CloudyKit/jet/v6 v6.3.1 // indirect github.com/Joker/jade v1.1.3 // indirect github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 // indirect github.com/andybalholm/brotli v1.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/fatih/structs v1.1.0 // indirect github.com/flosch/pongo2/v4 v4.0.2 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/iris-contrib/schema v0.0.6 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/kataras/blocks v0.0.8 // indirect github.com/kataras/golog v0.1.12 // indirect github.com/kataras/pio v0.0.13 // indirect github.com/kataras/sitemap v0.0.6 // indirect github.com/kataras/tunnel v0.0.4 // indirect github.com/klauspost/compress v1.18.2 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mailgun/raymond/v2 v2.0.48 // indirect github.com/mailru/easyjson v0.9.1 // indirect github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/montanaflynn/stats v0.7.1 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/schollz/closestmatch v2.1.0+incompatible // indirect github.com/sirupsen/logrus v1.8.1 // indirect github.com/stretchr/testify v1.11.1 // indirect github.com/tdewolff/minify/v2 v2.24.8 // indirect github.com/tdewolff/parse/v2 v2.8.5 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.1.2 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect github.com/yosssi/ace v0.0.5 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect golang.org/x/crypto v0.47.0 // indirect golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect golang.org/x/net v0.49.0 // indirect golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.40.0 // indirect golang.org/x/text v0.33.0 // indirect golang.org/x/time v0.14.0 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: _examples/database/mongodb/go.sum ================================================ github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= github.com/CloudyKit/jet/v6 v6.3.1 h1:6IAo5Cx21xrHVaR8zzXN5gJatKV/wO7Nf6bfCnCSbUw= github.com/CloudyKit/jet/v6 v6.3.1/go.mod h1:lf8ksdNsxZt7/yH/3n4vJQWA9RUq4wpaHtArHhGVMOw= github.com/Joker/hpp v1.0.0 h1:65+iuJYdRXv/XyN62C1uEmmOx3432rNG/rKlX6V7Kkc= github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= github.com/Joker/jade v1.1.3 h1:Qbeh12Vq6BxURXT1qZBRHsDxeURB8ztcL6f3EXSGeHk= github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKduM= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 h1:dG7/gLroJGht/jSQtHiLvT48Hxn+crbmvyItZC8cWOs= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05/go.mod h1:NYezi6wtnJtBm5btoprXc5SvAdqH0XTXWnUup0MptAI= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/flosch/pongo2/v4 v4.0.2 h1:gv+5Pe3vaSVmiJvh/BZa82b7/00YUGm0PIyVVLop0Hw= github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+BWPhwZD8= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a h1:l7A0loSszR5zHd/qK53ZIHMO8b3bBSmENnQ6eKnUT0A= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/iris-contrib/httpexpect/v2 v2.15.2 h1:T9THsdP1woyAqKHwjkEsbCnMefsAFvk8iJJKokcJ3Go= github.com/iris-contrib/httpexpect/v2 v2.15.2/go.mod h1:JLDgIqnFy5loDSUv1OA2j0mb6p/rDhiCqigP22Uq9xE= github.com/iris-contrib/schema v0.0.6 h1:CPSBLyx2e91H2yJzPuhGuifVRnZBBJ3pCOMbOvPZaTw= github.com/iris-contrib/schema v0.0.6/go.mod h1:iYszG0IOsuIsfzjymw1kMzTL8YQcCWlm65f3wX8J5iA= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kataras/blocks v0.0.8 h1:MrpVhoFTCR2v1iOOfGng5VJSILKeZZI+7NGfxEh3SUM= github.com/kataras/blocks v0.0.8/go.mod h1:9Jm5zx6BB+06NwA+OhTbHW1xkMOYxahnqTN5DveZ2Yg= github.com/kataras/golog v0.1.12 h1:Bu7I/G4ilJlbfzjmU39O9N+2uO1pBcMK045fzZ4ytNg= github.com/kataras/golog v0.1.12/go.mod h1:wrGSbOiBqbQSQznleVNX4epWM8rl9SJ/rmEacl0yqy4= github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd h1:oMsQgqKACbUdlaIWnN+EZzzAnHkw90hE/y/R0BSij2U= github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd/go.mod h1:yucjtDl9Vn3OctWM1fi0MuM9OckemC7kEreFa9QtPF8= github.com/kataras/pio v0.0.13 h1:x0rXVX0fviDTXOOLOmr4MUxOabu1InVSTu5itF8CXCM= github.com/kataras/pio v0.0.13/go.mod h1:k3HNuSw+eJ8Pm2lA4lRhg3DiCjVgHlP8hmXApSej3oM= github.com/kataras/sitemap v0.0.6 h1:w71CRMMKYMJh6LR2wTgnk5hSgjVNB9KL60n5e2KHvLY= github.com/kataras/sitemap v0.0.6/go.mod h1:dW4dOCNs896OR1HmG+dMLdT7JjDk7mYBzoIRwuj5jA4= github.com/kataras/tunnel v0.0.4 h1:sCAqWuJV7nPzGrlb0os3j49lk2JhILT0rID38NHNLpA= github.com/kataras/tunnel v0.0.4/go.mod h1:9FkU4LaeifdMWqZu7o20ojmW4B7hdhv2CMLwfnHGpYw= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw= github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18= github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo= github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk= github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tdewolff/minify/v2 v2.24.8 h1:58/VjsbevI4d5FGV0ZSuBrHMSSkH4MCH0sIz/eKIauE= github.com/tdewolff/minify/v2 v2.24.8/go.mod h1:0Ukj0CRpo/sW/nd8uZ4ccXaV1rEVIWA3dj8U7+Shhfw= github.com/tdewolff/parse/v2 v2.8.5 h1:ZmBiA/8Do5Rpk7bDye0jbbDUpXXbCdc3iah4VeUvwYU= github.com/tdewolff/parse/v2 v2.8.5/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo= github.com/tdewolff/test v1.0.11 h1:FdLbwQVHxqG16SlkGveC0JVyrJN62COWTRyUFzfbtBE= github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA= github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss= go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs= moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHcE= ================================================ FILE: _examples/database/mongodb/httputil/error.go ================================================ package httputil import ( "errors" "fmt" "io" "net/http" "os" "runtime" "runtime/debug" "strings" "time" "github.com/kataras/iris/v12" ) var validStackFuncs = []func(string) bool{ func(file string) bool { return strings.Contains(file, "/mongodb/api/") }, } // RuntimeCallerStack returns the app's `file:line` stacktrace // to give more information about an error cause. func RuntimeCallerStack() (s string) { var pcs [10]uintptr n := runtime.Callers(1, pcs[:]) frames := runtime.CallersFrames(pcs[:n]) for { frame, more := frames.Next() for _, fn := range validStackFuncs { if fn(frame.File) { s += fmt.Sprintf("\n\t\t\t%s:%d", frame.File, frame.Line) } } if !more { break } } return s } // HTTPError describes an HTTP error. type HTTPError struct { error Stack string `json:"-"` // the whole stacktrace. CallerStack string `json:"-"` // the caller, file:lineNumber When time.Time `json:"-"` // the time that the error occurred. // ErrorCode int: maybe a collection of known error codes. StatusCode int `json:"statusCode"` // could be named as "reason" as well // it's the message of the error. Description string `json:"description"` } func newError(statusCode int, err error, format string, args ...any) HTTPError { if format == "" { format = http.StatusText(statusCode) } desc := fmt.Sprintf(format, args...) if err == nil { err = errors.New(desc) } return HTTPError{ err, string(debug.Stack()), RuntimeCallerStack(), time.Now(), statusCode, desc, } } func (err HTTPError) writeHeaders(ctx iris.Context) { ctx.StatusCode(err.StatusCode) ctx.Header("X-Content-Type-Options", "nosniff") } // LogFailure will print out the failure to the "logger". func LogFailure(logger io.Writer, ctx iris.Context, err HTTPError) { timeFmt := err.When.Format("2006/01/02 15:04:05") firstLine := fmt.Sprintf("%s %s: %s", timeFmt, http.StatusText(err.StatusCode), err.Error()) whitespace := strings.Repeat(" ", len(timeFmt)+1) fmt.Fprintf(logger, "%s\n%sIP: %s\n%sURL: %s\n%sSource: %s\n", firstLine, whitespace, ctx.RemoteAddr(), whitespace, ctx.FullRequestURI(), whitespace, err.CallerStack) } // Fail will send the status code, write the error's reason // and return the HTTPError for further use, i.e logging, see `InternalServerError`. func Fail(ctx iris.Context, statusCode int, err error, format string, args ...any) HTTPError { httpErr := newError(statusCode, err, format, args...) httpErr.writeHeaders(ctx) ctx.WriteString(httpErr.Description) return httpErr } // FailJSON will send to the client the error data as JSON. // Useful for APIs. func FailJSON(ctx iris.Context, statusCode int, err error, format string, args ...any) HTTPError { httpErr := newError(statusCode, err, format, args...) httpErr.writeHeaders(ctx) ctx.JSON(httpErr) return httpErr } // InternalServerError logs to the server's terminal // and dispatches to the client the 500 Internal Server Error. // Internal Server errors are critical, so we log them to the `os.Stderr`. func InternalServerError(ctx iris.Context, err error, format string, args ...any) { LogFailure(os.Stderr, ctx, Fail(ctx, iris.StatusInternalServerError, err, format, args...)) } // InternalServerErrorJSON acts exactly like `InternalServerError` but instead it sends the data as JSON. // Useful for APIs. func InternalServerErrorJSON(ctx iris.Context, err error, format string, args ...any) { LogFailure(os.Stderr, ctx, FailJSON(ctx, iris.StatusInternalServerError, err, format, args...)) } // UnauthorizedJSON sends JSON format of StatusUnauthorized(401) HTTPError value. func UnauthorizedJSON(ctx iris.Context, err error, format string, args ...any) HTTPError { return FailJSON(ctx, iris.StatusUnauthorized, err, format, args...) } ================================================ FILE: _examples/database/mongodb/main.go ================================================ package main // go get -u go.mongodb.org/mongo-driver // go get -u github.com/joho/godotenv import ( "context" "flag" "fmt" "log" "os" // APIs storeapi "myapp/api/store" // "myapp/env" "myapp/store" "github.com/kataras/iris/v12" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" ) const version = "0.0.1" func init() { envFileName := ".env" flagset := flag.CommandLine flagset.StringVar(&envFileName, "env", envFileName, "the env file which web app will use to extract its environment variables") flagset.Parse(os.Args[1:]) env.Load(envFileName) } func main() { clientOptions := options.Client().SetHosts([]string{env.DSN}) client, err := mongo.Connect(context.Background(), clientOptions) if err != nil { log.Fatal(err) } err = client.Ping(context.Background(), nil) if err != nil { log.Fatal(err) } defer client.Disconnect(context.TODO()) db := client.Database("store") var ( // Collections. moviesCollection = db.Collection("movies") // Services. movieService = store.NewMovieService(moviesCollection) ) app := iris.New() app.Use(func(ctx iris.Context) { ctx.Header("Server", "Iris MongoDB/"+version) ctx.Next() }) storeAPI := app.Party("/api/store") { movieHandler := storeapi.NewMovieHandler(movieService) storeAPI.Get("/movies", movieHandler.GetAll) storeAPI.Post("/movies", movieHandler.Add) storeAPI.Get("/movies/{id}", movieHandler.Get) storeAPI.Put("/movies/{id}", movieHandler.Update) storeAPI.Delete("/movies/{id}", movieHandler.Delete) } // GET: http://localhost:8080/api/store/movies // POST: http://localhost:8080/api/store/movies // GET: http://localhost:8080/api/store/movies/{id} // PUT: http://localhost:8080/api/store/movies/{id} // DELETE: http://localhost:8080/api/store/movies/{id} app.Listen(fmt.Sprintf(":%s", env.Port), iris.WithOptimizations) } ================================================ FILE: _examples/database/mongodb/store/movie.go ================================================ package store import ( "context" "errors" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo" // up to you: // "go.mongodb.org/mongo-driver/mongo/options" ) type Movie struct { ID primitive.ObjectID `json:"_id" bson:"_id"` /* you need the bson:"_id" to be able to retrieve with ID filled */ Name string `json:"name"` Cover string `json:"cover"` Description string `json:"description"` } type MovieService interface { GetAll(ctx context.Context) ([]Movie, error) GetByID(ctx context.Context, id string) (Movie, error) Create(ctx context.Context, m *Movie) error Update(ctx context.Context, id string, m Movie) error Delete(ctx context.Context, id string) error } type movieService struct { C *mongo.Collection } var _ MovieService = (*movieService)(nil) func NewMovieService(collection *mongo.Collection) MovieService { // up to you: // indexOpts := new(options.IndexOptions) // indexOpts.SetName("movieIndex"). // SetUnique(true). // SetBackground(true). // SetSparse(true) // collection.Indexes().CreateOne(context.Background(), mongo.IndexModel{ // Keys: []string{"_id", "name"}, // Options: indexOpts, // }) return &movieService{C: collection} } func (s *movieService) GetAll(ctx context.Context) ([]Movie, error) { // Note: // The mongodb's go-driver's docs says that you can pass `nil` to "find all" but this gives NilDocument error, // probably it's a bug or a documentation's mistake, you have to pass `bson.D{}` instead. cur, err := s.C.Find(ctx, bson.D{}) if err != nil { return nil, err } defer cur.Close(ctx) var results []Movie for cur.Next(ctx) { if err = cur.Err(); err != nil { return nil, err } // elem := bson.D{} var elem Movie err = cur.Decode(&elem) if err != nil { return nil, err } // results = append(results, Movie{ID: elem[0].Value.(primitive.ObjectID)}) results = append(results, elem) } return results, nil } func matchID(id string) (bson.D, error) { objectID, err := primitive.ObjectIDFromHex(id) if err != nil { return nil, err } filter := bson.D{{Key: "_id", Value: objectID}} return filter, nil } var ErrNotFound = errors.New("not found") func (s *movieService) GetByID(ctx context.Context, id string) (Movie, error) { var movie Movie filter, err := matchID(id) if err != nil { return movie, err } err = s.C.FindOne(ctx, filter).Decode(&movie) if err == mongo.ErrNoDocuments { return movie, ErrNotFound } return movie, err } func (s *movieService) Create(ctx context.Context, m *Movie) error { if m.ID.IsZero() { m.ID = primitive.NewObjectID() } _, err := s.C.InsertOne(ctx, m) if err != nil { return err } // The following doesn't work if you have the `bson:"_id` on Movie.ID field, // therefore we manually generate a new ID (look above). // res, err := ...InsertOne // objectID := res.InsertedID.(primitive.ObjectID) // m.ID = objectID return nil } func (s *movieService) Update(ctx context.Context, id string, m Movie) error { filter, err := matchID(id) if err != nil { return err } // update := bson.D{ // {Key: "$set", Value: m}, // } // ^ this will override all fields, you can do that, depending on your design. but let's check each field: elem := bson.D{} if m.Name != "" { elem = append(elem, bson.E{Key: "name", Value: m.Name}) } if m.Description != "" { elem = append(elem, bson.E{Key: "description", Value: m.Description}) } if m.Cover != "" { elem = append(elem, bson.E{Key: "cover", Value: m.Cover}) } update := bson.D{ {Key: "$set", Value: elem}, } _, err = s.C.UpdateOne(ctx, filter, update) if err != nil { if err == mongo.ErrNoDocuments { return ErrNotFound } return err } return nil } func (s *movieService) Delete(ctx context.Context, id string) error { filter, err := matchID(id) if err != nil { return err } _, err = s.C.DeleteOne(ctx, filter) if err != nil { if err == mongo.ErrNoDocuments { return ErrNotFound } return err } return nil } ================================================ FILE: _examples/database/mysql/Dockerfile ================================================ # docker build -t myapp . # docker run --rm -it -p 8080:8080 myapp:latest FROM golang:latest AS builder RUN apt-get update ENV GO111MODULE=on \ CGO_ENABLED=0 \ GOOS=linux \ GOARCH=amd64 WORKDIR /go/src/app COPY go.mod . RUN go mod download COPY . . RUN go install FROM scratch COPY --from=builder /go/bin/myapp . ENTRYPOINT ["./myapp"] ================================================ FILE: _examples/database/mysql/README.md ================================================ # Iris, MySQL, Groupcache & Docker Example ## 📘 Endpoints | Method | Path | Description | URL Parameters | Body | Auth Required | |--------|---------------------|------------------------|--------------- |----------------------------|---------------| | ANY | /token | Prints a new JWT Token | - | - | - | | GET | /category | Lists a set of Categories | offset, limit, order | - | Token | | POST | /category | Creates a Category | - | JSON [Full Category](migration/api_category/create_category.json) | Token | | PUT | /category | Fully-Updates a Category | - | JSON [Full Category](migration/api_category/update_category.json) | Token | | PATCH | /category/{id} | Partially-Updates a Category | - | JSON [Partial Category](migration/api_category/update_partial_category.json) | Token | | GET | /category/{id} | Prints a Category | - | - | Token | | DELETE | /category/{id} | Deletes a Category | - | - | Token | | GET | /category/{id}/products | Lists all Products from a Category | offset, limit, order | - | Token | | POST | /category/{id}/products | (Batch) Assigns one or more Products to a Category | - | JSON [Products](migration/api_category/insert_products_category.json) | Token | | GET | /product | Lists a set of Products (cache) | offset, limit, order | - | Token | | POST | /product | Creates a Product | - | JSON [Full Product](migration/api_product/create_product.json) | Token | | PUT | /product | Fully-Updates a Product | - | JSON [Full Product](migration/api_product/update_product.json) | Token | | PATCH | /product/{id} | Partially-Updates a Product | - | JSON [Partial Product](migration/api_product/update_partial_product.json) | Token | | GET | /product/{id} | Prints a Product (cache) | - | - | Token | | DELETE | /product/{id} | Deletes a Product | - | - | Token | ## 📑 Responses * **Content-Type** of `"application/json;charset=utf-8"`, snake_case naming (identical to the database columns) * **Status Codes** * 500 for server(db) errors, * 422 for validation errors, e.g. ```json { "code": 422, "message": "required fields are missing", "timestamp": 1589306271 } ``` * 400 for malformed syntax, e.g. ```json { "code": 400, "message": "json: cannot unmarshal number -2 into Go struct field Category.position of type uint64", "timestamp": 1589306325 } ``` ```json { "code": 400, "message": "json: unknown field \"field_not_exists\"", "timestamp": 1589306367 } ``` * 404 for entity not found, e.g. ```json { "code": 404, "message": "entity does not exist", "timestamp": 1589306199 } ``` * 304 for unaffected UPDATE or DELETE, * 201 for CREATE with the last inserted ID, * 200 for GET, UPDATE and DELETE ## ⚡ Get Started Download the folder. ### Install (Docker) Install [Docker](https://www.docker.com/) and execute the command below ```sh $ docker-compose up --build ``` ### Install (Manually) Run `go build -mod=mod` or `go run -mod=mod main.go` and read below. #### MySQL Environment variables: ```sh MYSQL_USER=user_myapp MYSQL_PASSWORD=dbpassword MYSQL_HOST=localhost MYSQL_DATABASE=myapp ``` Download the schema from [migration/db.sql](migration/db.sql) and execute it against your MySQL server instance. ```sql CREATE DATABASE IF NOT EXISTS myapp DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE myapp; SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; DROP TABLE IF EXISTS categories; CREATE TABLE categories ( id int(11) NOT NULL AUTO_INCREMENT, title varchar(255) NOT NULL, position int(11) NOT NULL, image_url varchar(255) NOT NULL, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id) ); DROP TABLE IF EXISTS products; CREATE TABLE products ( id int(11) NOT NULL AUTO_INCREMENT, category_id int, title varchar(255) NOT NULL, image_url varchar(255) NOT NULL, price decimal(10,2) NOT NULL, description text NOT NULL, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), FOREIGN KEY (category_id) REFERENCES categories(id) ); SET FOREIGN_KEY_CHECKS = 1; ``` ### Requests Some request bodies can be found at: [migration/api_category](migration/api_category) and [migration/api_product](migration/api_product). **However** I've provided a [postman.json](migration/myapp_postman.json) Collection that you can import to your [POSTMAN](https://learning.postman.com/docs/postman/collections/importing-and-exporting-data/#collections) and start playing with the API. All write-access endpoints are "protected" via JWT, a client should "verify" itself. You'll need to manually take the **token** from the `http://localhost:8080/token` and put it on url parameter `?token=$token` or to the `Authentication: Bearer $token` request header. ### Unit or End-To-End Testing? Testing is important. The code is written in a way that testing should be trivial (Pseudo/memory Database or SQLite local file could be integrated as well, for end-to-end tests a Docker image with MySQL and fire tests against that server). However, there is [nothing(?)](service/category_service_test.go) to see here. ## Packages - https://github.com/kataras/jwt (JWT parsing) - https://github.com/go-sql-driver/mysql (Go Driver for MySQL) - https://github.com/DATA-DOG/go-sqlmock (Testing DB see [service/category_service_test.go](service/category_service_test.go)) - https://github.com/kataras/iris (HTTP) - https://github.com/mailgun/groupcache (Caching) ================================================ FILE: _examples/database/mysql/api/api.go ================================================ // Package api contains the handlers for our HTTP Endpoints. package api import ( "time" "myapp/service" "myapp/sql" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/middleware/jwt" "github.com/kataras/iris/v12/middleware/requestid" ) // Router accepts any required dependencies and returns the main server's handler. func Router(db sql.Database, secret string) func(iris.Party) { return func(r iris.Party) { r.Use(requestid.New()) signer := jwt.NewSigner(jwt.HS256, secret, 15*time.Minute) r.Get("/token", writeToken(signer)) verify := jwt.NewVerifier(jwt.HS256, secret).Verify(nil) r.Use(verify) // Generate a token for testing by navigating to // http://localhost:8080/token endpoint. // Copy-paste it to a ?token=$token url parameter or // open postman and put an Authentication: Bearer $token to get // access on create, update and delete endpoinds. var ( categoryService = service.NewCategoryService(db) productService = service.NewProductService(db) ) cat := r.Party("/category") { // TODO: new Use to add middlewares to specific // routes per METHOD ( we already have the per path through parties.) handler := NewCategoryHandler(categoryService) cat.Get("/", handler.List) cat.Post("/", handler.Create) cat.Put("/", handler.Update) cat.Get("/{id:int64}", handler.GetByID) cat.Patch("/{id:int64}", handler.PartialUpdate) cat.Delete("/{id:int64}", handler.Delete) /* You can also do something like that: cat.PartyFunc("/{id:int64}", func(c iris.Party) { c.Get("/", handler.GetByID) c.Post("/", handler.PartialUpdate) c.Delete("/", handler.Delete) }) */ cat.Get("/{id:int64}/products", handler.ListProducts) cat.Post("/{id:int64}/products", handler.InsertProducts(productService)) } prod := r.Party("/product") { handler := NewProductHandler(productService) prod.Get("/", handler.List) prod.Post("/", handler.Create) prod.Put("/", handler.Update) prod.Get("/{id:int64}", handler.GetByID) prod.Patch("/{id:int64}", handler.PartialUpdate) prod.Delete("/{id:int64}", handler.Delete) } } } func writeToken(signer *jwt.Signer) iris.Handler { return func(ctx iris.Context) { claims := jwt.Claims{ Issuer: "https://iris-go.com", Audience: []string{requestid.Get(ctx)}, } token, err := signer.Sign(claims) if err != nil { ctx.StopWithStatus(iris.StatusInternalServerError) return } ctx.Write(token) } } ================================================ FILE: _examples/database/mysql/api/category_handler.go ================================================ package api import ( "myapp/entity" "myapp/service" "myapp/sql" "github.com/kataras/iris/v12" ) // CategoryHandler is the http mux for categories. type CategoryHandler struct { // [...options] service *service.CategoryService } // NewCategoryHandler returns the main controller for the categories API. func NewCategoryHandler(service *service.CategoryService) *CategoryHandler { return &CategoryHandler{service} } // GetByID fetches a single record from the database and sends it to the client. // Method: GET. func (h *CategoryHandler) GetByID(ctx iris.Context) { id := ctx.Params().GetInt64Default("id", 0) var cat entity.Category err := h.service.GetByID(ctx.Request().Context(), &cat, id) if err != nil { if err == sql.ErrNoRows { writeEntityNotFound(ctx) return } debugf("CategoryHandler.GetByID(id=%d): %v", id, err) writeInternalServerError(ctx) return } ctx.JSON(cat) } /* type ( List struct { Data any `json:"data"` Order string `json:"order"` Next Range `json:"next,omitempty"` Prev Range `json:"prev,omitempty"` } Range struct { Offset int64 `json:"offset"` Limit int64 `json:"limit` } ) */ // List lists a set of records from the database. // Method: GET. func (h *CategoryHandler) List(ctx iris.Context) { q := ctx.Request().URL.Query() opts := sql.ParseListOptions(q) // initialize here in order to return an empty json array `[]` instead of `null`. categories := entity.Categories{} err := h.service.List(ctx.Request().Context(), &categories, opts) if err != nil && err != sql.ErrNoRows { debugf("CategoryHandler.List(DB) (limit=%d offset=%d where=%s=%v): %v", opts.Limit, opts.Offset, opts.WhereColumn, opts.WhereValue, err) writeInternalServerError(ctx) return } ctx.JSON(categories) } // Create adds a record to the database. // Method: POST. func (h *CategoryHandler) Create(ctx iris.Context) { var cat entity.Category if err := ctx.ReadJSON(&cat); err != nil { return } id, err := h.service.Insert(ctx.Request().Context(), cat) if err != nil { if err == sql.ErrUnprocessable { ctx.StopWithJSON(iris.StatusUnprocessableEntity, newError(iris.StatusUnprocessableEntity, ctx.Request().Method, ctx.Path(), "required fields are missing")) return } debugf("CategoryHandler.Create(DB): %v", err) writeInternalServerError(ctx) return } // Send 201 with body of {"id":$last_inserted_id"}. ctx.StatusCode(iris.StatusCreated) ctx.JSON(iris.Map{cat.PrimaryKey(): id}) } // Update performs a full-update of a record in the database. // Method: PUT. func (h *CategoryHandler) Update(ctx iris.Context) { var cat entity.Category if err := ctx.ReadJSON(&cat); err != nil { return } affected, err := h.service.Update(ctx.Request().Context(), cat) if err != nil { if err == sql.ErrUnprocessable { ctx.StopWithJSON(iris.StatusUnprocessableEntity, newError(iris.StatusUnprocessableEntity, ctx.Request().Method, ctx.Path(), "required fields are missing")) return } debugf("CategoryHandler.Update(DB): %v", err) writeInternalServerError(ctx) return } status := iris.StatusOK if affected == 0 { status = iris.StatusNotModified } ctx.StatusCode(status) } // PartialUpdate is the handler for partially update one or more fields of the record. // Method: PATCH. func (h *CategoryHandler) PartialUpdate(ctx iris.Context) { id := ctx.Params().GetInt64Default("id", 0) var attrs map[string]any if err := ctx.ReadJSON(&attrs); err != nil { return } affected, err := h.service.PartialUpdate(ctx.Request().Context(), id, attrs) if err != nil { if err == sql.ErrUnprocessable { ctx.StopWithJSON(iris.StatusUnprocessableEntity, newError(iris.StatusUnprocessableEntity, ctx.Request().Method, ctx.Path(), "unsupported value(s)")) return } debugf("CategoryHandler.PartialUpdate(DB): %v", err) writeInternalServerError(ctx) return } status := iris.StatusOK if affected == 0 { status = iris.StatusNotModified } ctx.StatusCode(status) } // Delete removes a record from the database. // Method: DELETE. func (h *CategoryHandler) Delete(ctx iris.Context) { id := ctx.Params().GetInt64Default("id", 0) affected, err := h.service.DeleteByID(ctx.Request().Context(), id) if err != nil { if err == sql.ErrNoRows { writeEntityNotFound(ctx) return } debugf("CategoryHandler.Delete(DB): %v", err) writeInternalServerError(ctx) return } status := iris.StatusOK // StatusNoContent if affected == 0 { status = iris.StatusNotModified } ctx.StatusCode(status) } // Products. // ListProducts lists products of a Category. // Example: from cheap to expensive: // http://localhost:8080/category/3/products?offset=0&limit=30&by=price&order=asc // Method: GET. func (h *CategoryHandler) ListProducts(ctx iris.Context) { id := ctx.Params().GetInt64Default("id", 0) // NOTE: could add cache here too. q := ctx.Request().URL.Query() opts := sql.ParseListOptions(q).Where("category_id", id) opts.Table = "products" if opts.OrderByColumn == "" { opts.OrderByColumn = "updated_at" } var products entity.Products err := h.service.List(ctx.Request().Context(), &products, opts) if err != nil && err != sql.ErrNoRows { debugf("CategoryHandler.ListProducts(DB) (table=%s where=%s=%v limit=%d offset=%d): %v", opts.Table, opts.WhereColumn, opts.WhereValue, opts.Limit, opts.Offset, err) writeInternalServerError(ctx) return } ctx.JSON(products) } // InsertProducts assigns new products to a Category (accepts a list of products). // Method: POST. func (h *CategoryHandler) InsertProducts(productService *service.ProductService) iris.Handler { return func(ctx iris.Context) { categoryID := ctx.Params().GetInt64Default("id", 0) var products []entity.Product if err := ctx.ReadJSON(&products); err != nil { return } for i := range products { products[i].CategoryID = categoryID } inserted, err := productService.BatchInsert(ctx.Request().Context(), products) if err != nil { if err == sql.ErrUnprocessable { ctx.StopWithJSON(iris.StatusUnprocessableEntity, newError(iris.StatusUnprocessableEntity, ctx.Request().Method, ctx.Path(), "required fields are missing")) return } debugf("CategoryHandler.InsertProducts(DB): %v", err) writeInternalServerError(ctx) return } if inserted == 0 { ctx.StatusCode(iris.StatusNotModified) return } // Send 201 with body of {"inserted":$inserted"}. ctx.StatusCode(iris.StatusCreated) ctx.JSON(iris.Map{"inserted": inserted}) } } ================================================ FILE: _examples/database/mysql/api/helper.go ================================================ package api import ( "log" "github.com/kataras/iris/v12" ) const debug = true func debugf(format string, args ...any) { if !debug { return } log.Printf(format, args...) } func writeInternalServerError(ctx iris.Context) { ctx.StopWithJSON(iris.StatusInternalServerError, newError(iris.StatusInternalServerError, ctx.Request().Method, ctx.Path(), "")) } func writeEntityNotFound(ctx iris.Context) { ctx.StopWithJSON(iris.StatusNotFound, newError(iris.StatusNotFound, ctx.Request().Method, ctx.Path(), "entity does not exist")) } ================================================ FILE: _examples/database/mysql/api/httperror.go ================================================ package api import ( "fmt" "time" "github.com/kataras/iris/v12" ) // Error holds the error sent by server to clients (JSON). type Error struct { StatusCode int `json:"code"` Method string `json:"-"` Path string `json:"-"` Message string `json:"message"` Timestamp int64 `json:"timestamp"` } func newError(statusCode int, method, path, format string, args ...any) Error { msg := format if len(args) > 0 { // why we check for that? If the original error message came from our database // and it contains fmt-reserved words // like %s or %d we will get MISSING(=...) in our error message and we don't want that. msg = fmt.Sprintf(msg, args...) } if msg == "" { msg = iris.StatusText(statusCode) } return Error{ StatusCode: statusCode, Method: method, Path: path, Message: msg, Timestamp: time.Now().Unix(), } } // Error implements the internal Go error interface. func (e Error) Error() string { return fmt.Sprintf("[%d] %s: %s: %s", e.StatusCode, e.Method, e.Path, e.Message) } // Is implements the standard `errors.Is` internal interface. // Usage: errors.Is(e, target) func (e Error) Is(target error) bool { if target == nil { return false } err, ok := target.(Error) if !ok { return false } return (err.StatusCode == e.StatusCode || e.StatusCode == 0) && (err.Message == e.Message || e.Message == "") } ================================================ FILE: _examples/database/mysql/api/middleware/.gitkeep ================================================ ================================================ FILE: _examples/database/mysql/api/product_handler.go ================================================ package api import ( "time" "myapp/cache" "myapp/entity" "myapp/service" "myapp/sql" "github.com/kataras/iris/v12" ) // ProductHandler is the http mux for products. type ProductHandler struct { service *service.ProductService cache *cache.Cache } // NewProductHandler returns the main controller for the products API. func NewProductHandler(service *service.ProductService) *ProductHandler { return &ProductHandler{ service: service, cache: cache.New(service, "products", time.Minute), } } // GetByID fetches a single record from the database and sends it to the client. // Method: GET. func (h *ProductHandler) GetByID(ctx iris.Context) { id := ctx.Params().GetString("id") var product []byte err := h.cache.GetByID(ctx.Request().Context(), id, &product) if err != nil { if err == sql.ErrNoRows { writeEntityNotFound(ctx) return } debugf("ProductHandler.GetByID(id=%v): %v", id, err) writeInternalServerError(ctx) return } ctx.ContentType("application/json") ctx.Write(product) // ^ Could use our simple `noCache` or implement a Cache-Control (see kataras/iris/cache for that) // but let's keep it simple. } // List lists a set of records from the database. // Method: GET. func (h *ProductHandler) List(ctx iris.Context) { key := ctx.Request().URL.RawQuery products := []byte("[]") err := h.cache.List(ctx.Request().Context(), key, &products) if err != nil && err != sql.ErrNoRows { debugf("ProductHandler.List(DB) (%s): %v", key, err) writeInternalServerError(ctx) return } ctx.ContentType("application/json") ctx.Write(products) } // Create adds a record to the database. // Method: POST. func (h *ProductHandler) Create(ctx iris.Context) { var product entity.Product if err := ctx.ReadJSON(&product); err != nil { return } id, err := h.service.Insert(ctx.Request().Context(), product) if err != nil { if err == sql.ErrUnprocessable { ctx.StopWithJSON(iris.StatusUnprocessableEntity, newError(iris.StatusUnprocessableEntity, ctx.Request().Method, ctx.Path(), "required fields are missing")) return } debugf("ProductHandler.Create(DB): %v", err) writeInternalServerError(ctx) return } // Send 201 with body of {"id":$last_inserted_id"}. ctx.StatusCode(iris.StatusCreated) ctx.JSON(iris.Map{product.PrimaryKey(): id}) } // Update performs a full-update of a record in the database. // Method: PUT. func (h *ProductHandler) Update(ctx iris.Context) { var product entity.Product if err := ctx.ReadJSON(&product); err != nil { return } affected, err := h.service.Update(ctx.Request().Context(), product) if err != nil { if err == sql.ErrUnprocessable { ctx.StopWithJSON(iris.StatusUnprocessableEntity, newError(iris.StatusUnprocessableEntity, ctx.Request().Method, ctx.Path(), "required fields are missing")) return } debugf("ProductHandler.Update(DB): %v", err) writeInternalServerError(ctx) return } status := iris.StatusOK if affected == 0 { status = iris.StatusNotModified } ctx.StatusCode(status) } // PartialUpdate is the handler for partially update one or more fields of the record. // Method: PATCH. func (h *ProductHandler) PartialUpdate(ctx iris.Context) { id := ctx.Params().GetInt64Default("id", 0) var attrs map[string]any if err := ctx.ReadJSON(&attrs); err != nil { return } affected, err := h.service.PartialUpdate(ctx.Request().Context(), id, attrs) if err != nil { if err == sql.ErrUnprocessable { ctx.StopWithJSON(iris.StatusUnprocessableEntity, newError(iris.StatusUnprocessableEntity, ctx.Request().Method, ctx.Path(), "unsupported value(s)")) return } debugf("ProductHandler.PartialUpdate(DB): %v", err) writeInternalServerError(ctx) return } status := iris.StatusOK if affected == 0 { status = iris.StatusNotModified } ctx.StatusCode(status) } // Delete removes a record from the database. // Method: DELETE. func (h *ProductHandler) Delete(ctx iris.Context) { id := ctx.Params().GetInt64Default("id", 0) affected, err := h.service.DeleteByID(ctx.Request().Context(), id) if err != nil { debugf("ProductHandler.Delete(DB): %v", err) writeInternalServerError(ctx) return } status := iris.StatusOK // StatusNoContent if affected == 0 { status = iris.StatusNotModified } ctx.StatusCode(status) } ================================================ FILE: _examples/database/mysql/cache/groupcache.go ================================================ package cache import ( "context" "encoding/json" "net/url" "strconv" "time" "myapp/entity" "myapp/sql" "github.com/mailgun/groupcache/v2" ) // Service that cache will use to retrieve data. type Service interface { RecordInfo() sql.Record GetByID(ctx context.Context, dest any, id int64) error List(ctx context.Context, dest any, opts sql.ListOptions) error } // Cache is a simple structure which holds the groupcache and the database service, exposes // `GetByID` and `List` which returns cached (or stores new) items. type Cache struct { service Service maxAge time.Duration group *groupcache.Group } // Size default size to use on groupcache, defaults to 3MB. var Size int64 = 3 << (10 * 3) // New returns a new cache service which exposes `GetByID` and `List` methods to work with. // The "name" should be unique, "maxAge" for cache expiration. func New(service Service, name string, maxAge time.Duration) *Cache { c := new(Cache) c.service = service c.maxAge = maxAge c.group = groupcache.NewGroup(name, Size, c) return c } const ( prefixID = "#" prefixList = "[" ) // Get implements the groupcache.Getter interface. // Use `GetByID` and `List` instead. func (c *Cache) Get(ctx context.Context, key string, dest groupcache.Sink) error { if len(key) < 2 { // empty or missing prefix+key, should never happen. return sql.ErrUnprocessable } var v any prefix := key[0:1] key = key[1:] switch prefix { case prefixID: // Get by ID. id, err := strconv.ParseInt(key, 10, 64) if err != nil || id <= 0 { return err } switch c.service.RecordInfo().(type) { case *entity.Category: v = new(entity.Category) case *entity.Product: v = new(entity.Product) } err = c.service.GetByID(ctx, v, id) if err != nil { return err } case prefixList: // Get a set of records, list. q, err := url.ParseQuery(key) if err != nil { return err } opts := sql.ParseListOptions(q) switch c.service.RecordInfo().(type) { case *entity.Category: v = new(entity.Categories) case *entity.Product: v = new(entity.Products) } err = c.service.List(ctx, v, opts) if err != nil { return err } default: return sql.ErrUnprocessable } b, err := json.Marshal(v) if err != nil { return err } return dest.SetBytes(b, time.Now().Add(c.maxAge)) } // GetByID binds an item to "dest" an item based on its "id". func (c *Cache) GetByID(ctx context.Context, id string, dest *[]byte) error { return c.group.Get(ctx, prefixID+id, groupcache.AllocatingByteSliceSink(dest)) } // List binds item to "dest" based on the "rawQuery" of `url.Values` for `ListOptions`. func (c *Cache) List(ctx context.Context, rawQuery string, dest *[]byte) error { return c.group.Get(ctx, prefixList+rawQuery, groupcache.AllocatingByteSliceSink(dest)) } ================================================ FILE: _examples/database/mysql/docker-compose.yml ================================================ version: '3.1' services: db: image: mysql command: --default-authentication-plugin=mysql_native_password environment: MYSQL_ROOT_PASSWORD: dbpassword MYSQL_DATABASE: myapp MYSQL_USER: user_myapp MYSQL_PASSWORD: dbpassword tty: true volumes: - ./migration:/docker-entrypoint-initdb.d app: build: . ports: - 8080:8080 environment: PORT: 8080 MYSQL_USER: user_myapp MYSQL_PASSWORD: dbpassword MYSQL_DATABASE: myapp MYSQL_HOST: db restart: on-failure healthcheck: test: ["CMD", "curl", "-f", "tcp://db:3306"] interval: 30s timeout: 10s retries: 5 depends_on: - db ================================================ FILE: _examples/database/mysql/entity/category.go ================================================ package entity import ( "database/sql" "time" ) // Category represents the categories entity. // Each product belongs to a category, see `Product.CategoryID` field. // It implements the `sql.Record` and `sql.Sorted` interfaces. type Category struct { ID int64 `db:"id" json:"id"` Title string `db:"title" json:"title"` Position uint64 `db:"position" json:"position"` ImageURL string `db:"image_url" json:"image_url"` // We could use: sql.NullTime or unix time seconds (as int64), // note that the dsn parameter "parseTime=true" is required now in order to fill this field correctly. CreatedAt *time.Time `db:"created_at" json:"created_at"` UpdatedAt *time.Time `db:"updated_at" json:"updated_at"` } // TableName returns the database table name of a Category. func (c *Category) TableName() string { return "categories" } // PrimaryKey returns the primary key of a Category. func (c *Category) PrimaryKey() string { return "id" } // SortBy returns the column name that // should be used as a fallback for sorting a set of Category. func (c *Category) SortBy() string { return "position" } // Scan binds mysql rows to this Category. func (c *Category) Scan(rows *sql.Rows) error { c.CreatedAt = new(time.Time) c.UpdatedAt = new(time.Time) return rows.Scan(&c.ID, &c.Title, &c.Position, &c.ImageURL, &c.CreatedAt, &c.UpdatedAt) } // Categories a list of categories. Implements the `Scannable` interface. type Categories []*Category // Scan binds mysql rows to this Categories. func (cs *Categories) Scan(rows *sql.Rows) (err error) { cp := *cs for rows.Next() { c := new(Category) if err = c.Scan(rows); err != nil { return } cp = append(cp, c) } if len(cp) == 0 { return sql.ErrNoRows } *cs = cp return rows.Err() } /* // The requests. type ( CreateCategoryRequest struct { Title string `json:"title"` // all required. Position uint64 `json:"position"` ImageURL string `json:"imageURL"` } UpdateCategoryRequest CreateCategoryRequest // at least 1 required. GetCategoryRequest struct { ID int64 `json:"id"` // required. } DeleteCategoryRequest GetCategoryRequest GetCategoriesRequest struct { // [limit, offset...] } )*/ ================================================ FILE: _examples/database/mysql/entity/product.go ================================================ package entity import ( "database/sql" "time" ) // Product represents the products entity. // It implements the `sql.Record` and `sql.Sorted` interfaces. type Product struct { ID int64 `db:"id" json:"id"` CategoryID int64 `db:"category_id" json:"category_id"` Title string `db:"title" json:"title"` ImageURL string `db:"image_url" json:"image_url"` Price float32 `db:"price" json:"price"` Description string `db:"description" json:"description"` CreatedAt *time.Time `db:"created_at" json:"created_at"` UpdatedAt *time.Time `db:"updated_at" json:"updated_at"` } // TableName returns the database table name of a Product. func (p Product) TableName() string { return "products" } // PrimaryKey returns the primary key of a Product. func (p *Product) PrimaryKey() string { return "id" } // SortBy returns the column name that // should be used as a fallback for sorting a set of Product. func (p *Product) SortBy() string { return "updated_at" } // ValidateInsert simple check for empty fields that should be required. func (p *Product) ValidateInsert() bool { return p.CategoryID > 0 && p.Title != "" && p.ImageURL != "" && p.Price > 0 /* decimal* */ && p.Description != "" } // Scan binds mysql rows to this Product. func (p *Product) Scan(rows *sql.Rows) error { p.CreatedAt = new(time.Time) p.UpdatedAt = new(time.Time) return rows.Scan(&p.ID, &p.CategoryID, &p.Title, &p.ImageURL, &p.Price, &p.Description, &p.CreatedAt, &p.UpdatedAt) } // Products is a list of products. Implements the `Scannable` interface. type Products []*Product // Scan binds mysql rows to this Categories. func (ps *Products) Scan(rows *sql.Rows) (err error) { cp := *ps for rows.Next() { p := new(Product) if err = p.Scan(rows); err != nil { return } cp = append(cp, p) } if len(cp) == 0 { return sql.ErrNoRows } *ps = cp return rows.Err() } /* // The requests. type ( CreateProductRequest struct { // all required. CategoryID int64 `json:"categoryID"` Title string `json:"title"` ImageURL string `json:"imageURL"` Price float32 `json:"price"` Description string `json:"description"` } UpdateProductRequest CreateProductRequest // at least 1 required. GetProductRequest struct { ID int64 `json:"id"` // required. } DeleteProductRequest GetProductRequest GetProductsRequest struct { // [page, offset...] } ) */ ================================================ FILE: _examples/database/mysql/go.mod ================================================ module myapp go 1.25 require ( github.com/DATA-DOG/go-sqlmock v1.5.2 github.com/go-sql-driver/mysql v1.9.3 github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd github.com/mailgun/groupcache/v2 v2.6.0 ) require ( filippo.io/edwards25519 v1.1.0 // indirect github.com/BurntSushi/toml v1.6.0 // indirect github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect github.com/CloudyKit/jet/v6 v6.3.1 // indirect github.com/Joker/jade v1.1.3 // indirect github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 // indirect github.com/andybalholm/brotli v1.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/fatih/structs v1.1.0 // indirect github.com/flosch/pongo2/v4 v4.0.2 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/iris-contrib/schema v0.0.6 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/kataras/blocks v0.0.8 // indirect github.com/kataras/golog v0.1.12 // indirect github.com/kataras/jwt v0.1.17 // indirect github.com/kataras/pio v0.0.13 // indirect github.com/kataras/sitemap v0.0.6 // indirect github.com/kataras/tunnel v0.0.4 // indirect github.com/klauspost/compress v1.18.2 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mailgun/raymond/v2 v2.0.48 // indirect github.com/mailru/easyjson v0.9.1 // indirect github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/schollz/closestmatch v2.1.0+incompatible // indirect github.com/segmentio/fasthash v1.0.3 // indirect github.com/sirupsen/logrus v1.9.2 // indirect github.com/stretchr/testify v1.11.1 // indirect github.com/tdewolff/minify/v2 v2.24.8 // indirect github.com/tdewolff/parse/v2 v2.8.5 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/yosssi/ace v0.0.5 // indirect golang.org/x/crypto v0.47.0 // indirect golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect golang.org/x/net v0.49.0 // indirect golang.org/x/sys v0.40.0 // indirect golang.org/x/text v0.33.0 // indirect golang.org/x/time v0.14.0 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: _examples/database/mysql/go.sum ================================================ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= github.com/CloudyKit/jet/v6 v6.3.1 h1:6IAo5Cx21xrHVaR8zzXN5gJatKV/wO7Nf6bfCnCSbUw= github.com/CloudyKit/jet/v6 v6.3.1/go.mod h1:lf8ksdNsxZt7/yH/3n4vJQWA9RUq4wpaHtArHhGVMOw= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/Joker/hpp v1.0.0 h1:65+iuJYdRXv/XyN62C1uEmmOx3432rNG/rKlX6V7Kkc= github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= github.com/Joker/jade v1.1.3 h1:Qbeh12Vq6BxURXT1qZBRHsDxeURB8ztcL6f3EXSGeHk= github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKduM= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 h1:dG7/gLroJGht/jSQtHiLvT48Hxn+crbmvyItZC8cWOs= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05/go.mod h1:NYezi6wtnJtBm5btoprXc5SvAdqH0XTXWnUup0MptAI= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/flosch/pongo2/v4 v4.0.2 h1:gv+5Pe3vaSVmiJvh/BZa82b7/00YUGm0PIyVVLop0Hw= github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+BWPhwZD8= github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a h1:l7A0loSszR5zHd/qK53ZIHMO8b3bBSmENnQ6eKnUT0A= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/iris-contrib/httpexpect/v2 v2.15.2 h1:T9THsdP1woyAqKHwjkEsbCnMefsAFvk8iJJKokcJ3Go= github.com/iris-contrib/httpexpect/v2 v2.15.2/go.mod h1:JLDgIqnFy5loDSUv1OA2j0mb6p/rDhiCqigP22Uq9xE= github.com/iris-contrib/schema v0.0.6 h1:CPSBLyx2e91H2yJzPuhGuifVRnZBBJ3pCOMbOvPZaTw= github.com/iris-contrib/schema v0.0.6/go.mod h1:iYszG0IOsuIsfzjymw1kMzTL8YQcCWlm65f3wX8J5iA= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kataras/blocks v0.0.8 h1:MrpVhoFTCR2v1iOOfGng5VJSILKeZZI+7NGfxEh3SUM= github.com/kataras/blocks v0.0.8/go.mod h1:9Jm5zx6BB+06NwA+OhTbHW1xkMOYxahnqTN5DveZ2Yg= github.com/kataras/golog v0.1.12 h1:Bu7I/G4ilJlbfzjmU39O9N+2uO1pBcMK045fzZ4ytNg= github.com/kataras/golog v0.1.12/go.mod h1:wrGSbOiBqbQSQznleVNX4epWM8rl9SJ/rmEacl0yqy4= github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd h1:oMsQgqKACbUdlaIWnN+EZzzAnHkw90hE/y/R0BSij2U= github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd/go.mod h1:yucjtDl9Vn3OctWM1fi0MuM9OckemC7kEreFa9QtPF8= github.com/kataras/jwt v0.1.17 h1:dYjemzcdYqA4ylwq9/56MslCr/pNOyVUZ2bl3hYNHgc= github.com/kataras/jwt v0.1.17/go.mod h1:HUnU5HDBCDanVF8zrPVSE2VK8HicospKefZDD4DzOKU= github.com/kataras/pio v0.0.13 h1:x0rXVX0fviDTXOOLOmr4MUxOabu1InVSTu5itF8CXCM= github.com/kataras/pio v0.0.13/go.mod h1:k3HNuSw+eJ8Pm2lA4lRhg3DiCjVgHlP8hmXApSej3oM= github.com/kataras/sitemap v0.0.6 h1:w71CRMMKYMJh6LR2wTgnk5hSgjVNB9KL60n5e2KHvLY= github.com/kataras/sitemap v0.0.6/go.mod h1:dW4dOCNs896OR1HmG+dMLdT7JjDk7mYBzoIRwuj5jA4= github.com/kataras/tunnel v0.0.4 h1:sCAqWuJV7nPzGrlb0os3j49lk2JhILT0rID38NHNLpA= github.com/kataras/tunnel v0.0.4/go.mod h1:9FkU4LaeifdMWqZu7o20ojmW4B7hdhv2CMLwfnHGpYw= github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailgun/groupcache/v2 v2.6.0 h1:w7+5ltoEwbrCk2LWyRRa7Ui9sRlvyBUaIqEU97kdh+0= github.com/mailgun/groupcache/v2 v2.6.0/go.mod h1:s509cRKQkn9+FUC42BG7A8kbTAywikZUOJtr1guhOkY= github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw= github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18= github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo= github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk= github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= github.com/segmentio/fasthash v1.0.3 h1:EI9+KE1EwvMLBWwjpRDc+fEM+prwxDYbslddQGtrmhM= github.com/segmentio/fasthash v1.0.3/go.mod h1:waKX8l2N8yckOgmSsXJi7x1ZfdKZ4x7KRMzBtS3oedY= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y= github.com/sirupsen/logrus v1.9.2/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tdewolff/minify/v2 v2.24.8 h1:58/VjsbevI4d5FGV0ZSuBrHMSSkH4MCH0sIz/eKIauE= github.com/tdewolff/minify/v2 v2.24.8/go.mod h1:0Ukj0CRpo/sW/nd8uZ4ccXaV1rEVIWA3dj8U7+Shhfw= github.com/tdewolff/parse/v2 v2.8.5 h1:ZmBiA/8Do5Rpk7bDye0jbbDUpXXbCdc3iah4VeUvwYU= github.com/tdewolff/parse/v2 v2.8.5/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo= github.com/tdewolff/test v1.0.11 h1:FdLbwQVHxqG16SlkGveC0JVyrJN62COWTRyUFzfbtBE= github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA= github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0= github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs= moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHcE= ================================================ FILE: _examples/database/mysql/main.go ================================================ package main // Look README.md import ( "fmt" "log" "os" "myapp/api" "myapp/sql" "github.com/kataras/iris/v12" ) func main() { dsn := fmt.Sprintf("%s:%s@tcp(%s:3306)/%s?parseTime=true&charset=utf8mb4&collation=utf8mb4_unicode_ci", getenv("MYSQL_USER", "user_myapp"), getenv("MYSQL_PASSWORD", "dbpassword"), getenv("MYSQL_HOST", "localhost"), getenv("MYSQL_DATABASE", "myapp"), ) db, err := sql.ConnectMySQL(dsn) if err != nil { log.Fatalf("error connecting to the MySQL database: %v", err) } secret := getenv("JWT_SECRET", "EbnJO3bwmX") app := iris.New() subRouter := api.Router(db, secret) app.PartyFunc("/", subRouter) addr := fmt.Sprintf(":%s", getenv("PORT", "8080")) app.Listen(addr) } func getenv(key string, def string) string { v := os.Getenv(key) if v == "" { return def } return v } ================================================ FILE: _examples/database/mysql/migration/api_category/create_category.json ================================================ { "title": "computer-internet", "position": 2, "image_url": "https://bp.pstatic.gr/public/dist/images/1mOPxYtw1k.webp" } ================================================ FILE: _examples/database/mysql/migration/api_category/insert_products_category.json ================================================ [{ "title": "product-1", "image_url": "https://images.product1.png", "price": 42.42, "description": "a description for product-1" }, { "title": "product-2", "image_url": "https://images.product2.png", "price": 32.1, "description": "a description for product-2" }, { "title": "product-3", "image_url": "https://images.product3.png", "price": 52321321.32, "description": "a description for product-3" }, { "title": "product-4", "image_url": "https://images.product4.png", "price": 77.4221, "description": "a description for product-4" }, { "title": "product-5", "image_url": "https://images.product5.png", "price": 55.1, "description": "a description for product-5" }, { "title": "product-6", "image_url": "https://images.product6.png", "price": 53.32, "description": "a description for product-6" }] ================================================ FILE: _examples/database/mysql/migration/api_category/update_category.json ================================================ { "id": 2, "position": 1, "title": "computers", "image_url": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/d7/Desktop_computer_clipart_-_Yellow_theme.svg/1200px-Desktop_computer_clipart_-_Yellow_theme.svg.png" } ================================================ FILE: _examples/database/mysql/migration/api_category/update_partial_category.json ================================================ { "title": "computers-technology" } ================================================ FILE: _examples/database/mysql/migration/api_postman.json ================================================ { "info": { "_postman_id": "7b8b53f8-859a-425a-aa9c-28bc2a2d5ef7", "name": "myapp (api-test)", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, "item": [ { "name": "Category", "item": [ { "name": "Create", "request": { "method": "POST", "header": [ { "key": "Authorization", "value": "Bearer {{token}}", "type": "text", "disabled": true } ], "body": { "mode": "raw", "raw": "{\r\n \"title\": \"computer-internet\",\r\n \"position\": 1,\r\n \"image_url\": \"https://bp.pstatic.gr/public/dist/images/1mOPxYtw1k.webp\"\r\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "http://localhost:8080/category", "protocol": "http", "host": [ "localhost" ], "port": "8080", "path": [ "category" ] }, "description": "Create a Category" }, "response": [] }, { "name": "Get By ID", "request": { "method": "GET", "header": [ { "key": "Authorization", "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2lyaXMtZ28uY29tIiwiYXVkIjpbIjljOTY5ZDg5LTBkZGYtNDlkMC1hYzcxLTI3MmU3YmY5NjkwMCJdLCJpYXQiOjE2MDYyNzE1NDYsImV4cCI6MTYwNjI3MjQ0Nn0.l2_5iqfEaC68UySTQNoDx2sfzn031tHiTdm2kZoNkWQ", "type": "text", "disabled": true } ], "url": { "raw": "http://localhost:8080/category/1", "protocol": "http", "host": [ "localhost" ], "port": "8080", "path": [ "category", "1" ] }, "description": "Get By ID" }, "response": [] }, { "name": "List", "protocolProfileBehavior": { "disableBodyPruning": true }, "request": { "method": "GET", "header": [ { "key": "Authorization", "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2lyaXMtZ28uY29tIiwiYXVkIjpbIjljOTY5ZDg5LTBkZGYtNDlkMC1hYzcxLTI3MmU3YmY5NjkwMCJdLCJpYXQiOjE2MDYyNzE1NDYsImV4cCI6MTYwNjI3MjQ0Nn0.l2_5iqfEaC68UySTQNoDx2sfzn031tHiTdm2kZoNkWQ", "type": "text", "disabled": true } ], "body": { "mode": "raw", "raw": "" }, "url": { "raw": "http://localhost:8080/category?offset=0&limit=30&order=asc", "protocol": "http", "host": [ "localhost" ], "port": "8080", "path": [ "category" ], "query": [ { "key": "offset", "value": "0" }, { "key": "limit", "value": "30" }, { "key": "order", "value": "asc" } ] }, "description": "Get many with limit offset" }, "response": [] }, { "name": "Update (Full)", "request": { "method": "PUT", "header": [ { "key": "Authorization", "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODk1ODU1NjN9.PtfDS1niGoZ7pV6kplI-_q1fVKLnknQ3IwcrLZhoVCU", "type": "text", "disabled": true } ], "body": { "mode": "raw", "raw": "{\r\n\t\"id\": 1,\r\n\t\"position\": 3,\r\n \"title\": \"computers\",\r\n \"image_url\":\"https://upload.wikimedia.org/wikipedia/commons/thumb/d/d7/Desktop_computer_clipart_-_Yellow_theme.svg/1200px-Desktop_computer_clipart_-_Yellow_theme.svg.png\"\r\n}" }, "url": { "raw": "http://localhost:8080/category", "protocol": "http", "host": [ "localhost" ], "port": "8080", "path": [ "category" ], "query": [ { "key": "", "value": null, "disabled": true } ] }, "description": "Update a Category (full update)" }, "response": [] }, { "name": "Delete By ID", "request": { "method": "DELETE", "header": [ { "key": "Authorization", "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODk1ODU1NjN9.PtfDS1niGoZ7pV6kplI-_q1fVKLnknQ3IwcrLZhoVCU", "type": "text", "disabled": true } ], "url": { "raw": "http://localhost:8080/category/1", "protocol": "http", "host": [ "localhost" ], "port": "8080", "path": [ "category", "1" ] }, "description": "Delete a Category" }, "response": [] }, { "name": "Update (Partial)", "request": { "method": "PATCH", "header": [ { "key": "Authorization", "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODk1ODU1NjN9.PtfDS1niGoZ7pV6kplI-_q1fVKLnknQ3IwcrLZhoVCU", "type": "text", "disabled": true } ], "body": { "mode": "raw", "raw": "{\r\n \"title\": \"computers-technology\"\r\n}" }, "url": { "raw": "http://localhost:8080/category/3", "protocol": "http", "host": [ "localhost" ], "port": "8080", "path": [ "category", "3" ] }, "description": "Update a Category partially, e.g. title only" }, "response": [] }, { "name": "List Products", "request": { "method": "GET", "header": [ { "key": "Authorization", "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2lyaXMtZ28uY29tIiwiYXVkIjpbIjljOTY5ZDg5LTBkZGYtNDlkMC1hYzcxLTI3MmU3YmY5NjkwMCJdLCJpYXQiOjE2MDYyNzE1NDYsImV4cCI6MTYwNjI3MjQ0Nn0.l2_5iqfEaC68UySTQNoDx2sfzn031tHiTdm2kZoNkWQ", "type": "text", "disabled": true } ], "url": { "raw": "http://localhost:8080/category/1/products?offset=0&limit=30&by=price&order=asc", "protocol": "http", "host": [ "localhost" ], "port": "8080", "path": [ "category", "1", "products" ], "query": [ { "key": "offset", "value": "0" }, { "key": "limit", "value": "30" }, { "key": "by", "value": "price" }, { "key": "order", "value": "asc" } ] }, "description": "Get products from cheap to expensive" }, "response": [] }, { "name": "Insert Products", "request": { "method": "POST", "header": [ { "key": "Authorization", "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODk1ODU1NjN9.PtfDS1niGoZ7pV6kplI-_q1fVKLnknQ3IwcrLZhoVCU", "type": "text", "disabled": true } ], "body": { "mode": "raw", "raw": "[{\r\n \"title\": \"product-1\",\r\n \"image_url\": \"https://images.product1.png\",\r\n \"price\": 42.42,\r\n \"description\": \"a description for product-1\"\r\n}, {\r\n \"title\": \"product-2\",\r\n \"image_url\": \"https://images.product2.png\",\r\n \"price\": 32.1,\r\n \"description\": \"a description for product-2\"\r\n}, {\r\n \"title\": \"product-3\",\r\n \"image_url\": \"https://images.product3.png\",\r\n \"price\": 52321321.32,\r\n \"description\": \"a description for product-3\"\r\n}, {\r\n \"title\": \"product-4\",\r\n \"image_url\": \"https://images.product4.png\",\r\n \"price\": 77.4221,\r\n \"description\": \"a description for product-4\"\r\n}, {\r\n \"title\": \"product-5\",\r\n \"image_url\": \"https://images.product5.png\",\r\n \"price\": 55.1,\r\n \"description\": \"a description for product-5\"\r\n}, {\r\n \"title\": \"product-6\",\r\n \"image_url\": \"https://images.product6.png\",\r\n \"price\": 53.32,\r\n \"description\": \"a description for product-6\"\r\n}]" }, "url": { "raw": "http://localhost:8080/category/3/products", "protocol": "http", "host": [ "localhost" ], "port": "8080", "path": [ "category", "3", "products" ] }, "description": "Batch Insert Products to a Category" }, "response": [] } ], "protocolProfileBehavior": {} }, { "name": "Product", "item": [ { "name": "List", "request": { "method": "GET", "header": [ { "key": "Authorization", "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2lyaXMtZ28uY29tIiwiYXVkIjpbIjljOTY5ZDg5LTBkZGYtNDlkMC1hYzcxLTI3MmU3YmY5NjkwMCJdLCJpYXQiOjE2MDYyNzE1NDYsImV4cCI6MTYwNjI3MjQ0Nn0.l2_5iqfEaC68UySTQNoDx2sfzn031tHiTdm2kZoNkWQ", "type": "text", "disabled": true } ], "url": { "raw": "http://localhost:8080/product?offset=0&limit=30&by=price&order=asc", "protocol": "http", "host": [ "localhost" ], "port": "8080", "path": [ "product" ], "query": [ { "key": "offset", "value": "0" }, { "key": "limit", "value": "30" }, { "key": "by", "value": "price" }, { "key": "order", "value": "asc" } ] }, "description": "List products" }, "response": [] }, { "name": "Get By ID", "request": { "method": "GET", "header": [ { "key": "Authorization", "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2lyaXMtZ28uY29tIiwiYXVkIjpbIjljOTY5ZDg5LTBkZGYtNDlkMC1hYzcxLTI3MmU3YmY5NjkwMCJdLCJpYXQiOjE2MDYyNzE1NDYsImV4cCI6MTYwNjI3MjQ0Nn0.l2_5iqfEaC68UySTQNoDx2sfzn031tHiTdm2kZoNkWQ", "type": "text", "disabled": true } ], "url": { "raw": "http://localhost:8080/product/1", "protocol": "http", "host": [ "localhost" ], "port": "8080", "path": [ "product", "1" ] }, "description": "Get a Product" }, "response": [] }, { "name": "Delete By ID", "request": { "method": "DELETE", "header": [ { "key": "Authorization", "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODk1ODU1NjN9.PtfDS1niGoZ7pV6kplI-_q1fVKLnknQ3IwcrLZhoVCU", "type": "text", "disabled": true } ], "url": { "raw": "http://localhost:8080/product/3", "protocol": "http", "host": [ "localhost" ], "port": "8080", "path": [ "product", "3" ] }, "description": "Delete a Product" }, "response": [] }, { "name": "Create", "request": { "method": "POST", "header": [ { "key": "Authorization", "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODk1ODU1NjN9.PtfDS1niGoZ7pV6kplI-_q1fVKLnknQ3IwcrLZhoVCU", "type": "text", "disabled": true } ], "body": { "mode": "raw", "raw": "{\r\n \"title\": \"product-1\",\r\n \"category_id\": 1,\r\n \"image_url\": \"https://images.product1.png\",\r\n \"price\": 42.42,\r\n \"description\": \"a description for product-1\"\r\n}" }, "url": { "raw": "http://localhost:8080/product", "protocol": "http", "host": [ "localhost" ], "port": "8080", "path": [ "product" ] }, "description": "Create a Product (and assign a category)" }, "response": [] }, { "name": "Update (Full)", "request": { "method": "PUT", "header": [ { "key": "Authorization", "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODk1ODU1NjN9.PtfDS1niGoZ7pV6kplI-_q1fVKLnknQ3IwcrLZhoVCU", "type": "text", "disabled": true } ], "body": { "mode": "raw", "raw": "{\r\n\t\"id\":19,\r\n \"title\": \"product-9-new\",\r\n \"category_id\": 1,\r\n \"image_url\": \"https://images.product19.png\",\r\n \"price\": 20,\r\n \"description\": \"a description for product-9-new\"\r\n}" }, "url": { "raw": "http://localhost:8080/product", "protocol": "http", "host": [ "localhost" ], "port": "8080", "path": [ "product" ] }, "description": "Update a Product (full-update)" }, "response": [] }, { "name": "Update (Partial)", "request": { "method": "PATCH", "header": [ { "key": "Authorization", "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODk1ODU1NjN9.PtfDS1niGoZ7pV6kplI-_q1fVKLnknQ3IwcrLZhoVCU", "type": "text", "disabled": true } ], "body": { "mode": "raw", "raw": "{\r\n \"title\": \"product-9-new-title\"\r\n}" }, "url": { "raw": "http://localhost:8080/product/9", "protocol": "http", "host": [ "localhost" ], "port": "8080", "path": [ "product", "9" ] }, "description": "Update a Product (partially)" }, "response": [] } ], "description": "Product Client API", "protocolProfileBehavior": {} }, { "name": "Get Token", "request": { "method": "GET", "header": [], "url": { "raw": "http://localhost:8080/token", "protocol": "http", "host": [ "localhost" ], "port": "8080", "path": [ "token" ] }, "description": "Get Token to access \"write\" (create, update and delete) endpoints" }, "response": [] } ], "auth": { "type": "bearer", "bearer": [ { "key": "token", "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2lyaXMtZ28uY29tIiwiYXVkIjpbIjljOTY5ZDg5LTBkZGYtNDlkMC1hYzcxLTI3MmU3YmY5NjkwMCJdLCJpYXQiOjE2MDYyNzE1NDYsImV4cCI6MTYwNjI3MjQ0Nn0.l2_5iqfEaC68UySTQNoDx2sfzn031tHiTdm2kZoNkWQ", "type": "string" } ] }, "event": [ { "listen": "prerequest", "script": { "id": "f27c3c2d-efdc-4922-b70c-258784a1d59b", "type": "text/javascript", "exec": [ "" ] } }, { "listen": "test", "script": { "id": "44d94797-9cc6-4ecd-adc5-7ad5329d79c4", "type": "text/javascript", "exec": [ "" ] } } ], "variable": [ { "id": "38156b9f-e623-4974-a315-51c931670f23", "key": "token", "value": "token" } ], "protocolProfileBehavior": {} } ================================================ FILE: _examples/database/mysql/migration/api_product/create_product.json ================================================ { "title": "product-1", "category_id": 3, "image_url": "https://images.product1.png", "price": 42.42, "description": "a description for product-1" } ================================================ FILE: _examples/database/mysql/migration/api_product/update_partial_product.json ================================================ { "title": "product-19-new-title" } ================================================ FILE: _examples/database/mysql/migration/api_product/update_product.json ================================================ { "id":19, "title": "product-19", "category_id": 3, "image_url": "https://images.product19.png", "price": 20, "description": "a description for product-19" } ================================================ FILE: _examples/database/mysql/migration/db.sql ================================================ CREATE DATABASE IF NOT EXISTS myapp DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE myapp; SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; DROP TABLE IF EXISTS categories; CREATE TABLE categories ( id int(11) NOT NULL AUTO_INCREMENT, title varchar(255) NOT NULL, position int(11) NOT NULL, image_url varchar(255) NOT NULL, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id) ); DROP TABLE IF EXISTS products; CREATE TABLE products ( id int(11) NOT NULL AUTO_INCREMENT, category_id int, title varchar(255) NOT NULL, image_url varchar(255) NOT NULL, price decimal(10,2) NOT NULL, description text NOT NULL, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), FOREIGN KEY (category_id) REFERENCES categories(id) ); SET FOREIGN_KEY_CHECKS = 1; ================================================ FILE: _examples/database/mysql/service/category_service.go ================================================ package service import ( "context" "fmt" "reflect" "myapp/entity" "myapp/sql" ) // CategoryService represents the category entity service. // Note that the given entity (request) should be already validated // before service's calls. type CategoryService struct { *sql.Service } // NewCategoryService returns a new category service to communicate with the database. func NewCategoryService(db sql.Database) *CategoryService { return &CategoryService{Service: sql.NewService(db, new(entity.Category))} } // Insert stores a category to the database and returns its ID. func (s *CategoryService) Insert(ctx context.Context, e entity.Category) (int64, error) { if e.Title == "" || e.ImageURL == "" { return 0, sql.ErrUnprocessable } q := fmt.Sprintf(`INSERT INTO %s (title, position, image_url) VALUES (?,?,?);`, e.TableName()) res, err := s.DB().Exec(ctx, q, e.Title, e.Position, e.ImageURL) if err != nil { return 0, err } return res.LastInsertId() } // Update updates a category based on its `ID`. func (s *CategoryService) Update(ctx context.Context, e entity.Category) (int, error) { if e.ID == 0 || e.Title == "" || e.ImageURL == "" { return 0, sql.ErrUnprocessable } q := fmt.Sprintf(`UPDATE %s SET title = ?, position = ?, image_url = ? WHERE %s = ?;`, e.TableName(), e.PrimaryKey()) res, err := s.DB().Exec(ctx, q, e.Title, e.Position, e.ImageURL, e.ID) if err != nil { return 0, err } n := sql.GetAffectedRows(res) return n, nil } // The updatable fields, separately from that we create for any possible future necessities. var categoryUpdateSchema = map[string]reflect.Kind{ "title": reflect.String, "image_url": reflect.String, "position": reflect.Int, } // PartialUpdate accepts a key-value map to // update the record based on the given "id". func (s *CategoryService) PartialUpdate(ctx context.Context, id int64, attrs map[string]any) (int, error) { return s.Service.PartialUpdate(ctx, id, categoryUpdateSchema, attrs) } ================================================ FILE: _examples/database/mysql/service/category_service_test.go ================================================ package service import ( "context" "testing" "myapp/entity" "myapp/sql" "github.com/DATA-DOG/go-sqlmock" ) func TestCategoryServiceInsert(t *testing.T) { conn, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) if err != nil { t.Fatal(err) } defer conn.Close() db := &sql.MySQL{Conn: conn} service := NewCategoryService(db) newCategory := entity.Category{ Title: "computer-internet", Position: 2, ImageURL: "https://animage", } mock.ExpectExec("INSERT INTO categories (title, position, image_url) VALUES (?,?,?);"). WithArgs(newCategory.Title, newCategory.Position, newCategory.ImageURL).WillReturnResult(sqlmock.NewResult(1, 1)) id, err := service.Insert(context.TODO(), newCategory) if err != nil { t.Fatal(err) } if id != 1 { t.Fatalf("expected ID to be 1 as this is the first entry") } if err = mock.ExpectationsWereMet(); err != nil { t.Fatal(err) } } ================================================ FILE: _examples/database/mysql/service/product_service.go ================================================ package service import ( "context" "fmt" "reflect" "strings" "myapp/entity" "myapp/sql" ) // ProductService represents the product entity service. // Note that the given entity (request) should be already validated // before service's calls. type ProductService struct { *sql.Service rec sql.Record } // NewProductService returns a new product service to communicate with the database. func NewProductService(db sql.Database) *ProductService { return &ProductService{Service: sql.NewService(db, new(entity.Product))} } // Insert stores a product to the database and returns its ID. func (s *ProductService) Insert(ctx context.Context, e entity.Product) (int64, error) { if !e.ValidateInsert() { return 0, sql.ErrUnprocessable } q := fmt.Sprintf(`INSERT INTO %s (category_id, title, image_url, price, description) VALUES (?,?,?,?,?);`, e.TableName()) res, err := s.DB().Exec(ctx, q, e.CategoryID, e.Title, e.ImageURL, e.Price, e.Description) if err != nil { return 0, err } return res.LastInsertId() } // BatchInsert inserts one or more products at once and returns the total length created. func (s *ProductService) BatchInsert(ctx context.Context, products []entity.Product) (int, error) { if len(products) == 0 { return 0, nil } var ( valuesLines []string args []any ) for _, p := range products { if !p.ValidateInsert() { // all products should be "valid", we don't skip, we cancel. return 0, sql.ErrUnprocessable } valuesLines = append(valuesLines, "(?,?,?,?,?)") args = append(args, []any{p.CategoryID, p.Title, p.ImageURL, p.Price, p.Description}...) } q := fmt.Sprintf("INSERT INTO %s (category_id, title, image_url, price, description) VALUES %s;", s.RecordInfo().TableName(), strings.Join(valuesLines, ", ")) res, err := s.DB().Exec(ctx, q, args...) if err != nil { return 0, err } n := sql.GetAffectedRows(res) return n, nil } // Update updates a product based on its `ID` from the database // and returns the affected numbrer (0 when nothing changed otherwise 1). func (s *ProductService) Update(ctx context.Context, e entity.Product) (int, error) { q := fmt.Sprintf(`UPDATE %s SET category_id = ?, title = ?, image_url = ?, price = ?, description = ? WHERE %s = ?;`, e.TableName(), e.PrimaryKey()) res, err := s.DB().Exec(ctx, q, e.CategoryID, e.Title, e.ImageURL, e.Price, e.Description, e.ID) if err != nil { return 0, err } n := sql.GetAffectedRows(res) return n, nil } var productUpdateSchema = map[string]reflect.Kind{ "category_id": reflect.Int, "title": reflect.String, "image_url": reflect.String, "price": reflect.Float32, "description": reflect.String, } // PartialUpdate accepts a key-value map to // update the record based on the given "id". func (s *ProductService) PartialUpdate(ctx context.Context, id int64, attrs map[string]any) (int, error) { return s.Service.PartialUpdate(ctx, id, productUpdateSchema, attrs) } ================================================ FILE: _examples/database/mysql/sql/mysql.go ================================================ package sql import ( "context" "database/sql" "fmt" _ "github.com/go-sql-driver/mysql" // lint: mysql driver. ) // MySQL holds the underline connection of a MySQL (or MariaDB) database. // See the `ConnectMySQL` package-level function. type MySQL struct { Conn *sql.DB } var _ Database = (*MySQL)(nil) var ( // DefaultCharset default charset parameter for new databases. DefaultCharset = "utf8mb4" // DefaultCollation default collation parameter for new databases. DefaultCollation = "utf8mb4_unicode_ci" ) // ConnectMySQL returns a new ready to use MySQL Database instance. // Accepts a single argument of "dsn", i.e: // username:password@tcp(localhost:3306)/myapp?parseTime=true&charset=utf8mb4&collation=utf8mb4_unicode_ci func ConnectMySQL(dsn string) (*MySQL, error) { conn, err := sql.Open("mysql", dsn) if err != nil { return nil, err } err = conn.Ping() if err != nil { conn.Close() return nil, err } return &MySQL{ Conn: conn, }, nil } // CreateDatabase executes the CREATE DATABASE query. func (db *MySQL) CreateDatabase(database string) error { q := fmt.Sprintf("CREATE DATABASE %s DEFAULT CHARSET = %s COLLATE = %s;", database, DefaultCharset, DefaultCollation) _, err := db.Conn.Exec(q) return err } // Drop executes the DROP DATABASE query. func (db *MySQL) Drop(database string) error { q := fmt.Sprintf("DROP DATABASE %s;", database) _, err := db.Conn.Exec(q) return err } // Select performs the SELECT query for this database (dsn database name is required). func (db *MySQL) Select(ctx context.Context, dest any, query string, args ...any) error { rows, err := db.Conn.QueryContext(ctx, query, args...) if err != nil { return err } defer rows.Close() if scannable, ok := dest.(Scannable); ok { return scannable.Scan(rows) } if !rows.Next() { return ErrNoRows } return rows.Scan(dest) /* Uncomment this and pass a slice if u want to see reflection powers <3 v, ok := dest.(reflect.Value) if !ok { v = reflect.Indirect(reflect.ValueOf(dest)) } sliceTyp := v.Type() if sliceTyp.Kind() != reflect.Slice { sliceTyp = reflect.SliceOf(sliceTyp) } sliceElementTyp := deref(sliceTyp.Elem()) for rows.Next() { obj := reflect.New(sliceElementTyp) obj.Interface().(Scannable).Scan(rows) if err != nil { return err } v.Set(reflect.Append(v, reflect.Indirect(obj))) } */ } // Get same as `Select` but it moves the cursor to the first result. func (db *MySQL) Get(ctx context.Context, dest any, query string, args ...any) error { rows, err := db.Conn.QueryContext(ctx, query, args...) if err != nil { return err } defer rows.Close() if !rows.Next() { return ErrNoRows } if scannable, ok := dest.(Scannable); ok { return scannable.Scan(rows) } return rows.Scan(dest) } // Exec executes a query. It does not return any rows. // Use the first output parameter to count the affected rows on UPDATE, INSERT, or DELETE. func (db *MySQL) Exec(ctx context.Context, query string, args ...any) (sql.Result, error) { return db.Conn.ExecContext(ctx, query, args...) } ================================================ FILE: _examples/database/mysql/sql/service.go ================================================ package sql import ( "context" "database/sql" "errors" "fmt" "net/url" "reflect" "strconv" "strings" ) // Service holder for common queries. // Note: each entity service keeps its own base Service instance. type Service struct { db Database rec Record // see `Count`, `List` and `DeleteByID` methods. } // NewService returns a new (SQL) base service for common operations. func NewService(db Database, of Record) *Service { return &Service{db: db, rec: of} } // DB exposes the database instance. func (s *Service) DB() Database { return s.db } // RecordInfo returns the record info provided through `NewService`. func (s *Service) RecordInfo() Record { return s.rec } // ErrNoRows is returned when GET doesn't return a row. // A shortcut of sql.ErrNoRows. var ErrNoRows = sql.ErrNoRows // GetByID binds a single record from the databases to the "dest". func (s *Service) GetByID(ctx context.Context, dest any, id int64) error { q := fmt.Sprintf("SELECT * FROM %s WHERE %s = ? LIMIT 1", s.rec.TableName(), s.rec.PrimaryKey()) err := s.db.Get(ctx, dest, q, id) return err // if err != nil { // if err == sql.ErrNoRows { // return false, nil // } // return false, err // } // return true, nil } // Count returns the total records count in the table. func (s *Service) Count(ctx context.Context) (total int64, err error) { q := fmt.Sprintf("SELECT COUNT(DISTINCT %s) FROM %s", s.rec.PrimaryKey(), s.rec.TableName()) if err = s.db.Select(ctx, &total, q); err == sql.ErrNoRows { err = nil } return } // ListOptions holds the options to be passed on the `Service.List` method. type ListOptions struct { Table string // the table name. Offset uint64 // inclusive. Limit uint64 OrderByColumn string Order string // "ASC" or "DESC" (could be a bool type instead). WhereColumn string WhereValue any } // Where accepts a column name and column value to set // on the WHERE clause of the result query. // It returns a new `ListOptions` value. // Note that this is a basic implementation which just takes care our current needs. func (opt ListOptions) Where(colName string, colValue any) ListOptions { opt.WhereColumn = colName opt.WhereValue = colValue return opt } // BuildQuery returns the query and the arguments that // should be form a SELECT command. func (opt ListOptions) BuildQuery() (q string, args []any) { q = fmt.Sprintf("SELECT * FROM %s", opt.Table) if opt.WhereColumn != "" && opt.WhereValue != nil { q += fmt.Sprintf(" WHERE %s = ?", opt.WhereColumn) args = append(args, opt.WhereValue) } if opt.OrderByColumn != "" { q += fmt.Sprintf(" ORDER BY %s %s", opt.OrderByColumn, ParseOrder(opt.Order)) } if opt.Limit > 0 { q += fmt.Sprintf(" LIMIT %d", opt.Limit) // offset below. } if opt.Offset > 0 { q += fmt.Sprintf(" OFFSET %d", opt.Offset) } return } // const defaultLimit = 30 // default limit if not set. // ParseListOptions returns a `ListOptions` from a map[string][]string. func ParseListOptions(q url.Values) ListOptions { offset, _ := strconv.ParseUint(q.Get("offset"), 10, 64) limit, _ := strconv.ParseUint(q.Get("limit"), 10, 64) order := q.Get("order") // empty, asc(...) or desc(...). return ListOptions{Offset: offset, Limit: limit, Order: order} } // List binds one or more records from the database to the "dest". // If the record supports ordering then it will sort by the `Sorted.OrderBy` column name(s). // Use the "order" input parameter to set a descending order ("DESC"). func (s *Service) List(ctx context.Context, dest any, opts ListOptions) error { // Set table and order by column from record info for `List` by options // so it can be more flexible to perform read-only calls of other table's too. if opts.Table == "" { // If missing then try to set it by record info. opts.Table = s.rec.TableName() } if b, ok := s.rec.(Sorted); ok { opts.OrderByColumn = b.SortBy() } q, args := opts.BuildQuery() return s.db.Select(ctx, dest, q, args...) } // DeleteByID removes a single record of "dest" from the database. func (s *Service) DeleteByID(ctx context.Context, id int64) (int, error) { q := fmt.Sprintf("DELETE FROM %s WHERE %s = ? LIMIT 1", s.rec.TableName(), s.rec.PrimaryKey()) res, err := s.db.Exec(ctx, q, id) if err != nil { return 0, err } return GetAffectedRows(res), nil } // ErrUnprocessable indicates error caused by invalid entity (entity's key-values). // The syntax of the request entity is correct, but it was unable to process the contained instructions // e.g. empty or unsupported value. // // See `../service/XService.Insert` and `../service/XService.Update` // and `PartialUpdate`. var ErrUnprocessable = errors.New("invalid entity") // PartialUpdate accepts a columns schema and a key-value map to // update the record based on the given "id". // Note: Trivial string, int and boolean type validations are performed here. func (s *Service) PartialUpdate(ctx context.Context, id int64, schema map[string]reflect.Kind, attrs map[string]any) (int, error) { if len(schema) == 0 || len(attrs) == 0 { return 0, nil } var ( keyLines []string values []any ) for key, kind := range schema { v, ok := attrs[key] if !ok { continue } switch v.(type) { case string: if kind != reflect.String { return 0, ErrUnprocessable } case int: if kind != reflect.Int { return 0, ErrUnprocessable } case bool: if kind != reflect.Bool { return 0, ErrUnprocessable } } keyLines = append(keyLines, fmt.Sprintf("%s = ?", key)) values = append(values, v) } if len(values) == 0 { return 0, nil } q := fmt.Sprintf("UPDATE %s SET %s WHERE %s = ?;", s.rec.TableName(), strings.Join(keyLines, ", "), s.rec.PrimaryKey()) res, err := s.DB().Exec(ctx, q, append(values, id)...) if err != nil { return 0, err } n := GetAffectedRows(res) return n, nil } // GetAffectedRows returns the number of affected rows after // a DELETE or UPDATE operation. func GetAffectedRows(result sql.Result) int { if result == nil { return 0 } n, _ := result.RowsAffected() return int(n) } const ( ascending = "ASC" descending = "DESC" ) // ParseOrder accept an order string and returns a valid mysql ORDER clause. // Defaults to "ASC". Two possible outputs: "ASC" and "DESC". func ParseOrder(order string) string { order = strings.TrimSpace(order) if len(order) >= 4 { if strings.HasPrefix(strings.ToUpper(order), descending) { return descending } } return ascending } ================================================ FILE: _examples/database/mysql/sql/sql.go ================================================ package sql import ( "context" "database/sql" ) // Database is an interface which a database(sql) should implement. type Database interface { Get(ctx context.Context, dest any, q string, args ...any) error Select(ctx context.Context, dest any, q string, args ...any) error Exec(ctx context.Context, q string, args ...any) (sql.Result, error) } // Record should represent a database record. // It holds the table name and the primary key. // Entities should implement that // in order to use the BaseService's methods. type Record interface { TableName() string // the table name which record belongs to. PrimaryKey() string // the primary key of the record. } // Sorted should represent a set of database records // that should be rendered with order. // // It does NOT support the form of // column1 ASC, // column2 DESC // The OrderBy method should return text in form of: // column1 // or column1, column2 type Sorted interface { SortBy() string // column names separated by comma. } // Scannable for go structs to bind their fields. type Scannable interface { Scan(*sql.Rows) error } ================================================ FILE: _examples/database/orm/gorm/REAMDE.md ================================================ # GORM This example is pull by [#1275 PR](https://github.com/kataras/iris/pull/1275) by [@wuxiaoxiaoshen](https://github.com/wuxiaoxiaoshen). A more complete and real-world example can be found at the project created by [@snowlyg](https://github.com/snowlyg). ================================================ FILE: _examples/database/orm/gorm/main.go ================================================ package main import ( "net/http" "os" "time" "github.com/jinzhu/gorm" "github.com/kataras/iris/v12" _ "github.com/mattn/go-sqlite3" ) type User struct { gorm.Model Salt string `gorm:"type:varchar(255)" json:"salt"` Username string `gorm:"type:varchar(32)" json:"username"` Password string `gorm:"type:varchar(200);column:password" json:"-"` Languages string `gorm:"type:varchar(200);column:languages" json:"languages"` } func (u User) TableName() string { return "gorm_user" } type UserSerializer struct { ID uint `json:"id"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` Salt string `json:"salt"` UserName string `json:"user_name"` Password string `json:"-"` Languages string `json:"languages"` } func (self User) Serializer() UserSerializer { return UserSerializer{ ID: self.ID, CreatedAt: self.CreatedAt.Truncate(time.Second), UpdatedAt: self.UpdatedAt.Truncate(time.Second), Salt: self.Salt, Password: self.Password, Languages: self.Languages, UserName: self.Username, } } func main() { app := iris.Default() db, err := gorm.Open("sqlite3", "test.db") db.LogMode(true) // show SQL logger if err != nil { app.Logger().Fatalf("connect to sqlite3 failed") return } iris.RegisterOnInterrupt(func() { defer db.Close() }) if os.Getenv("ENV") != "" { db.DropTableIfExists(&User{}) // drop table } db.AutoMigrate(&User{}) // create table: // AutoMigrate run auto migration for given models, will only add missing fields, won't delete/change current data app.Post("/post_user", func(ctx iris.Context) { var user User user = User{ Username: "gorm", Salt: "hash---", Password: "admin", Languages: "gorm", } if err := db.FirstOrCreate(&user); err == nil { app.Logger().Fatalf("created one record failed: %s", err.Error) ctx.JSON(iris.Map{ "code": http.StatusBadRequest, "error": err.Error, }) return } ctx.JSON( iris.Map{ "code": http.StatusOK, "data": user.Serializer(), }) }) app.Get("/get_user/{id:uint}", func(ctx iris.Context) { var user User id, _ := ctx.Params().GetUint("id") app.Logger().Println(id) if err := db.Where("id = ?", int(id)).First(&user).Error; err != nil { app.Logger().Fatalf("find one record failed: %t", err == nil) ctx.JSON(iris.Map{ "code": http.StatusBadRequest, "error": err.Error, }) return } ctx.JSON(iris.Map{ "code": http.StatusOK, "data": user.Serializer(), }) }) app.Delete("/delete_user/{id:uint}", func(ctx iris.Context) { id, _ := ctx.Params().GetUint("id") if id == 0 { ctx.JSON(iris.Map{ "code": http.StatusOK, "detail": "query param id should not be nil", }) return } var user User if err := db.Where("id = ?", id).First(&user).Error; err != nil { app.Logger().Fatalf("record not found") ctx.JSON(iris.Map{ "code": http.StatusOK, "detail": err.Error, }) return } db.Delete(&user) ctx.JSON(iris.Map{ "code": http.StatusOK, "data": user.Serializer(), }) }) app.Patch("/patch_user/{id:uint}", func(ctx iris.Context) { id, _ := ctx.Params().GetUint("id") if id == 0 { ctx.JSON(iris.Map{ "code": http.StatusOK, "detail": "query param id should not be nil", }) return } var user User tx := db.Begin() if err := tx.Where("id = ?", id).First(&user).Error; err != nil { app.Logger().Fatalf("record not found") ctx.JSON(iris.Map{ "code": http.StatusOK, "detail": err.Error, }) return } var body patchParam ctx.ReadJSON(&body) app.Logger().Println(body) if err := tx.Model(&user).Updates(map[string]any{"username": body.Data.UserName, "password": body.Data.Password}).Error; err != nil { app.Logger().Fatalf("update record failed") tx.Rollback() ctx.JSON(iris.Map{ "code": http.StatusBadRequest, "error": err.Error, }) return } tx.Commit() ctx.JSON(iris.Map{ "code": http.StatusOK, "data": user.Serializer(), }) }) app.Listen(":8080") } type patchParam struct { Data struct { UserName string `json:"user_name" form:"user_name"` Password string `json:"password" form:"password"` } `json:"data"` } ================================================ FILE: _examples/database/orm/reform/controllers/person_controller.go ================================================ package controllers import ( "net" "time" "myapp/models" "github.com/kataras/golog" "gopkg.in/reform.v1" ) // PersonController is the model.Person's web controller. type PersonController struct { DB *reform.DB // Logger and IP fields are automatically binded by the framework. Logger *golog.Logger // binds to the application's logger. IP net.IP // binds to the client's IP. } // Get handles // GET /persons func (c *PersonController) Get() ([]reform.Struct, error) { return c.DB.SelectAllFrom(models.PersonTable, "") } // GetBy handles // GET /persons/{ID} func (c *PersonController) GetBy(id int32) (reform.Record, error) { return c.DB.FindByPrimaryKeyFrom(models.PersonTable, id) } // Post handles // POST /persons with JSON request body of model.Person. func (c *PersonController) Post(p *models.Person) int { p.CreatedAt = time.Now() if err := c.DB.Save(p); err != nil { c.Logger.Errorf("[%s] create person: %v", c.IP.String(), err) return 500 // iris.StatusInternalServerError } c.Logger.Debugf("[%s] create person [%s] succeed", c.IP.String(), p.Name) return 201 // iris.StatusCreated } ================================================ FILE: _examples/database/orm/reform/go.mod ================================================ module myapp go 1.25 require ( github.com/kataras/golog v0.1.12 github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd github.com/mattn/go-sqlite3 v1.14.33 gopkg.in/reform.v1 v1.5.1 ) require ( github.com/BurntSushi/toml v1.6.0 // indirect github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect github.com/CloudyKit/jet/v6 v6.3.1 // indirect github.com/Joker/jade v1.1.3 // indirect github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 // indirect github.com/andybalholm/brotli v1.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/fatih/structs v1.1.0 // indirect github.com/flosch/pongo2/v4 v4.0.2 // indirect github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/ws v1.4.0 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/iris-contrib/schema v0.0.6 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/kataras/blocks v0.0.8 // indirect github.com/kataras/neffos v0.0.24 // indirect github.com/kataras/pio v0.0.13 // indirect github.com/kataras/sitemap v0.0.6 // indirect github.com/kataras/tunnel v0.0.4 // indirect github.com/klauspost/compress v1.18.2 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mailgun/raymond/v2 v2.0.48 // indirect github.com/mailru/easyjson v0.9.1 // indirect github.com/mediocregopher/radix/v3 v3.8.1 // indirect github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/nats-io/nats.go v1.40.1 // indirect github.com/nats-io/nkeys v0.4.9 // indirect github.com/nats-io/nuid v1.0.1 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/schollz/closestmatch v2.1.0+incompatible // indirect github.com/sirupsen/logrus v1.8.1 // indirect github.com/stretchr/testify v1.11.1 // indirect github.com/tdewolff/minify/v2 v2.24.8 // indirect github.com/tdewolff/parse/v2 v2.8.5 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/yosssi/ace v0.0.5 // indirect golang.org/x/crypto v0.47.0 // indirect golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect golang.org/x/net v0.49.0 // indirect golang.org/x/sys v0.40.0 // indirect golang.org/x/text v0.33.0 // indirect golang.org/x/time v0.14.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: _examples/database/orm/reform/go.sum ================================================ github.com/AlekSi/pointer v1.1.0 h1:SSDMPcXD9jSl8FPy9cRzoRaMJtm9g9ggGTxecRUbQoI= github.com/AlekSi/pointer v1.1.0/go.mod h1:y7BvfRI3wXPWKXEBhU71nbnIEEZX0QTSB2Bj48UJIZE= github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= github.com/CloudyKit/jet/v6 v6.3.1 h1:6IAo5Cx21xrHVaR8zzXN5gJatKV/wO7Nf6bfCnCSbUw= github.com/CloudyKit/jet/v6 v6.3.1/go.mod h1:lf8ksdNsxZt7/yH/3n4vJQWA9RUq4wpaHtArHhGVMOw= github.com/Joker/hpp v1.0.0 h1:65+iuJYdRXv/XyN62C1uEmmOx3432rNG/rKlX6V7Kkc= github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= github.com/Joker/jade v1.1.3 h1:Qbeh12Vq6BxURXT1qZBRHsDxeURB8ztcL6f3EXSGeHk= github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKduM= github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 h1:dG7/gLroJGht/jSQtHiLvT48Hxn+crbmvyItZC8cWOs= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05/go.mod h1:NYezi6wtnJtBm5btoprXc5SvAdqH0XTXWnUup0MptAI= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/brianvoe/gofakeit v3.18.0+incompatible h1:wDOmHc9DLG4nRjUVVaxA+CEglKOW72Y5+4WNxUIkjM8= github.com/brianvoe/gofakeit v3.18.0+incompatible/go.mod h1:kfwdRA90vvNhPutZWfH7WPaDzUjz+CZFqG+rPkOjGOc= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/denisenkom/go-mssqldb v0.9.0 h1:RSohk2RsiZqLZ0zCjtfn3S4Gp4exhpBWHyQ7D0yGjAk= github.com/denisenkom/go-mssqldb v0.9.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/flosch/pongo2/v4 v4.0.2 h1:gv+5Pe3vaSVmiJvh/BZa82b7/00YUGm0PIyVVLop0Hw= github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+BWPhwZD8= github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a h1:l7A0loSszR5zHd/qK53ZIHMO8b3bBSmENnQ6eKnUT0A= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/iris-contrib/httpexpect/v2 v2.15.2 h1:T9THsdP1woyAqKHwjkEsbCnMefsAFvk8iJJKokcJ3Go= github.com/iris-contrib/httpexpect/v2 v2.15.2/go.mod h1:JLDgIqnFy5loDSUv1OA2j0mb6p/rDhiCqigP22Uq9xE= github.com/iris-contrib/schema v0.0.6 h1:CPSBLyx2e91H2yJzPuhGuifVRnZBBJ3pCOMbOvPZaTw= github.com/iris-contrib/schema v0.0.6/go.mod h1:iYszG0IOsuIsfzjymw1kMzTL8YQcCWlm65f3wX8J5iA= github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ= github.com/jackc/pgx v3.6.2+incompatible h1:2zP5OD7kiyR3xzRYMhOcXVvkDZsImVXfj+yIyTQf3/o= github.com/jackc/pgx v3.6.2+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kataras/blocks v0.0.8 h1:MrpVhoFTCR2v1iOOfGng5VJSILKeZZI+7NGfxEh3SUM= github.com/kataras/blocks v0.0.8/go.mod h1:9Jm5zx6BB+06NwA+OhTbHW1xkMOYxahnqTN5DveZ2Yg= github.com/kataras/golog v0.1.12 h1:Bu7I/G4ilJlbfzjmU39O9N+2uO1pBcMK045fzZ4ytNg= github.com/kataras/golog v0.1.12/go.mod h1:wrGSbOiBqbQSQznleVNX4epWM8rl9SJ/rmEacl0yqy4= github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd h1:oMsQgqKACbUdlaIWnN+EZzzAnHkw90hE/y/R0BSij2U= github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd/go.mod h1:yucjtDl9Vn3OctWM1fi0MuM9OckemC7kEreFa9QtPF8= github.com/kataras/neffos v0.0.24 h1:S3lHqJopCfXN285VdlbGeOj+Id83u4xdQKToa+w1vW0= github.com/kataras/neffos v0.0.24/go.mod h1:/3K9zQ0yEC5/xUiSQx46ToWa3xneGfUo/nMit/F5g+U= github.com/kataras/pio v0.0.13 h1:x0rXVX0fviDTXOOLOmr4MUxOabu1InVSTu5itF8CXCM= github.com/kataras/pio v0.0.13/go.mod h1:k3HNuSw+eJ8Pm2lA4lRhg3DiCjVgHlP8hmXApSej3oM= github.com/kataras/sitemap v0.0.6 h1:w71CRMMKYMJh6LR2wTgnk5hSgjVNB9KL60n5e2KHvLY= github.com/kataras/sitemap v0.0.6/go.mod h1:dW4dOCNs896OR1HmG+dMLdT7JjDk7mYBzoIRwuj5jA4= github.com/kataras/tunnel v0.0.4 h1:sCAqWuJV7nPzGrlb0os3j49lk2JhILT0rID38NHNLpA= github.com/kataras/tunnel v0.0.4/go.mod h1:9FkU4LaeifdMWqZu7o20ojmW4B7hdhv2CMLwfnHGpYw= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v1.8.0 h1:9xohqzkUwzR4Ga4ivdTcawVS89YSDVxXMa3xJX3cGzg= github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw= github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18= github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0= github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mediocregopher/radix/v3 v3.8.1 h1:rOkHflVuulFKlwsLY01/M2cM2tWCjDoETcMqKbAWu1M= github.com/mediocregopher/radix/v3 v3.8.1/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/nats-io/nats.go v1.40.1 h1:MLjDkdsbGUeCMKFyCFoLnNn/HDTqcgVa3EQm+pMNDPk= github.com/nats-io/nats.go v1.40.1/go.mod h1:wV73x0FSI/orHPSYoyMeJB+KajMDoWyXmFaRrrYaaTo= github.com/nats-io/nkeys v0.4.9 h1:qe9Faq2Gxwi6RZnZMXfmGMZkg3afLLOtrU+gDZJ35b0= github.com/nats-io/nkeys v0.4.9/go.mod h1:jcMqs+FLG+W5YO36OX6wFIFcmpdAns+w1Wm6D3I/evE= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo= github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk= github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tdewolff/minify/v2 v2.24.8 h1:58/VjsbevI4d5FGV0ZSuBrHMSSkH4MCH0sIz/eKIauE= github.com/tdewolff/minify/v2 v2.24.8/go.mod h1:0Ukj0CRpo/sW/nd8uZ4ccXaV1rEVIWA3dj8U7+Shhfw= github.com/tdewolff/parse/v2 v2.8.5 h1:ZmBiA/8Do5Rpk7bDye0jbbDUpXXbCdc3iah4VeUvwYU= github.com/tdewolff/parse/v2 v2.8.5/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo= github.com/tdewolff/test v1.0.11 h1:FdLbwQVHxqG16SlkGveC0JVyrJN62COWTRyUFzfbtBE= github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA= github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0= github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/reform.v1 v1.5.1 h1:7vhDFW1n1xAPC6oDSvIvVvpRkaRpXlxgJ4QB4s3aDdo= gopkg.in/reform.v1 v1.5.1/go.mod h1:AIv0CbDRJ0ljQwptGeaIXfpDRo02uJwTq92aMFELEeU= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs= moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHcE= ================================================ FILE: _examples/database/orm/reform/main.go ================================================ package main /* $ go get gopkg.in/reform.v1/reform $ go generate ./models $ go run . Read more at: https://github.com/go-reform/reform */ import ( "database/sql" "myapp/controllers" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/mvc" _ "github.com/mattn/go-sqlite3" "gopkg.in/reform.v1" "gopkg.in/reform.v1/dialects/sqlite3" ) func main() { app := iris.New() app.Logger().SetLevel("debug") sqlDB, err := sql.Open("sqlite3", "./myapp.db") if err != nil { panic(err) } defer sqlDB.Close() sqlStmt := ` drop table people; create table people (id integer not null primary key, name text, email text, created_at datetime not null, updated_at datetime null); delete from people; ` _, err = sqlDB.Exec(sqlStmt) if err != nil { panic(err) } db := reform.NewDB(sqlDB, sqlite3.Dialect, reform.NewPrintfLogger(app.Logger().Debugf)) mvcApp := mvc.New(app.Party("/persons")) mvcApp.Register(db) mvcApp.Handle(new(controllers.PersonController)) app.Listen(":8080") } ================================================ FILE: _examples/database/orm/reform/models/person.go ================================================ //go:generate reform package models import "time" //reform:people type Person struct { ID int32 `reform:"id,pk" json:"id"` Name string `reform:"name" json:"name"` Email *string `reform:"email" json:"email"` CreatedAt time.Time `reform:"created_at" json:"created_at"` UpdatedAt *time.Time `reform:"updated_at" json:"updated_at"` } ================================================ FILE: _examples/database/orm/reform/models/person_reform.go ================================================ // Code generated by gopkg.in/reform.v1. DO NOT EDIT. package models import ( "fmt" "strings" "gopkg.in/reform.v1" "gopkg.in/reform.v1/parse" ) type personTableType struct { s parse.StructInfo z []any } // Schema returns a schema name in SQL database (""). func (v *personTableType) Schema() string { return v.s.SQLSchema } // Name returns a view or table name in SQL database ("people"). func (v *personTableType) Name() string { return v.s.SQLName } // Columns returns a new slice of column names for that view or table in SQL database. func (v *personTableType) Columns() []string { return []string{"id", "name", "email", "created_at", "updated_at"} } // NewStruct makes a new struct for that view or table. func (v *personTableType) NewStruct() reform.Struct { return new(Person) } // NewRecord makes a new record for that table. func (v *personTableType) NewRecord() reform.Record { return new(Person) } // PKColumnIndex returns an index of primary key column for that table in SQL database. func (v *personTableType) PKColumnIndex() uint { return uint(v.s.PKFieldIndex) } // PersonTable represents people view or table in SQL database. var PersonTable = &personTableType{ s: parse.StructInfo{Type: "Person", SQLSchema: "", SQLName: "people", Fields: []parse.FieldInfo{{Name: "ID", Type: "int32", Column: "id"}, {Name: "Name", Type: "string", Column: "name"}, {Name: "Email", Type: "*string", Column: "email"}, {Name: "CreatedAt", Type: "time.Time", Column: "created_at"}, {Name: "UpdatedAt", Type: "*time.Time", Column: "updated_at"}}, PKFieldIndex: 0}, z: new(Person).Values(), } // String returns a string representation of this struct or record. func (s Person) String() string { res := make([]string, 5) res[0] = "ID: " + reform.Inspect(s.ID, true) res[1] = "Name: " + reform.Inspect(s.Name, true) res[2] = "Email: " + reform.Inspect(s.Email, true) res[3] = "CreatedAt: " + reform.Inspect(s.CreatedAt, true) res[4] = "UpdatedAt: " + reform.Inspect(s.UpdatedAt, true) return strings.Join(res, ", ") } // Values returns a slice of struct or record field values. // Returned any values are never untyped nils. func (s *Person) Values() []any { return []any{ s.ID, s.Name, s.Email, s.CreatedAt, s.UpdatedAt, } } // Pointers returns a slice of pointers to struct or record fields. // Returned any values are never untyped nils. func (s *Person) Pointers() []any { return []any{ &s.ID, &s.Name, &s.Email, &s.CreatedAt, &s.UpdatedAt, } } // View returns View object for that struct. func (s *Person) View() reform.View { return PersonTable } // Table returns Table object for that record. func (s *Person) Table() reform.Table { return PersonTable } // PKValue returns a value of primary key for that record. // Returned any value is never untyped nil. func (s *Person) PKValue() any { return s.ID } // PKPointer returns a pointer to primary key field for that record. // Returned any value is never untyped nil. func (s *Person) PKPointer() any { return &s.ID } // HasPK returns true if record has non-zero primary key set, false otherwise. func (s *Person) HasPK() bool { return s.ID != PersonTable.z[PersonTable.s.PKFieldIndex] } // SetPK sets record primary key. func (s *Person) SetPK(pk any) { if i64, ok := pk.(int64); ok { s.ID = int32(i64) } else { s.ID = pk.(int32) } } // check interfaces var ( _ reform.View = PersonTable _ reform.Struct = (*Person)(nil) _ reform.Table = PersonTable _ reform.Record = (*Person)(nil) _ fmt.Stringer = (*Person)(nil) ) func init() { parse.AssertUpToDate(&PersonTable.s, new(Person)) } ================================================ FILE: _examples/database/orm/reform/postman_collection.json ================================================ { "info": { "_postman_id": "6b66000d-9c04-4d0a-b55c-8a493bf59015", "name": "iris-reform-example", "description": "Example API calls for iris reform example.", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, "item": [ { "name": "http://localhost:8080/persons", "request": { "method": "POST", "header": [], "body": { "mode": "raw", "raw": "{\r\n \"name\": \"John\",\r\n \"email\": \"example@example.com\"\r\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "http://localhost:8080/persons", "protocol": "http", "host": [ "localhost" ], "port": "8080", "path": [ "persons" ] } }, "response": [] }, { "name": "http://localhost:8080/persons", "request": { "method": "GET", "header": [], "url": { "raw": "http://localhost:8080/persons", "protocol": "http", "host": [ "localhost" ], "port": "8080", "path": [ "persons" ] } }, "response": [] } ], "protocolProfileBehavior": {} } ================================================ FILE: _examples/database/orm/sqlx/main.go ================================================ package main import ( "github.com/kataras/iris/v12" "github.com/jmoiron/sqlx" _ "github.com/mattn/go-sqlite3" ) /* go get -u github.com/mattn/go-sqlite3 go get -u github.com/jmoiron/sqlx If you're on win64 and you can't install go-sqlite3: 1. Download: https://sourceforge.net/projects/mingw-w64/files/latest/download 2. Select "x86_x64" and "posix" 3. Add C:\Program Files\mingw-w64\x86_64-7.1.0-posix-seh-rt_v5-rev1\mingw64\bin to your PATH env variable. Docs: https://github.com/jmoiron/sqlx */ // Person is our person table structure. type Person struct { ID int64 `db:"person_id"` FirstName string `db:"first_name"` LastName string `db:"last_name"` Email string } const schema = ` CREATE TABLE IF NOT EXISTS person ( person_id INTEGER PRIMARY KEY, first_name text, last_name text, email text );` func main() { app := iris.New() db, err := sqlx.Connect("sqlite3", "./test.db") if err != nil { app.Logger().Fatalf("db failed to initialized: %v", err) } iris.RegisterOnInterrupt(func() { db.Close() }) db.MustExec(schema) app.Get("/insert", func(ctx iris.Context) { res, err := db.NamedExec(`INSERT INTO person (first_name,last_name,email) VALUES (:first,:last,:email)`, map[string]any{ "first": "John", "last": "Doe", "email": "johndoe@example.com", }) if err != nil { // Note: on production, don't give the error back to the user. // However for the sake of the example we do: ctx.StopWithError(iris.StatusInternalServerError, err) return } id, err := res.LastInsertId() if err != nil { ctx.StopWithError(iris.StatusInternalServerError, err) return } ctx.Writef("person inserted: id: %d", id) }) app.Get("/get", func(ctx iris.Context) { // Select all persons. people := []Person{} db.Select(&people, "SELECT * FROM person ORDER BY first_name ASC") if err != nil { ctx.StopWithError(iris.StatusInternalServerError, err) return } if len(people) == 0 { ctx.Writef("no persons found, use /insert first.") return } ctx.Writef("persons found: %#v", people) /* Select a single or more with a first name of John from the database: person := Person{FirstName: "John"} rows, err := db.NamedQuery(`SELECT * FROM person WHERE first_name=:first_name`, person) if err != nil { ... } defer rows.Close() for rows.Next() { if err := rows.StructScan(&person); err != nil { if err == sql.ErrNoRows { ctx.StopWithText(iris.StatusNotFound, "Person: %s not found", person.FirstName) } else { ctx.StopWithError(iris.StatusInternalServerError, err) } return } } */ }) // http://localhost:8080/insert // http://localhost:8080/get app.Listen(":8080") } ================================================ FILE: _examples/database/sqlx/main.go ================================================ package main import ( "context" "database/sql" "encoding/json" "fmt" "time" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/x/errors" "github.com/kataras/iris/v12/x/sqlx" _ "github.com/lib/pq" ) const ( host = "localhost" port = 5432 user = "postgres" password = "admin!123" dbname = "test" ) func main() { app := iris.New() db := mustConnectDB() mustCreateExtensions(context.Background(), db) mustCreateTables(context.Background(), db) app.Post("/", insert(db)) app.Get("/", list(db)) app.Get("/{event_id:uuid}", getByID(db)) /* curl --location --request POST 'http://localhost:8080' \ --header 'Content-Type: application/json' \ --data-raw '{ "name": "second_test_event", "data": { "key": "value", "year": 2022 } }' curl --location --request GET 'http://localhost:8080' curl --location --request GET 'http://localhost:8080/4fc0363f-1d1f-4a43-8608-5ed266485645' */ app.Listen(":8080") } func mustConnectDB() *sql.DB { connString := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", host, port, user, password, dbname) db, err := sql.Open("postgres", connString) if err != nil { panic(err) } err = db.Ping() if err != nil { panic(err) } return db } func mustCreateExtensions(ctx context.Context, db *sql.DB) { query := `CREATE EXTENSION IF NOT EXISTS pgcrypto;` _, err := db.ExecContext(ctx, query) if err != nil { panic(err) } } func mustCreateTables(ctx context.Context, db *sql.DB) { query := `CREATE TABLE IF NOT EXISTS "events" ( "id" uuid PRIMARY KEY NOT NULL DEFAULT gen_random_uuid(), "created_at" timestamp(6) DEFAULT now(), "name" text COLLATE "pg_catalog"."default", "data" jsonb );` _, err := db.ExecContext(ctx, query) if err != nil { panic(err) } sqlx.Register("events", Event{}) } type Event struct { ID string `json:"id"` CreatedAt time.Time `json:"created_at"` Name string `json:"name"` Data json.RawMessage `json:"data"` Presenter string `db:"-" json:"-"` } func insertEvent(ctx context.Context, db *sql.DB, evt Event) (id string, err error) { query := `INSERT INTO events(name,data) VALUES($1,$2) RETURNING id;` err = db.QueryRowContext(ctx, query, evt.Name, evt.Data).Scan(&id) return } func listEvents(ctx context.Context, db *sql.DB) ([]Event, error) { list := make([]Event, 0) query := `SELECT * FROM events ORDER BY created_at;` rows, err := db.QueryContext(ctx, query) if err != nil { return nil, err } // Not required. See sqlx.DefaultSchema.AutoCloseRows field. // defer rows.Close() if err = sqlx.Bind(&list, rows); err != nil { return nil, err } return list, nil } func getEvent(ctx context.Context, db *sql.DB, id string) (evt Event, err error) { query := `SELECT * FROM events WHERE id = $1 LIMIT 1;` err = sqlx.Query(ctx, db, &evt, query, id) return // // Same as: // // rows, err := db.QueryContext(ctx, query, id) // if err != nil { // return Event{}, err // } // // var evt Event // err = sqlx.Bind(&evt, rows) // // return evt, err } func insert(db *sql.DB) iris.Handler { return func(ctx iris.Context) { var evt Event if err := ctx.ReadJSON(&evt); err != nil { errors.InvalidArgument.Details(ctx, "unable to read body", err.Error()) return } id, err := insertEvent(ctx, db, evt) if err != nil { errors.Internal.LogErr(ctx, err) return } ctx.JSON(iris.Map{"id": id}) } } func list(db *sql.DB) iris.Handler { return func(ctx iris.Context) { events, err := listEvents(ctx, db) if err != nil { errors.Internal.LogErr(ctx, err) return } ctx.JSON(events) } } func getByID(db *sql.DB) iris.Handler { return func(ctx iris.Context) { eventID := ctx.Params().Get("event_id") evt, err := getEvent(ctx, db, eventID) if err != nil { errors.Internal.LogErr(ctx, err) return } ctx.JSON(evt) } } ================================================ FILE: _examples/dependency-injection/basic/main.go ================================================ package main import ( "github.com/kataras/iris/v12" "github.com/go-playground/validator/v10" ) type ( testInput struct { Email string `json:"email" validate:"required"` } testOutput struct { ID int `json:"id"` Name string `json:"name"` } ) func handler(id int, in testInput) testOutput { return testOutput{ ID: id, Name: in.Email, } } func configureAPI(api *iris.APIContainer) { /* Here is how you can inject a return value from a handler, in this case the "testOutput": api.UseResultHandler(func(next iris.ResultHandler) iris.ResultHandler { return func(ctx iris.Context, v any) error { return next(ctx, map[string]any{"injected": true}) } }) */ api.Post("/{id:int}", handler) } func main() { app := iris.New() app.Validator = validator.New() app.Logger().SetLevel("debug") app.ConfigureContainer(configureAPI) app.Listen(":8080") } ================================================ FILE: _examples/dependency-injection/basic/middleware/main.go ================================================ package main import ( "errors" "github.com/kataras/iris/v12" ) type ( testInput struct { Email string `json:"email"` } testOutput struct { ID int `json:"id"` Name string `json:"name"` } ) func handler(id int, in testInput) testOutput { return testOutput{ ID: id, Name: in.Email, } } var errCustom = errors.New("my_error") func middleware(in testInput) (int, error) { if in.Email == "invalid" { // stop the execution and don't continue to "handler" // without firing an error. return iris.StatusAccepted, iris.ErrStopExecution } else if in.Email == "error" { // stop the execution and fire a custom error. return iris.StatusConflict, errCustom } return iris.StatusOK, nil } func newApp() *iris.Application { app := iris.New() // handle the route, respond with // a JSON and 200 status code // or 202 status code and empty body // or a 409 status code and "my_error" body. app.ConfigureContainer(func(api *iris.APIContainer) { // Enable execution of middlewares without ctx.Next requirement. api.Self.SetExecutionRules(iris.ExecutionRules{ Begin: iris.ExecutionOptions{ Force: true, }, }) api.Use(middleware) api.Post("/{id:int}", handler) }) app.Configure( iris.WithOptimizations, /* optional */ iris.WithoutBodyConsumptionOnUnmarshal, /* required when more than one handler is consuming request payload(testInput) */ ) return app } func main() { app := newApp() app.Listen(":8080") } ================================================ FILE: _examples/dependency-injection/basic/middleware/main_test.go ================================================ package main import ( "testing" "github.com/kataras/iris/v12/httptest" ) func TestDependencyInjectionBasic_Middleware(t *testing.T) { app := newApp() e := httptest.New(t, app) e.POST("/42").WithJSON(testInput{Email: "my_email"}).Expect(). Status(httptest.StatusOK). JSON().IsEqual(testOutput{ID: 42, Name: "my_email"}) // it should stop the execution at the middleware and return the middleware's status code, // because the error is `ErrStopExecution`. e.POST("/42").WithJSON(testInput{Email: "invalid"}).Expect(). Status(httptest.StatusAccepted).Body().IsEmpty() // it should stop the execution at the middleware and return the error's text. e.POST("/42").WithJSON(testInput{Email: "error"}).Expect(). Status(httptest.StatusConflict).Body().IsEqual("my_error") } ================================================ FILE: _examples/dependency-injection/context-register-dependency/main.go ================================================ package main import "github.com/kataras/iris/v12" func main() { app := iris.New() app.Use(RoleMiddleware) app.Get("/", commonHandler) c := app.ConfigureContainer() /* When you do NOT have access to the middleware code itself then you can register a request dependency which retrieves the value from the Context and returns it, so handler/function's input arguments with that `Role` type can be binded. c.RegisterDependency(func(ctx iris.Context) Role { role, ok := GetRole(ctx) if !ok { // This codeblock will never be executed here // but you can stop executing a handler which depends on // that dependency with `ctx.StopExecution/ctx.StopWithXXX` methods // or by returning a second output argument of `error` type. ctx.StopExecution() return Role{} } return role }) */ c.Get("/dep", handlerWithDependencies) // http://localhost:8080?name=kataras // http://localhost:8080/dep?name=kataras app.Listen(":8080") } func commonHandler(ctx iris.Context) { role, _ := GetRole(ctx) ctx.WriteString(role.Name) } func handlerWithDependencies(role Role) string { return role.Name } // Code for an example middleware. // Role struct value example. type Role struct { Name string } const roleContextKey = "myapp.role" // RoleMiddleware example of a custom middleware. func RoleMiddleware(ctx iris.Context) { // [do it yourself: extract the role from the request...] if ctx.URLParam("name") != "kataras" { ctx.StopWithStatus(iris.StatusUnauthorized) return } // role := Role{Name: "admin"} ctx.Values().Set(roleContextKey, role) // When you have access to the middleware itself: // Use the `RegisterDependency` to register // struct type values as dependencies at request-time for // any potential dependency injection-ed user handler. // This way the user of your middleware can get rid of // manually register a dependency for that `Role` type with calls of // `APIContainer.RegisterDependency` (and `mvc.Application.Register`). ctx.RegisterDependency(role) ctx.Next() } // GetRole returns the role inside the context values, // the `roleMiddleware` should be executed first. func GetRole(ctx iris.Context) (Role, bool) { v := ctx.Values().Get(roleContextKey) if v != nil { if role, ok := v.(Role); ok { return role, true } } return Role{}, false } // End Code of our example middleware. ================================================ FILE: _examples/dependency-injection/jwt/contrib/go.mod ================================================ module github.com/kataras/iris/_examples/dependency-injection/jwt/contrib go 1.25 require ( github.com/iris-contrib/middleware/jwt v0.0.0-20251225090426-92c6f28facda github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd ) require ( github.com/BurntSushi/toml v1.6.0 // indirect github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect github.com/CloudyKit/jet/v6 v6.3.1 // indirect github.com/Joker/jade v1.1.3 // indirect github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 // indirect github.com/andybalholm/brotli v1.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/fatih/structs v1.1.0 // indirect github.com/flosch/pongo2/v4 v4.0.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/iris-contrib/schema v0.0.6 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/kataras/blocks v0.0.8 // indirect github.com/kataras/golog v0.1.12 // indirect github.com/kataras/pio v0.0.13 // indirect github.com/kataras/sitemap v0.0.6 // indirect github.com/kataras/tunnel v0.0.4 // indirect github.com/klauspost/compress v1.18.2 // indirect github.com/kr/text v0.2.0 // indirect github.com/mailgun/raymond/v2 v2.0.48 // indirect github.com/mailru/easyjson v0.9.1 // indirect github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/schollz/closestmatch v2.1.0+incompatible // indirect github.com/sirupsen/logrus v1.8.1 // indirect github.com/tdewolff/minify/v2 v2.24.8 // indirect github.com/tdewolff/parse/v2 v2.8.5 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/yosssi/ace v0.0.5 // indirect golang.org/x/crypto v0.47.0 // indirect golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect golang.org/x/net v0.49.0 // indirect golang.org/x/sys v0.40.0 // indirect golang.org/x/text v0.33.0 // indirect golang.org/x/time v0.14.0 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: _examples/dependency-injection/jwt/contrib/go.sum ================================================ github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= github.com/CloudyKit/jet/v6 v6.3.1 h1:6IAo5Cx21xrHVaR8zzXN5gJatKV/wO7Nf6bfCnCSbUw= github.com/CloudyKit/jet/v6 v6.3.1/go.mod h1:lf8ksdNsxZt7/yH/3n4vJQWA9RUq4wpaHtArHhGVMOw= github.com/Joker/hpp v1.0.0 h1:65+iuJYdRXv/XyN62C1uEmmOx3432rNG/rKlX6V7Kkc= github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= github.com/Joker/jade v1.1.3 h1:Qbeh12Vq6BxURXT1qZBRHsDxeURB8ztcL6f3EXSGeHk= github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKduM= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 h1:dG7/gLroJGht/jSQtHiLvT48Hxn+crbmvyItZC8cWOs= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05/go.mod h1:NYezi6wtnJtBm5btoprXc5SvAdqH0XTXWnUup0MptAI= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/flosch/pongo2/v4 v4.0.2 h1:gv+5Pe3vaSVmiJvh/BZa82b7/00YUGm0PIyVVLop0Hw= github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+BWPhwZD8= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a h1:l7A0loSszR5zHd/qK53ZIHMO8b3bBSmENnQ6eKnUT0A= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/iris-contrib/httpexpect/v2 v2.15.2 h1:T9THsdP1woyAqKHwjkEsbCnMefsAFvk8iJJKokcJ3Go= github.com/iris-contrib/httpexpect/v2 v2.15.2/go.mod h1:JLDgIqnFy5loDSUv1OA2j0mb6p/rDhiCqigP22Uq9xE= github.com/iris-contrib/middleware/jwt v0.0.0-20251225090426-92c6f28facda h1:1VquMwe3hyozXkURE0uJLqMCiTmJpTYwLFCEZSkXzs0= github.com/iris-contrib/middleware/jwt v0.0.0-20251225090426-92c6f28facda/go.mod h1:gvZR1e7IFVaT5ph6WFdleMXYSsocncDG+0BvKyJerTc= github.com/iris-contrib/schema v0.0.6 h1:CPSBLyx2e91H2yJzPuhGuifVRnZBBJ3pCOMbOvPZaTw= github.com/iris-contrib/schema v0.0.6/go.mod h1:iYszG0IOsuIsfzjymw1kMzTL8YQcCWlm65f3wX8J5iA= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kataras/blocks v0.0.8 h1:MrpVhoFTCR2v1iOOfGng5VJSILKeZZI+7NGfxEh3SUM= github.com/kataras/blocks v0.0.8/go.mod h1:9Jm5zx6BB+06NwA+OhTbHW1xkMOYxahnqTN5DveZ2Yg= github.com/kataras/golog v0.1.12 h1:Bu7I/G4ilJlbfzjmU39O9N+2uO1pBcMK045fzZ4ytNg= github.com/kataras/golog v0.1.12/go.mod h1:wrGSbOiBqbQSQznleVNX4epWM8rl9SJ/rmEacl0yqy4= github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd h1:oMsQgqKACbUdlaIWnN+EZzzAnHkw90hE/y/R0BSij2U= github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd/go.mod h1:yucjtDl9Vn3OctWM1fi0MuM9OckemC7kEreFa9QtPF8= github.com/kataras/pio v0.0.13 h1:x0rXVX0fviDTXOOLOmr4MUxOabu1InVSTu5itF8CXCM= github.com/kataras/pio v0.0.13/go.mod h1:k3HNuSw+eJ8Pm2lA4lRhg3DiCjVgHlP8hmXApSej3oM= github.com/kataras/sitemap v0.0.6 h1:w71CRMMKYMJh6LR2wTgnk5hSgjVNB9KL60n5e2KHvLY= github.com/kataras/sitemap v0.0.6/go.mod h1:dW4dOCNs896OR1HmG+dMLdT7JjDk7mYBzoIRwuj5jA4= github.com/kataras/tunnel v0.0.4 h1:sCAqWuJV7nPzGrlb0os3j49lk2JhILT0rID38NHNLpA= github.com/kataras/tunnel v0.0.4/go.mod h1:9FkU4LaeifdMWqZu7o20ojmW4B7hdhv2CMLwfnHGpYw= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw= github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18= github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo= github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk= github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tdewolff/minify/v2 v2.24.8 h1:58/VjsbevI4d5FGV0ZSuBrHMSSkH4MCH0sIz/eKIauE= github.com/tdewolff/minify/v2 v2.24.8/go.mod h1:0Ukj0CRpo/sW/nd8uZ4ccXaV1rEVIWA3dj8U7+Shhfw= github.com/tdewolff/parse/v2 v2.8.5 h1:ZmBiA/8Do5Rpk7bDye0jbbDUpXXbCdc3iah4VeUvwYU= github.com/tdewolff/parse/v2 v2.8.5/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo= github.com/tdewolff/test v1.0.11 h1:FdLbwQVHxqG16SlkGveC0JVyrJN62COWTRyUFzfbtBE= github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA= github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0= github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs= moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHcE= ================================================ FILE: _examples/dependency-injection/jwt/contrib/main.go ================================================ package main import ( "github.com/kataras/iris/v12" "github.com/iris-contrib/middleware/jwt" ) var secret = []byte("My Secret Key") func main() { app := iris.New() app.ConfigureContainer(register) app.Listen(":8080") } func register(api *iris.APIContainer) { j := jwt.New(jwt.Config{ // Extract by "token" url parameter. Extractor: jwt.FromFirst(jwt.FromParameter("token"), jwt.FromAuthHeader), ValidationKeyGetter: func(token *jwt.Token) (any, error) { return secret, nil }, SigningMethod: jwt.SigningMethodHS256, }) api.Get("/authenticate", writeToken) // This works as usually: api.Get("/restricted", j.Serve, restrictedPage) // You can also bind the *jwt.Token (see `verifiedWithBindedTokenPage`) // by registering a *jwt.Token dependency. // // api.RegisterDependency(func(ctx iris.Context) *jwt.Token { // if err := j.CheckJWT(ctx); err != nil { // ctx.StopWithStatus(iris.StatusUnauthorized) // return nil // } // // token := j.Get(ctx) // return token // }) // ^ You can do the same with MVC too, as the container is shared and works // the same way in both functions-as-handlers and structs-as-controllers. // // api.Get("/", restrictedPageWithBindedTokenPage) } func writeToken() string { token := jwt.NewTokenWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ "foo": "bar", }) tokenString, _ := token.SignedString(secret) return tokenString } func restrictedPage() string { return "This page can only be seen by verified clients" } func restrictedPageWithBindedTokenPage(token *jwt.Token) string { // Token[foo] value: bar return "Token[foo] value: " + token.Claims.(jwt.MapClaims)["foo"].(string) } ================================================ FILE: _examples/dependency-injection/jwt/main.go ================================================ package main import ( "time" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/middleware/jwt" ) func main() { app := iris.New() app.ConfigureContainer(register) // http://localhost:8080/authenticate // http://localhost:8080/restricted (Header: Authorization = Bearer $token) app.Listen(":8080") } var secret = []byte("secret") func register(api *iris.APIContainer) { api.RegisterDependency(func(ctx iris.Context) (claims userClaims) { /* Using the middleware: if ctx.Proceed(verify) { // ^ the "verify" middleware will stop the execution if it's failed to verify the request. // Map the input parameter of "restricted" function with the claims. return jwt.Get(ctx).(*userClaims) }*/ token := jwt.FromHeader(ctx) if token == "" { ctx.StopWithError(iris.StatusUnauthorized, jwt.ErrMissing) return } verifiedToken, err := jwt.Verify(jwt.HS256, secret, []byte(token)) if err != nil { ctx.StopWithError(iris.StatusUnauthorized, err) return } verifiedToken.Claims(&claims) return }) api.Get("/authenticate", writeToken) api.Get("/restricted", restrictedPage) } type userClaims struct { Username string `json:"username"` } func writeToken(ctx iris.Context) { claims := userClaims{ Username: "kataras", } token, err := jwt.Sign(jwt.HS256, secret, claims, jwt.MaxAge(1*time.Minute)) if err != nil { ctx.StopWithError(iris.StatusInternalServerError, err) return } ctx.Write(token) } func restrictedPage(claims userClaims) string { // userClaims.Username: kataras return "userClaims.Username: " + claims.Username } ================================================ FILE: _examples/dependency-injection/overview/datamodels/movie.go ================================================ // file: datamodels/movie.go package datamodels // Movie is our sample data structure. // Keep note that the tags for public-use (for our web app) // should be kept in other file like "web/viewmodels/movie.go" // which could wrap by embedding the datamodels.Movie or // declare new fields instead butwe will use this datamodel // as the only one Movie model in our application, // for the sake of simplicty. type Movie struct { ID uint64 `json:"id"` Name string `json:"name"` Year int `json:"year"` Genre string `json:"genre"` Poster string `json:"poster"` } ================================================ FILE: _examples/dependency-injection/overview/datasource/movies.go ================================================ // file: datasource/movies.go package datasource import "github.com/kataras/iris/v12/_examples/dependency-injection/overview/datamodels" // Movies is our imaginary data source. var Movies = map[uint64]datamodels.Movie{ 1: { ID: 1, Name: "Casablanca", Year: 1942, Genre: "Romance", Poster: "https://iris-go.com/static/images/examples/mvc-movies/1.jpg", }, 2: { ID: 2, Name: "Gone with the Wind", Year: 1939, Genre: "Romance", Poster: "https://iris-go.com/static/images/examples/mvc-movies/2.jpg", }, 3: { ID: 3, Name: "Citizen Kane", Year: 1941, Genre: "Mystery", Poster: "https://iris-go.com/static/images/examples/mvc-movies/3.jpg", }, 4: { ID: 4, Name: "The Wizard of Oz", Year: 1939, Genre: "Fantasy", Poster: "https://iris-go.com/static/images/examples/mvc-movies/4.jpg", }, 5: { ID: 5, Name: "North by Northwest", Year: 1959, Genre: "Thriller", Poster: "https://iris-go.com/static/images/examples/mvc-movies/5.jpg", }, } ================================================ FILE: _examples/dependency-injection/overview/main.go ================================================ // file: main.go package main import ( "github.com/kataras/iris/v12/_examples/dependency-injection/overview/datasource" "github.com/kataras/iris/v12/_examples/dependency-injection/overview/repositories" "github.com/kataras/iris/v12/_examples/dependency-injection/overview/services" "github.com/kataras/iris/v12/_examples/dependency-injection/overview/web/middleware" "github.com/kataras/iris/v12/_examples/dependency-injection/overview/web/routes" "github.com/kataras/iris/v12" ) func main() { app := iris.New() app.Logger().SetLevel("debug") // Load the template files. app.RegisterView(iris.HTML("./web/views", ".html")) // Create our movie repository with some (memory) data from the datasource. repo := repositories.NewMovieRepository(datasource.Movies) app.Party("/hello").ConfigureContainer(func(r *iris.APIContainer) { r.Get("/", routes.Hello) r.Get("/{name}", routes.HelloName) }) app.Party("/movies").ConfigureContainer(func(r *iris.APIContainer) { // Create our movie service, we will bind it to the movie app's dependencies. movieService := services.NewMovieService(repo) r.RegisterDependency(movieService) // Add the basic authentication(admin:password) middleware // for the /movies based requests. r.Use(middleware.BasicAuth) r.Get("/", routes.Movies) r.Get("/{id:uint64}", routes.MovieByID) r.Put("/{id:uint64}", routes.UpdateMovieByID) r.Delete("/{id:uint64}", routes.DeleteMovieByID) }) // http://localhost:8080/hello // http://localhost:8080/hello/iris // http://localhost:8080/movies ("admin": "password") // http://localhost:8080/movies/1 app.Listen( // Start the web server at localhost:8080 "localhost:8080", // enables faster json serialization and more: iris.WithOptimizations, ) } ================================================ FILE: _examples/dependency-injection/overview/repositories/movie_repository.go ================================================ // file: repositories/movie_repository.go package repositories import ( "errors" "sync" "github.com/kataras/iris/v12/_examples/dependency-injection/overview/datamodels" ) // Query represents the visitor and action queries. type Query func(datamodels.Movie) bool // MovieRepository handles the basic operations of a movie entity/model. // It's an interface in order to be testable, i.e a memory movie repository or // a connected to an sql database. type MovieRepository interface { Exec(query Query, action Query, limit int, mode int) (ok bool) Select(query Query) (movie datamodels.Movie, found bool) SelectMany(query Query, limit int) (results []datamodels.Movie) InsertOrUpdate(movie datamodels.Movie) (updatedMovie datamodels.Movie, err error) Delete(query Query, limit int) (deleted bool) } // NewMovieRepository returns a new movie memory-based repository, // the one and only repository type in our example. func NewMovieRepository(source map[uint64]datamodels.Movie) MovieRepository { return &movieMemoryRepository{source: source} } // movieMemoryRepository is a "MovieRepository" // which manages the movies using the memory data source (map). type movieMemoryRepository struct { source map[uint64]datamodels.Movie mu sync.RWMutex } const ( // ReadOnlyMode will RLock(read) the data . ReadOnlyMode = iota // ReadWriteMode will Lock(read/write) the data. ReadWriteMode ) func (r *movieMemoryRepository) Exec(query Query, action Query, actionLimit int, mode int) (ok bool) { loops := 0 if mode == ReadOnlyMode { r.mu.RLock() defer r.mu.RUnlock() } else { r.mu.Lock() defer r.mu.Unlock() } for _, movie := range r.source { ok = query(movie) if ok { if action(movie) { loops++ if actionLimit >= loops { break // break } } } } return } // Select receives a query function // which is fired for every single movie model inside // our imaginary data source. // When that function returns true then it stops the iteration. // // It returns the query's return last known "found" value // and the last known movie model // to help callers to reduce the LOC. // // It's actually a simple but very clever prototype function // I'm using everywhere since I firstly think of it, // hope you'll find it very useful as well. func (r *movieMemoryRepository) Select(query Query) (movie datamodels.Movie, found bool) { found = r.Exec(query, func(m datamodels.Movie) bool { movie = m return true }, 1, ReadOnlyMode) // set an empty datamodels.Movie if not found at all. if !found { movie = datamodels.Movie{} } return } // SelectMany same as Select but returns one or more datamodels.Movie as a slice. // If limit <=0 then it returns everything. func (r *movieMemoryRepository) SelectMany(query Query, limit int) (results []datamodels.Movie) { r.Exec(query, func(m datamodels.Movie) bool { results = append(results, m) return true }, limit, ReadOnlyMode) return } // InsertOrUpdate adds or updates a movie to the (memory) storage. // // Returns the new movie and an error if any. func (r *movieMemoryRepository) InsertOrUpdate(movie datamodels.Movie) (datamodels.Movie, error) { id := movie.ID if id == 0 { // Create new action var lastID uint64 // find the biggest ID in order to not have duplications // in productions apps you can use a third-party // library to generate a UUID as string. r.mu.RLock() for _, item := range r.source { if item.ID > lastID { lastID = item.ID } } r.mu.RUnlock() id = lastID + 1 movie.ID = id // map-specific thing r.mu.Lock() r.source[id] = movie r.mu.Unlock() return movie, nil } // Update action based on the movie.ID, // here we will allow updating the poster and genre if not empty. // Alternatively we could do pure replace instead: // r.source[id] = movie // and comment the code below; current, exists := r.Select(func(m datamodels.Movie) bool { return m.ID == id }) if !exists { // ID is not a real one, return an error. return datamodels.Movie{}, errors.New("failed to update a nonexistent movie") } // or comment these and r.source[id] = m for pure replace if movie.Poster != "" { current.Poster = movie.Poster } if movie.Genre != "" { current.Genre = movie.Genre } // map-specific thing r.mu.Lock() r.source[id] = current r.mu.Unlock() return movie, nil } func (r *movieMemoryRepository) Delete(query Query, limit int) bool { return r.Exec(query, func(m datamodels.Movie) bool { delete(r.source, m.ID) return true }, limit, ReadWriteMode) } ================================================ FILE: _examples/dependency-injection/overview/services/movie_service.go ================================================ // file: services/movie_service.go package services import ( "github.com/kataras/iris/v12/_examples/dependency-injection/overview/datamodels" "github.com/kataras/iris/v12/_examples/dependency-injection/overview/repositories" ) // MovieService handles some of the CRUID operations of the movie datamodel. // It depends on a movie repository for its actions. // It's here to decouple the data source from the higher level compoments. // As a result a different repository type can be used with the same logic without any aditional changes. // It's an interface and it's used as interface everywhere // because we may need to change or try an experimental different domain logic at the future. type MovieService interface { GetAll() []datamodels.Movie GetByID(id uint64) (datamodels.Movie, bool) DeleteByID(id uint64) bool UpdatePosterAndGenreByID(id uint64, poster string, genre string) (datamodels.Movie, error) } // NewMovieService returns the default movie service. func NewMovieService(repo repositories.MovieRepository) MovieService { return &movieService{ repo: repo, } } type movieService struct { repo repositories.MovieRepository } // GetAll returns all movies. func (s *movieService) GetAll() []datamodels.Movie { return s.repo.SelectMany(func(_ datamodels.Movie) bool { return true }, -1) } // GetByID returns a movie based on its id. func (s *movieService) GetByID(id uint64) (datamodels.Movie, bool) { return s.repo.Select(func(m datamodels.Movie) bool { return m.ID == id }) } // UpdatePosterAndGenreByID updates a movie's poster and genre. func (s *movieService) UpdatePosterAndGenreByID(id uint64, poster string, genre string) (datamodels.Movie, error) { // update the movie and return it. return s.repo.InsertOrUpdate(datamodels.Movie{ ID: id, Poster: poster, Genre: genre, }) } // DeleteByID deletes a movie by its id. // // Returns true if deleted otherwise false. func (s *movieService) DeleteByID(id uint64) bool { return s.repo.Delete(func(m datamodels.Movie) bool { return m.ID == id }, 1) } ================================================ FILE: _examples/dependency-injection/overview/web/middleware/basicauth.go ================================================ // file: web/middleware/basicauth.go package middleware import "github.com/kataras/iris/v12/middleware/basicauth" // BasicAuth middleware sample. var BasicAuth = basicauth.Default(map[string]string{ "admin": "password", }) ================================================ FILE: _examples/dependency-injection/overview/web/routes/hello.go ================================================ // file: web/routes/hello.go package routes import ( "errors" "github.com/kataras/iris/v12/hero" ) var helloView = hero.View{ Name: "hello/index.html", Data: map[string]any{ "Title": "Hello Page", "MyMessage": "Welcome to my awesome website", }, } // Hello will return a predefined view with bind data. // // `hero.Result` is just an interface with a `Dispatch` function. // `hero.Response` and `hero.View` are the builtin result type dispatchers // you can even create custom response dispatchers by // implementing the `github.com/kataras/iris/hero#Result` interface. func Hello() hero.Result { return helloView } // you can define a standard error in order to re-use anywhere in your app. var errBadName = errors.New("bad name") // you can just return it as error or even better // wrap this error with an hero.Response to make it an hero.Result compatible type. var badName = hero.Response{Err: errBadName, Code: 400} // HelloName returns a "Hello {name}" response. // Demos: // curl -i http://localhost:8080/hello/iris // curl -i http://localhost:8080/hello/anything func HelloName(name string) hero.Result { if name != "iris" { return badName } // return hero.Response{Text: "Hello " + name} OR: return hero.View{ Name: "hello/name.html", Data: name, } } ================================================ FILE: _examples/dependency-injection/overview/web/routes/movies.go ================================================ // file: web/routes/movie.go package routes import ( "errors" "github.com/kataras/iris/v12/_examples/dependency-injection/overview/datamodels" "github.com/kataras/iris/v12/_examples/dependency-injection/overview/services" "github.com/kataras/iris/v12" ) // Movies returns list of the movies. // Demo: // curl -i http://localhost:8080/movies func Movies(service services.MovieService) (results []datamodels.Movie) { return service.GetAll() } // MovieByID returns a movie. // Demo: // curl -i http://localhost:8080/movies/1 func MovieByID(service services.MovieService, id uint64) (movie datamodels.Movie, found bool) { return service.GetByID(id) // it will throw 404 if not found. } // UpdateMovieByID updates a movie. // Demo: // curl -i -X PUT -F "genre=Thriller" -F "poster=@/Users/kataras/Downloads/out.gif" http://localhost:8080/movies/1 func UpdateMovieByID(ctx iris.Context, service services.MovieService, id uint64) (datamodels.Movie, error) { // get the request data for poster and genre file, info, err := ctx.FormFile("poster") if err != nil { return datamodels.Movie{}, errors.New("failed due form file 'poster' missing") } // we don't need the file so close it now. file.Close() // imagine that is the url of the uploaded file... poster := info.Filename genre := ctx.FormValue("genre") return service.UpdatePosterAndGenreByID(id, poster, genre) } // DeleteMovieByID deletes a movie. // Demo: // curl -i -X DELETE -u admin:password http://localhost:8080/movies/1 func DeleteMovieByID(service services.MovieService, id uint64) any { wasDel := service.DeleteByID(id) if wasDel { // return the deleted movie's ID return iris.Map{"deleted": id} } // right here we can see that a method function can return any of those two types(map or int), // we don't have to specify the return type to a specific type. return iris.StatusBadRequest } ================================================ FILE: _examples/dependency-injection/overview/web/views/hello/index.html ================================================ {{.Title}} - My App

{{.MyMessage}}

================================================ FILE: _examples/dependency-injection/overview/web/views/hello/name.html ================================================ {{.}}' Portfolio - My App

Hello {{.}}

================================================ FILE: _examples/dependency-injection/sessions/main.go ================================================ package main import ( "time" "github.com/kataras/iris/v12/_examples/dependency-injection/sessions/routes" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/sessions" ) func main() { app := iris.New() sessionManager := sessions.New(sessions.Config{ Cookie: "site_session_id", Expires: 60 * time.Minute, AllowReclaim: true, }) // Session is automatically binded through `sessions.Get(ctx)` // if a *sessions.Session input argument is present on the handler's function, // which `routes.Index` does. app.Use(sessionManager.Handler()) // Method: GET // Path: http://localhost:8080 app.ConfigureContainer(registerRoutes) app.Listen(":8080") } func registerRoutes(api *iris.APIContainer) { api.Get("/", routes.Index) } ================================================ FILE: _examples/dependency-injection/sessions/routes/index.go ================================================ package routes import ( "fmt" "github.com/kataras/iris/v12/sessions" ) // Index will increment a simple int version based on the visits that this user/session did. func Index(session *sessions.Session) string { // it increments a "visits" value of integer by one, // if the entry with key 'visits' doesn't exist it will create it for you. visits := session.Increment("visits", 1) // write the current, updated visits. return fmt.Sprintf("%d visit(s) from my current session", visits) } ================================================ FILE: _examples/dependency-injection/smart-contract/main.go ================================================ package main import ( "fmt" "strings" "github.com/kataras/iris/v12" // External package to optionally filter JSON responses before sent, // see `sendJSON` for more. "github.com/jmespath/go-jmespath" ) /* $ go get github.com/jmespath/go-jmespath */ func main() { app := newApp() app.Logger().SetLevel("debug") // http://localhost:8080/users?query=[?Name == 'John Doe'].Age // <- client will receive the age of a user which his name is "John Doe". // You can also test query=[0].Name to retrieve the first user's name. // Or even query=[0:3].Age to print the first three ages. // Learn more about jmespath and how to filter: // http://jmespath.readthedocs.io/en/latest/ and // https://github.com/jmespath/go-jmespath/tree/master/fuzz/testdata // // http://localhost:8080/users // http://localhost:8080/users/William%20Woe // http://localhost:8080/users/William%20Woe/age app.Listen(":8080") } func newApp() *iris.Application { app := iris.New() // PartyFunc is the same as usersRouter := app.Party("/users") // but it gives us an easy way to call router's registration functions, // i.e functions from another package that can handle this group of routes. app.PartyFunc("/users", registerUsersRoutes) return app } /* START OF USERS ROUTER */ func registerUsersRoutes(usersRouter iris.Party) { // GET: /users usersRouter.Get("/", getAllUsersHandler) usersRouter.Party("/{name}").ConfigureContainer(registerUserRoutes) } type user struct { Name string `json:"name"` Age int `json:"age"` } var usersSample = []*user{ {"William Woe", 25}, {"Mary Moe", 15}, {"John Doe", 17}, } func getAllUsersHandler(ctx iris.Context) { err := sendJSON(ctx, usersSample) if err != nil { fail(ctx, iris.StatusInternalServerError, "unable to send a list of all users: %v", err) return } } /* START OF USERS.USER SUB ROUTER */ func registerUserRoutes(userRouter *iris.APIContainer) { userRouter.RegisterDependency(userDependency) // GET: /users/{name:string} userRouter.Get("/", getUserHandler) // GET: /users/{name:string}/age userRouter.Get("/age", getUserAgeHandler) } var userDependency = func(ctx iris.Context) *user { name := strings.Title(ctx.Params().Get("name")) for _, u := range usersSample { if u.Name == name { return u } } // you may want or no to handle the error here, either way the main route handler // is going to be executed, always. A dynamic dependency(per-request) is not a middleware, so things like `ctx.Next()` or `ctx.StopExecution()` // do not apply here, look the `getUserHandler`'s first lines; we stop/exit the handler manually // if the received user is nil but depending on your app's needs, it is possible to do other things too. // A dynamic dependency like this can return more output values, i.e (*user, bool). fail(ctx, iris.StatusNotFound, "user with name '%s' not found", name) return nil } func getUserHandler(ctx iris.Context, u *user) { sendJSON(ctx, u) } func getUserAgeHandler(u *user) string { return fmt.Sprintf("%d", u.Age) } /* END OF USERS.USER SUB ROUTER */ /* END OF USERS ROUTER */ // common JSON response for manual HTTP errors, optionally. type httpError struct { Code int `json:"code"` Reason string `json:"reason"` } func (h httpError) Error() string { return fmt.Sprintf("Status Code: %d\nReason: %s", h.Code, h.Reason) } func fail(ctx iris.Context, statusCode int, format string, a ...any) { err := httpError{ Code: statusCode, Reason: fmt.Sprintf(format, a...), } // log all the >= 500 internal errors. if statusCode >= 500 { ctx.Application().Logger().Error(err) } // no next handlers will run. ctx.StopWithJSON(statusCode, err) } // JSON helper to give end-user the ability to put indention chars or filtering the response, you can do that, optionally. // If you'd like to see that function inside the Iris' Context itself raise a [Feature Request] issue and link this example. func sendJSON(ctx iris.Context, resp any) (err error) { indent := ctx.URLParamDefault("indent", " ") // i.e [?Name == 'John Doe'].Age # to output the [age] of a user which his name is "John Doe". if query := ctx.URLParam("query"); query != "" && query != "[]" { resp, err = jmespath.Search(query, resp) if err != nil { return } } err = ctx.JSON(resp, iris.JSON{Indent: indent, UnescapeHTML: true}) return err } ================================================ FILE: _examples/desktop/blink/main.go ================================================ //go:build windows // +build windows package main import ( "github.com/kataras/iris/v12" "github.com/raintean/blink" ) const addr = "127.0.0.1:8080" /* $ go build -mod=mod -ldflags -H=windowsgui -o myapp.exe $ ./myapp.exe # run the app */ func main() { go runServer() showAndWaitWindow() } func runServer() { app := iris.New() app.Get("/", func(ctx iris.Context) { ctx.HTML("

Hello Desktop

") }) app.Listen(addr) } func showAndWaitWindow() { blink.SetDebugMode(true) if err := blink.InitBlink(); err != nil { panic(err) } view := blink.NewWebView(false, 800, 600) view.LoadURL(addr) view.SetWindowTitle("My App") view.MoveToCenter() view.ShowWindow() <-view.Destroy } ================================================ FILE: _examples/desktop/lorca/main.go ================================================ package main import ( "github.com/kataras/iris/v12" "github.com/zserge/lorca" ) const addr = "127.0.0.1:8080" /* $ go build -mod=mod -ldflags="-H windowsgui" -o myapp.exe # build for windows $ ./myapp.exe # run */ func main() { go runServer() showAndWaitWindow() } func runServer() { app := iris.New() app.Get("/", func(ctx iris.Context) { ctx.HTML("My App

Hello Desktop

") }) app.Listen(addr) } func showAndWaitWindow() { webview, err := lorca.New("http://"+addr, "", 800, 600) if err != nil { panic(err) } defer webview.Close() // webview.SetBounds(lorca.Bounds{ // WindowState: lorca.WindowStateFullscreen, // }) // Wait for the browser window to be closed <-webview.Done() } ================================================ FILE: _examples/desktop/webview/go.mod ================================================ module github.com/kataras/iris/_examples/desktop/webview go 1.25 require ( github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd github.com/webview/webview_go v0.0.0-20240831120633-6173450d4dd6 ) require ( github.com/BurntSushi/toml v1.6.0 // indirect github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect github.com/CloudyKit/jet/v6 v6.3.1 // indirect github.com/Joker/jade v1.1.3 // indirect github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 // indirect github.com/andybalholm/brotli v1.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/fatih/structs v1.1.0 // indirect github.com/flosch/pongo2/v4 v4.0.2 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/iris-contrib/schema v0.0.6 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/kataras/blocks v0.0.8 // indirect github.com/kataras/golog v0.1.12 // indirect github.com/kataras/pio v0.0.13 // indirect github.com/kataras/sitemap v0.0.6 // indirect github.com/kataras/tunnel v0.0.4 // indirect github.com/klauspost/compress v1.18.2 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mailgun/raymond/v2 v2.0.48 // indirect github.com/mailru/easyjson v0.9.1 // indirect github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/schollz/closestmatch v2.1.0+incompatible // indirect github.com/sirupsen/logrus v1.8.1 // indirect github.com/stretchr/testify v1.11.1 // indirect github.com/tdewolff/minify/v2 v2.24.8 // indirect github.com/tdewolff/parse/v2 v2.8.5 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/yosssi/ace v0.0.5 // indirect golang.org/x/crypto v0.47.0 // indirect golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect golang.org/x/net v0.49.0 // indirect golang.org/x/sys v0.40.0 // indirect golang.org/x/text v0.33.0 // indirect golang.org/x/time v0.14.0 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: _examples/desktop/webview/go.sum ================================================ github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= github.com/CloudyKit/jet/v6 v6.3.1 h1:6IAo5Cx21xrHVaR8zzXN5gJatKV/wO7Nf6bfCnCSbUw= github.com/CloudyKit/jet/v6 v6.3.1/go.mod h1:lf8ksdNsxZt7/yH/3n4vJQWA9RUq4wpaHtArHhGVMOw= github.com/Joker/hpp v1.0.0 h1:65+iuJYdRXv/XyN62C1uEmmOx3432rNG/rKlX6V7Kkc= github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= github.com/Joker/jade v1.1.3 h1:Qbeh12Vq6BxURXT1qZBRHsDxeURB8ztcL6f3EXSGeHk= github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKduM= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 h1:dG7/gLroJGht/jSQtHiLvT48Hxn+crbmvyItZC8cWOs= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05/go.mod h1:NYezi6wtnJtBm5btoprXc5SvAdqH0XTXWnUup0MptAI= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/flosch/pongo2/v4 v4.0.2 h1:gv+5Pe3vaSVmiJvh/BZa82b7/00YUGm0PIyVVLop0Hw= github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+BWPhwZD8= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a h1:l7A0loSszR5zHd/qK53ZIHMO8b3bBSmENnQ6eKnUT0A= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/iris-contrib/httpexpect/v2 v2.15.2 h1:T9THsdP1woyAqKHwjkEsbCnMefsAFvk8iJJKokcJ3Go= github.com/iris-contrib/httpexpect/v2 v2.15.2/go.mod h1:JLDgIqnFy5loDSUv1OA2j0mb6p/rDhiCqigP22Uq9xE= github.com/iris-contrib/schema v0.0.6 h1:CPSBLyx2e91H2yJzPuhGuifVRnZBBJ3pCOMbOvPZaTw= github.com/iris-contrib/schema v0.0.6/go.mod h1:iYszG0IOsuIsfzjymw1kMzTL8YQcCWlm65f3wX8J5iA= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kataras/blocks v0.0.8 h1:MrpVhoFTCR2v1iOOfGng5VJSILKeZZI+7NGfxEh3SUM= github.com/kataras/blocks v0.0.8/go.mod h1:9Jm5zx6BB+06NwA+OhTbHW1xkMOYxahnqTN5DveZ2Yg= github.com/kataras/golog v0.1.12 h1:Bu7I/G4ilJlbfzjmU39O9N+2uO1pBcMK045fzZ4ytNg= github.com/kataras/golog v0.1.12/go.mod h1:wrGSbOiBqbQSQznleVNX4epWM8rl9SJ/rmEacl0yqy4= github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd h1:oMsQgqKACbUdlaIWnN+EZzzAnHkw90hE/y/R0BSij2U= github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd/go.mod h1:yucjtDl9Vn3OctWM1fi0MuM9OckemC7kEreFa9QtPF8= github.com/kataras/pio v0.0.13 h1:x0rXVX0fviDTXOOLOmr4MUxOabu1InVSTu5itF8CXCM= github.com/kataras/pio v0.0.13/go.mod h1:k3HNuSw+eJ8Pm2lA4lRhg3DiCjVgHlP8hmXApSej3oM= github.com/kataras/sitemap v0.0.6 h1:w71CRMMKYMJh6LR2wTgnk5hSgjVNB9KL60n5e2KHvLY= github.com/kataras/sitemap v0.0.6/go.mod h1:dW4dOCNs896OR1HmG+dMLdT7JjDk7mYBzoIRwuj5jA4= github.com/kataras/tunnel v0.0.4 h1:sCAqWuJV7nPzGrlb0os3j49lk2JhILT0rID38NHNLpA= github.com/kataras/tunnel v0.0.4/go.mod h1:9FkU4LaeifdMWqZu7o20ojmW4B7hdhv2CMLwfnHGpYw= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw= github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18= github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo= github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk= github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tdewolff/minify/v2 v2.24.8 h1:58/VjsbevI4d5FGV0ZSuBrHMSSkH4MCH0sIz/eKIauE= github.com/tdewolff/minify/v2 v2.24.8/go.mod h1:0Ukj0CRpo/sW/nd8uZ4ccXaV1rEVIWA3dj8U7+Shhfw= github.com/tdewolff/parse/v2 v2.8.5 h1:ZmBiA/8Do5Rpk7bDye0jbbDUpXXbCdc3iah4VeUvwYU= github.com/tdewolff/parse/v2 v2.8.5/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo= github.com/tdewolff/test v1.0.11 h1:FdLbwQVHxqG16SlkGveC0JVyrJN62COWTRyUFzfbtBE= github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/webview/webview_go v0.0.0-20240831120633-6173450d4dd6 h1:VQpB2SpK88C6B5lPHTuSZKb2Qee1QWwiFlC5CKY4AW0= github.com/webview/webview_go v0.0.0-20240831120633-6173450d4dd6/go.mod h1:yE65LFCeWf4kyWD5re+h4XNvOHJEXOCOuJZ4v8l5sgk= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA= github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0= github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs= moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHcE= ================================================ FILE: _examples/desktop/webview/main.go ================================================ package main import ( "github.com/kataras/iris/v12" "github.com/webview/webview_go" ) const addr = "127.0.0.1:8080" /* # Windows requires special linker flags for GUI apps. # It's also recommended to use TDM-GCC-64 compiler for CGo. # http://tdm-gcc.tdragon.net/download # # $ go build -mod=mod -ldflags="-H windowsgui" -o myapp.exe # build for windows $ ./myapp.exe # run # # MacOS uses app bundles for GUI apps $ mkdir -p example.app/Contents/MacOS $ go build -o example.app/Contents/MacOS/example $ open example.app # Or click on the app in Finder # # Note: if you see "use option -std=c99 or -std=gnu99 to compile your code" # please refer to: https://github.com/webview/webview/issues/188. # New repository: https://github.com/webview/webview_go. */ func main() { go runServer() showAndWaitWindow() } func runServer() { app := iris.New() app.Get("/", func(ctx iris.Context) { ctx.HTML("

Hello Desktop

") }) app.Listen(addr) } func showAndWaitWindow() { debug := true w := webview.New(debug) defer w.Destroy() w.SetTitle("Minimal webview example") w.SetSize(800, 600, webview.HintNone) w.Navigate("http://" + addr) w.Run() } ================================================ FILE: _examples/dropzonejs/README.md ================================================ # Articles * [How to build a file upload form using DropzoneJS and Go](https://hackernoon.com/how-to-build-a-file-upload-form-using-dropzonejs-and-go-8fb9f258a991) * [How to display existing files on server using DropzoneJS and Go](https://hackernoon.com/how-to-display-existing-files-on-server-using-dropzonejs-and-go-53e24b57ba19) # Content This is the part 1 of 2 in DropzoneJS + Go series. - [Part 1: How to build a file upload form](README.md) - [Part 2: How to display existing files on server](README_PART2.md) # DropzoneJS + Go: How to build a file upload form [DropzoneJS](https://github.com/enyo/dropzone) is an open source library that provides drag'n'drop file uploads with image previews. It is a great JavaScript library which actually does not even rely on JQuery. In this tutorial, we are building a multiple file upload form using DropzoneJS, and the backend will be handled by Go and [Iris](https://iris-go.com). ## Table Of Content - [Preparation](#preparation) - [Work with DropzoneJS](#work-with-dropzonejs) - [Work with Go](#work-with-go) ## Preparation 1. Download [Go(Golang)](https://golang.org/dl), setup your computer as shown there and continue to 2. 2. Install [Iris](https://github.com/kataras/iris); open a terminal and execute `go get -u github.com/kataras/iris` 3. Download DropzoneJS from [this URL](https://raw.githubusercontent.com/enyo/dropzone/master/dist/dropzone.js). DropzoneJS does not rely on JQuery, you will not have to worry that, upgrading JQuery version breaks your application. 4. Download dropzone.css from [this URL](https://raw.githubusercontent.com/enyo/dropzone/master/dist/dropzone.css), if you want some already made css. 5. Create a folder "./public/uploads", this is for storing uploaded files. 6. Create a file "./views/upload.html", this is for the front form page. 7. Create a file "./main.go", this is for handling backend file upload process. Your folder&file structure should look like this after the preparation: ![folder&file structure](folder_structure.png) ## Work with DropzoneJS Open file "./views/upload.html" and let us create a DropzoneJs form. Copy the content below to "./views/upload.html" and we will go through each line of code individually. ```html DropzoneJS Uploader
``` 1. Include the CSS Stylesheet. 2. Include DropzoneJS JavaScript library. 3. Create an upload form with css class "dropzone" and "action" is the route path "/upload". Note that we did create an input filed for fallback mode. This is all handled by DropzoneJS library itself. All we need to do is assign css class "dropzone" to the form. By default, DropzoneJS will find all forms with class "dropzone" and automatically attach itself to it. ## Work with Go Now you have come to Last part of the tutorial. In this section, we will store files sent from DropzoneJS to the "./public/uploads" folder. Open "main.go" and copy the code below: ```go // main.go package main import ( "os" "io" "strings" "github.com/kataras/iris/v12" ) const uploadsDir = "./public/uploads/" func main() { app := iris.New() // Register templates app.RegisterView(iris.HTML("./views", ".html")) // Make the /public route path to statically serve the ./public/... contents app.HandleDir("/public", iris.Dir("./public")) // Render the actual form // GET: http://localhost:8080 app.Get("/", func(ctx iris.Context) { if err := ctx.View("upload.html"); err != nil { ctx.HTML(fmt.Sprintf("

%s

", err.Error())) return } }) // Upload the file to the server // POST: http://localhost:8080/upload app.Post("/upload", iris.LimitRequestBodySize(10<<20), func(ctx iris.Context) { // Get the file from the dropzone request file, info, err := ctx.FormFile("file") if err != nil { ctx.StatusCode(iris.StatusInternalServerError) ctx.Application().Logger().Warnf("Error while uploading: %v", err.Error()) return } defer file.Close() fname := info.Filename // Create a file with the same name // assuming that you have a folder named 'uploads' out, err := os.OpenFile(uploadsDir+fname, os.O_WRONLY|os.O_CREATE, 0666) if err != nil { ctx.StatusCode(iris.StatusInternalServerError) ctx.Application().Logger().Warnf("Error while preparing the new file: %v", err.Error()) return } defer out.Close() io.Copy(out, file) }) // Start the server at http://localhost:8080 app.Listen(":8080") } ``` 1. Create a new Iris app. 2. Register and load templates from the "views" folder. 3. Make the "/public" route path to statically serve the ./public/... folder's contents 4. Create a route to serve the upload form. 5. Create a route to handle the POST form data from the DropzoneJS' form 6. Declare a variable for destination folder. 7. If file is sent to the page, store the file object to a temporary "file" variable. 8. Move uploaded file to destination based on the uploadsDir+uploaded file's name. ### Running the server Open the terminal at the current project's folder and execute: ```bash $ go run main.go Now listening on: http://localhost:8080 Application started. Press CTRL+C to shut down. ``` Now go to browser, and navigate to http://localhost:8080, you should be able to see a page as below: ![no files screenshot](no_files.png) ![with uploaded files screenshot](with_files.png) ================================================ FILE: _examples/dropzonejs/README_PART2.md ================================================ # Articles * [How to build a file upload form using DropzoneJS and Go](https://hackernoon.com/how-to-build-a-file-upload-form-using-dropzonejs-and-go-8fb9f258a991) * [How to display existing files on server using DropzoneJS and Go](https://hackernoon.com/how-to-display-existing-files-on-server-using-dropzonejs-and-go-53e24b57ba19) # Content This is the part 2 of 2 in DropzoneJS + Go series. - [Part 1: How to build a file upload form](README.md) - [Part 2: How to display existing files on server](README_PART2.md) # DropzoneJS + Go: How to display existing files on server In this tutorial, we will show you how to display existing files on the server when using DropzoneJS and Go. This tutorial is based on [How to build a file upload form using DropzoneJS and Go](README.md). Make sure you have read it before proceeding to content in this tutorial. ## Table Of Content - [Preparation](#preparation) - [Modify the Server side](#modify-the-server-side) - [Modify the Client side](#modify-the-client-side) - [References](#references) - [The End](#the-end) ## Preparation Install the go package "github.com/nfnt/resize" with `go get github.com/nfnt/resize`, we need it to create thumbnails. In previous [tutorial](README.md). We have already set up a proper working DropzoneJs upload form. There is no additional file needed for this tutorial. What we need to do is to make some modifications to file below: 1. main.go 2. views/upload.html Let us get started! ## Modify the Server side In previous tutorial. All "/upload" does is to store uploaded files to the server directory "./public/uploads". So we need to add a piece of code to retrieve stored files' information (name and size), and return it in JSON format. Copy the content below to "main.go". Read comments for details. ```go // main.go package main import ( "image/jpeg" "image/png" "io" "os" "path" "path/filepath" "strings" "sync" "github.com/kataras/iris/v12" "github.com/nfnt/resize" // $ go get -u github.com/nfnt/resize ) const uploadsDir = "./public/uploads/" type uploadedFile struct { // {name: "", size: } are the dropzone's only requirements. Name string `json:"name"` Size int64 `json:"size"` } type uploadedFiles struct { dir string items []uploadedFile mu sync.RWMutex // slices are safe but RWMutex is a good practise for you. } // scan the ./public/uploads folder for any files // add them to a new uploadedFiles list. func scanUploads(dir string) *uploadedFiles { f := new(uploadedFiles) lindex := dir[len(dir)-1] if lindex != os.PathSeparator && lindex != '/' { dir += string(os.PathSeparator) } // create directories if necessary // and if, then return empty uploaded files; skipping the scan. if err := os.MkdirAll(dir, os.FileMode(0666)); err != nil { return f } // otherwise scan the given "dir" for files. f.scan(dir) return f } func (f *uploadedFiles) scan(dir string) { f.dir = dir filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { // if it's directory or a thumbnail we saved earlier, skip it. if info.IsDir() || strings.HasPrefix(info.Name(), "thumbnail_") { return nil } f.add(info.Name(), info.Size()) return nil }) } // add the file's Name and Size to the uploadedFiles memory list func (f *uploadedFiles) add(name string, size int64) uploadedFile { uf := uploadedFile{ Name: name, Size: size, } f.mu.Lock() f.items = append(f.items, uf) f.mu.Unlock() return uf } // create thumbnail 100x100 // and save that to the ./public/uploads/thumbnail_$FILENAME func (f *uploadedFiles) createThumbnail(uf uploadedFile) { file, err := os.Open(path.Join(f.dir, uf.Name)) if err != nil { return } defer file.Close() name := strings.ToLower(uf.Name) out, err := os.OpenFile(f.dir+"thumbnail_"+uf.Name, os.O_WRONLY|os.O_CREATE, 0666) if err != nil { return } defer out.Close() if strings.HasSuffix(name, ".jpg") { // decode jpeg into image.Image img, err := jpeg.Decode(file) if err != nil { return } // write new image to file resized := resize.Thumbnail(180, 180, img, resize.Lanczos3) jpeg.Encode(out, resized, &jpeg.Options{Quality: jpeg.DefaultQuality}) } else if strings.HasSuffix(name, ".png") { img, err := png.Decode(file) if err != nil { return } // write new image to file resized := resize.Thumbnail(180, 180, img, resize.Lanczos3) // slower but better res png.Encode(out, resized) } // and so on... you got the point, this code can be simplify, as a practise. } func main() { app := iris.New() app.RegisterView(iris.HTML("./views", ".html")) app.HandleDir("/public", iris.Dir("./public")) app.Get("/", func(ctx iris.Context) { if err := ctx.View("upload.html"); err != nil { ctx.HTML(fmt.Sprintf("

%s

", err.Error())) return } }) files := scanUploads(uploadsDir) app.Get("/uploads", func(ctx iris.Context) { ctx.JSON(files.items) }) app.Post("/upload", iris.LimitRequestBodySize(10<<20), func(ctx iris.Context) { // Get the file from the dropzone request file, info, err := ctx.FormFile("file") if err != nil { ctx.StatusCode(iris.StatusInternalServerError) ctx.Application().Logger().Warnf("Error while uploading: %v", err.Error()) return } defer file.Close() fname := info.Filename // Create a file with the same name // assuming that you have a folder named 'uploads' out, err := os.OpenFile(uploadsDir+fname, os.O_WRONLY|os.O_CREATE, 0666) if err != nil { ctx.StatusCode(iris.StatusInternalServerError) ctx.Application().Logger().Warnf("Error while preparing the new file: %v", err.Error()) return } defer out.Close() io.Copy(out, file) // optionally, add that file to the list in order to be visible when refresh. uploadedFile := files.add(fname, info.Size) go files.createThumbnail(uploadedFile) }) // start the server at http://localhost:8080 app.Listen(":8080") } ``` ## Modify the Client side Copy content below to "./views/upload.html". We will go through modifications individually. ```html DropzoneJS Uploader
``` 1. We added Jquery library into our page. This actually not for DropzoneJs directly. We are using Jquery's ajax function **$.get** only. You will see below 2. We added an ID element (my-dropzone) to the form. This is needed because we need to pass configuration values to Dropzone. And to do it, we must have an ID reference of it. So that we can configure it by assigning values to Dropzone.options.myDropzone. A lot of people face confusion when configuring Dropzone. To put it in a simple way. Do not take Dropzone as a Jquery plugin, it has its own syntax and you need to follow it. 3. This starts the main part of modification. What we did here is to pass a function to listen to Dropzone's init event. This event is called when Dropzone is initialized. 4. Retrieve files details from the new "/uploads" via ajax. 5. Create mockFile using values from server. mockFile is simply JavaScript objects with properties of name and size. Then we call Dropzone's **addedfile** and **thumbnail** functions explicitly to put existing files to Dropzone upload area and generate its thumbnail. ### Running the server Open the terminal at the current project's folder and execute: ```bash $ go run main.go Now listening on: http://localhost:8080 Application started. Press CTRL+C to shut down. ``` If you have done it successfully. Now go and upload some images and reload the upload page. Already uploaded files should auto display in Dropzone area. ![with uploaded files screenshot](with_files.png) ## References - http://www.dropzonejs.com/#server-side-implementation - https://www.startutorial.com/articles/view/how-to-build-a-file-upload-form-using-dropzonejs-and-php - https://docs.iris-go.com - https://github.com/kataras/iris/tree/main/_examples/dropzonejs ## The end Hopefully this simple tutorial helped you with your development. If you like my post, please follow me on [Twitter](https://twitter.com/makismaropoulos) and help spread the word. I need your support to continue. ================================================ FILE: _examples/dropzonejs/meta.yml ================================================ Name: DropzoneJS Articles: - Title: How to build a file upload form using DropzoneJS and Go Source: https://medium.com/hackernoon/how-to-build-a-file-upload-form-using-dropzonejs-and-go-8fb9f258a991 Author: https://twitter.com/@kataras - Title: How to display existing files on server using DropzoneJS and Go Source: https://medium.com/@kataras/how-to-display-existing-files-on-server-using-dropzonejs-and-go-53e24b57ba19 Author: https://twitter.com/@kataras ================================================ FILE: _examples/dropzonejs/src/main.go ================================================ package main import ( "fmt" "image/jpeg" "image/png" "io" "os" "path" "path/filepath" "strings" "sync" "github.com/kataras/iris/v12" "github.com/nfnt/resize" ) // $ go get -u github.com/nfnt/resize const uploadsDir = "./public/uploads/" type uploadedFile struct { // {name: "", size: } are the dropzone's only requirements. Name string `json:"name"` Size int64 `json:"size"` } type uploadedFiles struct { dir string items []uploadedFile mu sync.RWMutex // slices are safe but RWMutex is a good practise for you. } func scanUploads(dir string) *uploadedFiles { f := new(uploadedFiles) lindex := dir[len(dir)-1] if lindex != os.PathSeparator && lindex != '/' { dir += string(os.PathSeparator) } // create directories if necessary // and if, then return empty uploaded files; skipping the scan. if err := os.MkdirAll(dir, os.FileMode(0666)); err != nil { return f } // otherwise scan the given "dir" for files. f.scan(dir) return f } func (f *uploadedFiles) scan(dir string) { f.dir = dir filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { // if it's directory or a thumbnail we saved earlier, skip it. if info.IsDir() || strings.HasPrefix(info.Name(), "thumbnail_") { return nil } f.add(info.Name(), info.Size()) return nil }) } func (f *uploadedFiles) add(name string, size int64) uploadedFile { uf := uploadedFile{ Name: name, Size: size, } f.mu.Lock() f.items = append(f.items, uf) f.mu.Unlock() return uf } func (f *uploadedFiles) createThumbnail(uf uploadedFile) { file, err := os.Open(path.Join(f.dir, uf.Name)) if err != nil { return } defer file.Close() name := strings.ToLower(uf.Name) out, err := os.OpenFile(f.dir+"thumbnail_"+uf.Name, os.O_WRONLY|os.O_CREATE, 0666) if err != nil { return } defer out.Close() if strings.HasSuffix(name, ".jpg") { // decode jpeg into image.Image img, err := jpeg.Decode(file) if err != nil { return } // write new image to file resized := resize.Thumbnail(180, 180, img, resize.Lanczos3) jpeg.Encode(out, resized, &jpeg.Options{Quality: jpeg.DefaultQuality}) } else if strings.HasSuffix(name, ".png") { img, err := png.Decode(file) if err != nil { return } // write new image to file resized := resize.Thumbnail(180, 180, img, resize.Lanczos3) // slower but better res png.Encode(out, resized) } // and so on... you got the point, this code can be simplify, as a practise. } func main() { app := iris.New() app.RegisterView(iris.HTML("./views", ".html")) app.HandleDir("/public", iris.Dir("./public")) app.Get("/", func(ctx iris.Context) { if err := ctx.View("upload.html"); err != nil { ctx.HTML(fmt.Sprintf("

%s

", err.Error())) return } }) files := scanUploads(uploadsDir) app.Get("/uploads", func(ctx iris.Context) { ctx.JSON(files.items) }) app.Post("/upload", iris.LimitRequestBodySize(10<<20), func(ctx iris.Context) { // Get the file from the dropzone request file, info, err := ctx.FormFile("file") if err != nil { ctx.StatusCode(iris.StatusInternalServerError) ctx.Application().Logger().Warnf("Error while uploading: %v", err.Error()) return } defer file.Close() fname := info.Filename // Create a file with the same name // assuming that you have a folder named 'uploads' out, err := os.OpenFile(uploadsDir+fname, os.O_WRONLY|os.O_CREATE, 0666) if err != nil { ctx.StatusCode(iris.StatusInternalServerError) ctx.Application().Logger().Warnf("Error while preparing the new file: %v", err.Error()) return } defer out.Close() io.Copy(out, file) // optionally, add that file to the list in order to be visible when refresh. uploadedFile := files.add(fname, info.Size) go files.createThumbnail(uploadedFile) }) // start the server at http://localhost:8080 app.Listen(":8080") } ================================================ FILE: _examples/dropzonejs/src/public/css/dropzone.css ================================================ /* * The MIT License * Copyright (c) 2012 Matias Meno */ @-webkit-keyframes passing-through { 0% { opacity: 0; -webkit-transform: translateY(40px); -moz-transform: translateY(40px); -ms-transform: translateY(40px); -o-transform: translateY(40px); transform: translateY(40px); } 30%, 70% { opacity: 1; -webkit-transform: translateY(0px); -moz-transform: translateY(0px); -ms-transform: translateY(0px); -o-transform: translateY(0px); transform: translateY(0px); } 100% { opacity: 0; -webkit-transform: translateY(-40px); -moz-transform: translateY(-40px); -ms-transform: translateY(-40px); -o-transform: translateY(-40px); transform: translateY(-40px); } } @-moz-keyframes passing-through { 0% { opacity: 0; -webkit-transform: translateY(40px); -moz-transform: translateY(40px); -ms-transform: translateY(40px); -o-transform: translateY(40px); transform: translateY(40px); } 30%, 70% { opacity: 1; -webkit-transform: translateY(0px); -moz-transform: translateY(0px); -ms-transform: translateY(0px); -o-transform: translateY(0px); transform: translateY(0px); } 100% { opacity: 0; -webkit-transform: translateY(-40px); -moz-transform: translateY(-40px); -ms-transform: translateY(-40px); -o-transform: translateY(-40px); transform: translateY(-40px); } } @keyframes passing-through { 0% { opacity: 0; -webkit-transform: translateY(40px); -moz-transform: translateY(40px); -ms-transform: translateY(40px); -o-transform: translateY(40px); transform: translateY(40px); } 30%, 70% { opacity: 1; -webkit-transform: translateY(0px); -moz-transform: translateY(0px); -ms-transform: translateY(0px); -o-transform: translateY(0px); transform: translateY(0px); } 100% { opacity: 0; -webkit-transform: translateY(-40px); -moz-transform: translateY(-40px); -ms-transform: translateY(-40px); -o-transform: translateY(-40px); transform: translateY(-40px); } } @-webkit-keyframes slide-in { 0% { opacity: 0; -webkit-transform: translateY(40px); -moz-transform: translateY(40px); -ms-transform: translateY(40px); -o-transform: translateY(40px); transform: translateY(40px); } 30% { opacity: 1; -webkit-transform: translateY(0px); -moz-transform: translateY(0px); -ms-transform: translateY(0px); -o-transform: translateY(0px); transform: translateY(0px); } } @-moz-keyframes slide-in { 0% { opacity: 0; -webkit-transform: translateY(40px); -moz-transform: translateY(40px); -ms-transform: translateY(40px); -o-transform: translateY(40px); transform: translateY(40px); } 30% { opacity: 1; -webkit-transform: translateY(0px); -moz-transform: translateY(0px); -ms-transform: translateY(0px); -o-transform: translateY(0px); transform: translateY(0px); } } @keyframes slide-in { 0% { opacity: 0; -webkit-transform: translateY(40px); -moz-transform: translateY(40px); -ms-transform: translateY(40px); -o-transform: translateY(40px); transform: translateY(40px); } 30% { opacity: 1; -webkit-transform: translateY(0px); -moz-transform: translateY(0px); -ms-transform: translateY(0px); -o-transform: translateY(0px); transform: translateY(0px); } } @-webkit-keyframes pulse { 0% { -webkit-transform: scale(1); -moz-transform: scale(1); -ms-transform: scale(1); -o-transform: scale(1); transform: scale(1); } 10% { -webkit-transform: scale(1.1); -moz-transform: scale(1.1); -ms-transform: scale(1.1); -o-transform: scale(1.1); transform: scale(1.1); } 20% { -webkit-transform: scale(1); -moz-transform: scale(1); -ms-transform: scale(1); -o-transform: scale(1); transform: scale(1); } } @-moz-keyframes pulse { 0% { -webkit-transform: scale(1); -moz-transform: scale(1); -ms-transform: scale(1); -o-transform: scale(1); transform: scale(1); } 10% { -webkit-transform: scale(1.1); -moz-transform: scale(1.1); -ms-transform: scale(1.1); -o-transform: scale(1.1); transform: scale(1.1); } 20% { -webkit-transform: scale(1); -moz-transform: scale(1); -ms-transform: scale(1); -o-transform: scale(1); transform: scale(1); } } @keyframes pulse { 0% { -webkit-transform: scale(1); -moz-transform: scale(1); -ms-transform: scale(1); -o-transform: scale(1); transform: scale(1); } 10% { -webkit-transform: scale(1.1); -moz-transform: scale(1.1); -ms-transform: scale(1.1); -o-transform: scale(1.1); transform: scale(1.1); } 20% { -webkit-transform: scale(1); -moz-transform: scale(1); -ms-transform: scale(1); -o-transform: scale(1); transform: scale(1); } } .dropzone, .dropzone * { box-sizing: border-box; } .dropzone { min-height: 150px; border: 2px solid rgba(0, 0, 0, 0.3); background: white; padding: 20px 20px; } .dropzone.dz-clickable { cursor: pointer; } .dropzone.dz-clickable * { cursor: default; } .dropzone.dz-clickable .dz-message, .dropzone.dz-clickable .dz-message * { cursor: pointer; } .dropzone.dz-started .dz-message { display: none; } .dropzone.dz-drag-hover { border-style: solid; } .dropzone.dz-drag-hover .dz-message { opacity: 0.5; } .dropzone .dz-message { text-align: center; margin: 2em 0; } .dropzone .dz-preview { position: relative; display: inline-block; vertical-align: top; margin: 16px; min-height: 100px; } .dropzone .dz-preview:hover { z-index: 1000; } .dropzone .dz-preview:hover .dz-details { opacity: 1; } .dropzone .dz-preview.dz-file-preview .dz-image { border-radius: 20px; background: #999; background: linear-gradient(to bottom, #eee, #ddd); } .dropzone .dz-preview.dz-file-preview .dz-details { opacity: 1; } .dropzone .dz-preview.dz-image-preview { background: white; } .dropzone .dz-preview.dz-image-preview .dz-details { -webkit-transition: opacity 0.2s linear; -moz-transition: opacity 0.2s linear; -ms-transition: opacity 0.2s linear; -o-transition: opacity 0.2s linear; transition: opacity 0.2s linear; } .dropzone .dz-preview .dz-remove { font-size: 14px; text-align: center; display: block; cursor: pointer; border: none; } .dropzone .dz-preview .dz-remove:hover { text-decoration: underline; } .dropzone .dz-preview:hover .dz-details { opacity: 1; } .dropzone .dz-preview .dz-details { z-index: 20; position: absolute; top: 0; left: 0; opacity: 0; font-size: 13px; min-width: 100%; max-width: 100%; padding: 2em 1em; text-align: center; color: rgba(0, 0, 0, 0.9); line-height: 150%; } .dropzone .dz-preview .dz-details .dz-size { margin-bottom: 1em; font-size: 16px; } .dropzone .dz-preview .dz-details .dz-filename { white-space: nowrap; } .dropzone .dz-preview .dz-details .dz-filename:hover span { border: 1px solid rgba(200, 200, 200, 0.8); background-color: rgba(255, 255, 255, 0.8); } .dropzone .dz-preview .dz-details .dz-filename:not(:hover) { overflow: hidden; text-overflow: ellipsis; } .dropzone .dz-preview .dz-details .dz-filename:not(:hover) span { border: 1px solid transparent; } .dropzone .dz-preview .dz-details .dz-filename span, .dropzone .dz-preview .dz-details .dz-size span { background-color: rgba(255, 255, 255, 0.4); padding: 0 0.4em; border-radius: 3px; } .dropzone .dz-preview:hover .dz-image img { -webkit-transform: scale(1.05, 1.05); -moz-transform: scale(1.05, 1.05); -ms-transform: scale(1.05, 1.05); -o-transform: scale(1.05, 1.05); transform: scale(1.05, 1.05); -webkit-filter: blur(8px); filter: blur(8px); } .dropzone .dz-preview .dz-image { border-radius: 20px; overflow: hidden; width: 120px; height: 120px; position: relative; display: block; z-index: 10; } .dropzone .dz-preview .dz-image img { display: block; } .dropzone .dz-preview.dz-success .dz-success-mark { -webkit-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1); -moz-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1); -ms-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1); -o-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1); animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1); } .dropzone .dz-preview.dz-error .dz-error-mark { opacity: 1; -webkit-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1); -moz-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1); -ms-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1); -o-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1); animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1); } .dropzone .dz-preview .dz-success-mark, .dropzone .dz-preview .dz-error-mark { pointer-events: none; opacity: 0; z-index: 500; position: absolute; display: block; top: 50%; left: 50%; margin-left: -27px; margin-top: -27px; } .dropzone .dz-preview .dz-success-mark svg, .dropzone .dz-preview .dz-error-mark svg { display: block; width: 54px; height: 54px; } .dropzone .dz-preview.dz-processing .dz-progress { opacity: 1; -webkit-transition: all 0.2s linear; -moz-transition: all 0.2s linear; -ms-transition: all 0.2s linear; -o-transition: all 0.2s linear; transition: all 0.2s linear; } .dropzone .dz-preview.dz-complete .dz-progress { opacity: 0; -webkit-transition: opacity 0.4s ease-in; -moz-transition: opacity 0.4s ease-in; -ms-transition: opacity 0.4s ease-in; -o-transition: opacity 0.4s ease-in; transition: opacity 0.4s ease-in; } .dropzone .dz-preview:not(.dz-processing) .dz-progress { -webkit-animation: pulse 6s ease infinite; -moz-animation: pulse 6s ease infinite; -ms-animation: pulse 6s ease infinite; -o-animation: pulse 6s ease infinite; animation: pulse 6s ease infinite; } .dropzone .dz-preview .dz-progress { opacity: 1; z-index: 1000; pointer-events: none; position: absolute; height: 16px; left: 50%; top: 50%; margin-top: -8px; width: 80px; margin-left: -40px; background: rgba(255, 255, 255, 0.9); -webkit-transform: scale(1); border-radius: 8px; overflow: hidden; } .dropzone .dz-preview .dz-progress .dz-upload { background: #333; background: linear-gradient(to bottom, #666, #444); position: absolute; top: 0; left: 0; bottom: 0; width: 0; -webkit-transition: width 300ms ease-in-out; -moz-transition: width 300ms ease-in-out; -ms-transition: width 300ms ease-in-out; -o-transition: width 300ms ease-in-out; transition: width 300ms ease-in-out; } .dropzone .dz-preview.dz-error .dz-error-message { display: block; } .dropzone .dz-preview.dz-error:hover .dz-error-message { opacity: 1; pointer-events: auto; } .dropzone .dz-preview .dz-error-message { pointer-events: none; z-index: 1000; position: absolute; display: block; display: none; opacity: 0; -webkit-transition: opacity 0.3s ease; -moz-transition: opacity 0.3s ease; -ms-transition: opacity 0.3s ease; -o-transition: opacity 0.3s ease; transition: opacity 0.3s ease; border-radius: 8px; font-size: 13px; top: 130px; left: -10px; width: 140px; background: #be2626; background: linear-gradient(to bottom, #be2626, #a92222); padding: 0.5em 1.2em; color: white; } .dropzone .dz-preview .dz-error-message:after { content: ''; position: absolute; top: -6px; left: 64px; width: 0; height: 0; border-left: 6px solid transparent; border-right: 6px solid transparent; border-bottom: 6px solid #be2626; } ================================================ FILE: _examples/dropzonejs/src/public/js/dropzone.js ================================================ /* * * More info at [www.dropzonejs.com](http://www.dropzonejs.com) * * Copyright (c) 2012, Matias Meno * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. * */ (function() { var Dropzone, Emitter, ExifRestore, camelize, contentLoaded, detectVerticalSquash, drawImageIOSFix, noop, without, slice = [].slice, extend1 = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, hasProp = {}.hasOwnProperty; noop = function() {}; Emitter = (function() { function Emitter() {} Emitter.prototype.addEventListener = Emitter.prototype.on; Emitter.prototype.on = function(event, fn) { this._callbacks = this._callbacks || {}; if (!this._callbacks[event]) { this._callbacks[event] = []; } this._callbacks[event].push(fn); return this; }; Emitter.prototype.emit = function() { var args, callback, callbacks, event, j, len; event = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : []; this._callbacks = this._callbacks || {}; callbacks = this._callbacks[event]; if (callbacks) { for (j = 0, len = callbacks.length; j < len; j++) { callback = callbacks[j]; callback.apply(this, args); } } return this; }; Emitter.prototype.removeListener = Emitter.prototype.off; Emitter.prototype.removeAllListeners = Emitter.prototype.off; Emitter.prototype.removeEventListener = Emitter.prototype.off; Emitter.prototype.off = function(event, fn) { var callback, callbacks, i, j, len; if (!this._callbacks || arguments.length === 0) { this._callbacks = {}; return this; } callbacks = this._callbacks[event]; if (!callbacks) { return this; } if (arguments.length === 1) { delete this._callbacks[event]; return this; } for (i = j = 0, len = callbacks.length; j < len; i = ++j) { callback = callbacks[i]; if (callback === fn) { callbacks.splice(i, 1); break; } } return this; }; return Emitter; })(); Dropzone = (function(superClass) { var extend, resolveOption; extend1(Dropzone, superClass); Dropzone.prototype.Emitter = Emitter; /* This is a list of all available events you can register on a dropzone object. You can register an event handler like this: dropzone.on("dragEnter", function() { }); */ Dropzone.prototype.events = ["drop", "dragstart", "dragend", "dragenter", "dragover", "dragleave", "addedfile", "addedfiles", "removedfile", "thumbnail", "error", "errormultiple", "processing", "processingmultiple", "uploadprogress", "totaluploadprogress", "sending", "sendingmultiple", "success", "successmultiple", "canceled", "canceledmultiple", "complete", "completemultiple", "reset", "maxfilesexceeded", "maxfilesreached", "queuecomplete"]; Dropzone.prototype.defaultOptions = { url: null, method: "post", withCredentials: false, timeout: 30000, parallelUploads: 2, uploadMultiple: false, maxFilesize: 256, paramName: "file", createImageThumbnails: true, maxThumbnailFilesize: 10, thumbnailWidth: 120, thumbnailHeight: 120, thumbnailMethod: 'crop', resizeWidth: null, resizeHeight: null, resizeMimeType: null, resizeQuality: 0.8, resizeMethod: 'contain', filesizeBase: 1000, maxFiles: null, params: {}, headers: null, clickable: true, ignoreHiddenFiles: true, acceptedFiles: null, acceptedMimeTypes: null, autoProcessQueue: true, autoQueue: true, addRemoveLinks: false, previewsContainer: null, hiddenInputContainer: "body", capture: null, renameFilename: null, renameFile: null, forceFallback: false, dictDefaultMessage: "Drop files here to upload", dictFallbackMessage: "Your browser does not support drag'n'drop file uploads.", dictFallbackText: "Please use the fallback form below to upload your files like in the olden days.", dictFileTooBig: "File is too big ({{filesize}}MiB). Max filesize: {{maxFilesize}}MiB.", dictInvalidFileType: "You can't upload files of this type.", dictResponseError: "Server responded with {{statusCode}} code.", dictCancelUpload: "Cancel upload", dictCancelUploadConfirmation: "Are you sure you want to cancel this upload?", dictRemoveFile: "Remove file", dictRemoveFileConfirmation: null, dictMaxFilesExceeded: "You can not upload any more files.", dictFileSizeUnits: { tb: "TB", gb: "GB", mb: "MB", kb: "KB", b: "b" }, init: function() { return noop; }, accept: function(file, done) { return done(); }, fallback: function() { var child, j, len, messageElement, ref, span; this.element.className = this.element.className + " dz-browser-not-supported"; ref = this.element.getElementsByTagName("div"); for (j = 0, len = ref.length; j < len; j++) { child = ref[j]; if (/(^| )dz-message($| )/.test(child.className)) { messageElement = child; child.className = "dz-message"; continue; } } if (!messageElement) { messageElement = Dropzone.createElement("
"); this.element.appendChild(messageElement); } span = messageElement.getElementsByTagName("span")[0]; if (span) { if (span.textContent != null) { span.textContent = this.options.dictFallbackMessage; } else if (span.innerText != null) { span.innerText = this.options.dictFallbackMessage; } } return this.element.appendChild(this.getFallbackForm()); }, resize: function(file, width, height, resizeMethod) { var info, srcRatio, trgRatio; info = { srcX: 0, srcY: 0, srcWidth: file.width, srcHeight: file.height }; srcRatio = file.width / file.height; if ((width == null) && (height == null)) { width = info.srcWidth; height = info.srcHeight; } else if (width == null) { width = height * srcRatio; } else if (height == null) { height = width / srcRatio; } width = Math.min(width, info.srcWidth); height = Math.min(height, info.srcHeight); trgRatio = width / height; if (info.srcWidth > width || info.srcHeight > height) { if (resizeMethod === 'crop') { if (srcRatio > trgRatio) { info.srcHeight = file.height; info.srcWidth = info.srcHeight * trgRatio; } else { info.srcWidth = file.width; info.srcHeight = info.srcWidth / trgRatio; } } else if (resizeMethod === 'contain') { if (srcRatio > trgRatio) { height = width / srcRatio; } else { width = height * srcRatio; } } else { throw new Error("Unknown resizeMethod '" + resizeMethod + "'"); } } info.srcX = (file.width - info.srcWidth) / 2; info.srcY = (file.height - info.srcHeight) / 2; info.trgWidth = width; info.trgHeight = height; return info; }, transformFile: function(file, done) { if ((this.options.resizeWidth || this.options.resizeHeight) && file.type.match(/image.*/)) { return this.resizeImage(file, this.options.resizeWidth, this.options.resizeHeight, this.options.resizeMethod, done); } else { return done(file); } }, previewTemplate: "
\n
\n
\n
\n
\n
\n
\n
\n
\n \n Check\n \n \n \n \n \n
\n
\n \n Error\n \n \n \n \n \n \n \n
\n
", /* Those functions register themselves to the events on init and handle all the user interface specific stuff. Overwriting them won't break the upload but can break the way it's displayed. You can overwrite them if you don't like the default behavior. If you just want to add an additional event handler, register it on the dropzone object and don't overwrite those options. */ drop: function(e) { return this.element.classList.remove("dz-drag-hover"); }, dragstart: noop, dragend: function(e) { return this.element.classList.remove("dz-drag-hover"); }, dragenter: function(e) { return this.element.classList.add("dz-drag-hover"); }, dragover: function(e) { return this.element.classList.add("dz-drag-hover"); }, dragleave: function(e) { return this.element.classList.remove("dz-drag-hover"); }, paste: noop, reset: function() { return this.element.classList.remove("dz-started"); }, addedfile: function(file) { var j, k, l, len, len1, len2, node, ref, ref1, ref2, removeFileEvent, removeLink, results; if (this.element === this.previewsContainer) { this.element.classList.add("dz-started"); } if (this.previewsContainer) { file.previewElement = Dropzone.createElement(this.options.previewTemplate.trim()); file.previewTemplate = file.previewElement; this.previewsContainer.appendChild(file.previewElement); ref = file.previewElement.querySelectorAll("[data-dz-name]"); for (j = 0, len = ref.length; j < len; j++) { node = ref[j]; node.textContent = file.name; } ref1 = file.previewElement.querySelectorAll("[data-dz-size]"); for (k = 0, len1 = ref1.length; k < len1; k++) { node = ref1[k]; node.innerHTML = this.filesize(file.size); } if (this.options.addRemoveLinks) { file._removeLink = Dropzone.createElement("" + this.options.dictRemoveFile + ""); file.previewElement.appendChild(file._removeLink); } removeFileEvent = (function(_this) { return function(e) { e.preventDefault(); e.stopPropagation(); if (file.status === Dropzone.UPLOADING) { return Dropzone.confirm(_this.options.dictCancelUploadConfirmation, function() { return _this.removeFile(file); }); } else { if (_this.options.dictRemoveFileConfirmation) { return Dropzone.confirm(_this.options.dictRemoveFileConfirmation, function() { return _this.removeFile(file); }); } else { return _this.removeFile(file); } } }; })(this); ref2 = file.previewElement.querySelectorAll("[data-dz-remove]"); results = []; for (l = 0, len2 = ref2.length; l < len2; l++) { removeLink = ref2[l]; results.push(removeLink.addEventListener("click", removeFileEvent)); } return results; } }, removedfile: function(file) { var ref; if (file.previewElement) { if ((ref = file.previewElement) != null) { ref.parentNode.removeChild(file.previewElement); } } return this._updateMaxFilesReachedClass(); }, thumbnail: function(file, dataUrl) { var j, len, ref, thumbnailElement; if (file.previewElement) { file.previewElement.classList.remove("dz-file-preview"); ref = file.previewElement.querySelectorAll("[data-dz-thumbnail]"); for (j = 0, len = ref.length; j < len; j++) { thumbnailElement = ref[j]; thumbnailElement.alt = file.name; thumbnailElement.src = dataUrl; } return setTimeout(((function(_this) { return function() { return file.previewElement.classList.add("dz-image-preview"); }; })(this)), 1); } }, error: function(file, message) { var j, len, node, ref, results; if (file.previewElement) { file.previewElement.classList.add("dz-error"); if (typeof message !== "String" && message.error) { message = message.error; } ref = file.previewElement.querySelectorAll("[data-dz-errormessage]"); results = []; for (j = 0, len = ref.length; j < len; j++) { node = ref[j]; results.push(node.textContent = message); } return results; } }, errormultiple: noop, processing: function(file) { if (file.previewElement) { file.previewElement.classList.add("dz-processing"); if (file._removeLink) { return file._removeLink.textContent = this.options.dictCancelUpload; } } }, processingmultiple: noop, uploadprogress: function(file, progress, bytesSent) { var j, len, node, ref, results; if (file.previewElement) { ref = file.previewElement.querySelectorAll("[data-dz-uploadprogress]"); results = []; for (j = 0, len = ref.length; j < len; j++) { node = ref[j]; if (node.nodeName === 'PROGRESS') { results.push(node.value = progress); } else { results.push(node.style.width = progress + "%"); } } return results; } }, totaluploadprogress: noop, sending: noop, sendingmultiple: noop, success: function(file) { if (file.previewElement) { return file.previewElement.classList.add("dz-success"); } }, successmultiple: noop, canceled: function(file) { return this.emit("error", file, "Upload canceled."); }, canceledmultiple: noop, complete: function(file) { if (file._removeLink) { file._removeLink.textContent = this.options.dictRemoveFile; } if (file.previewElement) { return file.previewElement.classList.add("dz-complete"); } }, completemultiple: noop, maxfilesexceeded: noop, maxfilesreached: noop, queuecomplete: noop, addedfiles: noop }; extend = function() { var j, key, len, object, objects, target, val; target = arguments[0], objects = 2 <= arguments.length ? slice.call(arguments, 1) : []; for (j = 0, len = objects.length; j < len; j++) { object = objects[j]; for (key in object) { val = object[key]; target[key] = val; } } return target; }; function Dropzone(element1, options) { var elementOptions, fallback, ref; this.element = element1; this.version = Dropzone.version; this.defaultOptions.previewTemplate = this.defaultOptions.previewTemplate.replace(/\n*/g, ""); this.clickableElements = []; this.listeners = []; this.files = []; if (typeof this.element === "string") { this.element = document.querySelector(this.element); } if (!(this.element && (this.element.nodeType != null))) { throw new Error("Invalid dropzone element."); } if (this.element.dropzone) { throw new Error("Dropzone already attached."); } Dropzone.instances.push(this); this.element.dropzone = this; elementOptions = (ref = Dropzone.optionsForElement(this.element)) != null ? ref : {}; this.options = extend({}, this.defaultOptions, elementOptions, options != null ? options : {}); if (this.options.forceFallback || !Dropzone.isBrowserSupported()) { return this.options.fallback.call(this); } if (this.options.url == null) { this.options.url = this.element.getAttribute("action"); } if (!this.options.url) { throw new Error("No URL provided."); } if (this.options.acceptedFiles && this.options.acceptedMimeTypes) { throw new Error("You can't provide both 'acceptedFiles' and 'acceptedMimeTypes'. 'acceptedMimeTypes' is deprecated."); } if (this.options.acceptedMimeTypes) { this.options.acceptedFiles = this.options.acceptedMimeTypes; delete this.options.acceptedMimeTypes; } if (this.options.renameFilename != null) { this.options.renameFile = (function(_this) { return function(file) { return _this.options.renameFilename.call(_this, file.name, file); }; })(this); } this.options.method = this.options.method.toUpperCase(); if ((fallback = this.getExistingFallback()) && fallback.parentNode) { fallback.parentNode.removeChild(fallback); } if (this.options.previewsContainer !== false) { if (this.options.previewsContainer) { this.previewsContainer = Dropzone.getElement(this.options.previewsContainer, "previewsContainer"); } else { this.previewsContainer = this.element; } } if (this.options.clickable) { if (this.options.clickable === true) { this.clickableElements = [this.element]; } else { this.clickableElements = Dropzone.getElements(this.options.clickable, "clickable"); } } this.init(); } Dropzone.prototype.getAcceptedFiles = function() { var file, j, len, ref, results; ref = this.files; results = []; for (j = 0, len = ref.length; j < len; j++) { file = ref[j]; if (file.accepted) { results.push(file); } } return results; }; Dropzone.prototype.getRejectedFiles = function() { var file, j, len, ref, results; ref = this.files; results = []; for (j = 0, len = ref.length; j < len; j++) { file = ref[j]; if (!file.accepted) { results.push(file); } } return results; }; Dropzone.prototype.getFilesWithStatus = function(status) { var file, j, len, ref, results; ref = this.files; results = []; for (j = 0, len = ref.length; j < len; j++) { file = ref[j]; if (file.status === status) { results.push(file); } } return results; }; Dropzone.prototype.getQueuedFiles = function() { return this.getFilesWithStatus(Dropzone.QUEUED); }; Dropzone.prototype.getUploadingFiles = function() { return this.getFilesWithStatus(Dropzone.UPLOADING); }; Dropzone.prototype.getAddedFiles = function() { return this.getFilesWithStatus(Dropzone.ADDED); }; Dropzone.prototype.getActiveFiles = function() { var file, j, len, ref, results; ref = this.files; results = []; for (j = 0, len = ref.length; j < len; j++) { file = ref[j]; if (file.status === Dropzone.UPLOADING || file.status === Dropzone.QUEUED) { results.push(file); } } return results; }; Dropzone.prototype.init = function() { var eventName, j, len, noPropagation, ref, ref1, setupHiddenFileInput; if (this.element.tagName === "form") { this.element.setAttribute("enctype", "multipart/form-data"); } if (this.element.classList.contains("dropzone") && !this.element.querySelector(".dz-message")) { this.element.appendChild(Dropzone.createElement("
" + this.options.dictDefaultMessage + "
")); } if (this.clickableElements.length) { setupHiddenFileInput = (function(_this) { return function() { if (_this.hiddenFileInput) { _this.hiddenFileInput.parentNode.removeChild(_this.hiddenFileInput); } _this.hiddenFileInput = document.createElement("input"); _this.hiddenFileInput.setAttribute("type", "file"); if ((_this.options.maxFiles == null) || _this.options.maxFiles > 1) { _this.hiddenFileInput.setAttribute("multiple", "multiple"); } _this.hiddenFileInput.className = "dz-hidden-input"; if (_this.options.acceptedFiles != null) { _this.hiddenFileInput.setAttribute("accept", _this.options.acceptedFiles); } if (_this.options.capture != null) { _this.hiddenFileInput.setAttribute("capture", _this.options.capture); } _this.hiddenFileInput.style.visibility = "hidden"; _this.hiddenFileInput.style.position = "absolute"; _this.hiddenFileInput.style.top = "0"; _this.hiddenFileInput.style.left = "0"; _this.hiddenFileInput.style.height = "0"; _this.hiddenFileInput.style.width = "0"; document.querySelector(_this.options.hiddenInputContainer).appendChild(_this.hiddenFileInput); return _this.hiddenFileInput.addEventListener("change", function() { var file, files, j, len; files = _this.hiddenFileInput.files; if (files.length) { for (j = 0, len = files.length; j < len; j++) { file = files[j]; _this.addFile(file); } } _this.emit("addedfiles", files); return setupHiddenFileInput(); }); }; })(this); setupHiddenFileInput(); } this.URL = (ref = window.URL) != null ? ref : window.webkitURL; ref1 = this.events; for (j = 0, len = ref1.length; j < len; j++) { eventName = ref1[j]; this.on(eventName, this.options[eventName]); } this.on("uploadprogress", (function(_this) { return function() { return _this.updateTotalUploadProgress(); }; })(this)); this.on("removedfile", (function(_this) { return function() { return _this.updateTotalUploadProgress(); }; })(this)); this.on("canceled", (function(_this) { return function(file) { return _this.emit("complete", file); }; })(this)); this.on("complete", (function(_this) { return function(file) { if (_this.getAddedFiles().length === 0 && _this.getUploadingFiles().length === 0 && _this.getQueuedFiles().length === 0) { return setTimeout((function() { return _this.emit("queuecomplete"); }), 0); } }; })(this)); noPropagation = function(e) { e.stopPropagation(); if (e.preventDefault) { return e.preventDefault(); } else { return e.returnValue = false; } }; this.listeners = [ { element: this.element, events: { "dragstart": (function(_this) { return function(e) { return _this.emit("dragstart", e); }; })(this), "dragenter": (function(_this) { return function(e) { noPropagation(e); return _this.emit("dragenter", e); }; })(this), "dragover": (function(_this) { return function(e) { var efct; try { efct = e.dataTransfer.effectAllowed; } catch (undefined) {} e.dataTransfer.dropEffect = 'move' === efct || 'linkMove' === efct ? 'move' : 'copy'; noPropagation(e); return _this.emit("dragover", e); }; })(this), "dragleave": (function(_this) { return function(e) { return _this.emit("dragleave", e); }; })(this), "drop": (function(_this) { return function(e) { noPropagation(e); return _this.drop(e); }; })(this), "dragend": (function(_this) { return function(e) { return _this.emit("dragend", e); }; })(this) } } ]; this.clickableElements.forEach((function(_this) { return function(clickableElement) { return _this.listeners.push({ element: clickableElement, events: { "click": function(evt) { if ((clickableElement !== _this.element) || (evt.target === _this.element || Dropzone.elementInside(evt.target, _this.element.querySelector(".dz-message")))) { _this.hiddenFileInput.click(); } return true; } } }); }; })(this)); this.enable(); return this.options.init.call(this); }; Dropzone.prototype.destroy = function() { var ref; this.disable(); this.removeAllFiles(true); if ((ref = this.hiddenFileInput) != null ? ref.parentNode : void 0) { this.hiddenFileInput.parentNode.removeChild(this.hiddenFileInput); this.hiddenFileInput = null; } delete this.element.dropzone; return Dropzone.instances.splice(Dropzone.instances.indexOf(this), 1); }; Dropzone.prototype.updateTotalUploadProgress = function() { var activeFiles, file, j, len, ref, totalBytes, totalBytesSent, totalUploadProgress; totalBytesSent = 0; totalBytes = 0; activeFiles = this.getActiveFiles(); if (activeFiles.length) { ref = this.getActiveFiles(); for (j = 0, len = ref.length; j < len; j++) { file = ref[j]; totalBytesSent += file.upload.bytesSent; totalBytes += file.upload.total; } totalUploadProgress = 100 * totalBytesSent / totalBytes; } else { totalUploadProgress = 100; } return this.emit("totaluploadprogress", totalUploadProgress, totalBytes, totalBytesSent); }; Dropzone.prototype._getParamName = function(n) { if (typeof this.options.paramName === "function") { return this.options.paramName(n); } else { return "" + this.options.paramName + (this.options.uploadMultiple ? "[" + n + "]" : ""); } }; Dropzone.prototype._renameFile = function(file) { if (typeof this.options.renameFile !== "function") { return file.name; } return this.options.renameFile(file); }; Dropzone.prototype.getFallbackForm = function() { var existingFallback, fields, fieldsString, form; if (existingFallback = this.getExistingFallback()) { return existingFallback; } fieldsString = "
"; if (this.options.dictFallbackText) { fieldsString += "

" + this.options.dictFallbackText + "

"; } fieldsString += "
"; fields = Dropzone.createElement(fieldsString); if (this.element.tagName !== "FORM") { form = Dropzone.createElement("
"); form.appendChild(fields); } else { this.element.setAttribute("enctype", "multipart/form-data"); this.element.setAttribute("method", this.options.method); } return form != null ? form : fields; }; Dropzone.prototype.getExistingFallback = function() { var fallback, getFallback, j, len, ref, tagName; getFallback = function(elements) { var el, j, len; for (j = 0, len = elements.length; j < len; j++) { el = elements[j]; if (/(^| )fallback($| )/.test(el.className)) { return el; } } }; ref = ["div", "form"]; for (j = 0, len = ref.length; j < len; j++) { tagName = ref[j]; if (fallback = getFallback(this.element.getElementsByTagName(tagName))) { return fallback; } } }; Dropzone.prototype.setupEventListeners = function() { var elementListeners, event, j, len, listener, ref, results; ref = this.listeners; results = []; for (j = 0, len = ref.length; j < len; j++) { elementListeners = ref[j]; results.push((function() { var ref1, results1; ref1 = elementListeners.events; results1 = []; for (event in ref1) { listener = ref1[event]; results1.push(elementListeners.element.addEventListener(event, listener, false)); } return results1; })()); } return results; }; Dropzone.prototype.removeEventListeners = function() { var elementListeners, event, j, len, listener, ref, results; ref = this.listeners; results = []; for (j = 0, len = ref.length; j < len; j++) { elementListeners = ref[j]; results.push((function() { var ref1, results1; ref1 = elementListeners.events; results1 = []; for (event in ref1) { listener = ref1[event]; results1.push(elementListeners.element.removeEventListener(event, listener, false)); } return results1; })()); } return results; }; Dropzone.prototype.disable = function() { var file, j, len, ref, results; this.clickableElements.forEach(function(element) { return element.classList.remove("dz-clickable"); }); this.removeEventListeners(); ref = this.files; results = []; for (j = 0, len = ref.length; j < len; j++) { file = ref[j]; results.push(this.cancelUpload(file)); } return results; }; Dropzone.prototype.enable = function() { this.clickableElements.forEach(function(element) { return element.classList.add("dz-clickable"); }); return this.setupEventListeners(); }; Dropzone.prototype.filesize = function(size) { var cutoff, i, j, len, selectedSize, selectedUnit, unit, units; selectedSize = 0; selectedUnit = "b"; if (size > 0) { units = ['tb', 'gb', 'mb', 'kb', 'b']; for (i = j = 0, len = units.length; j < len; i = ++j) { unit = units[i]; cutoff = Math.pow(this.options.filesizeBase, 4 - i) / 10; if (size >= cutoff) { selectedSize = size / Math.pow(this.options.filesizeBase, 4 - i); selectedUnit = unit; break; } } selectedSize = Math.round(10 * selectedSize) / 10; } return "" + selectedSize + " " + this.options.dictFileSizeUnits[selectedUnit]; }; Dropzone.prototype._updateMaxFilesReachedClass = function() { if ((this.options.maxFiles != null) && this.getAcceptedFiles().length >= this.options.maxFiles) { if (this.getAcceptedFiles().length === this.options.maxFiles) { this.emit('maxfilesreached', this.files); } return this.element.classList.add("dz-max-files-reached"); } else { return this.element.classList.remove("dz-max-files-reached"); } }; Dropzone.prototype.drop = function(e) { var files, items; if (!e.dataTransfer) { return; } this.emit("drop", e); files = e.dataTransfer.files; this.emit("addedfiles", files); if (files.length) { items = e.dataTransfer.items; if (items && items.length && (items[0].webkitGetAsEntry != null)) { this._addFilesFromItems(items); } else { this.handleFiles(files); } } }; Dropzone.prototype.paste = function(e) { var items, ref; if ((e != null ? (ref = e.clipboardData) != null ? ref.items : void 0 : void 0) == null) { return; } this.emit("paste", e); items = e.clipboardData.items; if (items.length) { return this._addFilesFromItems(items); } }; Dropzone.prototype.handleFiles = function(files) { var file, j, len, results; results = []; for (j = 0, len = files.length; j < len; j++) { file = files[j]; results.push(this.addFile(file)); } return results; }; Dropzone.prototype._addFilesFromItems = function(items) { var entry, item, j, len, results; results = []; for (j = 0, len = items.length; j < len; j++) { item = items[j]; if ((item.webkitGetAsEntry != null) && (entry = item.webkitGetAsEntry())) { if (entry.isFile) { results.push(this.addFile(item.getAsFile())); } else if (entry.isDirectory) { results.push(this._addFilesFromDirectory(entry, entry.name)); } else { results.push(void 0); } } else if (item.getAsFile != null) { if ((item.kind == null) || item.kind === "file") { results.push(this.addFile(item.getAsFile())); } else { results.push(void 0); } } else { results.push(void 0); } } return results; }; Dropzone.prototype._addFilesFromDirectory = function(directory, path) { var dirReader, errorHandler, readEntries; dirReader = directory.createReader(); errorHandler = function(error) { return typeof console !== "undefined" && console !== null ? typeof console.log === "function" ? console.log(error) : void 0 : void 0; }; readEntries = (function(_this) { return function() { return dirReader.readEntries(function(entries) { var entry, j, len; if (entries.length > 0) { for (j = 0, len = entries.length; j < len; j++) { entry = entries[j]; if (entry.isFile) { entry.file(function(file) { if (_this.options.ignoreHiddenFiles && file.name.substring(0, 1) === '.') { return; } file.fullPath = path + "/" + file.name; return _this.addFile(file); }); } else if (entry.isDirectory) { _this._addFilesFromDirectory(entry, path + "/" + entry.name); } } readEntries(); } return null; }, errorHandler); }; })(this); return readEntries(); }; Dropzone.prototype.accept = function(file, done) { if (file.size > this.options.maxFilesize * 1024 * 1024) { return done(this.options.dictFileTooBig.replace("{{filesize}}", Math.round(file.size / 1024 / 10.24) / 100).replace("{{maxFilesize}}", this.options.maxFilesize)); } else if (!Dropzone.isValidFile(file, this.options.acceptedFiles)) { return done(this.options.dictInvalidFileType); } else if ((this.options.maxFiles != null) && this.getAcceptedFiles().length >= this.options.maxFiles) { done(this.options.dictMaxFilesExceeded.replace("{{maxFiles}}", this.options.maxFiles)); return this.emit("maxfilesexceeded", file); } else { return this.options.accept.call(this, file, done); } }; Dropzone.prototype.addFile = function(file) { file.upload = { progress: 0, total: file.size, bytesSent: 0, filename: this._renameFile(file) }; this.files.push(file); file.status = Dropzone.ADDED; this.emit("addedfile", file); this._enqueueThumbnail(file); return this.accept(file, (function(_this) { return function(error) { if (error) { file.accepted = false; _this._errorProcessing([file], error); } else { file.accepted = true; if (_this.options.autoQueue) { _this.enqueueFile(file); } } return _this._updateMaxFilesReachedClass(); }; })(this)); }; Dropzone.prototype.enqueueFiles = function(files) { var file, j, len; for (j = 0, len = files.length; j < len; j++) { file = files[j]; this.enqueueFile(file); } return null; }; Dropzone.prototype.enqueueFile = function(file) { if (file.status === Dropzone.ADDED && file.accepted === true) { file.status = Dropzone.QUEUED; if (this.options.autoProcessQueue) { return setTimeout(((function(_this) { return function() { return _this.processQueue(); }; })(this)), 0); } } else { throw new Error("This file can't be queued because it has already been processed or was rejected."); } }; Dropzone.prototype._thumbnailQueue = []; Dropzone.prototype._processingThumbnail = false; Dropzone.prototype._enqueueThumbnail = function(file) { if (this.options.createImageThumbnails && file.type.match(/image.*/) && file.size <= this.options.maxThumbnailFilesize * 1024 * 1024) { this._thumbnailQueue.push(file); return setTimeout(((function(_this) { return function() { return _this._processThumbnailQueue(); }; })(this)), 0); } }; Dropzone.prototype._processThumbnailQueue = function() { var file; if (this._processingThumbnail || this._thumbnailQueue.length === 0) { return; } this._processingThumbnail = true; file = this._thumbnailQueue.shift(); return this.createThumbnail(file, this.options.thumbnailWidth, this.options.thumbnailHeight, this.options.thumbnailMethod, true, (function(_this) { return function(dataUrl) { _this.emit("thumbnail", file, dataUrl); _this._processingThumbnail = false; return _this._processThumbnailQueue(); }; })(this)); }; Dropzone.prototype.removeFile = function(file) { if (file.status === Dropzone.UPLOADING) { this.cancelUpload(file); } this.files = without(this.files, file); this.emit("removedfile", file); if (this.files.length === 0) { return this.emit("reset"); } }; Dropzone.prototype.removeAllFiles = function(cancelIfNecessary) { var file, j, len, ref; if (cancelIfNecessary == null) { cancelIfNecessary = false; } ref = this.files.slice(); for (j = 0, len = ref.length; j < len; j++) { file = ref[j]; if (file.status !== Dropzone.UPLOADING || cancelIfNecessary) { this.removeFile(file); } } return null; }; Dropzone.prototype.resizeImage = function(file, width, height, resizeMethod, callback) { return this.createThumbnail(file, width, height, resizeMethod, false, (function(_this) { return function(dataUrl, canvas) { var resizeMimeType, resizedDataURL; if (canvas === null) { return callback(file); } else { resizeMimeType = _this.options.resizeMimeType; if (resizeMimeType == null) { resizeMimeType = file.type; } resizedDataURL = canvas.toDataURL(resizeMimeType, _this.options.resizeQuality); if (resizeMimeType === 'image/jpeg' || resizeMimeType === 'image/jpg') { resizedDataURL = ExifRestore.restore(file.dataURL, resizedDataURL); } return callback(Dropzone.dataURItoBlob(resizedDataURL)); } }; })(this)); }; Dropzone.prototype.createThumbnail = function(file, width, height, resizeMethod, fixOrientation, callback) { var fileReader; fileReader = new FileReader; fileReader.onload = (function(_this) { return function() { file.dataURL = fileReader.result; if (file.type === "image/svg+xml") { if (callback != null) { callback(fileReader.result); } return; } return _this.createThumbnailFromUrl(file, width, height, resizeMethod, fixOrientation, callback); }; })(this); return fileReader.readAsDataURL(file); }; Dropzone.prototype.createThumbnailFromUrl = function(file, width, height, resizeMethod, fixOrientation, callback, crossOrigin) { var img; img = document.createElement("img"); if (crossOrigin) { img.crossOrigin = crossOrigin; } img.onload = (function(_this) { return function() { var loadExif; loadExif = function(callback) { return callback(1); }; if ((typeof EXIF !== "undefined" && EXIF !== null) && fixOrientation) { loadExif = function(callback) { return EXIF.getData(img, function() { return callback(EXIF.getTag(this, 'Orientation')); }); }; } return loadExif(function(orientation) { var canvas, ctx, ref, ref1, ref2, ref3, resizeInfo, thumbnail; file.width = img.width; file.height = img.height; resizeInfo = _this.options.resize.call(_this, file, width, height, resizeMethod); canvas = document.createElement("canvas"); ctx = canvas.getContext("2d"); canvas.width = resizeInfo.trgWidth; canvas.height = resizeInfo.trgHeight; if (orientation > 4) { canvas.width = resizeInfo.trgHeight; canvas.height = resizeInfo.trgWidth; } switch (orientation) { case 2: ctx.translate(canvas.width, 0); ctx.scale(-1, 1); break; case 3: ctx.translate(canvas.width, canvas.height); ctx.rotate(Math.PI); break; case 4: ctx.translate(0, canvas.height); ctx.scale(1, -1); break; case 5: ctx.rotate(0.5 * Math.PI); ctx.scale(1, -1); break; case 6: ctx.rotate(0.5 * Math.PI); ctx.translate(0, -canvas.height); break; case 7: ctx.rotate(0.5 * Math.PI); ctx.translate(canvas.width, -canvas.height); ctx.scale(-1, 1); break; case 8: ctx.rotate(-0.5 * Math.PI); ctx.translate(-canvas.width, 0); } drawImageIOSFix(ctx, img, (ref = resizeInfo.srcX) != null ? ref : 0, (ref1 = resizeInfo.srcY) != null ? ref1 : 0, resizeInfo.srcWidth, resizeInfo.srcHeight, (ref2 = resizeInfo.trgX) != null ? ref2 : 0, (ref3 = resizeInfo.trgY) != null ? ref3 : 0, resizeInfo.trgWidth, resizeInfo.trgHeight); thumbnail = canvas.toDataURL("image/png"); if (callback != null) { return callback(thumbnail, canvas); } }); }; })(this); if (callback != null) { img.onerror = callback; } return img.src = file.dataURL; }; Dropzone.prototype.processQueue = function() { var i, parallelUploads, processingLength, queuedFiles; parallelUploads = this.options.parallelUploads; processingLength = this.getUploadingFiles().length; i = processingLength; if (processingLength >= parallelUploads) { return; } queuedFiles = this.getQueuedFiles(); if (!(queuedFiles.length > 0)) { return; } if (this.options.uploadMultiple) { return this.processFiles(queuedFiles.slice(0, parallelUploads - processingLength)); } else { while (i < parallelUploads) { if (!queuedFiles.length) { return; } this.processFile(queuedFiles.shift()); i++; } } }; Dropzone.prototype.processFile = function(file) { return this.processFiles([file]); }; Dropzone.prototype.processFiles = function(files) { var file, j, len; for (j = 0, len = files.length; j < len; j++) { file = files[j]; file.processing = true; file.status = Dropzone.UPLOADING; this.emit("processing", file); } if (this.options.uploadMultiple) { this.emit("processingmultiple", files); } return this.uploadFiles(files); }; Dropzone.prototype._getFilesWithXhr = function(xhr) { var file, files; return files = (function() { var j, len, ref, results; ref = this.files; results = []; for (j = 0, len = ref.length; j < len; j++) { file = ref[j]; if (file.xhr === xhr) { results.push(file); } } return results; }).call(this); }; Dropzone.prototype.cancelUpload = function(file) { var groupedFile, groupedFiles, j, k, len, len1, ref; if (file.status === Dropzone.UPLOADING) { groupedFiles = this._getFilesWithXhr(file.xhr); for (j = 0, len = groupedFiles.length; j < len; j++) { groupedFile = groupedFiles[j]; groupedFile.status = Dropzone.CANCELED; } file.xhr.abort(); for (k = 0, len1 = groupedFiles.length; k < len1; k++) { groupedFile = groupedFiles[k]; this.emit("canceled", groupedFile); } if (this.options.uploadMultiple) { this.emit("canceledmultiple", groupedFiles); } } else if ((ref = file.status) === Dropzone.ADDED || ref === Dropzone.QUEUED) { file.status = Dropzone.CANCELED; this.emit("canceled", file); if (this.options.uploadMultiple) { this.emit("canceledmultiple", [file]); } } if (this.options.autoProcessQueue) { return this.processQueue(); } }; resolveOption = function() { var args, option; option = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : []; if (typeof option === 'function') { return option.apply(this, args); } return option; }; Dropzone.prototype.uploadFile = function(file) { return this.uploadFiles([file]); }; Dropzone.prototype.uploadFiles = function(files) { var doneCounter, doneFunction, file, formData, handleError, headerName, headerValue, headers, i, input, inputName, inputType, j, k, key, l, len, len1, len2, len3, m, method, o, option, progressObj, ref, ref1, ref2, ref3, ref4, ref5, response, results, updateProgress, url, value, xhr; xhr = new XMLHttpRequest(); for (j = 0, len = files.length; j < len; j++) { file = files[j]; file.xhr = xhr; } method = resolveOption(this.options.method, files); url = resolveOption(this.options.url, files); xhr.open(method, url, true); xhr.timeout = resolveOption(this.options.timeout, files); xhr.withCredentials = !!this.options.withCredentials; response = null; handleError = (function(_this) { return function() { var k, len1, results; results = []; for (k = 0, len1 = files.length; k < len1; k++) { file = files[k]; results.push(_this._errorProcessing(files, response || _this.options.dictResponseError.replace("{{statusCode}}", xhr.status), xhr)); } return results; }; })(this); updateProgress = (function(_this) { return function(e) { var allFilesFinished, k, l, len1, len2, len3, m, progress, results; if (e != null) { progress = 100 * e.loaded / e.total; for (k = 0, len1 = files.length; k < len1; k++) { file = files[k]; file.upload.progress = progress; file.upload.total = e.total; file.upload.bytesSent = e.loaded; } } else { allFilesFinished = true; progress = 100; for (l = 0, len2 = files.length; l < len2; l++) { file = files[l]; if (!(file.upload.progress === 100 && file.upload.bytesSent === file.upload.total)) { allFilesFinished = false; } file.upload.progress = progress; file.upload.bytesSent = file.upload.total; } if (allFilesFinished) { return; } } results = []; for (m = 0, len3 = files.length; m < len3; m++) { file = files[m]; results.push(_this.emit("uploadprogress", file, progress, file.upload.bytesSent)); } return results; }; })(this); xhr.onload = (function(_this) { return function(e) { var error1, ref; if (files[0].status === Dropzone.CANCELED) { return; } if (xhr.readyState !== 4) { return; } if (xhr.responseType !== 'arraybuffer' && xhr.responseType !== 'blob') { response = xhr.responseText; if (xhr.getResponseHeader("content-type") && ~xhr.getResponseHeader("content-type").indexOf("application/json")) { try { response = JSON.parse(response); } catch (error1) { e = error1; response = "Invalid JSON response from server."; } } } updateProgress(); if (!((200 <= (ref = xhr.status) && ref < 300))) { return handleError(); } else { return _this._finished(files, response, e); } }; })(this); xhr.onerror = (function(_this) { return function() { if (files[0].status === Dropzone.CANCELED) { return; } return handleError(); }; })(this); progressObj = (ref = xhr.upload) != null ? ref : xhr; progressObj.onprogress = updateProgress; headers = { "Accept": "application/json", "Cache-Control": "no-cache", "X-Requested-With": "XMLHttpRequest" }; if (this.options.headers) { extend(headers, this.options.headers); } for (headerName in headers) { headerValue = headers[headerName]; if (headerValue) { xhr.setRequestHeader(headerName, headerValue); } } formData = new FormData(); if (this.options.params) { ref1 = this.options.params; for (key in ref1) { value = ref1[key]; formData.append(key, value); } } for (k = 0, len1 = files.length; k < len1; k++) { file = files[k]; this.emit("sending", file, xhr, formData); } if (this.options.uploadMultiple) { this.emit("sendingmultiple", files, xhr, formData); } if (this.element.tagName === "FORM") { ref2 = this.element.querySelectorAll("input, textarea, select, button"); for (l = 0, len2 = ref2.length; l < len2; l++) { input = ref2[l]; inputName = input.getAttribute("name"); inputType = input.getAttribute("type"); if (input.tagName === "SELECT" && input.hasAttribute("multiple")) { ref3 = input.options; for (m = 0, len3 = ref3.length; m < len3; m++) { option = ref3[m]; if (option.selected) { formData.append(inputName, option.value); } } } else if (!inputType || ((ref4 = inputType.toLowerCase()) !== "checkbox" && ref4 !== "radio") || input.checked) { formData.append(inputName, input.value); } } } doneCounter = 0; results = []; for (i = o = 0, ref5 = files.length - 1; 0 <= ref5 ? o <= ref5 : o >= ref5; i = 0 <= ref5 ? ++o : --o) { doneFunction = (function(_this) { return function(file, paramName, fileName) { return function(transformedFile) { formData.append(paramName, transformedFile, fileName); if (++doneCounter === files.length) { return _this.submitRequest(xhr, formData, files); } }; }; })(this); results.push(this.options.transformFile.call(this, files[i], doneFunction(files[i], this._getParamName(i), files[i].upload.filename))); } return results; }; Dropzone.prototype.submitRequest = function(xhr, formData, files) { return xhr.send(formData); }; Dropzone.prototype._finished = function(files, responseText, e) { var file, j, len; for (j = 0, len = files.length; j < len; j++) { file = files[j]; file.status = Dropzone.SUCCESS; this.emit("success", file, responseText, e); this.emit("complete", file); } if (this.options.uploadMultiple) { this.emit("successmultiple", files, responseText, e); this.emit("completemultiple", files); } if (this.options.autoProcessQueue) { return this.processQueue(); } }; Dropzone.prototype._errorProcessing = function(files, message, xhr) { var file, j, len; for (j = 0, len = files.length; j < len; j++) { file = files[j]; file.status = Dropzone.ERROR; this.emit("error", file, message, xhr); this.emit("complete", file); } if (this.options.uploadMultiple) { this.emit("errormultiple", files, message, xhr); this.emit("completemultiple", files); } if (this.options.autoProcessQueue) { return this.processQueue(); } }; return Dropzone; })(Emitter); Dropzone.version = "5.1.1"; Dropzone.options = {}; Dropzone.optionsForElement = function(element) { if (element.getAttribute("id")) { return Dropzone.options[camelize(element.getAttribute("id"))]; } else { return void 0; } }; Dropzone.instances = []; Dropzone.forElement = function(element) { if (typeof element === "string") { element = document.querySelector(element); } if ((element != null ? element.dropzone : void 0) == null) { throw new Error("No Dropzone found for given element. This is probably because you're trying to access it before Dropzone had the time to initialize. Use the `init` option to setup any additional observers on your Dropzone."); } return element.dropzone; }; Dropzone.autoDiscover = true; Dropzone.discover = function() { var checkElements, dropzone, dropzones, j, len, results; if (document.querySelectorAll) { dropzones = document.querySelectorAll(".dropzone"); } else { dropzones = []; checkElements = function(elements) { var el, j, len, results; results = []; for (j = 0, len = elements.length; j < len; j++) { el = elements[j]; if (/(^| )dropzone($| )/.test(el.className)) { results.push(dropzones.push(el)); } else { results.push(void 0); } } return results; }; checkElements(document.getElementsByTagName("div")); checkElements(document.getElementsByTagName("form")); } results = []; for (j = 0, len = dropzones.length; j < len; j++) { dropzone = dropzones[j]; if (Dropzone.optionsForElement(dropzone) !== false) { results.push(new Dropzone(dropzone)); } else { results.push(void 0); } } return results; }; Dropzone.blacklistedBrowsers = [/opera.*Macintosh.*version\/12/i]; Dropzone.isBrowserSupported = function() { var capableBrowser, j, len, ref, regex; capableBrowser = true; if (window.File && window.FileReader && window.FileList && window.Blob && window.FormData && document.querySelector) { if (!("classList" in document.createElement("a"))) { capableBrowser = false; } else { ref = Dropzone.blacklistedBrowsers; for (j = 0, len = ref.length; j < len; j++) { regex = ref[j]; if (regex.test(navigator.userAgent)) { capableBrowser = false; continue; } } } } else { capableBrowser = false; } return capableBrowser; }; Dropzone.dataURItoBlob = function(dataURI) { var ab, byteString, i, ia, j, mimeString, ref; byteString = atob(dataURI.split(',')[1]); mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0]; ab = new ArrayBuffer(byteString.length); ia = new Uint8Array(ab); for (i = j = 0, ref = byteString.length; 0 <= ref ? j <= ref : j >= ref; i = 0 <= ref ? ++j : --j) { ia[i] = byteString.charCodeAt(i); } return new Blob([ab], { type: mimeString }); }; without = function(list, rejectedItem) { var item, j, len, results; results = []; for (j = 0, len = list.length; j < len; j++) { item = list[j]; if (item !== rejectedItem) { results.push(item); } } return results; }; camelize = function(str) { return str.replace(/[\-_](\w)/g, function(match) { return match.charAt(1).toUpperCase(); }); }; Dropzone.createElement = function(string) { var div; div = document.createElement("div"); div.innerHTML = string; return div.childNodes[0]; }; Dropzone.elementInside = function(element, container) { if (element === container) { return true; } while (element = element.parentNode) { if (element === container) { return true; } } return false; }; Dropzone.getElement = function(el, name) { var element; if (typeof el === "string") { element = document.querySelector(el); } else if (el.nodeType != null) { element = el; } if (element == null) { throw new Error("Invalid `" + name + "` option provided. Please provide a CSS selector or a plain HTML element."); } return element; }; Dropzone.getElements = function(els, name) { var e, el, elements, error1, j, k, len, len1, ref; if (els instanceof Array) { elements = []; try { for (j = 0, len = els.length; j < len; j++) { el = els[j]; elements.push(this.getElement(el, name)); } } catch (error1) { e = error1; elements = null; } } else if (typeof els === "string") { elements = []; ref = document.querySelectorAll(els); for (k = 0, len1 = ref.length; k < len1; k++) { el = ref[k]; elements.push(el); } } else if (els.nodeType != null) { elements = [els]; } if (!((elements != null) && elements.length)) { throw new Error("Invalid `" + name + "` option provided. Please provide a CSS selector, a plain HTML element or a list of those."); } return elements; }; Dropzone.confirm = function(question, accepted, rejected) { if (window.confirm(question)) { return accepted(); } else if (rejected != null) { return rejected(); } }; Dropzone.isValidFile = function(file, acceptedFiles) { var baseMimeType, j, len, mimeType, validType; if (!acceptedFiles) { return true; } acceptedFiles = acceptedFiles.split(","); mimeType = file.type; baseMimeType = mimeType.replace(/\/.*$/, ""); for (j = 0, len = acceptedFiles.length; j < len; j++) { validType = acceptedFiles[j]; validType = validType.trim(); if (validType.charAt(0) === ".") { if (file.name.toLowerCase().indexOf(validType.toLowerCase(), file.name.length - validType.length) !== -1) { return true; } } else if (/\/\*$/.test(validType)) { if (baseMimeType === validType.replace(/\/.*$/, "")) { return true; } } else { if (mimeType === validType) { return true; } } } return false; }; if (typeof jQuery !== "undefined" && jQuery !== null) { jQuery.fn.dropzone = function(options) { return this.each(function() { return new Dropzone(this, options); }); }; } if (typeof module !== "undefined" && module !== null) { module.exports = Dropzone; } else { window.Dropzone = Dropzone; } Dropzone.ADDED = "added"; Dropzone.QUEUED = "queued"; Dropzone.ACCEPTED = Dropzone.QUEUED; Dropzone.UPLOADING = "uploading"; Dropzone.PROCESSING = Dropzone.UPLOADING; Dropzone.CANCELED = "canceled"; Dropzone.ERROR = "error"; Dropzone.SUCCESS = "success"; /* Bugfix for iOS 6 and 7 Source: http://stackoverflow.com/questions/11929099/html5-canvas-drawimage-ratio-bug-ios based on the work of https://github.com/stomita/ios-imagefile-megapixel */ detectVerticalSquash = function(img) { var alpha, canvas, ctx, data, ey, ih, iw, py, ratio, sy; iw = img.naturalWidth; ih = img.naturalHeight; canvas = document.createElement("canvas"); canvas.width = 1; canvas.height = ih; ctx = canvas.getContext("2d"); ctx.drawImage(img, 0, 0); data = ctx.getImageData(1, 0, 1, ih).data; sy = 0; ey = ih; py = ih; while (py > sy) { alpha = data[(py - 1) * 4 + 3]; if (alpha === 0) { ey = py; } else { sy = py; } py = (ey + sy) >> 1; } ratio = py / ih; if (ratio === 0) { return 1; } else { return ratio; } }; drawImageIOSFix = function(ctx, img, sx, sy, sw, sh, dx, dy, dw, dh) { var vertSquashRatio; vertSquashRatio = detectVerticalSquash(img); return ctx.drawImage(img, sx, sy, sw, sh, dx, dy, dw, dh / vertSquashRatio); }; ExifRestore = (function() { function ExifRestore() {} ExifRestore.KEY_STR = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; ExifRestore.encode64 = function(input) { var chr1, chr2, chr3, enc1, enc2, enc3, enc4, i, output; output = ''; chr1 = void 0; chr2 = void 0; chr3 = ''; enc1 = void 0; enc2 = void 0; enc3 = void 0; enc4 = ''; i = 0; while (true) { chr1 = input[i++]; chr2 = input[i++]; chr3 = input[i++]; enc1 = chr1 >> 2; enc2 = (chr1 & 3) << 4 | chr2 >> 4; enc3 = (chr2 & 15) << 2 | chr3 >> 6; enc4 = chr3 & 63; if (isNaN(chr2)) { enc3 = enc4 = 64; } else if (isNaN(chr3)) { enc4 = 64; } output = output + this.KEY_STR.charAt(enc1) + this.KEY_STR.charAt(enc2) + this.KEY_STR.charAt(enc3) + this.KEY_STR.charAt(enc4); chr1 = chr2 = chr3 = ''; enc1 = enc2 = enc3 = enc4 = ''; if (!(i < input.length)) { break; } } return output; }; ExifRestore.restore = function(origFileBase64, resizedFileBase64) { var image, rawImage, segments; if (!origFileBase64.match('data:image/jpeg;base64,')) { return resizedFileBase64; } rawImage = this.decode64(origFileBase64.replace('data:image/jpeg;base64,', '')); segments = this.slice2Segments(rawImage); image = this.exifManipulation(resizedFileBase64, segments); return 'data:image/jpeg;base64,' + this.encode64(image); }; ExifRestore.exifManipulation = function(resizedFileBase64, segments) { var aBuffer, exifArray, newImageArray; exifArray = this.getExifArray(segments); newImageArray = this.insertExif(resizedFileBase64, exifArray); aBuffer = new Uint8Array(newImageArray); return aBuffer; }; ExifRestore.getExifArray = function(segments) { var seg, x; seg = void 0; x = 0; while (x < segments.length) { seg = segments[x]; if (seg[0] === 255 & seg[1] === 225) { return seg; } x++; } return []; }; ExifRestore.insertExif = function(resizedFileBase64, exifArray) { var array, ato, buf, imageData, mae, separatePoint; imageData = resizedFileBase64.replace('data:image/jpeg;base64,', ''); buf = this.decode64(imageData); separatePoint = buf.indexOf(255, 3); mae = buf.slice(0, separatePoint); ato = buf.slice(separatePoint); array = mae; array = array.concat(exifArray); array = array.concat(ato); return array; }; ExifRestore.slice2Segments = function(rawImageArray) { var endPoint, head, length, seg, segments; head = 0; segments = []; while (true) { if (rawImageArray[head] === 255 & rawImageArray[head + 1] === 218) { break; } if (rawImageArray[head] === 255 & rawImageArray[head + 1] === 216) { head += 2; } else { length = rawImageArray[head + 2] * 256 + rawImageArray[head + 3]; endPoint = head + length + 2; seg = rawImageArray.slice(head, endPoint); segments.push(seg); head = endPoint; } if (head > rawImageArray.length) { break; } } return segments; }; ExifRestore.decode64 = function(input) { var base64test, buf, chr1, chr2, chr3, enc1, enc2, enc3, enc4, i, output; output = ''; chr1 = void 0; chr2 = void 0; chr3 = ''; enc1 = void 0; enc2 = void 0; enc3 = void 0; enc4 = ''; i = 0; buf = []; base64test = /[^A-Za-z0-9\+\/\=]/g; if (base64test.exec(input)) { console.warning('There were invalid base64 characters in the input text.\n' + 'Valid base64 characters are A-Z, a-z, 0-9, \'+\', \'/\',and \'=\'\n' + 'Expect errors in decoding.'); } input = input.replace(/[^A-Za-z0-9\+\/\=]/g, ''); while (true) { enc1 = this.KEY_STR.indexOf(input.charAt(i++)); enc2 = this.KEY_STR.indexOf(input.charAt(i++)); enc3 = this.KEY_STR.indexOf(input.charAt(i++)); enc4 = this.KEY_STR.indexOf(input.charAt(i++)); chr1 = enc1 << 2 | enc2 >> 4; chr2 = (enc2 & 15) << 4 | enc3 >> 2; chr3 = (enc3 & 3) << 6 | enc4; buf.push(chr1); if (enc3 !== 64) { buf.push(chr2); } if (enc4 !== 64) { buf.push(chr3); } chr1 = chr2 = chr3 = ''; enc1 = enc2 = enc3 = enc4 = ''; if (!(i < input.length)) { break; } } return buf; }; return ExifRestore; })(); /* * contentloaded.js * * Author: Diego Perini (diego.perini at gmail.com) * Summary: cross-browser wrapper for DOMContentLoaded * Updated: 20101020 * License: MIT * Version: 1.2 * * URL: * http://javascript.nwbox.com/ContentLoaded/ * http://javascript.nwbox.com/ContentLoaded/MIT-LICENSE */ contentLoaded = function(win, fn) { var add, doc, done, init, poll, pre, rem, root, top; done = false; top = true; doc = win.document; root = doc.documentElement; add = (doc.addEventListener ? "addEventListener" : "attachEvent"); rem = (doc.addEventListener ? "removeEventListener" : "detachEvent"); pre = (doc.addEventListener ? "" : "on"); init = function(e) { if (e.type === "readystatechange" && doc.readyState !== "complete") { return; } (e.type === "load" ? win : doc)[rem](pre + e.type, init, false); if (!done && (done = true)) { return fn.call(win, e.type || e); } }; poll = function() { var e, error1; try { root.doScroll("left"); } catch (error1) { e = error1; setTimeout(poll, 50); return; } return init("poll"); }; if (doc.readyState !== "complete") { if (doc.createEventObject && root.doScroll) { try { top = !win.frameElement; } catch (undefined) {} if (top) { poll(); } } doc[add](pre + "DOMContentLoaded", init, false); doc[add](pre + "readystatechange", init, false); return win[add](pre + "load", init, false); } }; Dropzone._autoDiscoverFunction = function() { if (Dropzone.autoDiscover) { return Dropzone.discover(); } }; contentLoaded(window, Dropzone._autoDiscoverFunction); }).call(this); ================================================ FILE: _examples/dropzonejs/src/views/upload.html ================================================ DropzoneJS Uploader
================================================ FILE: _examples/file-server/basic/assets/app2/app22/just_a_text_no_index.txt ================================================ just a text. ================================================ FILE: _examples/file-server/basic/assets/app2/app2app3/index.html ================================================

Hello App2App3 index

================================================ FILE: _examples/file-server/basic/assets/app2/index.html ================================================

Hello App2 index

================================================ FILE: _examples/file-server/basic/assets/css/main.css ================================================ body { background-color: black; } ================================================ FILE: _examples/file-server/basic/assets/index.html ================================================

Hello index

================================================ FILE: _examples/file-server/basic/assets/js/main.js ================================================ console.log("example"); ================================================ FILE: _examples/file-server/basic/assets.system/css/main.css ================================================ body { background-color: black; } ================================================ FILE: _examples/file-server/basic/assets.system/test.txt ================================================ main_test.go#TestHandleDirDot ================================================ FILE: _examples/file-server/basic/main.go ================================================ package main import ( "github.com/kataras/iris/v12" ) func newApp() *iris.Application { app := iris.New() app.Favicon("./assets/favicon.ico") // first parameter is the request path // second is the system directory // // app.HandleDir("/css", iris.Dir("./assets/css")) // app.HandleDir("/js", iris.Dir("./assets/js")) v1 := app.Party("/v1") v1.HandleDir("/static", iris.Dir("./assets"), iris.DirOptions{ // Defaults to "/index.html", if request path is ending with **/*/$IndexName // then it redirects to **/*(/) which another handler is handling it, // that another handler, called index handler, is auto-registered by the framework // if end developer does not managed to handle it by hand. IndexName: "/index.html", // When files should served under compression. Compress: false, // List the files inside the current requested directory if `IndexName` not found. ShowList: false, // When ShowList is true you can configure if you want to show or hide hidden files. ShowHidden: false, Cache: iris.DirCacheOptions{ // enable in-memory cache and pre-compress the files. Enable: true, // ignore image types (and pdf). CompressIgnore: iris.MatchImagesAssets, // do not compress files smaller than size. CompressMinSize: 300, // available encodings that will be negotiated with client's needs. Encodings: []string{"gzip", "br" /* you can also add: deflate, snappy */}, }, DirList: iris.DirListRich(), // If `ShowList` is true then this function will be used instead of the default // one to show the list of files of a current requested directory(dir). // DirList: func(ctx iris.Context, dirName string, dir http.File) error { ... } // // Optional validator that loops through each requested resource. // AssetValidator: func(ctx iris.Context, name string) bool { ... } }) // You can also register any index handler manually, order of registration does not matter: // v1.Get("/static", [...custom middleware...], func(ctx iris.Context) { // [...custom code...] // ctx.ServeFile("./assets/index.html") // }) // http://localhost:8080/v1/static // http://localhost:8080/v1/static/css/main.css // http://localhost:8080/v1/static/js/jquery-2.1.1.js // http://localhost:8080/v1/static/favicon.ico return app } func main() { app := newApp() app.Logger().SetLevel("debug") app.Listen(":8080") } ================================================ FILE: _examples/file-server/basic/main_test.go ================================================ package main import ( "os" "path/filepath" "strings" "testing" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/httptest" ) type resource string func (r resource) contentType() string { switch filepath.Ext(r.String()) { case ".js": return "text/javascript" case ".css": return "text/css" case ".ico": return "image/x-icon" case ".html", "": return "text/html" default: return "text/plain" } } func (r resource) String() string { return string(r) } func (r resource) strip(strip string) string { s := r.String() return strings.TrimPrefix(s, strip) } func (r resource) loadFromBase(dir string, strip string) string { filename := r.String() filename = r.strip(strip) if filepath.Ext(filename) == "" { // root /. filename = filename + "/index.html" } fullpath := filepath.Join(dir, filename) b, err := os.ReadFile(fullpath) if err != nil { panic(fullpath + " failed with error: " + err.Error()) } result := string(b) return result } func TestFileServerBasic(t *testing.T) { urls := []resource{ "/v1/static/css/main.css", "/v1/static/js/main.js", "/v1/static/favicon.ico", "/v1/static/app2", "/v1/static/app2/app2app3", "/v1/static", } app := newApp() // route := app.GetRouteReadOnly("GET/{file:path}") // if route == nil { // app.Logger().Fatalf("expected a route to serve files") // } // if expected, got := "./assets", route.StaticDir(); expected != got { // app.Logger().Fatalf("expected route's static directory to be: '%s' but got: '%s'", expected, got) // } // if !route.StaticDirContainsIndex() { // app.Logger().Fatalf("epxected ./assets to contain an %s file", "/index.html") // } e := httptest.New(t, app) for _, u := range urls { url := u.String() contents := u.loadFromBase("./assets", "/v1/static") e.GET(url).Expect(). Status(httptest.StatusOK). ContentType(u.contentType(), app.ConfigurationReadOnly().GetCharset()). Body().IsEqual(contents) } } // Tests subdomain + request path and system directory with a name that contains a dot(.) func TestHandleDirDot(t *testing.T) { urls := []resource{ "/v1/assets.system/css/main.css", } app := newApp() app.Subdomain("test").Party("/v1").HandleDir("/assets.system", iris.Dir("./assets.system")) e := httptest.New(t, app, httptest.URL("http://test.example.com")) for _, u := range urls { url := u.String() contents := u.loadFromBase("./assets.system", "/v1/assets.system") e.GET(url).Expect(). Status(httptest.StatusOK). ContentType(u.contentType(), app.ConfigurationReadOnly().GetCharset()). Body().IsEqual(contents) } } ================================================ FILE: _examples/file-server/embedding-files-into-app/assets/css/main.css ================================================ html { font-family: sans-serif; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100% } ================================================ FILE: _examples/file-server/embedding-files-into-app/assets/js/main.js ================================================ console.log("example"); ================================================ FILE: _examples/file-server/embedding-files-into-app/main.go ================================================ package main import ( "embed" "github.com/kataras/iris/v12" ) //go:embed assets/* var fs embed.FS func newApp() *iris.Application { app := iris.New() app.Logger().SetLevel("debug") app.HandleDir("/static", fs) /* Or if you need to cache them inside the memory (requires the assets folder to be located near the executable program): app.HandleDir("/static", iris.Dir("./assets"), iris.DirOptions{ IndexName: "index.html", Cache: iris.DirCacheOptions{ Enable: true, Encodings: []string{"gzip"}, CompressIgnore: iris.MatchImagesAssets, CompressMinSize: 30 * iris.B, }, }) */ return app } func main() { app := newApp() // http://localhost:8080/static/css/main.css // http://localhost:8080/static/js/main.js // http://localhost:8080/static/favicon.ico app.Listen(":8080") } ================================================ FILE: _examples/file-server/embedding-files-into-app/main_test.go ================================================ package main import ( "os" "path/filepath" "runtime" "strings" "testing" "github.com/kataras/iris/v12/httptest" ) type resource string // content types that are used in the ./assets, // we could use the detectContentType that iris do but it's better // to do it manually so we can test if that returns the correct result on embedding files. func (r resource) contentType() string { switch filepath.Ext(r.String()) { case ".js": return "text/javascript" case ".css": return "text/css" case ".ico": return "image/x-icon" case ".html": return "text/html" default: return "text/plain" } } func (r resource) String() string { return string(r) } func (r resource) strip(strip string) string { s := r.String() return strings.TrimPrefix(s, strip) } func (r resource) loadFromBase(dir string) string { filename := r.strip("/static") fullpath := filepath.Join(dir, filename) b, err := os.ReadFile(fullpath) if err != nil { panic(fullpath + " failed with error: " + err.Error()) } result := string(b) if runtime.GOOS != "windows" { result = strings.ReplaceAll(result, "\n", "\r\n") result = strings.ReplaceAll(result, "\r\r", "") } return result } var urls = []resource{ "/static/css/main.css", "/static/js/main.js", "/static/favicon.ico", } // if bindata's values matches with the assets/... contents // and secondly if the HandleDir had successfully registered // the routes and gave the correct response. func TestEmbeddingFilesIntoApp(t *testing.T) { app := newApp() e := httptest.New(t, app) route := app.GetRouteReadOnly("GET/static/{file:path}") if route == nil { t.Fatalf("expected a route to serve embedded files") } if runtime.GOOS != "windows" { // remove the embedded static favicon for !windows, // it should be built for unix-specific in order to be work urls = urls[0 : len(urls)-1] } for _, u := range urls { url := u.String() contents := u.loadFromBase("./assets") e.GET(url).Expect(). Status(httptest.StatusOK). ContentType(u.contentType()). Body().IsEqual(contents) } } ================================================ FILE: _examples/file-server/embedding-files-into-app-bindata/assets/css/main.css ================================================ html { font-family: sans-serif; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100% } ================================================ FILE: _examples/file-server/embedding-files-into-app-bindata/assets/js/main.js ================================================ console.log("example"); ================================================ FILE: _examples/file-server/embedding-files-into-app-bindata/bindata.go ================================================ // Code generated by go-bindata. (@generated) DO NOT EDIT. // Package main generated by go-bindata.// sources: // assets/css/main.css // assets/favicon.ico // assets/js/main.js package main import ( "bytes" "compress/gzip" "fmt" "io" "net/http" "os" "path/filepath" "strings" "time" ) func bindataRead(data []byte, name string) ([]byte, error) { gz, err := gzip.NewReader(bytes.NewBuffer(data)) if err != nil { return nil, fmt.Errorf("read %q: %v", name, err) } var buf bytes.Buffer _, err = io.Copy(&buf, gz) clErr := gz.Close() if err != nil { return nil, fmt.Errorf("read %q: %v", name, err) } if clErr != nil { return nil, err } return buf.Bytes(), nil } type asset struct { bytes []byte info os.FileInfo } type bindataFileInfo struct { name string size int64 mode os.FileMode modTime time.Time } // Name return file name func (fi bindataFileInfo) Name() string { return fi.name } // Size return file size func (fi bindataFileInfo) Size() int64 { return fi.size } // Mode return file mode func (fi bindataFileInfo) Mode() os.FileMode { return fi.mode } // ModTime return file modify time func (fi bindataFileInfo) ModTime() time.Time { return fi.modTime } // IsDir return file whether a directory func (fi bindataFileInfo) IsDir() bool { return fi.mode&os.ModeDir != 0 } // Sys return file is sys mode func (fi bindataFileInfo) Sys() any { return nil } type assetFile struct { *bytes.Reader name string childInfos []os.FileInfo childInfoOffset int } type assetOperator struct{} // Open implement http.FileSystem interface func (f *assetOperator) Open(name string) (http.File, error) { var err error if len(name) > 0 && name[0] == '/' { name = name[1:] } content, err := Asset(name) if err == nil { return &assetFile{name: name, Reader: bytes.NewReader(content)}, nil } children, err := AssetDir(name) if err == nil { childInfos := make([]os.FileInfo, 0, len(children)) for _, child := range children { childPath := filepath.Join(name, child) info, errInfo := AssetInfo(filepath.Join(name, child)) if errInfo == nil { childInfos = append(childInfos, info) } else { childInfos = append(childInfos, newDirFileInfo(childPath)) } } return &assetFile{name: name, childInfos: childInfos}, nil } else { // If the error is not found, return an error that will // result in a 404 error. Otherwise the server returns // a 500 error for files not found. if strings.Contains(err.Error(), "not found") { return nil, os.ErrNotExist } return nil, err } } // Close no need do anything func (f *assetFile) Close() error { return nil } // Readdir read dir's children file info func (f *assetFile) Readdir(count int) ([]os.FileInfo, error) { if len(f.childInfos) == 0 { return nil, os.ErrNotExist } if count <= 0 { return f.childInfos, nil } if f.childInfoOffset+count > len(f.childInfos) { count = len(f.childInfos) - f.childInfoOffset } offset := f.childInfoOffset f.childInfoOffset += count return f.childInfos[offset : offset+count], nil } // Stat read file info from asset item func (f *assetFile) Stat() (os.FileInfo, error) { if len(f.childInfos) != 0 { return newDirFileInfo(f.name), nil } return AssetInfo(f.name) } // newDirFileInfo return default dir file info func newDirFileInfo(name string) os.FileInfo { return &bindataFileInfo{ name: name, size: 0, mode: os.FileMode(2147484068), // equal os.FileMode(0644)|os.ModeDir modTime: time.Time{}} } // AssetFile return a http.FileSystem instance that data backend by asset func AssetFile() http.FileSystem { return &assetOperator{} } var _cssMainCss = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xca\x28\xc9\xcd\x51\xa8\xe6\xe5\xe2\x4c\xcb\xcf\x2b\xd1\x4d\x4b\xcc\xcd\xcc\xa9\xb4\x52\x28\x4e\xcc\x2b\xd6\x2d\x4e\x2d\xca\x4c\xb3\xe6\xe5\xe2\xd4\x2d\x4f\x4d\xca\xce\x2c\xd1\x2d\x49\xad\x28\xd1\x2d\xce\xac\x4a\xd5\x4d\x4c\xc9\x2a\x2d\x2e\xb1\x52\x30\x34\x30\x50\x05\xab\xc8\x2d\xc6\x21\xcb\xcb\x55\x0b\x08\x00\x00\xff\xff\x32\x4c\x06\xc6\x63\x00\x00\x00") func cssMainCssBytes() ([]byte, error) { return bindataRead( _cssMainCss, "css/main.css", ) } func cssMainCss() (*asset, error) { bytes, err := cssMainCssBytes() if err != nil { return nil, err } info := bindataFileInfo{name: "css/main.css", size: 99, mode: os.FileMode(438), modTime: time.Unix(1613718750, 0)} a := &asset{bytes: bytes, info: info} return a, nil } var _faviconIco = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xec\x5a\x0b\x70\x55\xc7\x79\xde\x7b\xce\x45\x12\xb2\x90\xc4\xc3\xe6\x61\x3b\x90\xf8\x31\xc4\x19\x6c\x32\xe3\xc4\x34\xe3\xc6\x34\x6d\xed\xd4\x69\x62\x32\x49\x9a\xa6\x75\xea\x36\x33\xee\xd8\x9e\xb4\x75\xdc\x7a\xa6\xc5\x31\x02\xa6\xd3\x84\x47\x28\x6f\x3b\xc6\x3c\xcc\xeb\x9e\xbd\x08\x21\x04\x92\x28\x12\x0e\x08\x5b\x3c\xec\x02\x92\x78\x08\x21\x09\x41\x25\x24\x10\xe8\x71\xb5\xe7\xdc\xd7\xb9\xf7\xef\xfc\xff\x9e\x73\x74\x25\xdd\x97\x24\x82\x33\x1e\xce\xcc\x3f\x7b\xee\x9e\xdd\xff\xff\xf6\xdf\x7f\xff\xfd\xf7\xdf\xcb\x98\x8b\xa9\x2c\x3f\x1f\xcb\x19\xec\x15\x37\x63\x5f\x67\x8c\xcd\x98\x21\x7f\x6b\xf9\x8c\x6d\x72\x33\x36\x7b\xb6\xf5\xfb\x11\xc6\x9e\xbd\x97\xb1\x99\x8c\xb1\x7c\x6c\xc7\x64\x3d\x3d\x6e\x76\xdb\x1f\xe1\x75\xab\x42\x53\x7e\x22\x3c\x6c\x91\xf0\xb0\x02\x22\xae\x14\x08\x8f\xab\x40\xec\x64\x92\xf0\x5d\x53\xfe\x59\xec\x64\x5f\x15\x1e\xa6\x08\x4f\x7f\x7f\xdf\x7a\x96\xa1\x97\x3c\xb8\x3f\x78\xfa\x0d\x08\x9e\x5d\x08\xc1\xda\x5f\x82\x51\xf6\x65\x30\xca\x67\x41\xf0\xcc\xbf\x11\x19\x07\x1e\x07\xbd\x78\x2a\x18\x65\x8f\xb5\x0b\x4d\x7d\xad\x6f\x07\x73\x0b\xcd\x45\xfd\xbb\x17\xb3\x0c\xa3\x62\xce\xfe\xa8\xd1\x0a\xf8\x98\x6d\x25\x10\x38\xfe\x53\x88\xdc\x3a\x01\x66\x6b\x11\x11\xbe\x07\x8e\xfd\x2d\x84\x2e\x2c\x05\xff\xd1\x79\x3d\xc2\xc3\x7e\x20\x78\x06\x13\x5c\x89\xe9\xdf\x06\xd1\x50\x2f\x04\x4e\xfc\x0c\xcc\x6b\xa5\x60\x1c\xfd\x21\x74\x6c\x1e\x47\x84\xef\x58\x17\xf8\xe4\x65\x30\x6f\x54\x81\x71\xe0\x89\xea\xbe\x2d\x6c\xd2\x80\xfe\xfe\x76\x30\x6f\x1c\x81\x60\xed\x7c\x08\x9e\x5d\x0c\x2d\xeb\xc7\x41\xed\xd2\x29\x50\xbb\x74\x32\xbd\x63\x1d\x7e\xc3\x36\xa1\x73\xff\x19\x14\x1e\x36\x4f\x78\xc7\xc4\xf4\xef\x80\x70\xd3\xfb\x10\x6e\x5c\x0f\xdd\x65\x73\xe1\xdc\xf2\x89\xd0\xb1\x25\x0f\x6e\x7c\x90\x07\xe7\x7f\x33\x81\xea\xf0\x5b\xb8\x79\x13\x61\xd0\x8b\xa7\x2c\x09\x7c\xfc\x57\xac\x7b\x11\xf6\x7f\x6a\x5f\xd4\xb8\x06\xa1\xfa\x15\x10\x6e\x7a\x0f\x3a\xb4\xc7\xa0\x61\x55\x3e\x04\x8f\xfc\x39\x84\x3e\xfa\x4b\x68\x5c\x93\x4f\x75\xf8\x2d\x74\x71\x05\x44\x45\x13\xea\x54\xeb\x7c\x85\xa9\x48\xf8\x1e\x15\xcd\xf4\xcd\xee\x7f\x71\x65\x3e\x04\x3e\x7c\x86\x78\x5c\x5a\x1d\xd3\xbf\x7e\x05\xa0\x2c\x94\x89\xb2\x03\xd5\x3f\x66\x88\x05\x31\x21\x36\x1b\xff\xd9\xe5\x13\xe1\xda\xc6\x5c\x68\xdf\x94\x4b\x63\x71\xf0\x37\xbd\x0f\x38\x56\x1c\x33\x8e\x1d\x75\x80\xba\x40\x9d\x0c\xd6\x5f\xcd\x92\x29\x44\x83\xf5\x87\xba\x76\xfa\x73\x85\xe1\x5c\x18\x07\x9e\x38\x86\x18\x68\x8e\xac\xf9\xbb\xbe\x39\x87\xc8\x99\xbf\x13\x3f\xa3\x39\xc6\xb9\x8e\xed\x4f\xb6\xe0\x61\x3f\x40\xdb\x40\x1b\x41\x5b\x19\x62\x3f\xc7\x7f\x4a\xb6\x85\x0f\xda\x9a\xdd\x9f\xd6\x80\xe6\x62\xd2\x26\xd5\xd7\xd0\x46\xc9\x56\x0f\x3c\xde\x6f\xbf\xe5\xb3\xc8\xa6\xd1\xb6\xc9\xc6\x4f\xbf\x01\x68\xf3\x68\xfb\xce\x3a\xf2\x30\x26\x76\x32\x85\xd6\x08\xae\x95\xc1\xeb\x87\xd6\x14\xb3\x69\x11\xad\x39\xaf\x5b\x1d\xed\xfa\x05\x60\x8c\x65\x32\xc6\x54\x8b\x5c\x31\x64\x3d\x0b\x63\xe8\xb0\x45\x2d\x56\xdf\x99\x96\x8f\x99\x1b\xeb\x67\xf2\x47\x8b\xea\xf3\xf9\x08\xae\x22\xe5\x09\xae\x4e\x17\x5c\xfd\xc2\x28\x69\x8a\xe0\xca\x58\xb4\x3d\xdb\x17\xa6\x29\xff\x5f\x04\x57\x5b\x85\xe6\xba\x92\x90\xb8\x1a\x43\x4a\x4c\xbd\x62\xd7\xb7\x08\xae\xd6\x0b\xae\xfe\x8f\xe0\xea\x9b\x38\x1e\x1f\x1f\x4b\xfe\x34\xb5\x7c\xa5\x40\xec\xca\x02\xbd\x64\x3a\xe8\xfb\xbe\x38\x94\x4a\xa6\x83\xf0\x66\x82\xd0\x14\x10\x9a\x0b\x44\xe1\x38\x5c\x2b\x44\xf8\x4e\x75\x5c\x01\xe1\x75\x03\xf2\x11\x9a\x2b\x2a\xb8\x52\x2b\xb8\x3a\x4f\x68\x8a\x92\x0c\x03\xc9\xf7\xb0\x02\xa3\xfc\x2b\x10\xe9\xae\x85\xa8\x7e\x15\xa2\xfa\x95\x18\xba\x0a\xa1\xb3\x8b\x49\xbe\xbe\xe7\x5e\x08\xfe\xef\x3f\x81\xd9\x51\x09\x91\xbe\x4b\x44\xf8\x8e\x75\xf8\x4d\x70\x37\x04\x3e\xfe\x11\xf9\x14\xbd\x68\x3c\xe2\xe8\x14\x5c\x7d\x49\x78\x99\x2b\x11\x06\x47\xfe\x81\xd9\x10\x0d\xde\x82\xc1\x8f\x79\xe3\x30\xe8\x7b\xa7\x81\x51\xfa\x28\x98\xd7\xca\x00\xa2\xa6\xfc\x80\x65\xcc\x3b\x7e\xc3\x36\xfa\xde\x07\x08\x93\xd9\x5a\x0c\xfa\xfe\x87\x41\x68\xac\x43\x70\xf5\x59\x92\xe3\x1d\xea\x1a\x92\xc9\x8f\x06\x3a\xc1\xff\xe1\x5c\xda\x67\x91\x27\x3e\x91\xde\x0b\xe4\xaf\xfc\x47\x5f\x20\xc2\x77\xac\x23\xac\x1d\x95\xd4\xd6\xff\xe1\x9f\x10\x2f\xfa\x5d\xf2\x05\xc4\x70\x4c\x70\xf5\x7e\x94\x35\x1c\xf9\xa1\xc6\x77\x68\x3e\x43\x17\x57\x4a\xfe\xe8\xc3\xcb\xbe\x8c\xfc\xa0\xcf\x23\x09\xdf\xb1\x0e\xbf\x51\x9f\x8b\x2b\x65\x9f\xc6\x77\xe8\x37\xee\x4f\x62\xd7\x58\xb4\x8f\x05\x3e\x9e\x35\xc4\x1e\x13\xc9\x8f\x06\xbb\xc0\xa8\xfc\x23\xf0\x1f\xfa\x63\x88\x86\xba\x21\x72\xeb\x24\x18\xfb\x1f\x82\xde\x1d\x2e\x68\xdd\x30\x0e\x1a\x56\x4f\x24\xc2\x77\xac\xc3\x6f\xd8\x06\xdb\x62\x1f\xec\x8b\xef\x10\xd6\xc1\xff\xd1\xf7\x11\x67\x83\xe0\xea\x97\x06\xeb\x20\x91\x7c\xd2\xdd\xee\x3c\x08\x37\x6f\x04\x30\x0d\x08\x7c\x34\x8f\xe4\x34\xae\x99\x00\xa7\x7f\x3d\x0d\x4e\xfd\xea\x7e\x22\x7c\xc7\x3a\xfc\x86\x6d\xb0\x6d\xb8\x69\x23\xf5\xb5\xe7\xcc\x6c\x3f\x00\x62\x77\x6e\x54\x68\xae\x57\xad\xf5\x96\x52\x7e\xb0\x6e\x01\xe8\xfb\x66\xd0\x1a\x30\xaf\x95\x83\x28\xcc\x81\x2b\xef\xe6\x92\x3c\x24\x8a\x63\x96\x4d\x71\x7e\xe3\x37\x6c\x83\x6d\xb1\x0f\xf6\x0d\xd6\x15\x48\x5d\x86\x7c\x64\x47\xc2\xc3\xf6\x0a\xae\x66\xc6\xea\x20\xae\xfc\x48\x08\xfc\x55\xdf\x05\x7f\xd5\x77\x00\x22\x41\x08\x7e\xfa\x2a\x74\x6d\x53\xa1\x6e\xf9\x64\x92\x75\x69\xcd\x44\xb8\xb9\x35\x13\x6e\x6d\xcb\x80\xe6\x75\xe3\xa9\x0e\xbf\x61\x1b\x6c\x8b\x7d\xb0\xaf\xbf\xea\x7b\xc4\x8b\xec\xe2\xec\x22\x9c\x83\x66\xc1\xd5\x19\xa9\xe4\xd3\xdc\x1f\x98\x0d\xc1\x33\x6f\x42\x34\xec\x03\x7f\xe5\x1c\x68\xdb\x30\x96\xe4\x9c\xfb\xcd\x7d\xd0\xbd\x63\x0c\xe8\x85\x63\x41\x2f\xbc\x07\x7a\x3d\x6e\xa8\x5f\x39\x89\xbe\x61\x1b\x6c\x8b\x7d\xb0\xaf\xe4\xd9\x65\xd9\x6e\x19\x88\x5d\xd9\xba\xd0\x5c\x73\x53\xca\xd7\xff\x0f\xf4\xfd\x0f\x41\xa8\x61\x35\xf9\x1f\x7d\xdf\x74\x68\x5e\x97\x47\xf3\xdd\xf2\x4e\x1e\xc9\x0d\x9f\xfc\x3b\x30\x4f\xbd\x02\xc6\x9e\x09\xd0\xba\x21\x07\x4e\xfd\x6a\x1a\xb5\xc1\xb6\xe4\xb3\x1a\x56\x13\x0f\xe4\x25\xd7\xed\x39\x5c\x9b\x51\xa1\xb1\x17\xe3\xc8\x7f\x0b\x63\x13\x5c\xef\xd4\xb6\xaf\x91\x7c\x6b\xf8\xf2\x16\x5a\xdb\x62\xcf\x64\xb8\xb4\x7a\x3c\xc9\x68\xdb\x90\x0d\xfe\xb2\x87\x01\x2e\x2d\x07\x68\x5a\x05\x81\xca\x27\xe1\xfa\xe6\x4c\x39\x2f\xab\xc7\x53\xdb\x88\xaf\x9e\xfa\x22\x0f\xe4\xe5\xc4\x47\xa5\x8f\xa2\x0d\xfc\x22\x8e\xfc\x5f\xe0\x37\x3b\x5e\x4f\x2e\xff\x1e\xf0\x97\x7e\x11\xa2\x0d\x4b\x00\x9a\x56\x42\xe0\xe0\x13\x70\x7d\x53\x56\x6a\xf9\x81\x4e\x8a\xbf\x70\xac\x43\xe4\x6b\xec\x45\xd4\x0d\xea\x28\xbe\xfe\x67\x38\xfa\x6f\x5e\x97\x2f\xfd\xcb\xd1\xe7\x21\x74\xec\x87\xa0\xef\xce\x81\xab\xbf\x1d\x67\x7d\xcb\xb3\xd6\xcc\x50\xfd\xe3\xdc\xe2\x1c\xcb\xb3\xd0\x60\xf9\xae\xb9\x68\x1b\xe4\xdf\x53\xd8\x1f\xda\x39\xda\xbe\xee\x55\x41\xf7\xba\xc9\x16\xcf\xaf\xb8\x2f\xa5\xfd\x25\x95\x8f\x6b\x42\x63\xcd\xb8\xcf\x25\x5c\x7f\x5b\xe5\xfa\xc3\x39\xb8\xf0\xdf\xf7\x42\xfb\xc6\x7b\xa0\x63\x53\x36\x5c\x5c\x35\x29\xad\xf5\x97\x42\x7e\x26\xfa\x06\xda\x37\xc2\xbe\xb4\xfc\x0f\xd2\x99\x25\xd3\xd2\xf2\x3f\xc9\xe5\x2b\xf6\x1c\xbc\x86\x3e\x12\x7d\xe5\x50\xff\xeb\x8f\xe3\x7f\x25\x0d\xf5\xbf\xfe\x21\xfe\x37\x99\xfc\x18\x1d\x7c\x09\xf7\x08\xdc\x2b\xc0\xd4\x69\xef\x30\x2a\x46\xb9\xff\x58\x73\x9f\x5a\xbe\xc2\x7c\x1a\xed\x8d\x0b\x70\xaf\xc4\x3d\x53\xee\xbf\xef\x8e\x7a\xff\x4d\x47\x7e\x8c\x0e\xee\xc7\x58\x01\x63\x06\xd4\x1d\xf6\x41\x9b\x18\x51\xfc\x61\xf9\xb2\xb4\xe5\x7b\x55\x1b\xc3\xb3\x18\x33\x61\xec\x84\x31\x14\xf1\xdc\xfb\x40\x5a\xf1\x97\x4e\xf1\xd7\x34\x8a\xd9\x06\x3f\xa9\xe4\x3b\xb6\x48\xb1\xa2\xfa\x12\xc6\x8e\x18\x43\x62\x2c\x89\x31\x25\xc6\x96\x29\xe3\x4f\x6f\x26\xc5\xaa\xf1\x62\x58\x8c\x6d\x31\xc6\x4d\x26\xbf\x7f\x3d\x60\xcc\xac\xce\xa3\x18\x1a\x63\x69\x8c\xa9\xb9\x5b\xc6\xd8\x14\x7f\xe7\xc4\xc4\xdf\x39\xb2\x0e\x63\x73\x8c\x91\x93\xc5\xf0\xc4\x47\x49\x2a\x9f\x30\x68\x0a\xf3\x15\x65\x31\x6b\xaf\x7e\xd3\x3a\x53\xd4\x5b\x67\x8c\x44\xe7\x0f\x49\xc9\xcf\x30\xad\xd6\x59\x27\xa9\xfc\x58\x5d\xf4\x69\x14\x2f\x8d\x95\x67\xab\x51\x9f\xcf\xa6\x5b\x67\xbd\xb4\xe4\x7f\xde\x1f\x7b\x6d\x1c\x66\x2a\x1c\x66\x0c\xe9\x99\xc3\x8c\x4d\xb7\x28\xcf\xa2\x4c\x8b\xd4\x74\xa8\xc5\xa2\x1e\x8b\x02\x16\x99\x8c\xa9\xc0\x48\x90\x6a\xcb\x9d\xc9\x18\x9b\xcd\x18\xfb\xfb\xd8\x3c\xc5\xc3\x9f\xb5\x56\xee\x3e\x77\x9f\x3f\xcc\xc7\xda\x1f\x1f\x16\x5c\x7d\x5b\x70\x75\xa1\xe0\x6a\xc1\xef\x89\x6c\xde\xff\x2a\xb8\xfa\x37\x82\x2b\xb3\x05\x57\x72\x7a\x35\xc6\x0c\x2d\x79\x3e\x29\x0d\xfc\xdf\x16\x5c\x0d\x0a\xae\xc2\x1d\x22\x53\x70\xb5\x53\x70\xf5\x20\xed\xeb\x5c\xc9\xc7\x58\xc3\x48\x33\x3f\x97\x18\xbf\x42\xb1\x57\x5a\x84\x6d\x07\x60\x4a\xd2\x37\x6e\x5b\x97\xfd\x1b\xf5\x56\x2a\xb8\xfa\x24\xec\x67\x4c\xe7\xc3\x1b\x83\x83\x5f\x73\x05\x31\xc6\x0b\xd6\xbe\x45\xe7\xf2\x64\x14\x3c\xbb\x08\x8c\x8a\xa7\xfa\x71\x21\xc6\xc2\x1c\x30\x0e\x7e\x0d\x02\x27\xfe\x81\x78\x20\xe1\x3b\xd6\xc9\x78\x84\x39\xd8\xf5\xe2\x29\x60\x94\xcd\x04\xe1\xcd\x8a\x1d\x47\xa3\xe0\xea\x0b\x7e\xce\x5c\x7a\x8a\xfc\x64\x7c\xfc\x2c\x48\xf1\x75\xb8\x6f\x48\x9c\x37\xf8\x31\xdb\x4a\x64\x9c\x84\xb2\xbd\x19\xe0\x3f\xf2\x17\x60\xb6\xee\x85\x68\xe0\x26\x40\x34\x12\x13\x20\x46\xa8\x0e\xbf\x61\x1b\x6c\x8b\x7d\xf4\xe2\xc9\x10\x6a\x58\x0b\xe1\xcb\x5b\xc1\xa8\xfc\x46\xff\x9c\x70\xb5\x0d\xc7\xd0\xa3\x65\x33\x91\xe6\x3c\x0c\x17\x7f\xa4\xeb\x14\xc5\xb4\xc2\xc3\xe8\x3c\x43\xb1\x65\xb0\x3b\xe5\x98\xb1\x0d\xb6\xc5\x3e\xf2\x7c\x30\x13\x22\xdd\x35\x10\xf5\x5f\xa7\x73\xa9\x28\xbc\xc7\x1e\x43\x93\xe0\xca\x53\x14\x73\x16\xa6\x8e\x89\x86\x83\x9f\xce\x41\x55\xdf\x23\xec\x62\x77\x2e\x9d\xc7\xed\xb3\xa8\x1c\x5c\x10\xa2\xa2\x05\x22\xb7\x3e\x21\xc2\x77\xac\xeb\xff\x1e\xa2\x3e\xd8\x17\x79\x20\xaf\x68\xa8\x87\xda\x84\xea\x97\xcb\xbc\xb3\x1c\x43\xa5\xe0\xea\xe4\x74\x62\xba\xe1\xe0\x0f\x35\xac\x91\x36\xc0\xdd\x10\xac\x7b\xbb\x1f\x7b\x34\x0c\x66\x47\x05\xdd\x3d\xe9\xa5\x8f\x80\x5e\x34\x51\x52\xe9\x23\xf2\x3e\xaa\xa3\x82\xda\xd8\x63\x08\xd6\xfe\x52\xc6\xee\xde\x0c\xe2\x69\x8f\x1d\xcf\x4d\x92\xbf\x1a\x45\x7f\xeb\xf3\x32\x57\x5f\x0a\x3b\x4a\x17\x3f\xea\xd2\x28\x9f\x25\xf5\x76\xe4\x3b\xfd\x79\xad\x70\x1f\x84\xce\xff\x17\xe8\x7b\x26\x59\xfe\xc6\xca\xdb\xdb\x3e\x46\x63\xf4\x0d\xdb\xd8\xbc\xe9\x0c\x78\xe4\x79\xe2\x85\x3c\x69\x9e\x68\x7e\x7b\x21\x50\xfd\x63\x7b\x4d\x5f\x13\x5c\xfd\x7a\xca\x73\x45\x9a\xf8\x69\x7e\xad\xb5\x67\xde\xa8\xea\xd7\x59\xdd\x02\x79\x0f\xc1\xa5\x1f\xe9\xd9\x39\x06\x6e\x6e\xcd\x22\xc2\x77\xb9\x36\x5d\xd4\x06\xdb\xda\xf6\x64\xde\x38\x42\xbc\x90\x27\xf2\x76\x4c\xac\xbb\x86\x72\x44\xd6\x18\xde\xd3\xb9\xcb\x9d\xcc\x1f\xa5\x83\x9f\xf2\x5c\x15\x73\x48\x5f\x81\x4f\x5f\x75\xce\xa8\xe1\xcb\x5b\x1c\x9b\x45\xac\x57\xde\xcd\xa3\x5c\x66\xcd\xd2\x29\x44\xf8\x8e\x75\x72\x1c\x0a\xb5\xc5\x3e\x92\xa9\x49\xbc\x68\x0e\x2a\xe6\x0c\x38\x5b\x87\x2e\x2c\xb1\xce\x86\x6a\xbb\xe0\xea\x57\x93\x9f\x2d\x53\xe3\xa7\x5c\xcf\xae\x6c\xb2\x03\xb3\xb3\x5a\xea\xa9\xaf\x51\x9e\x5d\x35\x06\xdd\xdb\x33\x9c\x9c\x53\x3c\xc2\x6f\xd8\x86\xfc\x4e\xf9\x57\x9c\xfc\x9f\xd9\xf9\xb1\xb4\xbb\xc2\x6c\x30\xdb\xcb\xfb\xf5\xa5\x5f\x95\x79\x41\xb9\x67\x2c\x12\xdc\x9d\xf0\xce\x2a\x1d\xfc\xe4\xdf\xc8\x5f\x7c\x97\x72\xf1\x54\x57\xf3\xef\x64\x17\xbd\x1e\x37\xe5\x6c\x12\x61\xb7\x09\xdb\x60\x5b\xec\x83\x7d\xe5\x00\x0c\xe2\x89\xbc\x51\xc6\x00\x99\x75\x0b\x6d\x1b\x3a\x2e\xb8\x32\x61\xa4\xf8\xf1\x37\xe5\xe9\x35\x06\xa1\x86\x55\x96\x7e\x5a\xc0\x28\x9d\x49\x75\xad\xef\x8d\x1b\x82\xb5\x76\xd9\xc0\x7b\x00\x9b\xb0\xad\xed\xfb\xa3\xfa\x15\x69\x2b\x17\x57\x51\x1d\xca\x88\x95\x8d\xfe\x57\xde\x9f\x29\x3e\xc1\xd5\x6f\x26\xce\x8f\x24\xc7\x1f\xf1\x35\x80\x5e\xf2\x00\xe8\xbb\xf3\x21\x72\xf3\x98\x65\xf7\x1f\x80\xf0\x8e\x81\x9e\x1d\x63\xc8\xc6\x6d\x7c\x35\x4b\xa7\x92\xbd\xdf\xda\x96\x09\x5d\xdb\x33\xa1\x6d\x43\x8e\x73\xbf\x60\xe7\xf9\xb1\x0f\xfa\x48\xdc\x7b\x89\xff\xcd\x6a\xe2\x4d\x79\x65\x5f\xc3\x40\xbd\xfd\xee\x5b\xb6\x0d\x51\x7e\x3b\xde\x3a\x4e\x85\xdf\xec\x38\x44\xb6\x6f\x94\x3d\x46\xff\x67\xc0\x27\x70\xf2\x65\xe2\x7b\xed\xfd\x9c\x98\xbc\xe7\x54\xa9\x5f\x27\x16\x50\x40\xe7\x2a\x5c\xdf\x9c\x4d\xf7\x32\x76\x3b\xec\x83\x7d\x03\x9f\xfc\xa3\xc4\xe9\x6f\x27\xde\x28\x03\x65\x0d\xb0\xa1\x9a\xff\xb0\xf1\x6f\xd3\xb9\x2b\xee\x9d\x69\x2a\xfc\xa4\x6b\x9c\xdf\xc3\xcf\x01\x44\x02\x94\xcb\x35\x2a\x9f\xa6\xba\xa6\xb5\x13\x9c\x7c\x29\xae\x51\x9f\x47\xe6\x93\xf4\xc2\x6c\xd0\x77\x8f\x73\xe2\xcd\xcb\xeb\xf3\x9d\xfc\x2a\xf6\x21\x7e\x87\x9e\x96\x79\xe1\x48\x80\x78\x63\x1d\xca\x1a\x20\xfb\x2a\xb7\xf7\xb3\x6a\xc1\x95\xdc\x91\xe0\x0f\x5d\x58\x26\xfd\xe6\xf1\x97\xa4\xbe\xc4\x65\xca\x5d\xf5\xee\x54\x29\xe7\x6d\xeb\xb5\x0d\xf5\xca\x5d\x60\xec\x9d\x4a\x77\x30\x91\xda\x37\xc0\x5f\x3e\x93\xc6\xd3\xf9\xc1\x58\xb2\x2d\x3b\x4f\xee\xdb\xa9\x12\x0f\xe4\x45\xf3\x79\xfc\x25\x92\x81\xb2\x06\xd8\xee\xcd\x13\xf2\xce\x98\x2b\x4d\x82\xab\x0f\x26\xc9\x91\x3e\x97\x08\x3f\xed\x4f\x3b\x19\x04\x4f\xbd\x2e\x79\xf6\xd4\xd2\xbe\xd3\xbd\x7d\x0c\x9c\xb5\x6c\x1b\xb1\xe1\x7e\xa5\x7b\x55\x08\x1d\xfb\x11\x40\xf3\x5a\x22\xf3\xf4\xcf\x69\x2e\xd0\xff\xa3\xed\x23\x7e\xec\xd3\xbd\xdd\x4d\x31\x74\xa4\xa7\x4e\xca\x38\xf5\xba\x94\x81\xfb\x5b\xac\xef\x10\xcd\x74\xf7\x64\xdd\x79\xcf\x4a\x82\xff\x5b\x42\x63\x86\x71\xf0\xc9\x21\xb1\x24\xf9\x4e\xe4\x5d\x3b\xdf\xd2\xc9\x71\xd0\x8b\xf2\xa1\x6b\x5b\x86\xe3\x63\xb0\xec\xda\x9e\x41\xf7\x6f\x66\xcd\xeb\x00\xcd\x6b\x00\x9a\x56\x43\xb4\x7e\x31\x18\x7b\x27\x83\xcf\x23\xe7\x0a\xf1\x3b\x6d\x8b\xf2\xe9\xff\x3a\x24\xa3\x76\xbe\x94\x31\xc8\x87\x62\x6c\x4a\x31\x8b\xc6\xfa\x04\x57\x9f\x4e\x82\xff\x69\x6c\x43\xb1\x88\xff\xfa\xc8\xf0\x6f\xcb\x00\x7d\x57\x16\x98\x67\x7e\xde\x8f\xff\x42\x01\x18\xc5\x93\x08\xff\xf9\x91\xe0\x0f\x76\x03\xea\x14\x75\x4b\x3a\x4e\x8c\x7f\x16\xe5\x96\xf7\xcd\xa0\x39\x4b\x6e\x3f\x75\x34\xf7\x68\x03\xb6\xfd\xa0\xef\xb9\xb1\x85\xee\xd3\x21\x78\xe4\xcf\xe8\x0e\x10\xed\x27\x7c\xe2\x45\xd0\xbd\x19\xd0\xbd\x23\x83\xda\x0e\xdb\x7e\xc2\x7d\x74\x67\x81\xb6\x4d\x36\x9e\x18\xff\x83\xb8\x46\xf4\xa2\x09\x74\xdf\x31\x92\xf5\x7b\xf5\xb7\xb9\x96\xef\xc9\x81\x60\xd5\x73\x10\xaa\xfe\x3e\xe8\xc5\xf7\x82\xce\x15\xba\xd3\xc2\x31\x9e\xfa\xb5\xbd\x7e\x95\xb4\xd6\xef\x20\xfc\xdf\x8e\x8f\x9f\x72\xeb\xb9\xe4\xa3\x70\x5f\xb9\xca\x07\xf0\x48\xd7\x7f\x9e\x5b\x81\x7b\x53\x46\x7f\xec\x6c\x91\x4f\xeb\x8f\x2f\xb0\x6d\xe3\x30\xfc\x67\xba\xf8\xe5\xde\xa0\x6e\x47\x1e\xc1\x9a\xf9\x03\x78\xa4\xbb\x7f\x21\x35\xad\x1b\x0f\xbd\x9e\x31\xd6\x3d\xa6\x0a\x7d\x9a\x9b\xe6\x05\x75\x3f\x64\xff\x3a\xf9\xb2\xb5\x46\x13\xef\x5f\xe9\xe0\xd7\xed\xbb\x3e\xdc\xa3\x51\x2f\xbf\xfb\xd3\x81\x71\x48\xdc\xf8\x61\x6b\xdc\xf8\xc1\xde\xc7\x30\x6e\x40\x9c\xa8\xeb\x58\xec\xc3\x89\x1f\xd2\xc5\x1f\xb3\x06\xbe\x29\xb8\xd2\xa7\xef\xb9\x0f\x22\x5d\x9f\x0e\xe0\xe1\xc4\x6f\x17\xd3\x8b\xdf\x12\xd1\x70\xe2\xb7\xe1\xe1\xa7\x35\x30\x81\x62\x55\xcd\x45\xe7\xd0\xd8\xe7\xb3\x88\x9f\x87\x85\x5f\x53\x18\xfd\x3f\x95\xab\x8b\xe5\x19\xe3\x71\x3a\x43\x38\x6b\xe0\xf7\x76\x7e\xa9\x8e\x7b\x7e\x19\x2e\xfe\x18\x1b\xc2\xb3\x5a\x07\x9e\xdd\xf0\x0c\xe7\xf0\xb9\x23\xe7\xc7\x9b\xa3\xc4\x4f\x7e\xc8\x8d\x67\x66\x3a\xa7\xef\x7f\x88\xce\xd2\xf6\x93\xf6\xf9\x7d\x47\xcc\xf9\x7d\x47\xb2\xf3\x7b\x55\xdc\xf3\xfb\x48\xf1\xc7\xcc\xc1\xd7\x28\x77\x81\x3e\xae\xfa\xaf\x29\xa7\x01\x43\xf2\x27\xcf\x0f\xcc\x9f\x9c\xbb\x3d\xf9\x93\xd1\xe2\xef\xe3\x2e\xe6\xe3\x4c\x91\x67\x66\x35\x8a\x7e\x8e\xd6\xb2\xa5\xb3\xd4\xf9\xab\x17\xc1\x28\x7d\x04\xf4\x3d\x13\x89\x8c\x44\xf9\xab\xba\xb7\x87\xe6\xaf\x6e\x03\xfe\x98\x39\x98\x2c\xb8\x7a\xc8\xb6\xd9\x50\xfd\x32\x99\x1b\x0c\xf5\x38\xfe\xe2\xf6\xe5\x0f\x13\xe7\x4e\x47\x84\xbf\x50\xb5\xfd\xe9\x1c\x2b\x97\x4a\xb9\x55\xfa\xdf\x50\xe0\x3a\x44\xba\xcf\xc8\xdc\x37\xda\x8a\x9d\xbf\x4d\x82\xc1\xc1\x62\xe5\x6f\xb1\x0f\xe5\x7e\x4b\x1f\xa5\x5c\x70\xd2\x3e\x23\xc0\x4f\x63\xf0\xba\x58\x8f\x27\x1b\xe7\xe1\x05\x99\xd3\x56\x68\xbe\x8d\xca\x6f\xd0\xbe\x19\x6a\x58\xeb\xac\x3d\x27\x7f\xde\x56\x92\x7e\xfe\xbc\xe4\x41\x30\xdb\xf6\xa5\x1e\xf3\x08\xf1\xe3\xa3\x7b\x15\x66\xec\xa2\xff\x6d\xbc\x60\xdd\x2d\x48\xbc\xbb\xb2\xe8\xbf\x31\x7a\xf1\xd4\x24\xf7\x17\xf3\x89\x12\xdd\x5f\x18\x15\x4f\xd1\x1d\x48\xca\x7b\x92\xda\xb7\xac\xff\xda\xba\x86\x8d\x9f\xc6\xc0\x5d\x0c\xbc\xe4\x5b\xd1\x27\x95\x39\x77\x64\x8e\x8f\x19\xcd\xfd\xd1\xb0\xee\xa9\x46\x84\x1f\x1f\x43\x73\xd9\x6b\x7a\xbc\xbc\x6b\xa3\x3b\xb7\x4e\xeb\x0e\xee\x4e\xdd\xf7\x8d\x18\xbf\x33\x0e\x8f\xc2\x7a\x8b\x54\x8c\x35\x72\x04\x57\x67\x0b\xae\xfe\xc4\xba\x0b\x2d\xb8\x03\xf7\xae\x6f\x5b\x77\xbc\x23\xc6\x7f\xf7\xb9\xfb\x7c\x9e\x1e\xb9\x43\x24\x2e\x5b\x18\x63\xcf\x58\x65\x9e\x55\x66\x5a\xa5\x6b\x50\xc9\xec\xb2\xc0\x2a\x9f\x19\x54\x4e\x4f\x50\xe6\x25\x28\x33\x6f\x5f\xd9\x93\xa0\x0c\x24\x28\xcd\x41\x65\xd4\x2a\xc1\x2e\x17\x0e\x2a\x5b\xac\xb2\xc7\x2a\x4d\xab\x4c\xa1\xdf\xff\x0f\x00\x00\xff\xff\xc6\xb9\x24\x2f\xee\x3a\x00\x00") func faviconIcoBytes() ([]byte, error) { return bindataRead( _faviconIco, "favicon.ico", ) } func faviconIco() (*asset, error) { bytes, err := faviconIcoBytes() if err != nil { return nil, err } info := bindataFileInfo{name: "favicon.ico", size: 15086, mode: os.FileMode(438), modTime: time.Unix(1612640817, 0)} a := &asset{bytes: bytes, info: info} return a, nil } var _jsMainJs = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x4a\xce\xcf\x2b\xce\xcf\x49\xd5\xcb\xc9\x4f\xd7\x50\x4a\xad\x48\xcc\x2d\xc8\x49\x55\xd2\xb4\x06\x04\x00\x00\xff\xff\xc8\x9f\xbd\x5f\x17\x00\x00\x00") func jsMainJsBytes() ([]byte, error) { return bindataRead( _jsMainJs, "js/main.js", ) } func jsMainJs() (*asset, error) { bytes, err := jsMainJsBytes() if err != nil { return nil, err } info := bindataFileInfo{name: "js/main.js", size: 23, mode: os.FileMode(438), modTime: time.Unix(1613718745, 0)} a := &asset{bytes: bytes, info: info} return a, nil } // Asset loads and returns the asset for the given name. // It returns an error if the asset could not be found or // could not be loaded. func Asset(name string) ([]byte, error) { cannonicalName := strings.Replace(name, "\\", "/", -1) if f, ok := _bindata[cannonicalName]; ok { a, err := f() if err != nil { return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err) } return a.bytes, nil } return nil, fmt.Errorf("Asset %s not found", name) } // MustAsset is like Asset but panics when Asset would return an error. // It simplifies safe initialization of global variables. func MustAsset(name string) []byte { a, err := Asset(name) if err != nil { panic("asset: Asset(" + name + "): " + err.Error()) } return a } // AssetInfo loads and returns the asset info for the given name. // It returns an error if the asset could not be found or // could not be loaded. func AssetInfo(name string) (os.FileInfo, error) { cannonicalName := strings.Replace(name, "\\", "/", -1) if f, ok := _bindata[cannonicalName]; ok { a, err := f() if err != nil { return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err) } return a.info, nil } return nil, fmt.Errorf("AssetInfo %s not found", name) } // AssetNames returns the names of the assets. func AssetNames() []string { names := make([]string, 0, len(_bindata)) for name := range _bindata { names = append(names, name) } return names } // _bindata is a table, holding each asset generator, mapped to its name. var _bindata = map[string]func() (*asset, error){ "css/main.css": cssMainCss, "favicon.ico": faviconIco, "js/main.js": jsMainJs, } // AssetDir returns the file names below a certain // directory embedded in the file by go-bindata. // For example if you run go-bindata on data/... and data contains the // following hierarchy: // // data/ // foo.txt // img/ // a.png // b.png // // then AssetDir("data") would return []string{"foo.txt", "img"} // AssetDir("data/img") would return []string{"a.png", "b.png"} // AssetDir("foo.txt") and AssetDir("notexist") would return an error // AssetDir("") will return []string{"data"}. func AssetDir(name string) ([]string, error) { node := _bintree if len(name) != 0 { cannonicalName := strings.Replace(name, "\\", "/", -1) pathList := strings.Split(cannonicalName, "/") for _, p := range pathList { node = node.Children[p] if node == nil { return nil, fmt.Errorf("Asset %s not found", name) } } } if node.Func != nil { return nil, fmt.Errorf("Asset %s not found", name) } rv := make([]string, 0, len(node.Children)) for childName := range node.Children { rv = append(rv, childName) } return rv, nil } type bintree struct { Func func() (*asset, error) Children map[string]*bintree } var _bintree = &bintree{nil, map[string]*bintree{ "css": {nil, map[string]*bintree{ "main.css": {cssMainCss, map[string]*bintree{}}, }}, "favicon.ico": {faviconIco, map[string]*bintree{}}, "js": {nil, map[string]*bintree{ "main.js": {jsMainJs, map[string]*bintree{}}, }}, }} // RestoreAsset restores an asset under the given directory func RestoreAsset(dir, name string) error { data, err := Asset(name) if err != nil { return err } info, err := AssetInfo(name) if err != nil { return err } err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755)) if err != nil { return err } err = os.WriteFile(_filePath(dir, name), data, info.Mode()) if err != nil { return err } err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime()) if err != nil { return err } return nil } // RestoreAssets restores an asset under the given directory recursively func RestoreAssets(dir, name string) error { children, err := AssetDir(name) // File if err != nil { return RestoreAsset(dir, name) } // Dir for _, child := range children { err = RestoreAssets(dir, filepath.Join(name, child)) if err != nil { return err } } return nil } func _filePath(dir, name string) string { cannonicalName := strings.Replace(name, "\\", "/", -1) return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...) } ================================================ FILE: _examples/file-server/embedding-files-into-app-bindata/main.go ================================================ package main import ( "github.com/kataras/iris/v12" ) // Follow these steps first: // $ go install github.com/go-bindata/go-bindata/v3/go-bindata@latest // $ go-bindata -prefix "assets" -fs ./assets/... // $ go run . // "physical" files are not used, you can delete the "assets" folder and run the example. // // See `file-server/embedding-gzipped-files-into-app-bindata` and // 'file-server/embedding-files-into-app` examples as well. func newApp() *iris.Application { app := iris.New() app.Logger().SetLevel("debug") app.HandleDir("/static", AssetFile()) /* Or if you need to cache them inside the memory (requires the assets folder to be located near the executable program): app.HandleDir("/static", iris.Dir("./assets"), iris.DirOptions{ IndexName: "index.html", Cache: iris.DirCacheOptions{ Enable: true, Encodings: []string{"gzip"}, CompressIgnore: iris.MatchImagesAssets, CompressMinSize: 30 * iris.B, }, }) */ return app } func main() { app := newApp() // http://localhost:8080/static/css/main.css // http://localhost:8080/static/js/main.js // http://localhost:8080/static/favicon.ico app.Listen(":8080") } ================================================ FILE: _examples/file-server/embedding-files-into-app-bindata/main_test.go ================================================ package main import ( "os" "path/filepath" "runtime" "strings" "testing" "github.com/kataras/iris/v12/httptest" ) type resource string // content types that are used in the ./assets, // we could use the detectContentType that iris do but it's better // to do it manually so we can test if that returns the correct result on embedding files. func (r resource) contentType() string { switch filepath.Ext(r.String()) { case ".js": return "text/javascript" case ".css": return "text/css" case ".ico": return "image/x-icon" case ".html": return "text/html" default: return "text/plain" } } func (r resource) String() string { return string(r) } func (r resource) strip(strip string) string { s := r.String() return strings.TrimPrefix(s, strip) } func (r resource) loadFromBase(dir string) string { filename := r.strip("/static") fullpath := filepath.Join(dir, filename) b, err := os.ReadFile(fullpath) if err != nil { panic(fullpath + " failed with error: " + err.Error()) } result := string(b) if runtime.GOOS != "windows" { result = strings.ReplaceAll(result, "\n", "\r\n") result = strings.ReplaceAll(result, "\r\r", "") } return result } var urls = []resource{ "/static/css/main.css", "/static/js/main.js", "/static/favicon.ico", } // if bindata's values matches with the assets/... contents // and secondly if the HandleDir had successfully registered // the routes and gave the correct response. func TestEmbeddingFilesIntoApp(t *testing.T) { app := newApp() e := httptest.New(t, app) route := app.GetRouteReadOnly("GET/static/{file:path}") if route == nil { t.Fatalf("expected a route to serve embedded files") } if runtime.GOOS != "windows" { // remove the embedded static favicon for !windows, // it should be built for unix-specific in order to be work urls = urls[0 : len(urls)-1] } for _, u := range urls { url := u.String() contents := u.loadFromBase("./assets") e.GET(url).Expect(). Status(httptest.StatusOK). ContentType(u.contentType()). Body().IsEqual(contents) } } ================================================ FILE: _examples/file-server/embedding-gzipped-files-into-app-bindata/bindata.go ================================================ // Code generated by go-bindata. (@generated) DO NOT EDIT. // Package main generated by go-bindata.// sources: // ../embedding-files-into-app-bindata/assets/css/main.css // ../embedding-files-into-app-bindata/assets/favicon.ico // ../embedding-files-into-app-bindata/assets/js/main.js package main import ( "bytes" "compress/gzip" "fmt" "io" "net/http" "os" "path/filepath" "strings" "time" ) func bindataRead(data []byte, name string) ([]byte, error) { gz, err := gzip.NewReader(bytes.NewBuffer(data)) if err != nil { return nil, fmt.Errorf("read %q: %v", name, err) } var buf bytes.Buffer _, err = io.Copy(&buf, gz) clErr := gz.Close() if err != nil { return nil, fmt.Errorf("read %q: %v", name, err) } if clErr != nil { return nil, err } return buf.Bytes(), nil } type asset struct { bytes []byte info os.FileInfo } type bindataFileInfo struct { name string size int64 mode os.FileMode modTime time.Time } // Name return file name func (fi bindataFileInfo) Name() string { return fi.name } // Size return file size func (fi bindataFileInfo) Size() int64 { return fi.size } // Mode return file mode func (fi bindataFileInfo) Mode() os.FileMode { return fi.mode } // ModTime return file modify time func (fi bindataFileInfo) ModTime() time.Time { return fi.modTime } // IsDir return file whether a directory func (fi bindataFileInfo) IsDir() bool { return fi.mode&os.ModeDir != 0 } // Sys return file is sys mode func (fi bindataFileInfo) Sys() any { return nil } type assetFile struct { *bytes.Reader name string childInfos []os.FileInfo childInfoOffset int } type assetOperator struct{} // Open implement http.FileSystem interface func (f *assetOperator) Open(name string) (http.File, error) { var err error if len(name) > 0 && name[0] == '/' { name = name[1:] } content, err := Asset(name) if err == nil { return &assetFile{name: name, Reader: bytes.NewReader(content)}, nil } children, err := AssetDir(name) if err == nil { childInfos := make([]os.FileInfo, 0, len(children)) for _, child := range children { childPath := filepath.Join(name, child) info, errInfo := AssetInfo(filepath.Join(name, child)) if errInfo == nil { childInfos = append(childInfos, info) } else { childInfos = append(childInfos, newDirFileInfo(childPath)) } } return &assetFile{name: name, childInfos: childInfos}, nil } else { // If the error is not found, return an error that will // result in a 404 error. Otherwise the server returns // a 500 error for files not found. if strings.Contains(err.Error(), "not found") { return nil, os.ErrNotExist } return nil, err } } // Close no need do anything func (f *assetFile) Close() error { return nil } // Readdir read dir's children file info func (f *assetFile) Readdir(count int) ([]os.FileInfo, error) { if len(f.childInfos) == 0 { return nil, os.ErrNotExist } if count <= 0 { return f.childInfos, nil } if f.childInfoOffset+count > len(f.childInfos) { count = len(f.childInfos) - f.childInfoOffset } offset := f.childInfoOffset f.childInfoOffset += count return f.childInfos[offset : offset+count], nil } // Stat read file info from asset item func (f *assetFile) Stat() (os.FileInfo, error) { if len(f.childInfos) != 0 { return newDirFileInfo(f.name), nil } return AssetInfo(f.name) } // newDirFileInfo return default dir file info func newDirFileInfo(name string) os.FileInfo { return &bindataFileInfo{ name: name, size: 0, mode: os.FileMode(2147484068), // equal os.FileMode(0644)|os.ModeDir modTime: time.Time{}} } // AssetFile return a http.FileSystem instance that data backend by asset func AssetFile() http.FileSystem { return &assetOperator{} } var _cssMainCss = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xca\x28\xc9\xcd\x51\xa8\xe6\xe5\xe2\x4c\xcb\xcf\x2b\xd1\x4d\x4b\xcc\xcd\xcc\xa9\xb4\x52\x28\x4e\xcc\x2b\xd6\x2d\x4e\x2d\xca\x4c\xb3\xe6\xe5\xe2\xd4\x2d\x4f\x4d\xca\xce\x2c\xd1\x2d\x49\xad\x28\xd1\x2d\xce\xac\x4a\xd5\x4d\x4c\xc9\x2a\x2d\x2e\xb1\x52\x30\x34\x30\x50\x05\xab\xc8\x2d\xc6\x21\xcb\xcb\x55\x0b\x08\x00\x00\xff\xff\x32\x4c\x06\xc6\x63\x00\x00\x00") func cssMainCssBytes() ([]byte, error) { return bindataRead( _cssMainCss, "css/main.css", ) } func cssMainCss() (*asset, error) { bytes, err := cssMainCssBytes() if err != nil { return nil, err } info := bindataFileInfo{name: "css/main.css", size: 99, mode: os.FileMode(438), modTime: time.Unix(1613718750, 0)} a := &asset{bytes: bytes, info: info} return a, nil } var _faviconIco = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xec\x5a\x0b\x70\x55\xc7\x79\xde\x7b\xce\x45\x12\xb2\x90\xc4\xc3\xe6\x61\x3b\x90\xf8\x31\xc4\x19\x6c\x32\xe3\xc4\x34\xe3\xc6\x34\x6d\xed\xd4\x69\x62\x32\x49\x9a\xa6\x75\xea\x36\x33\xee\xd8\x9e\xb4\x75\xdc\x7a\xa6\xc5\x31\x02\xa6\xd3\x84\x47\x28\x6f\x3b\xc6\x3c\xcc\xeb\x9e\xbd\x08\x21\x04\x92\x28\x12\x0e\x08\x5b\x3c\xec\x02\x92\x78\x08\x21\x09\x41\x25\x24\x10\xe8\x71\xb5\xe7\xdc\xd7\xb9\xf7\xef\xfc\xff\x9e\x73\x74\x25\xdd\x97\x24\x82\x33\x1e\xce\xcc\x3f\x7b\xee\x9e\xdd\xff\xff\xf6\xdf\x7f\xff\xfd\xf7\xdf\xcb\x98\x8b\xa9\x2c\x3f\x1f\xcb\x19\xec\x15\x37\x63\x5f\x67\x8c\xcd\x98\x21\x7f\x6b\xf9\x8c\x6d\x72\x33\x36\x7b\xb6\xf5\xfb\x11\xc6\x9e\xbd\x97\xb1\x99\x8c\xb1\x7c\x6c\xc7\x64\x3d\x3d\x6e\x76\xdb\x1f\xe1\x75\xab\x42\x53\x7e\x22\x3c\x6c\x91\xf0\xb0\x02\x22\xae\x14\x08\x8f\xab\x40\xec\x64\x92\xf0\x5d\x53\xfe\x59\xec\x64\x5f\x15\x1e\xa6\x08\x4f\x7f\x7f\xdf\x7a\x96\xa1\x97\x3c\xb8\x3f\x78\xfa\x0d\x08\x9e\x5d\x08\xc1\xda\x5f\x82\x51\xf6\x65\x30\xca\x67\x41\xf0\xcc\xbf\x11\x19\x07\x1e\x07\xbd\x78\x2a\x18\x65\x8f\xb5\x0b\x4d\x7d\xad\x6f\x07\x73\x0b\xcd\x45\xfd\xbb\x17\xb3\x0c\xa3\x62\xce\xfe\xa8\xd1\x0a\xf8\x98\x6d\x25\x10\x38\xfe\x53\x88\xdc\x3a\x01\x66\x6b\x11\x11\xbe\x07\x8e\xfd\x2d\x84\x2e\x2c\x05\xff\xd1\x79\x3d\xc2\xc3\x7e\x20\x78\x06\x13\x5c\x89\xe9\xdf\x06\xd1\x50\x2f\x04\x4e\xfc\x0c\xcc\x6b\xa5\x60\x1c\xfd\x21\x74\x6c\x1e\x47\x84\xef\x58\x17\xf8\xe4\x65\x30\x6f\x54\x81\x71\xe0\x89\xea\xbe\x2d\x6c\xd2\x80\xfe\xfe\x76\x30\x6f\x1c\x81\x60\xed\x7c\x08\x9e\x5d\x0c\x2d\xeb\xc7\x41\xed\xd2\x29\x50\xbb\x74\x32\xbd\x63\x1d\x7e\xc3\x36\xa1\x73\xff\x19\x14\x1e\x36\x4f\x78\xc7\xc4\xf4\xef\x80\x70\xd3\xfb\x10\x6e\x5c\x0f\xdd\x65\x73\xe1\xdc\xf2\x89\xd0\xb1\x25\x0f\x6e\x7c\x90\x07\xe7\x7f\x33\x81\xea\xf0\x5b\xb8\x79\x13\x61\xd0\x8b\xa7\x2c\x09\x7c\xfc\x57\xac\x7b\x11\xf6\x7f\x6a\x5f\xd4\xb8\x06\xa1\xfa\x15\x10\x6e\x7a\x0f\x3a\xb4\xc7\xa0\x61\x55\x3e\x04\x8f\xfc\x39\x84\x3e\xfa\x4b\x68\x5c\x93\x4f\x75\xf8\x2d\x74\x71\x05\x44\x45\x13\xea\x54\xeb\x7c\x85\xa9\x48\xf8\x1e\x15\xcd\xf4\xcd\xee\x7f\x71\x65\x3e\x04\x3e\x7c\x86\x78\x5c\x5a\x1d\xd3\xbf\x7e\x05\xa0\x2c\x94\x89\xb2\x03\xd5\x3f\x66\x88\x05\x31\x21\x36\x1b\xff\xd9\xe5\x13\xe1\xda\xc6\x5c\x68\xdf\x94\x4b\x63\x71\xf0\x37\xbd\x0f\x38\x56\x1c\x33\x8e\x1d\x75\x80\xba\x40\x9d\x0c\xd6\x5f\xcd\x92\x29\x44\x83\xf5\x87\xba\x76\xfa\x73\x85\xe1\x5c\x18\x07\x9e\x38\x86\x18\x68\x8e\xac\xf9\xbb\xbe\x39\x87\xc8\x99\xbf\x13\x3f\xa3\x39\xc6\xb9\x8e\xed\x4f\xb6\xe0\x61\x3f\x40\xdb\x40\x1b\x41\x5b\x19\x62\x3f\xc7\x7f\x4a\xb6\x85\x0f\xda\x9a\xdd\x9f\xd6\x80\xe6\x62\xd2\x26\xd5\xd7\xd0\x46\xc9\x56\x0f\x3c\xde\x6f\xbf\xe5\xb3\xc8\xa6\xd1\xb6\xc9\xc6\x4f\xbf\x01\x68\xf3\x68\xfb\xce\x3a\xf2\x30\x26\x76\x32\x85\xd6\x08\xae\x95\xc1\xeb\x87\xd6\x14\xb3\x69\x11\xad\x39\xaf\x5b\x1d\xed\xfa\x05\x60\x8c\x65\x32\xc6\x54\x8b\x5c\x31\x64\x3d\x0b\x63\xe8\xb0\x45\x2d\x56\xdf\x99\x96\x8f\x99\x1b\xeb\x67\xf2\x47\x8b\xea\xf3\xf9\x08\xae\x22\xe5\x09\xae\x4e\x17\x5c\xfd\xc2\x28\x69\x8a\xe0\xca\x58\xb4\x3d\xdb\x17\xa6\x29\xff\x5f\x04\x57\x5b\x85\xe6\xba\x92\x90\xb8\x1a\x43\x4a\x4c\xbd\x62\xd7\xb7\x08\xae\xd6\x0b\xae\xfe\x8f\xe0\xea\x9b\x38\x1e\x1f\x1f\x4b\xfe\x34\xb5\x7c\xa5\x40\xec\xca\x02\xbd\x64\x3a\xe8\xfb\xbe\x38\x94\x4a\xa6\x83\xf0\x66\x82\xd0\x14\x10\x9a\x0b\x44\xe1\x38\x5c\x2b\x44\xf8\x4e\x75\x5c\x01\xe1\x75\x03\xf2\x11\x9a\x2b\x2a\xb8\x52\x2b\xb8\x3a\x4f\x68\x8a\x92\x0c\x03\xc9\xf7\xb0\x02\xa3\xfc\x2b\x10\xe9\xae\x85\xa8\x7e\x15\xa2\xfa\x95\x18\xba\x0a\xa1\xb3\x8b\x49\xbe\xbe\xe7\x5e\x08\xfe\xef\x3f\x81\xd9\x51\x09\x91\xbe\x4b\x44\xf8\x8e\x75\xf8\x4d\x70\x37\x04\x3e\xfe\x11\xf9\x14\xbd\x68\x3c\xe2\xe8\x14\x5c\x7d\x49\x78\x99\x2b\x11\x06\x47\xfe\x81\xd9\x10\x0d\xde\x82\xc1\x8f\x79\xe3\x30\xe8\x7b\xa7\x81\x51\xfa\x28\x98\xd7\xca\x00\xa2\xa6\xfc\x80\x65\xcc\x3b\x7e\xc3\x36\xfa\xde\x07\x08\x93\xd9\x5a\x0c\xfa\xfe\x87\x41\x68\xac\x43\x70\xf5\x59\x92\xe3\x1d\xea\x1a\x92\xc9\x8f\x06\x3a\xc1\xff\xe1\x5c\xda\x67\x91\x27\x3e\x91\xde\x0b\xe4\xaf\xfc\x47\x5f\x20\xc2\x77\xac\x23\xac\x1d\x95\xd4\xd6\xff\xe1\x9f\x10\x2f\xfa\x5d\xf2\x05\xc4\x70\x4c\x70\xf5\x7e\x94\x35\x1c\xf9\xa1\xc6\x77\x68\x3e\x43\x17\x57\x4a\xfe\xe8\xc3\xcb\xbe\x8c\xfc\xa0\xcf\x23\x09\xdf\xb1\x0e\xbf\x51\x9f\x8b\x2b\x65\x9f\xc6\x77\xe8\x37\xee\x4f\x62\xd7\x58\xb4\x8f\x05\x3e\x9e\x35\xc4\x1e\x13\xc9\x8f\x06\xbb\xc0\xa8\xfc\x23\xf0\x1f\xfa\x63\x88\x86\xba\x21\x72\xeb\x24\x18\xfb\x1f\x82\xde\x1d\x2e\x68\xdd\x30\x0e\x1a\x56\x4f\x24\xc2\x77\xac\xc3\x6f\xd8\x06\xdb\x62\x1f\xec\x8b\xef\x10\xd6\xc1\xff\xd1\xf7\x11\x67\x83\xe0\xea\x97\x06\xeb\x20\x91\x7c\xd2\xdd\xee\x3c\x08\x37\x6f\x04\x30\x0d\x08\x7c\x34\x8f\xe4\x34\xae\x99\x00\xa7\x7f\x3d\x0d\x4e\xfd\xea\x7e\x22\x7c\xc7\x3a\xfc\x86\x6d\xb0\x6d\xb8\x69\x23\xf5\xb5\xe7\xcc\x6c\x3f\x00\x62\x77\x6e\x54\x68\xae\x57\xad\xf5\x96\x52\x7e\xb0\x6e\x01\xe8\xfb\x66\xd0\x1a\x30\xaf\x95\x83\x28\xcc\x81\x2b\xef\xe6\x92\x3c\x24\x8a\x63\x96\x4d\x71\x7e\xe3\x37\x6c\x83\x6d\xb1\x0f\xf6\x0d\xd6\x15\x48\x5d\x86\x7c\x64\x47\xc2\xc3\xf6\x0a\xae\x66\xc6\xea\x20\xae\xfc\x48\x08\xfc\x55\xdf\x05\x7f\xd5\x77\x00\x22\x41\x08\x7e\xfa\x2a\x74\x6d\x53\xa1\x6e\xf9\x64\x92\x75\x69\xcd\x44\xb8\xb9\x35\x13\x6e\x6d\xcb\x80\xe6\x75\xe3\xa9\x0e\xbf\x61\x1b\x6c\x8b\x7d\xb0\xaf\xbf\xea\x7b\xc4\x8b\xec\xe2\xec\x22\x9c\x83\x66\xc1\xd5\x19\xa9\xe4\xd3\xdc\x1f\x98\x0d\xc1\x33\x6f\x42\x34\xec\x03\x7f\xe5\x1c\x68\xdb\x30\x96\xe4\x9c\xfb\xcd\x7d\xd0\xbd\x63\x0c\xe8\x85\x63\x41\x2f\xbc\x07\x7a\x3d\x6e\xa8\x5f\x39\x89\xbe\x61\x1b\x6c\x8b\x7d\xb0\xaf\xe4\xd9\x65\xd9\x6e\x19\x88\x5d\xd9\xba\xd0\x5c\x73\x53\xca\xd7\xff\x0f\xf4\xfd\x0f\x41\xa8\x61\x35\xf9\x1f\x7d\xdf\x74\x68\x5e\x97\x47\xf3\xdd\xf2\x4e\x1e\xc9\x0d\x9f\xfc\x3b\x30\x4f\xbd\x02\xc6\x9e\x09\xd0\xba\x21\x07\x4e\xfd\x6a\x1a\xb5\xc1\xb6\xe4\xb3\x1a\x56\x13\x0f\xe4\x25\xd7\xed\x39\x5c\x9b\x51\xa1\xb1\x17\xe3\xc8\x7f\x0b\x63\x13\x5c\xef\xd4\xb6\xaf\x91\x7c\x6b\xf8\xf2\x16\x5a\xdb\x62\xcf\x64\xb8\xb4\x7a\x3c\xc9\x68\xdb\x90\x0d\xfe\xb2\x87\x01\x2e\x2d\x07\x68\x5a\x05\x81\xca\x27\xe1\xfa\xe6\x4c\x39\x2f\xab\xc7\x53\xdb\x88\xaf\x9e\xfa\x22\x0f\xe4\xe5\xc4\x47\xa5\x8f\xa2\x0d\xfc\x22\x8e\xfc\x5f\xe0\x37\x3b\x5e\x4f\x2e\xff\x1e\xf0\x97\x7e\x11\xa2\x0d\x4b\x00\x9a\x56\x42\xe0\xe0\x13\x70\x7d\x53\x56\x6a\xf9\x81\x4e\x8a\xbf\x70\xac\x43\xe4\x6b\xec\x45\xd4\x0d\xea\x28\xbe\xfe\x67\x38\xfa\x6f\x5e\x97\x2f\xfd\xcb\xd1\xe7\x21\x74\xec\x87\xa0\xef\xce\x81\xab\xbf\x1d\x67\x7d\xcb\xb3\xd6\xcc\x50\xfd\xe3\xdc\xe2\x1c\xcb\xb3\xd0\x60\xf9\xae\xb9\x68\x1b\xe4\xdf\x53\xd8\x1f\xda\x39\xda\xbe\xee\x55\x41\xf7\xba\xc9\x16\xcf\xaf\xb8\x2f\xa5\xfd\x25\x95\x8f\x6b\x42\x63\xcd\xb8\xcf\x25\x5c\x7f\x5b\xe5\xfa\xc3\x39\xb8\xf0\xdf\xf7\x42\xfb\xc6\x7b\xa0\x63\x53\x36\x5c\x5c\x35\x29\xad\xf5\x97\x42\x7e\x26\xfa\x06\xda\x37\xc2\xbe\xb4\xfc\x0f\xd2\x99\x25\xd3\xd2\xf2\x3f\xc9\xe5\x2b\xf6\x1c\xbc\x86\x3e\x12\x7d\xe5\x50\xff\xeb\x8f\xe3\x7f\x25\x0d\xf5\xbf\xfe\x21\xfe\x37\x99\xfc\x18\x1d\x7c\x09\xf7\x08\xdc\x2b\xc0\xd4\x69\xef\x30\x2a\x46\xb9\xff\x58\x73\x9f\x5a\xbe\xc2\x7c\x1a\xed\x8d\x0b\x70\xaf\xc4\x3d\x53\xee\xbf\xef\x8e\x7a\xff\x4d\x47\x7e\x8c\x0e\xee\xc7\x58\x01\x63\x06\xd4\x1d\xf6\x41\x9b\x18\x51\xfc\x61\xf9\xb2\xb4\xe5\x7b\x55\x1b\xc3\xb3\x18\x33\x61\xec\x84\x31\x14\xf1\xdc\xfb\x40\x5a\xf1\x97\x4e\xf1\xd7\x34\x8a\xd9\x06\x3f\xa9\xe4\x3b\xb6\x48\xb1\xa2\xfa\x12\xc6\x8e\x18\x43\x62\x2c\x89\x31\x25\xc6\x96\x29\xe3\x4f\x6f\x26\xc5\xaa\xf1\x62\x58\x8c\x6d\x31\xc6\x4d\x26\xbf\x7f\x3d\x60\xcc\xac\xce\xa3\x18\x1a\x63\x69\x8c\xa9\xb9\x5b\xc6\xd8\x14\x7f\xe7\xc4\xc4\xdf\x39\xb2\x0e\x63\x73\x8c\x91\x93\xc5\xf0\xc4\x47\x49\x2a\x9f\x30\x68\x0a\xf3\x15\x65\x31\x6b\xaf\x7e\xd3\x3a\x53\xd4\x5b\x67\x8c\x44\xe7\x0f\x49\xc9\xcf\x30\xad\xd6\x59\x27\xa9\xfc\x58\x5d\xf4\x69\x14\x2f\x8d\x95\x67\xab\x51\x9f\xcf\xa6\x5b\x67\xbd\xb4\xe4\x7f\xde\x1f\x7b\x6d\x1c\x66\x2a\x1c\x66\x0c\xe9\x99\xc3\x8c\x4d\xb7\x28\xcf\xa2\x4c\x8b\xd4\x74\xa8\xc5\xa2\x1e\x8b\x02\x16\x99\x8c\xa9\xc0\x48\x90\x6a\xcb\x9d\xc9\x18\x9b\xcd\x18\xfb\xfb\xd8\x3c\xc5\xc3\x9f\xb5\x56\xee\x3e\x77\x9f\x3f\xcc\xc7\xda\x1f\x1f\x16\x5c\x7d\x5b\x70\x75\xa1\xe0\x6a\xc1\xef\x89\x6c\xde\xff\x2a\xb8\xfa\x37\x82\x2b\xb3\x05\x57\x72\x7a\x35\xc6\x0c\x2d\x79\x3e\x29\x0d\xfc\xdf\x16\x5c\x0d\x0a\xae\xc2\x1d\x22\x53\x70\xb5\x53\x70\xf5\x20\xed\xeb\x5c\xc9\xc7\x58\xc3\x48\x33\x3f\x97\x18\xbf\x42\xb1\x57\x5a\x84\x6d\x07\x60\x4a\xd2\x37\x6e\x5b\x97\xfd\x1b\xf5\x56\x2a\xb8\xfa\x24\xec\x67\x4c\xe7\xc3\x1b\x83\x83\x5f\x73\x05\x31\xc6\x0b\xd6\xbe\x45\xe7\xf2\x64\x14\x3c\xbb\x08\x8c\x8a\xa7\xfa\x71\x21\xc6\xc2\x1c\x30\x0e\x7e\x0d\x02\x27\xfe\x81\x78\x20\xe1\x3b\xd6\xc9\x78\x84\x39\xd8\xf5\xe2\x29\x60\x94\xcd\x04\xe1\xcd\x8a\x1d\x47\xa3\xe0\xea\x0b\x7e\xce\x5c\x7a\x8a\xfc\x64\x7c\xfc\x2c\x48\xf1\x75\xb8\x6f\x48\x9c\x37\xf8\x31\xdb\x4a\x64\x9c\x84\xb2\xbd\x19\xe0\x3f\xf2\x17\x60\xb6\xee\x85\x68\xe0\x26\x40\x34\x12\x13\x20\x46\xa8\x0e\xbf\x61\x1b\x6c\x8b\x7d\xf4\xe2\xc9\x10\x6a\x58\x0b\xe1\xcb\x5b\xc1\xa8\xfc\x46\xff\x9c\x70\xb5\x0d\xc7\xd0\xa3\x65\x33\x91\xe6\x3c\x0c\x17\x7f\xa4\xeb\x14\xc5\xb4\xc2\xc3\xe8\x3c\x43\xb1\x65\xb0\x3b\xe5\x98\xb1\x0d\xb6\xc5\x3e\xf2\x7c\x30\x13\x22\xdd\x35\x10\xf5\x5f\xa7\x73\xa9\x28\xbc\xc7\x1e\x43\x93\xe0\xca\x53\x14\x73\x16\xa6\x8e\x89\x86\x83\x9f\xce\x41\x55\xdf\x23\xec\x62\x77\x2e\x9d\xc7\xed\xb3\xa8\x1c\x5c\x10\xa2\xa2\x05\x22\xb7\x3e\x21\xc2\x77\xac\xeb\xff\x1e\xa2\x3e\xd8\x17\x79\x20\xaf\x68\xa8\x87\xda\x84\xea\x97\xcb\xbc\xb3\x1c\x43\xa5\xe0\xea\xe4\x74\x62\xba\xe1\xe0\x0f\x35\xac\x91\x36\xc0\xdd\x10\xac\x7b\xbb\x1f\x7b\x34\x0c\x66\x47\x05\xdd\x3d\xe9\xa5\x8f\x80\x5e\x34\x51\x52\xe9\x23\xf2\x3e\xaa\xa3\x82\xda\xd8\x63\x08\xd6\xfe\x52\xc6\xee\xde\x0c\xe2\x69\x8f\x1d\xcf\x4d\x92\xbf\x1a\x45\x7f\xeb\xf3\x32\x57\x5f\x0a\x3b\x4a\x17\x3f\xea\xd2\x28\x9f\x25\xf5\x76\xe4\x3b\xfd\x79\xad\x70\x1f\x84\xce\xff\x17\xe8\x7b\x26\x59\xfe\xc6\xca\xdb\xdb\x3e\x46\x63\xf4\x0d\xdb\xd8\xbc\xe9\x0c\x78\xe4\x79\xe2\x85\x3c\x69\x9e\x68\x7e\x7b\x21\x50\xfd\x63\x7b\x4d\x5f\x13\x5c\xfd\x7a\xca\x73\x45\x9a\xf8\x69\x7e\xad\xb5\x67\xde\xa8\xea\xd7\x59\xdd\x02\x79\x0f\xc1\xa5\x1f\xe9\xd9\x39\x06\x6e\x6e\xcd\x22\xc2\x77\xb9\x36\x5d\xd4\x06\xdb\xda\xf6\x64\xde\x38\x42\xbc\x90\x27\xf2\x76\x4c\xac\xbb\x86\x72\x44\xd6\x18\xde\xd3\xb9\xcb\x9d\xcc\x1f\xa5\x83\x9f\xf2\x5c\x15\x73\x48\x5f\x81\x4f\x5f\x75\xce\xa8\xe1\xcb\x5b\x1c\x9b\x45\xac\x57\xde\xcd\xa3\x5c\x66\xcd\xd2\x29\x44\xf8\x8e\x75\x72\x1c\x0a\xb5\xc5\x3e\x92\xa9\x49\xbc\x68\x0e\x2a\xe6\x0c\x38\x5b\x87\x2e\x2c\xb1\xce\x86\x6a\xbb\xe0\xea\x57\x93\x9f\x2d\x53\xe3\xa7\x5c\xcf\xae\x6c\xb2\x03\xb3\xb3\x5a\xea\xa9\xaf\x51\x9e\x5d\x35\x06\xdd\xdb\x33\x9c\x9c\x53\x3c\xc2\x6f\xd8\x86\xfc\x4e\xf9\x57\x9c\xfc\x9f\xd9\xf9\xb1\xb4\xbb\xc2\x6c\x30\xdb\xcb\xfb\xf5\xa5\x5f\x95\x79\x41\xb9\x67\x2c\x12\xdc\x9d\xf0\xce\x2a\x1d\xfc\xe4\xdf\xc8\x5f\x7c\x97\x72\xf1\x54\x57\xf3\xef\x64\x17\xbd\x1e\x37\xe5\x6c\x12\x61\xb7\x09\xdb\x60\x5b\xec\x83\x7d\xe5\x00\x0c\xe2\x89\xbc\x51\xc6\x00\x99\x75\x0b\x6d\x1b\x3a\x2e\xb8\x32\x61\xa4\xf8\xf1\x37\xe5\xe9\x35\x06\xa1\x86\x55\x96\x7e\x5a\xc0\x28\x9d\x49\x75\xad\xef\x8d\x1b\x82\xb5\x76\xd9\xc0\x7b\x00\x9b\xb0\xad\xed\xfb\xa3\xfa\x15\x69\x2b\x17\x57\x51\x1d\xca\x88\x95\x8d\xfe\x57\xde\x9f\x29\x3e\xc1\xd5\x6f\x26\xce\x8f\x24\xc7\x1f\xf1\x35\x80\x5e\xf2\x00\xe8\xbb\xf3\x21\x72\xf3\x98\x65\xf7\x1f\x80\xf0\x8e\x81\x9e\x1d\x63\xc8\xc6\x6d\x7c\x35\x4b\xa7\x92\xbd\xdf\xda\x96\x09\x5d\xdb\x33\xa1\x6d\x43\x8e\x73\xbf\x60\xe7\xf9\xb1\x0f\xfa\x48\xdc\x7b\x89\xff\xcd\x6a\xe2\x4d\x79\x65\x5f\xc3\x40\xbd\xfd\xee\x5b\xb6\x0d\x51\x7e\x3b\xde\x3a\x4e\x85\xdf\xec\x38\x44\xb6\x6f\x94\x3d\x46\xff\x67\xc0\x27\x70\xf2\x65\xe2\x7b\xed\xfd\x9c\x98\xbc\xe7\x54\xa9\x5f\x27\x16\x50\x40\xe7\x2a\x5c\xdf\x9c\x4d\xf7\x32\x76\x3b\xec\x83\x7d\x03\x9f\xfc\xa3\xc4\xe9\x6f\x27\xde\x28\x03\x65\x0d\xb0\xa1\x9a\xff\xb0\xf1\x6f\xd3\xb9\x2b\xee\x9d\x69\x2a\xfc\xa4\x6b\x9c\xdf\xc3\xcf\x01\x44\x02\x94\xcb\x35\x2a\x9f\xa6\xba\xa6\xb5\x13\x9c\x7c\x29\xae\x51\x9f\x47\xe6\x93\xf4\xc2\x6c\xd0\x77\x8f\x73\xe2\xcd\xcb\xeb\xf3\x9d\xfc\x2a\xf6\x21\x7e\x87\x9e\x96\x79\xe1\x48\x80\x78\x63\x1d\xca\x1a\x20\xfb\x2a\xb7\xf7\xb3\x6a\xc1\x95\xdc\x91\xe0\x0f\x5d\x58\x26\xfd\xe6\xf1\x97\xa4\xbe\xc4\x65\xca\x5d\xf5\xee\x54\x29\xe7\x6d\xeb\xb5\x0d\xf5\xca\x5d\x60\xec\x9d\x4a\x77\x30\x91\xda\x37\xc0\x5f\x3e\x93\xc6\xd3\xf9\xc1\x58\xb2\x2d\x3b\x4f\xee\xdb\xa9\x12\x0f\xe4\x45\xf3\x79\xfc\x25\x92\x81\xb2\x06\xd8\xee\xcd\x13\xf2\xce\x98\x2b\x4d\x82\xab\x0f\x26\xc9\x91\x3e\x97\x08\x3f\xed\x4f\x3b\x19\x04\x4f\xbd\x2e\x79\xf6\xd4\xd2\xbe\xd3\xbd\x7d\x0c\x9c\xb5\x6c\x1b\xb1\xe1\x7e\xa5\x7b\x55\x08\x1d\xfb\x11\x40\xf3\x5a\x22\xf3\xf4\xcf\x69\x2e\xd0\xff\xa3\xed\x23\x7e\xec\xd3\xbd\xdd\x4d\x31\x74\xa4\xa7\x4e\xca\x38\xf5\xba\x94\x81\xfb\x5b\xac\xef\x10\xcd\x74\xf7\x64\xdd\x79\xcf\x4a\x82\xff\x5b\x42\x63\x86\x71\xf0\xc9\x21\xb1\x24\xf9\x4e\xe4\x5d\x3b\xdf\xd2\xc9\x71\xd0\x8b\xf2\xa1\x6b\x5b\x86\xe3\x63\xb0\xec\xda\x9e\x41\xf7\x6f\x66\xcd\xeb\x00\xcd\x6b\x00\x9a\x56\x43\xb4\x7e\x31\x18\x7b\x27\x83\xcf\x23\xe7\x0a\xf1\x3b\x6d\x8b\xf2\xe9\xff\x3a\x24\xa3\x76\xbe\x94\x31\xc8\x87\x62\x6c\x4a\x31\x8b\xc6\xfa\x04\x57\x9f\x4e\x82\xff\x69\x6c\x43\xb1\x88\xff\xfa\xc8\xf0\x6f\xcb\x00\x7d\x57\x16\x98\x67\x7e\xde\x8f\xff\x42\x01\x18\xc5\x93\x08\xff\xf9\x91\xe0\x0f\x76\x03\xea\x14\x75\x4b\x3a\x4e\x8c\x7f\x16\xe5\x96\xf7\xcd\xa0\x39\x4b\x6e\x3f\x75\x34\xf7\x68\x03\xb6\xfd\xa0\xef\xb9\xb1\x85\xee\xd3\x21\x78\xe4\xcf\xe8\x0e\x10\xed\x27\x7c\xe2\x45\xd0\xbd\x19\xd0\xbd\x23\x83\xda\x0e\xdb\x7e\xc2\x7d\x74\x67\x81\xb6\x4d\x36\x9e\x18\xff\x83\xb8\x46\xf4\xa2\x09\x74\xdf\x31\x92\xf5\x7b\xf5\xb7\xb9\x96\xef\xc9\x81\x60\xd5\x73\x10\xaa\xfe\x3e\xe8\xc5\xf7\x82\xce\x15\xba\xd3\xc2\x31\x9e\xfa\xb5\xbd\x7e\x95\xb4\xd6\xef\x20\xfc\xdf\x8e\x8f\x9f\x72\xeb\xb9\xe4\xa3\x70\x5f\xb9\xca\x07\xf0\x48\xd7\x7f\x9e\x5b\x81\x7b\x53\x46\x7f\xec\x6c\x91\x4f\xeb\x8f\x2f\xb0\x6d\xe3\x30\xfc\x67\xba\xf8\xe5\xde\xa0\x6e\x47\x1e\xc1\x9a\xf9\x03\x78\xa4\xbb\x7f\x21\x35\xad\x1b\x0f\xbd\x9e\x31\xd6\x3d\xa6\x0a\x7d\x9a\x9b\xe6\x05\x75\x3f\x64\xff\x3a\xf9\xb2\xb5\x46\x13\xef\x5f\xe9\xe0\xd7\xed\xbb\x3e\xdc\xa3\x51\x2f\xbf\xfb\xd3\x81\x71\x48\xdc\xf8\x61\x6b\xdc\xf8\xc1\xde\xc7\x30\x6e\x40\x9c\xa8\xeb\x58\xec\xc3\x89\x1f\xd2\xc5\x1f\xb3\x06\xbe\x29\xb8\xd2\xa7\xef\xb9\x0f\x22\x5d\x9f\x0e\xe0\xe1\xc4\x6f\x17\xd3\x8b\xdf\x12\xd1\x70\xe2\xb7\xe1\xe1\xa7\x35\x30\x81\x62\x55\xcd\x45\xe7\xd0\xd8\xe7\xb3\x88\x9f\x87\x85\x5f\x53\x18\xfd\x3f\x95\xab\x8b\xe5\x19\xe3\x71\x3a\x43\x38\x6b\xe0\xf7\x76\x7e\xa9\x8e\x7b\x7e\x19\x2e\xfe\x18\x1b\xc2\xb3\x5a\x07\x9e\xdd\xf0\x0c\xe7\xf0\xb9\x23\xe7\xc7\x9b\xa3\xc4\x4f\x7e\xc8\x8d\x67\x66\x3a\xa7\xef\x7f\x88\xce\xd2\xf6\x93\xf6\xf9\x7d\x47\xcc\xf9\x7d\x47\xb2\xf3\x7b\x55\xdc\xf3\xfb\x48\xf1\xc7\xcc\xc1\xd7\x28\x77\x81\x3e\xae\xfa\xaf\x29\xa7\x01\x43\xf2\x27\xcf\x0f\xcc\x9f\x9c\xbb\x3d\xf9\x93\xd1\xe2\xef\xe3\x2e\xe6\xe3\x4c\x91\x67\x66\x35\x8a\x7e\x8e\xd6\xb2\xa5\xb3\xd4\xf9\xab\x17\xc1\x28\x7d\x04\xf4\x3d\x13\x89\x8c\x44\xf9\xab\xba\xb7\x87\xe6\xaf\x6e\x03\xfe\x98\x39\x98\x2c\xb8\x7a\xc8\xb6\xd9\x50\xfd\x32\x99\x1b\x0c\xf5\x38\xfe\xe2\xf6\xe5\x0f\x13\xe7\x4e\x47\x84\xbf\x50\xb5\xfd\xe9\x1c\x2b\x97\x4a\xb9\x55\xfa\xdf\x50\xe0\x3a\x44\xba\xcf\xc8\xdc\x37\xda\x8a\x9d\xbf\x4d\x82\xc1\xc1\x62\xe5\x6f\xb1\x0f\xe5\x7e\x4b\x1f\xa5\x5c\x70\xd2\x3e\x23\xc0\x4f\x63\xf0\xba\x58\x8f\x27\x1b\xe7\xe1\x05\x99\xd3\x56\x68\xbe\x8d\xca\x6f\xd0\xbe\x19\x6a\x58\xeb\xac\x3d\x27\x7f\xde\x56\x92\x7e\xfe\xbc\xe4\x41\x30\xdb\xf6\xa5\x1e\xf3\x08\xf1\xe3\xa3\x7b\x15\x66\xec\xa2\xff\x6d\xbc\x60\xdd\x2d\x48\xbc\xbb\xb2\xe8\xbf\x31\x7a\xf1\xd4\x24\xf7\x17\xf3\x89\x12\xdd\x5f\x18\x15\x4f\xd1\x1d\x48\xca\x7b\x92\xda\xb7\xac\xff\xda\xba\x86\x8d\x9f\xc6\xc0\x5d\x0c\xbc\xe4\x5b\xd1\x27\x95\x39\x77\x64\x8e\x8f\x19\xcd\xfd\xd1\xb0\xee\xa9\x46\x84\x1f\x1f\x43\x73\xd9\x6b\x7a\xbc\xbc\x6b\xa3\x3b\xb7\x4e\xeb\x0e\xee\x4e\xdd\xf7\x8d\x18\xbf\x33\x0e\x8f\xc2\x7a\x8b\x54\x8c\x35\x72\x04\x57\x67\x0b\xae\xfe\xc4\xba\x0b\x2d\xb8\x03\xf7\xae\x6f\x5b\x77\xbc\x23\xc6\x7f\xf7\xb9\xfb\x7c\x9e\x1e\xb9\x43\x24\x2e\x5b\x18\x63\xcf\x58\x65\x9e\x55\x66\x5a\xa5\x6b\x50\xc9\xec\xb2\xc0\x2a\x9f\x19\x54\x4e\x4f\x50\xe6\x25\x28\x33\x6f\x5f\xd9\x93\xa0\x0c\x24\x28\xcd\x41\x65\xd4\x2a\xc1\x2e\x17\x0e\x2a\x5b\xac\xb2\xc7\x2a\x4d\xab\x4c\xa1\xdf\xff\x0f\x00\x00\xff\xff\xc6\xb9\x24\x2f\xee\x3a\x00\x00") func faviconIcoBytes() ([]byte, error) { return bindataRead( _faviconIco, "favicon.ico", ) } func faviconIco() (*asset, error) { bytes, err := faviconIcoBytes() if err != nil { return nil, err } info := bindataFileInfo{name: "favicon.ico", size: 15086, mode: os.FileMode(438), modTime: time.Unix(1612640817, 0)} a := &asset{bytes: bytes, info: info} return a, nil } var _jsMainJs = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x4a\xce\xcf\x2b\xce\xcf\x49\xd5\xcb\xc9\x4f\xd7\x50\x4a\xad\x48\xcc\x2d\xc8\x49\x55\xd2\xb4\x06\x04\x00\x00\xff\xff\xc8\x9f\xbd\x5f\x17\x00\x00\x00") func jsMainJsBytes() ([]byte, error) { return bindataRead( _jsMainJs, "js/main.js", ) } func jsMainJs() (*asset, error) { bytes, err := jsMainJsBytes() if err != nil { return nil, err } info := bindataFileInfo{name: "js/main.js", size: 23, mode: os.FileMode(438), modTime: time.Unix(1613718745, 0)} a := &asset{bytes: bytes, info: info} return a, nil } // Asset loads and returns the asset for the given name. // It returns an error if the asset could not be found or // could not be loaded. func Asset(name string) ([]byte, error) { cannonicalName := strings.Replace(name, "\\", "/", -1) if f, ok := _bindata[cannonicalName]; ok { a, err := f() if err != nil { return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err) } return a.bytes, nil } return nil, fmt.Errorf("Asset %s not found", name) } // MustAsset is like Asset but panics when Asset would return an error. // It simplifies safe initialization of global variables. func MustAsset(name string) []byte { a, err := Asset(name) if err != nil { panic("asset: Asset(" + name + "): " + err.Error()) } return a } // AssetInfo loads and returns the asset info for the given name. // It returns an error if the asset could not be found or // could not be loaded. func AssetInfo(name string) (os.FileInfo, error) { cannonicalName := strings.Replace(name, "\\", "/", -1) if f, ok := _bindata[cannonicalName]; ok { a, err := f() if err != nil { return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err) } return a.info, nil } return nil, fmt.Errorf("AssetInfo %s not found", name) } // AssetNames returns the names of the assets. func AssetNames() []string { names := make([]string, 0, len(_bindata)) for name := range _bindata { names = append(names, name) } return names } // _bindata is a table, holding each asset generator, mapped to its name. var _bindata = map[string]func() (*asset, error){ "css/main.css": cssMainCss, "favicon.ico": faviconIco, "js/main.js": jsMainJs, } // AssetDir returns the file names below a certain // directory embedded in the file by go-bindata. // For example if you run go-bindata on data/... and data contains the // following hierarchy: // // data/ // foo.txt // img/ // a.png // b.png // // then AssetDir("data") would return []string{"foo.txt", "img"} // AssetDir("data/img") would return []string{"a.png", "b.png"} // AssetDir("foo.txt") and AssetDir("notexist") would return an error // AssetDir("") will return []string{"data"}. func AssetDir(name string) ([]string, error) { node := _bintree if len(name) != 0 { cannonicalName := strings.Replace(name, "\\", "/", -1) pathList := strings.Split(cannonicalName, "/") for _, p := range pathList { node = node.Children[p] if node == nil { return nil, fmt.Errorf("Asset %s not found", name) } } } if node.Func != nil { return nil, fmt.Errorf("Asset %s not found", name) } rv := make([]string, 0, len(node.Children)) for childName := range node.Children { rv = append(rv, childName) } return rv, nil } type bintree struct { Func func() (*asset, error) Children map[string]*bintree } var _bintree = &bintree{nil, map[string]*bintree{ "css": {nil, map[string]*bintree{ "main.css": {cssMainCss, map[string]*bintree{}}, }}, "favicon.ico": {faviconIco, map[string]*bintree{}}, "js": {nil, map[string]*bintree{ "main.js": {jsMainJs, map[string]*bintree{}}, }}, }} // RestoreAsset restores an asset under the given directory func RestoreAsset(dir, name string) error { data, err := Asset(name) if err != nil { return err } info, err := AssetInfo(name) if err != nil { return err } err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755)) if err != nil { return err } err = os.WriteFile(_filePath(dir, name), data, info.Mode()) if err != nil { return err } err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime()) if err != nil { return err } return nil } // RestoreAssets restores an asset under the given directory recursively func RestoreAssets(dir, name string) error { children, err := AssetDir(name) // File if err != nil { return RestoreAsset(dir, name) } // Dir for _, child := range children { err = RestoreAssets(dir, filepath.Join(name, child)) if err != nil { return err } } return nil } func _filePath(dir, name string) string { cannonicalName := strings.Replace(name, "\\", "/", -1) return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...) } ================================================ FILE: _examples/file-server/embedding-gzipped-files-into-app-bindata/main.go ================================================ package main import ( "github.com/kataras/iris/v12" ) // How to run: // // $ go install github.com/go-bindata/go-bindata/v3/go-bindata@latest // $ go-bindata -prefix "../embedding-files-into-app-bindata/assets/" -fs ../embedding-files-into-app-bindata/assets/... // $ go run -mod=mod . // Time to complete the compression and caching of [2/3] files: 31.9998ms // Total size reduced from 156.6 kB to: // br (22.9 kB) [85.37%] // snappy (41.7 kB) [73.37%] // gzip (27.9 kB) [82.16%] // deflate (27.9 kB) [82.19%] var dirOptions = iris.DirOptions{ IndexName: "index.html", // The `Compress` field is ignored // when the file is cached (when Cache.Enable is true), // because the cache file has a map of pre-compressed contents for each encoding // that is served based on client's accept-encoding. Compress: true, // true or false does not matter here. Cache: iris.DirCacheOptions{ Enable: true, CompressIgnore: iris.MatchImagesAssets, // Here, define the encodings that the cached files should be pre-compressed // and served based on client's needs. Encodings: []string{"gzip", "deflate", "br", "snappy"}, CompressMinSize: 50, // files smaller than this size will NOT be compressed. Verbose: 1, }, } func newApp() *iris.Application { app := iris.New() app.HandleDir("/static", AssetFile(), dirOptions) return app } func main() { app := newApp() // http://localhost:8080/static/css/main.css // http://localhost:8080/static/js/main.js // http://localhost:8080/static/favicon.ico app.Listen(":8080") } ================================================ FILE: _examples/file-server/embedding-gzipped-files-into-app-bindata/main_test.go ================================================ package main import ( "bytes" "os" "path/filepath" "runtime" "strings" "testing" "github.com/kataras/iris/v12/httptest" "github.com/klauspost/compress/gzip" ) type resource string // content types that are used in the ./assets, // we could use the detectContentType that iris do but it's better // to do it manually so we can test if that returns the correct result on embedding files. func (r resource) contentType() string { switch filepath.Ext(r.String()) { case ".js": return "text/javascript" case ".css": return "text/css" case ".ico": return "image/x-icon" case ".html": return "text/html" default: return "text/plain" } } func (r resource) String() string { return string(r) } func (r resource) strip(strip string) string { s := r.String() return strings.TrimPrefix(s, strip) } func (r resource) loadFromBase(dir string) string { filename := r.String() filename = r.strip("/static") fullpath := filepath.Join(dir, filename) b, err := os.ReadFile(fullpath) if err != nil { panic(fullpath + " failed with error: " + err.Error()) } result := string(b) if runtime.GOOS != "windows" { result = strings.ReplaceAll(result, "\n", "\r\n") result = strings.ReplaceAll(result, "\r\r", "") } return result } var urls = []resource{ "/static/css/main.css", "/static/js/main.js", "/static/favicon.ico", } // if bindata's values matches with the assets/... contents // and secondly if the HandleDir had successfully registered // the routes and gave the correct response. func TestEmbeddingGzipFilesIntoApp(t *testing.T) { dirOptions.Cache.Verbose = 0 app := newApp() e := httptest.New(t, app) if runtime.GOOS != "windows" { // remove the embedded static favicon for !windows, // it should be built for unix-specific in order to be work urls = urls[0 : len(urls)-1] } for i, u := range urls { url := u.String() rawContents := u.loadFromBase("../embedding-files-into-app-bindata/assets") shouldBeCompressed := int64(len(rawContents)) >= dirOptions.Cache.CompressMinSize request := e.GET(url) if shouldBeCompressed { request.WithHeader("Accept-Encoding", "gzip") } response := request.Expect() response.ContentType(u.contentType(), app.ConfigurationReadOnly().GetCharset()) if shouldBeCompressed { response.ContentEncoding("gzip") } if expected, got := response.Raw().StatusCode, httptest.StatusOK; expected != got { t.Fatalf("[%d] of '%s': expected %d status code but got %d", i, url, expected, got) } rawBody := response.Body().Raw() if shouldBeCompressed { reader, err := gzip.NewReader(strings.NewReader(rawBody)) defer reader.Close() if err != nil { t.Fatalf("[%d] of '%s': %v", i, url, err) } buf := new(bytes.Buffer) reader.WriteTo(buf) if expected, got := rawContents, buf.String(); expected != got { // t.Fatalf("[%d] of '%s': expected body:\n%s but got:\n%s", i, url, expected, got) // let's reduce the output here... // they are big files, no need to check for length here. t.Fatalf("[%d] %s, expected body to look like: '%s...%s' but got '%s...%s'", i, url, expected[:40], expected[len(rawContents)-40:], got[:40], got[len(got)-40:]) } } else { if expected, got := rawContents, rawBody; expected != got { t.Fatalf("[%d] %s, expected body to look like: '%s...%s' but got '%s...%s'", i, url, expected[:40], expected[len(rawContents)-40:], got[:40], got[len(got)-40:]) } } } } ================================================ FILE: _examples/file-server/favicon/main.go ================================================ package main import ( "github.com/kataras/iris/v12" ) func main() { app := iris.New() // This will serve the ./static/favicons/favicon.ico to: localhost:8080/favicon.ico app.Favicon("./static/favicons/favicon.ico") // app.Favicon("./static/favicons/favicon.\\.ico", "/favicon_16_16.ico") // This will serve the ./static/favicons/favicon.ico to: localhost:8080/favicon_16_16.ico app.Get("/", func(ctx iris.Context) { ctx.HTML(` press here to see the favicon.ico. At some browsers like chrome, it should be visible at the top-left side of the browser's window, because some browsers make requests to the /favicon.ico automatically, so iris serves your favicon in that path too (you can change it).`) }) // if favicon doesn't show to you, try to clear your browser's cache. app.Listen(":8080") } ================================================ FILE: _examples/file-server/file-server/main.go ================================================ package main import ( "crypto/md5" "fmt" "io" "mime/multipart" "os" "path" "strconv" "strings" "time" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/middleware/basicauth" ) func init() { os.Mkdir("./uploads", 0700) } const ( maxSize = 1 * iris.GB uploadDir = "./uploads" ) func main() { app := iris.New() view := iris.HTML("./views", ".html") view.AddFunc("formatBytes", func(b int64) string { const unit = 1000 if b < unit { return fmt.Sprintf("%d B", b) } div, exp := int64(unit), 0 for n := b / unit; n >= unit; n /= unit { div *= unit exp++ } return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "kMGTPE"[exp]) }) app.RegisterView(view) // Serve assets (e.g. javascript, css). // app.HandleDir("/public", iris.Dir("./public")) app.Get("/", index) app.Get("/upload", uploadView) app.Post("/upload", upload) filesRouter := app.Party("/files") { filesRouter.HandleDir("/", iris.Dir(uploadDir), iris.DirOptions{ Compress: true, ShowList: true, // Optionally, force-send files to the client inside of showing to the browser. Attachments: iris.Attachments{ Enable: true, // Optionally, control data sent per second: Limit: 50.0 * iris.KB, Burst: 100 * iris.KB, // Change the destination name through: // NameFunc: func(systemName string) string {...} }, DirList: iris.DirListRich(iris.DirListRichOptions{ // Optionally, use a custom template for listing: // Tmpl: dirListRichTemplate, TmplName: "dirlist.html", }), }) auth := basicauth.Default(map[string]string{ "myusername": "mypassword", }) filesRouter.Delete("/{file:path}", auth, deleteFile) } app.Listen(":8080") } func index(ctx iris.Context) { ctx.Redirect("/upload") } func uploadView(ctx iris.Context) { now := time.Now().Unix() h := md5.New() io.WriteString(h, strconv.FormatInt(now, 10)) token := fmt.Sprintf("%x", h.Sum(nil)) if err := ctx.View("upload.html", token); err != nil { ctx.HTML(fmt.Sprintf("

%s

", err.Error())) return } } func upload(ctx iris.Context) { ctx.SetMaxRequestBodySize(maxSize) _, _, err := ctx.UploadFormFiles(uploadDir, beforeSave) if err != nil { ctx.StopWithError(iris.StatusRequestEntityTooLarge, err) return } ctx.Redirect("/files") } func beforeSave(ctx iris.Context, file *multipart.FileHeader) bool { ip := ctx.RemoteAddr() ip = strings.ReplaceAll(ip, ".", "_") ip = strings.ReplaceAll(ip, ":", "_") file.Filename = ip + "-" + file.Filename return true } func deleteFile(ctx iris.Context) { // It does not contain the system path, // as we are not exposing it to the user. fileName := ctx.Params().Get("file") filePath := path.Join(uploadDir, fileName) if err := os.RemoveAll(filePath); err != nil { ctx.StopWithError(iris.StatusInternalServerError, err) return } ctx.Redirect("/files") } ================================================ FILE: _examples/file-server/file-server/views/dirlist.html ================================================ {{.Title}} {{ range $idx, $file := .Files }} {{ if $file.Download }} {{ else }} {{ end }} {{ if $file.Info.IsDir }} {{ else }} {{ end }} {{ end }}
# Name Size Actions
{{ $idx }}{{ $file.Name }}{{ $file.Name }}Dir{{ formatBytes $file.Info.Size }}
================================================ FILE: _examples/file-server/file-server/views/upload.html ================================================ Upload Files
================================================ FILE: _examples/file-server/http2push/assets/app2/app2app3/css/main.css ================================================ body { background-color: blue; } ================================================ FILE: _examples/file-server/http2push/assets/app2/app2app3/dirs/dir1/text.txt ================================================ app2/app2app3/dirs/dir1/text.txt ================================================ FILE: _examples/file-server/http2push/assets/app2/app2app3/dirs/dir2/text.txt ================================================ app2/app2app3/dirs/dir2/text.txt ================================================ FILE: _examples/file-server/http2push/assets/app2/app2app3/dirs/text.txt ================================================ app2/app2app3/dirs/text.txt ================================================ FILE: _examples/file-server/http2push/assets/app2/app2app3/index.html ================================================ App2App3

Hello App2App3 index

================================================ FILE: _examples/file-server/http2push/assets/app2/index.html ================================================

Hello App2 index

================================================ FILE: _examples/file-server/http2push/assets/app2/mydir/text.txt ================================================ just a text. ================================================ FILE: _examples/file-server/http2push/assets/css/main.css ================================================ body { background-color: black; } ================================================ FILE: _examples/file-server/http2push/assets/index.html ================================================ File Server ================================================ FILE: _examples/file-server/http2push/assets/js/main.js ================================================ console.log("example"); function onClick() { window.alert("button clicked"); } ================================================ FILE: _examples/file-server/http2push/main.go ================================================ package main import ( "regexp" "github.com/kataras/iris/v12" ) var opts = iris.DirOptions{ IndexName: "index.html", // Optionally register files (map's values) to be served // when a specific path (map's key WITHOUT prefix) is requested // is fired before client asks (HTTP/2 Push). // E.g. "/" (which serves the `IndexName` if not empty). // // Note: Requires running server under TLS, // that's why we use `iris.TLS` below. // PushTargets: map[string][]string{ // "/": { // Relative path without prefix. // "favicon.ico", // "js/main.js", // "css/main.css", // // ^ Relative to the index, if need absolute ones start with a slash ('/'). // }, // }, // OR use regexp: PushTargetsRegexp: map[string]*regexp.Regexp{ // Match all js, css and ico files // from all files (recursively). // "/": regexp.MustCompile("((.*).js|(.*).css|(.*).ico)$"), // OR: "/": iris.MatchCommonAssets, "/app2/app2app3": iris.MatchCommonAssets, }, Compress: true, ShowList: true, } func main() { app := iris.New() app.HandleDir("/public", iris.Dir("./assets"), opts) // Open your browser's Network tools, // navigate to https://127.0.0.1/public. // you should see `Initiator` tab: "Push / public". // // https://127.0.0.1/public // https://127.0.0.1/public/app2 // https://127.0.0.1/public/app2/app2app3 // https://127.0.0.1/public/app2/app2app3/dirs app.Run(iris.TLS(":443", "mycert.crt", "mykey.key")) } ================================================ FILE: _examples/file-server/http2push/mycert.crt ================================================ -----BEGIN CERTIFICATE----- MIIFazCCA1OgAwIBAgIUfwMd9auWixp19UnXOmyxJ9Jkv7IwDQYJKoZIhvcNAQEL BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMDA2MjUwOTUxNDdaFw0yMTA2 MjUwOTUxNDdaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggIiMA0GCSqGSIb3DQEB AQUAA4ICDwAwggIKAoICAQDlVGyGAQ9uyfNbwZyrtYOSjLpxf5NpNToh2OzU7gy2 OexBji5lmWBQ3oYDG+FjAkbHORPzOMNpeMwje+IjGZBw8x6E+8WoGdSzbrEZ6pUV wKJGKEuDlx6g6HEmtv3ZwgGe20gvPjjW+oCO888dwK/mbIHrHTq4nO3o0gAdAJwu amn9BlHU5O4RW7BQ4tLF+j/fBCACWRG1NHXA0AT8eg544GyCdyteAH11oCDsHS8/ DAPsM6t+tZrMCIt9+9dzPdVoOmQNaMMrcz8eJohddRTK6zHe9ixZTt/soayOF7OS QQeekbr3HPYhD450zRVplLMHx7wnph/+O+Po6bqDnUzdnkqAAwwymQapHMuHXZKN rhdfKau3rVo1GeXLIRgeWLUoxFSm4TYshrgt+0AidLRH+dCY7MS9Ngga/sAK3vID gSF75mFgOhY+q7nvY9Ecao6TnoNNRY29hUat4y0VwSyysUy887vHr6lMK5CrAT/l Ch8fuu20HUCoiLwMJvA6+wpivZkuiIvWY7bVGYsEYrrW+bCNN9wCGYTZEyX++os9 v/38wdOqGUT00ewXkjIUFCWbrnxxSr98kF3w3wPf9K4Y40MNxeR90nyX4zjXGF1/ 91msUh+iivsz9mcN9DK83fgTyOsoVLX5cm/L2UBwMacsfjBbN4djOc5IuYMar/VN GQIDAQABo1MwUTAdBgNVHQ4EFgQUtkf+yAvqgZC8f22iJny9hFEDolMwHwYDVR0j BBgwFoAUtkf+yAvqgZC8f22iJny9hFEDolMwDwYDVR0TAQH/BAUwAwEB/zANBgkq hkiG9w0BAQsFAAOCAgEAE2QasBVru618rxupyJgEHw6r4iv7sz1Afz3Q5qJ4oSA9 xVsrVCjr3iHRFSw8Rf670E8Ffk/JjzS65mHw6zeZj/ANBKQWLjRlqzYXeetq5HzG SIgaG7p1RFvvzz3+leFGzjinZ6sKbfB4OB72o2YN+fO8DsDxgGKll0W4KAazizSe HY9Pgu437tWnwF16rFO3IL47n5HzYlRoGIPOpzFoNX5+fyn9GlnKEtONF2QBKTjY rdjvqFRByDiC74d8z/Yx8IiDRn1mTcG90JLR9+c6M7fruha9Y/rJfw+4AhVh5ZDz Bl9rGPjwEs5zwutYvVAJzs7AVcighYP1lHKoJ7DxBDQeyBsYlUNk2l6bmZgLgGUZ +2OyWlqc/jD2GdDsIaZ4i7QqhTI/6aYZIf5zUkblKV1aMSaDulKxRv//OwW28Jax 9EEoV7VaFb3sOkB/tZGhusXeQVtdrhahT3KkZLNwmNXoXWKJ5LjeUlFWJyV6JbDe y/PIWWCwWqyuFCSZS+Cg3RDgAzfSxkI8uVZ+IKKJS3UluDX45lxXtbRrvTQ+oDrA 6ga5c1Vz9C4kn1K5yW4d7QIvg6vPiy7gvl+//sz9oxUM3yswInDBY0HKLgT0Uq9b YzLDh2RSaHsgHMPy2BKqR+q2N+lpg7inAWuJM1Huq6eHFqhiyQkzsfscBd1Dpm8= -----END CERTIFICATE----- ================================================ FILE: _examples/file-server/http2push/mykey.key ================================================ -----BEGIN PRIVATE KEY----- MIIJRAIBADANBgkqhkiG9w0BAQEFAASCCS4wggkqAgEAAoICAQDlVGyGAQ9uyfNb wZyrtYOSjLpxf5NpNToh2OzU7gy2OexBji5lmWBQ3oYDG+FjAkbHORPzOMNpeMwj e+IjGZBw8x6E+8WoGdSzbrEZ6pUVwKJGKEuDlx6g6HEmtv3ZwgGe20gvPjjW+oCO 888dwK/mbIHrHTq4nO3o0gAdAJwuamn9BlHU5O4RW7BQ4tLF+j/fBCACWRG1NHXA 0AT8eg544GyCdyteAH11oCDsHS8/DAPsM6t+tZrMCIt9+9dzPdVoOmQNaMMrcz8e JohddRTK6zHe9ixZTt/soayOF7OSQQeekbr3HPYhD450zRVplLMHx7wnph/+O+Po 6bqDnUzdnkqAAwwymQapHMuHXZKNrhdfKau3rVo1GeXLIRgeWLUoxFSm4TYshrgt +0AidLRH+dCY7MS9Ngga/sAK3vIDgSF75mFgOhY+q7nvY9Ecao6TnoNNRY29hUat 4y0VwSyysUy887vHr6lMK5CrAT/lCh8fuu20HUCoiLwMJvA6+wpivZkuiIvWY7bV GYsEYrrW+bCNN9wCGYTZEyX++os9v/38wdOqGUT00ewXkjIUFCWbrnxxSr98kF3w 3wPf9K4Y40MNxeR90nyX4zjXGF1/91msUh+iivsz9mcN9DK83fgTyOsoVLX5cm/L 2UBwMacsfjBbN4djOc5IuYMar/VNGQIDAQABAoICAQCtWx1SSxjkcerxsLEDKApW zOTfiUXgoOjZz0ZwS6b2VWDfyWAPU1r4ps39KaU+F+lzDhWjpYQqhbMjG7G9QMTs bQvkEQLAaQ5duU5NPgQG1oCUsj8rMSBpGGz4jBnm834QHMk7VTjYYbKu3WTyo8cU U2/+UDEkfxRlC+IkCmMFv1FxgMZ5PbktC/eDnYMhP2Pq7Q5ZWAVHymk9IMK0LHwm Kdg842K4A3zTXwGkGwetDCMm+YQpG5TxqX/w82BRcCuTR5h8fnYSsWLEIvKwWyIl ppcjaUnrFPG2yhxLqWUIKPpehuEjjhQMt9rDNoh6MHsJZZY5Dp5eq91EIvLoLQ99 hXBmD4P8LDop4r0jniPZJi/ACsaD0jBooA4525+Kouq7RP28Jp/pek7lVOOcBgRv D3zyESbKfqoaOfyfQ2ff4sILnTAr4V2nq3ekphGEYJrWN0ZoADcLdnr1cZ8L+VBI o/4mi5/3HID/UEDliHSa97hxxGBEqTto0ZuXuNwfwx5ho33uVT6zNwRgiJ62Bgu3 Fhk/wVGuZxWvb1KHUNInG9cvsslhO4Vu9wJvYj91BnRq36rsyKKid5DrU+PNgmog lw3IXQpTojyRCYPuG9TKqEZ6b+so7GTKhBOjiwaupMOletVRGSAdbE81VN6HtxNW aj39+FnxzMAlsieib+PBAQKCAQEA+t1fOYSaZBo7pZUmo2S0zulUEJjrYRGKJlWJ 4psWSwFu/7/3UL4q0RBQaSRew9u/YSpaNlBYfcpnFVOjiLwHq5Hx46Eq0BuKsNlJ 1/qxw9qjHqcrOre6K4/7NaWLPuM9fEmV+3MhFVXgv+WC5BHOowRTlOG30vIcC1J2 L5xsBUsxDDY13cD1bLKRmFcyMFM8y7wMZmo7H/WfVmyoPKQaC43pTcmIXH0Jr2Ws Wsfh18mhjtamaOPEFx5K0x4d0PI8tW5ouiUUkVIDaue27XfS969qEChv768/44eX WeqcekaG9jv2noMClt79rYd3Lne9HkgY6IT9FT+JqXfu+KYwuQKCAQEA6gYzUsGB 9GQO8DE8AYn7JwNOtg1X4zKakXiGxH+nuZb7wJjAeGdYqTHySxPBXg0A2nDwoyz5 4sAdLAr3FZoIvTzo7M5KIKFDzfyDmQDavhroH1mBAEiqKGNniP+RND3nWBBqDK1R qcqbhI3Kj5Ycany6a4nP+hZRBIyT9sfJ0S0YruSY8IGXgDwhlJrZ7bsWMZylrgD/ 1qnPL0KqVBY8YR8msRj88h72IlD5o0kwvisOIvyhA0YgwGBb6lg7A+DifiF03ZlS 2yELbIkKDVr+p3jC7MBh4B+OJY68AMl6wVjAaDM1AZnpjKE5YmZg5+Ks5823zILo PrSB9hn0+DIPYQKCAQEAh9x+JuNmzhHa/dkiHNl8hpadHYQD7gUWwZ4P1/bQAv0a xU2MvmDPRXxFYDv/SqlnI1NRmhq3YiDM5SLv7SyQJt4al4IAcsaHvTFgqaSuw3hU YVR9uAYqwE7w6OPn3r4o3Xfoz05Ru4FP//1nfucZ9vVv4rC/4nGWuJcHRM+9PLy1 KnztfVR0VlL7QPrwRnW99kS4nnqn3K4khiTAlF73cAyCLsuXmydoqGIzDtMzv68G XRpo82NvHmoccevcj/2w3T2XYECWvAEjsrEdQ8xiKBwLIAcWYEOUIUCcumiyKBKs IwzkioI/U8AeuO0lobfdZ1n6i2sCuZA4mNxIQseWmQKCAQEA5YkfXdQeuq5JWJ1x 1bCYfjNoSHfd9CH2KSimRqVOxWGpm8Y3QeFbvNgYZjsCNlVauOZ9oA7FKfp0onY+ 0xk56SKM83eCjW6fKrK6AKAt7LhHZDhNpxGek+6r5luE+FCfUGkJG1YD+x2WW/UW 8K6zQF8GGeQZ8Zlh7axUlIBxGpG43BGrUHpLNqPD7BXWGq6dnhufBYRFay8y34/r sH3+yuPa92ki7/geQppZwCZRgLSKMRbIdoWaKhZZEQlpGOzCOiRmk9OGyRcoNVRU X7UYgPqZdc1cMo/AxGWzULJNjMaYMZvIKcHkqOKZfkIcWlSictn7pMPhN1+k+NWM yMORAQKCAQAyXl02h/c2ihx6cjKlnNeDr2ZfzkoiAvFuKaoAR+KVvb9F9X7ZgKSi wudZyelTglIVCYXeRmG09uX3rNGCzFrweRwgn6x/8DnN5pMRJVZOXFdgR+V9uKep K6F7DYbPyggvLOAsezB+09i9lwxM+XdA2whVpL5NFR1rGfFglnE1EQHcEvNONkcv 0h8x9cNSptJyRDLiTIKI9EhonuzwzkGpvjULQE8MLbT8PbjoLFINcE9ZWhwtyw0V XO32KE8iLKt3KzHz9CfTRCI3M7DwD752AC6zRr8ZS/HXzs+5WTkdVVEtRC7Abd3y W2TzuSMYNDu876twbTVQJED3mwOAQ3J7 -----END PRIVATE KEY----- ================================================ FILE: _examples/file-server/http2push-embedded/bindata.go ================================================ // Code generated by go-bindata. (@generated) DO NOT EDIT. // Package main generated by go-bindata.// sources: // ../http2push/assets/app2/app2app3/css/main.css // ../http2push/assets/app2/app2app3/dirs/dir1/text.txt // ../http2push/assets/app2/app2app3/dirs/dir2/text.txt // ../http2push/assets/app2/app2app3/dirs/text.txt // ../http2push/assets/app2/app2app3/index.html // ../http2push/assets/app2/index.html // ../http2push/assets/app2/mydir/text.txt // ../http2push/assets/css/main.css // ../http2push/assets/favicon.ico // ../http2push/assets/index.html // ../http2push/assets/js/main.js package main import ( "bytes" "compress/gzip" "fmt" "io" "net/http" "os" "path/filepath" "strings" "time" ) func bindataRead(data, name string) ([]byte, error) { gz, err := gzip.NewReader(strings.NewReader(data)) if err != nil { return nil, fmt.Errorf("read %q: %v", name, err) } var buf bytes.Buffer _, err = io.Copy(&buf, gz) clErr := gz.Close() if err != nil { return nil, fmt.Errorf("read %q: %v", name, err) } if clErr != nil { return nil, err } return buf.Bytes(), nil } type asset struct { bytes []byte info os.FileInfo } type bindataFileInfo struct { name string size int64 mode os.FileMode modTime time.Time } // Name return file name func (fi bindataFileInfo) Name() string { return fi.name } // Size return file size func (fi bindataFileInfo) Size() int64 { return fi.size } // Mode return file mode func (fi bindataFileInfo) Mode() os.FileMode { return fi.mode } // ModTime return file modify time func (fi bindataFileInfo) ModTime() time.Time { return fi.modTime } // IsDir return file whether a directory func (fi bindataFileInfo) IsDir() bool { return fi.mode&os.ModeDir != 0 } // Sys return file is sys mode func (fi bindataFileInfo) Sys() any { return nil } type assetFile struct { *bytes.Reader name string childInfos []os.FileInfo childInfoOffset int } type assetOperator struct{} // Open implement http.FileSystem interface func (f *assetOperator) Open(name string) (http.File, error) { var err error if len(name) > 0 && name[0] == '/' { name = name[1:] } content, err := Asset(name) if err == nil { return &assetFile{name: name, Reader: bytes.NewReader(content)}, nil } children, err := AssetDir(name) if err == nil { childInfos := make([]os.FileInfo, 0, len(children)) for _, child := range children { childPath := filepath.Join(name, child) info, errInfo := AssetInfo(filepath.Join(name, child)) if errInfo == nil { childInfos = append(childInfos, info) } else { childInfos = append(childInfos, newDirFileInfo(childPath)) } } return &assetFile{name: name, childInfos: childInfos}, nil } else { // If the error is not found, return an error that will // result in a 404 error. Otherwise the server returns // a 500 error for files not found. if strings.Contains(err.Error(), "not found") { return nil, os.ErrNotExist } return nil, err } } // Close no need do anything func (f *assetFile) Close() error { return nil } // Readdir read dir's children file info func (f *assetFile) Readdir(count int) ([]os.FileInfo, error) { if len(f.childInfos) == 0 { return nil, os.ErrNotExist } if count <= 0 { return f.childInfos, nil } if f.childInfoOffset+count > len(f.childInfos) { count = len(f.childInfos) - f.childInfoOffset } offset := f.childInfoOffset f.childInfoOffset += count return f.childInfos[offset : offset+count], nil } // Stat read file info from asset item func (f *assetFile) Stat() (os.FileInfo, error) { if len(f.childInfos) != 0 { return newDirFileInfo(f.name), nil } return AssetInfo(f.name) } // newDirFileInfo return default dir file info func newDirFileInfo(name string) os.FileInfo { return &bindataFileInfo{ name: name, size: 0, mode: os.FileMode(2147484068), // equal os.FileMode(0644)|os.ModeDir modTime: time.Time{}} } // AssetFile return a http.FileSystem instance that data backend by asset func AssetFile() http.FileSystem { return &assetOperator{} } var _app2App2app3CssMainCss = "\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x4a\xca\x4f\xa9\x54\xa8\xe6\xe5\x52\x50\x50\x50\x48\x4a\x4c\xce\x4e\x2f\xca\x2f\xcd\x4b\xd1\x4d\xce\xcf\xc9\x2f\xb2\x52\x48\xca\x29\x4d\xb5\xe6\xe5\xaa\x05\x04\x00\x00\xff\xff\x52\xd7\xbb\x8b\x26\x00\x00\x00" func app2App2app3CssMainCssBytes() ([]byte, error) { return bindataRead( _app2App2app3CssMainCss, "app2/app2app3/css/main.css", ) } func app2App2app3CssMainCss() (*asset, error) { bytes, err := app2App2app3CssMainCssBytes() if err != nil { return nil, err } info := bindataFileInfo{name: "app2/app2app3/css/main.css", size: 38, mode: os.FileMode(438), modTime: time.Unix(1595043712, 0)} a := &asset{bytes: bytes, info: info} return a, nil } var _app2App2app3DirsDir1TextTxt = "\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x4a\x2c\x28\x30\xd2\x07\x11\x89\x05\x05\xc6\xfa\x29\x99\x45\xc5\x20\xc2\x50\xbf\x24\xb5\xa2\x44\xaf\xa4\xa2\x04\x10\x00\x00\xff\xff\x87\xaf\x9d\x00\x20\x00\x00\x00" func app2App2app3DirsDir1TextTxtBytes() ([]byte, error) { return bindataRead( _app2App2app3DirsDir1TextTxt, "app2/app2app3/dirs/dir1/text.txt", ) } func app2App2app3DirsDir1TextTxt() (*asset, error) { bytes, err := app2App2app3DirsDir1TextTxtBytes() if err != nil { return nil, err } info := bindataFileInfo{name: "app2/app2app3/dirs/dir1/text.txt", size: 32, mode: os.FileMode(438), modTime: time.Unix(1594843207, 0)} a := &asset{bytes: bytes, info: info} return a, nil } var _app2App2app3DirsDir2TextTxt = "\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x4a\x2c\x28\x30\xd2\x07\x11\x89\x05\x05\xc6\xfa\x29\x99\x45\xc5\x20\xc2\x48\xbf\x24\xb5\xa2\x44\xaf\xa4\xa2\x04\x10\x00\x00\xff\xff\x84\x14\xaa\xeb\x20\x00\x00\x00" func app2App2app3DirsDir2TextTxtBytes() ([]byte, error) { return bindataRead( _app2App2app3DirsDir2TextTxt, "app2/app2app3/dirs/dir2/text.txt", ) } func app2App2app3DirsDir2TextTxt() (*asset, error) { bytes, err := app2App2app3DirsDir2TextTxtBytes() if err != nil { return nil, err } info := bindataFileInfo{name: "app2/app2app3/dirs/dir2/text.txt", size: 32, mode: os.FileMode(438), modTime: time.Unix(1594843207, 0)} a := &asset{bytes: bytes, info: info} return a, nil } var _app2App2app3DirsTextTxt = "\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x4a\x2c\x28\x30\xd2\x07\x11\x89\x05\x05\xc6\xfa\x29\x99\x45\xc5\xfa\x25\xa9\x15\x25\x7a\x25\x15\x25\x80\x00\x00\x00\xff\xff\x64\xfe\x96\xd6\x1b\x00\x00\x00" func app2App2app3DirsTextTxtBytes() ([]byte, error) { return bindataRead( _app2App2app3DirsTextTxt, "app2/app2app3/dirs/text.txt", ) } func app2App2app3DirsTextTxt() (*asset, error) { bytes, err := app2App2app3DirsTextTxtBytes() if err != nil { return nil, err } info := bindataFileInfo{name: "app2/app2app3/dirs/text.txt", size: 27, mode: os.FileMode(438), modTime: time.Unix(1594843207, 0)} a := &asset{bytes: bytes, info: info} return a, nil } var _app2App2app3IndexHtml = "\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x54\x90\x3f\x4f\xc3\x40\x0c\xc5\xf7\x48\xf9\x0e\xc6\x33\xed\x91\x76\x61\xb8\x8b\x54\xf1\x47\x6c\x30\x94\x81\xf1\x7a\x31\x9c\x85\x73\x39\xe5\x4c\x4b\xbf\x3d\x4a\x68\x90\x58\x6c\x3d\xfb\xbd\x9f\x64\xdb\xab\xfb\xe7\xbb\xfd\xdb\xcb\x03\x44\xed\xa5\xad\x2b\x3b\x75\x10\x9f\x3e\x1c\x52\xc2\xb6\xae\xa6\x19\xf9\xae\xad\x2b\x00\x00\xdb\x93\x7a\x08\xd1\x8f\x85\xd4\xe1\xeb\xfe\x71\x75\x8b\xff\x76\xc9\xf7\xe4\xf0\xc8\x74\xca\xc3\xa8\x08\x61\x48\x4a\x49\x1d\x9e\xb8\xd3\xe8\x3a\x3a\x72\xa0\xd5\x2c\xae\x81\x13\x2b\x7b\x59\x95\xe0\x85\x5c\xb3\xbe\xf9\x63\x09\xa7\x4f\x18\x49\x1c\x16\x3d\x0b\x95\x48\xa4\x08\x71\xa4\x77\x87\x26\x7f\x1d\x84\x83\xf1\x39\x6f\xe6\xe2\x73\xde\x9a\x50\x8a\xe9\x3d\xa7\x75\x28\x05\xc1\x2c\x20\x65\x15\x6a\x77\x39\x6f\x76\x39\x6f\xad\xf9\xd5\x75\x65\xcd\xe5\xac\xba\xb2\x87\xa1\x3b\x2f\xfe\xd8\xb4\x4f\x24\x32\xc0\x12\x01\x4e\x1d\x7d\x5b\x13\x9b\x39\x75\xf1\xce\x80\xe9\x67\x3f\x01\x00\x00\xff\xff\xef\x25\x54\xc8\x43\x01\x00\x00" func app2App2app3IndexHtmlBytes() ([]byte, error) { return bindataRead( _app2App2app3IndexHtml, "app2/app2app3/index.html", ) } func app2App2app3IndexHtml() (*asset, error) { bytes, err := app2App2app3IndexHtmlBytes() if err != nil { return nil, err } info := bindataFileInfo{name: "app2/app2app3/index.html", size: 323, mode: os.FileMode(438), modTime: time.Unix(1595043725, 0)} a := &asset{bytes: bytes, info: info} return a, nil } var _app2IndexHtml = "\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xb2\xc9\x30\xb4\xf3\x48\xcd\xc9\xc9\x57\x70\x2c\x28\x30\x52\xc8\xcc\x4b\x49\xad\xb0\xd1\xcf\x30\xb4\x03\x04\x00\x00\xff\xff\x75\x17\xab\xfa\x19\x00\x00\x00" func app2IndexHtmlBytes() ([]byte, error) { return bindataRead( _app2IndexHtml, "app2/index.html", ) } func app2IndexHtml() (*asset, error) { bytes, err := app2IndexHtmlBytes() if err != nil { return nil, err } info := bindataFileInfo{name: "app2/index.html", size: 25, mode: os.FileMode(438), modTime: time.Unix(1565946440, 0)} a := &asset{bytes: bytes, info: info} return a, nil } var _app2MydirTextTxt = "\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xca\x2a\x2d\x2e\x51\x48\x54\x28\x49\xad\x28\xd1\x03\x04\x00\x00\xff\xff\x2f\xf9\x22\x98\x0c\x00\x00\x00" func app2MydirTextTxtBytes() ([]byte, error) { return bindataRead( _app2MydirTextTxt, "app2/mydir/text.txt", ) } func app2MydirTextTxt() (*asset, error) { bytes, err := app2MydirTextTxtBytes() if err != nil { return nil, err } info := bindataFileInfo{name: "app2/mydir/text.txt", size: 12, mode: os.FileMode(438), modTime: time.Unix(1594787248, 0)} a := &asset{bytes: bytes, info: info} return a, nil } var _cssMainCss = "\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x4a\xca\x4f\xa9\x54\xa8\xe6\xe5\x52\x50\x50\x50\x48\x4a\x4c\xce\x4e\x2f\xca\x2f\xcd\x4b\xd1\x4d\xce\xcf\xc9\x2f\xb2\x52\x48\xca\x49\x4c\xce\xb6\xe6\xe5\xaa\xe5\xe5\x02\x04\x00\x00\xff\xff\x03\x25\x9c\x89\x29\x00\x00\x00" func cssMainCssBytes() ([]byte, error) { return bindataRead( _cssMainCss, "css/main.css", ) } func cssMainCss() (*asset, error) { bytes, err := cssMainCssBytes() if err != nil { return nil, err } info := bindataFileInfo{name: "css/main.css", size: 41, mode: os.FileMode(438), modTime: time.Unix(1565946440, 0)} a := &asset{bytes: bytes, info: info} return a, nil } var _faviconIco = "\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xec\x5a\x0b\x70\x55\xc7\x79\xde\x7b\xce\x45\x12\xb2\x90\xc4\xc3\xe6\x61\x3b\x90\xf8\x31\xc4\x19\x6c\x32\xe3\xc4\x34\xe3\xc6\x34\x6d\xed\xd4\x69\x62\x32\x49\x9a\xa6\x75\xea\x36\x33\xee\xd8\x9e\xb4\x75\xdc\x7a\xa6\xc5\x31\x02\xa6\xd3\x84\x47\x28\x6f\x3b\xc6\x3c\xcc\xeb\x9e\xbd\x08\x21\x04\x92\x28\x12\x0e\x08\x5b\x3c\xec\x02\x92\x78\x08\x21\x09\x41\x25\x24\x10\xe8\x71\xb5\xe7\xdc\xd7\xb9\xf7\xef\xfc\xff\x9e\x73\x74\x25\xdd\x97\x24\x82\x33\x1e\xce\xcc\x3f\x7b\xee\x9e\xdd\xff\xff\xf6\xdf\x7f\xff\xfd\xf7\xdf\xcb\x98\x8b\xa9\x2c\x3f\x1f\xcb\x19\xec\x15\x37\x63\x5f\x67\x8c\xcd\x98\x21\x7f\x6b\xf9\x8c\x6d\x72\x33\x36\x7b\xb6\xf5\xfb\x11\xc6\x9e\xbd\x97\xb1\x99\x8c\xb1\x7c\x6c\xc7\x64\x3d\x3d\x6e\x76\xdb\x1f\xe1\x75\xab\x42\x53\x7e\x22\x3c\x6c\x91\xf0\xb0\x02\x22\xae\x14\x08\x8f\xab\x40\xec\x64\x92\xf0\x5d\x53\xfe\x59\xec\x64\x5f\x15\x1e\xa6\x08\x4f\x7f\x7f\xdf\x7a\x96\xa1\x97\x3c\xb8\x3f\x78\xfa\x0d\x08\x9e\x5d\x08\xc1\xda\x5f\x82\x51\xf6\x65\x30\xca\x67\x41\xf0\xcc\xbf\x11\x19\x07\x1e\x07\xbd\x78\x2a\x18\x65\x8f\xb5\x0b\x4d\x7d\xad\x6f\x07\x73\x0b\xcd\x45\xfd\xbb\x17\xb3\x0c\xa3\x62\xce\xfe\xa8\xd1\x0a\xf8\x98\x6d\x25\x10\x38\xfe\x53\x88\xdc\x3a\x01\x66\x6b\x11\x11\xbe\x07\x8e\xfd\x2d\x84\x2e\x2c\x05\xff\xd1\x79\x3d\xc2\xc3\x7e\x20\x78\x06\x13\x5c\x89\xe9\xdf\x06\xd1\x50\x2f\x04\x4e\xfc\x0c\xcc\x6b\xa5\x60\x1c\xfd\x21\x74\x6c\x1e\x47\x84\xef\x58\x17\xf8\xe4\x65\x30\x6f\x54\x81\x71\xe0\x89\xea\xbe\x2d\x6c\xd2\x80\xfe\xfe\x76\x30\x6f\x1c\x81\x60\xed\x7c\x08\x9e\x5d\x0c\x2d\xeb\xc7\x41\xed\xd2\x29\x50\xbb\x74\x32\xbd\x63\x1d\x7e\xc3\x36\xa1\x73\xff\x19\x14\x1e\x36\x4f\x78\xc7\xc4\xf4\xef\x80\x70\xd3\xfb\x10\x6e\x5c\x0f\xdd\x65\x73\xe1\xdc\xf2\x89\xd0\xb1\x25\x0f\x6e\x7c\x90\x07\xe7\x7f\x33\x81\xea\xf0\x5b\xb8\x79\x13\x61\xd0\x8b\xa7\x2c\x09\x7c\xfc\x57\xac\x7b\x11\xf6\x7f\x6a\x5f\xd4\xb8\x06\xa1\xfa\x15\x10\x6e\x7a\x0f\x3a\xb4\xc7\xa0\x61\x55\x3e\x04\x8f\xfc\x39\x84\x3e\xfa\x4b\x68\x5c\x93\x4f\x75\xf8\x2d\x74\x71\x05\x44\x45\x13\xea\x54\xeb\x7c\x85\xa9\x48\xf8\x1e\x15\xcd\xf4\xcd\xee\x7f\x71\x65\x3e\x04\x3e\x7c\x86\x78\x5c\x5a\x1d\xd3\xbf\x7e\x05\xa0\x2c\x94\x89\xb2\x03\xd5\x3f\x66\x88\x05\x31\x21\x36\x1b\xff\xd9\xe5\x13\xe1\xda\xc6\x5c\x68\xdf\x94\x4b\x63\x71\xf0\x37\xbd\x0f\x38\x56\x1c\x33\x8e\x1d\x75\x80\xba\x40\x9d\x0c\xd6\x5f\xcd\x92\x29\x44\x83\xf5\x87\xba\x76\xfa\x73\x85\xe1\x5c\x18\x07\x9e\x38\x86\x18\x68\x8e\xac\xf9\xbb\xbe\x39\x87\xc8\x99\xbf\x13\x3f\xa3\x39\xc6\xb9\x8e\xed\x4f\xb6\xe0\x61\x3f\x40\xdb\x40\x1b\x41\x5b\x19\x62\x3f\xc7\x7f\x4a\xb6\x85\x0f\xda\x9a\xdd\x9f\xd6\x80\xe6\x62\xd2\x26\xd5\xd7\xd0\x46\xc9\x56\x0f\x3c\xde\x6f\xbf\xe5\xb3\xc8\xa6\xd1\xb6\xc9\xc6\x4f\xbf\x01\x68\xf3\x68\xfb\xce\x3a\xf2\x30\x26\x76\x32\x85\xd6\x08\xae\x95\xc1\xeb\x87\xd6\x14\xb3\x69\x11\xad\x39\xaf\x5b\x1d\xed\xfa\x05\x60\x8c\x65\x32\xc6\x54\x8b\x5c\x31\x64\x3d\x0b\x63\xe8\xb0\x45\x2d\x56\xdf\x99\x96\x8f\x99\x1b\xeb\x67\xf2\x47\x8b\xea\xf3\xf9\x08\xae\x22\xe5\x09\xae\x4e\x17\x5c\xfd\xc2\x28\x69\x8a\xe0\xca\x58\xb4\x3d\xdb\x17\xa6\x29\xff\x5f\x04\x57\x5b\x85\xe6\xba\x92\x90\xb8\x1a\x43\x4a\x4c\xbd\x62\xd7\xb7\x08\xae\xd6\x0b\xae\xfe\x8f\xe0\xea\x9b\x38\x1e\x1f\x1f\x4b\xfe\x34\xb5\x7c\xa5\x40\xec\xca\x02\xbd\x64\x3a\xe8\xfb\xbe\x38\x94\x4a\xa6\x83\xf0\x66\x82\xd0\x14\x10\x9a\x0b\x44\xe1\x38\x5c\x2b\x44\xf8\x4e\x75\x5c\x01\xe1\x75\x03\xf2\x11\x9a\x2b\x2a\xb8\x52\x2b\xb8\x3a\x4f\x68\x8a\x92\x0c\x03\xc9\xf7\xb0\x02\xa3\xfc\x2b\x10\xe9\xae\x85\xa8\x7e\x15\xa2\xfa\x95\x18\xba\x0a\xa1\xb3\x8b\x49\xbe\xbe\xe7\x5e\x08\xfe\xef\x3f\x81\xd9\x51\x09\x91\xbe\x4b\x44\xf8\x8e\x75\xf8\x4d\x70\x37\x04\x3e\xfe\x11\xf9\x14\xbd\x68\x3c\xe2\xe8\x14\x5c\x7d\x49\x78\x99\x2b\x11\x06\x47\xfe\x81\xd9\x10\x0d\xde\x82\xc1\x8f\x79\xe3\x30\xe8\x7b\xa7\x81\x51\xfa\x28\x98\xd7\xca\x00\xa2\xa6\xfc\x80\x65\xcc\x3b\x7e\xc3\x36\xfa\xde\x07\x08\x93\xd9\x5a\x0c\xfa\xfe\x87\x41\x68\xac\x43\x70\xf5\x59\x92\xe3\x1d\xea\x1a\x92\xc9\x8f\x06\x3a\xc1\xff\xe1\x5c\xda\x67\x91\x27\x3e\x91\xde\x0b\xe4\xaf\xfc\x47\x5f\x20\xc2\x77\xac\x23\xac\x1d\x95\xd4\xd6\xff\xe1\x9f\x10\x2f\xfa\x5d\xf2\x05\xc4\x70\x4c\x70\xf5\x7e\x94\x35\x1c\xf9\xa1\xc6\x77\x68\x3e\x43\x17\x57\x4a\xfe\xe8\xc3\xcb\xbe\x8c\xfc\xa0\xcf\x23\x09\xdf\xb1\x0e\xbf\x51\x9f\x8b\x2b\x65\x9f\xc6\x77\xe8\x37\xee\x4f\x62\xd7\x58\xb4\x8f\x05\x3e\x9e\x35\xc4\x1e\x13\xc9\x8f\x06\xbb\xc0\xa8\xfc\x23\xf0\x1f\xfa\x63\x88\x86\xba\x21\x72\xeb\x24\x18\xfb\x1f\x82\xde\x1d\x2e\x68\xdd\x30\x0e\x1a\x56\x4f\x24\xc2\x77\xac\xc3\x6f\xd8\x06\xdb\x62\x1f\xec\x8b\xef\x10\xd6\xc1\xff\xd1\xf7\x11\x67\x83\xe0\xea\x97\x06\xeb\x20\x91\x7c\xd2\xdd\xee\x3c\x08\x37\x6f\x04\x30\x0d\x08\x7c\x34\x8f\xe4\x34\xae\x99\x00\xa7\x7f\x3d\x0d\x4e\xfd\xea\x7e\x22\x7c\xc7\x3a\xfc\x86\x6d\xb0\x6d\xb8\x69\x23\xf5\xb5\xe7\xcc\x6c\x3f\x00\x62\x77\x6e\x54\x68\xae\x57\xad\xf5\x96\x52\x7e\xb0\x6e\x01\xe8\xfb\x66\xd0\x1a\x30\xaf\x95\x83\x28\xcc\x81\x2b\xef\xe6\x92\x3c\x24\x8a\x63\x96\x4d\x71\x7e\xe3\x37\x6c\x83\x6d\xb1\x0f\xf6\x0d\xd6\x15\x48\x5d\x86\x7c\x64\x47\xc2\xc3\xf6\x0a\xae\x66\xc6\xea\x20\xae\xfc\x48\x08\xfc\x55\xdf\x05\x7f\xd5\x77\x00\x22\x41\x08\x7e\xfa\x2a\x74\x6d\x53\xa1\x6e\xf9\x64\x92\x75\x69\xcd\x44\xb8\xb9\x35\x13\x6e\x6d\xcb\x80\xe6\x75\xe3\xa9\x0e\xbf\x61\x1b\x6c\x8b\x7d\xb0\xaf\xbf\xea\x7b\xc4\x8b\xec\xe2\xec\x22\x9c\x83\x66\xc1\xd5\x19\xa9\xe4\xd3\xdc\x1f\x98\x0d\xc1\x33\x6f\x42\x34\xec\x03\x7f\xe5\x1c\x68\xdb\x30\x96\xe4\x9c\xfb\xcd\x7d\xd0\xbd\x63\x0c\xe8\x85\x63\x41\x2f\xbc\x07\x7a\x3d\x6e\xa8\x5f\x39\x89\xbe\x61\x1b\x6c\x8b\x7d\xb0\xaf\xe4\xd9\x65\xd9\x6e\x19\x88\x5d\xd9\xba\xd0\x5c\x73\x53\xca\xd7\xff\x0f\xf4\xfd\x0f\x41\xa8\x61\x35\xf9\x1f\x7d\xdf\x74\x68\x5e\x97\x47\xf3\xdd\xf2\x4e\x1e\xc9\x0d\x9f\xfc\x3b\x30\x4f\xbd\x02\xc6\x9e\x09\xd0\xba\x21\x07\x4e\xfd\x6a\x1a\xb5\xc1\xb6\xe4\xb3\x1a\x56\x13\x0f\xe4\x25\xd7\xed\x39\x5c\x9b\x51\xa1\xb1\x17\xe3\xc8\x7f\x0b\x63\x13\x5c\xef\xd4\xb6\xaf\x91\x7c\x6b\xf8\xf2\x16\x5a\xdb\x62\xcf\x64\xb8\xb4\x7a\x3c\xc9\x68\xdb\x90\x0d\xfe\xb2\x87\x01\x2e\x2d\x07\x68\x5a\x05\x81\xca\x27\xe1\xfa\xe6\x4c\x39\x2f\xab\xc7\x53\xdb\x88\xaf\x9e\xfa\x22\x0f\xe4\xe5\xc4\x47\xa5\x8f\xa2\x0d\xfc\x22\x8e\xfc\x5f\xe0\x37\x3b\x5e\x4f\x2e\xff\x1e\xf0\x97\x7e\x11\xa2\x0d\x4b\x00\x9a\x56\x42\xe0\xe0\x13\x70\x7d\x53\x56\x6a\xf9\x81\x4e\x8a\xbf\x70\xac\x43\xe4\x6b\xec\x45\xd4\x0d\xea\x28\xbe\xfe\x67\x38\xfa\x6f\x5e\x97\x2f\xfd\xcb\xd1\xe7\x21\x74\xec\x87\xa0\xef\xce\x81\xab\xbf\x1d\x67\x7d\xcb\xb3\xd6\xcc\x50\xfd\xe3\xdc\xe2\x1c\xcb\xb3\xd0\x60\xf9\xae\xb9\x68\x1b\xe4\xdf\x53\xd8\x1f\xda\x39\xda\xbe\xee\x55\x41\xf7\xba\xc9\x16\xcf\xaf\xb8\x2f\xa5\xfd\x25\x95\x8f\x6b\x42\x63\xcd\xb8\xcf\x25\x5c\x7f\x5b\xe5\xfa\xc3\x39\xb8\xf0\xdf\xf7\x42\xfb\xc6\x7b\xa0\x63\x53\x36\x5c\x5c\x35\x29\xad\xf5\x97\x42\x7e\x26\xfa\x06\xda\x37\xc2\xbe\xb4\xfc\x0f\xd2\x99\x25\xd3\xd2\xf2\x3f\xc9\xe5\x2b\xf6\x1c\xbc\x86\x3e\x12\x7d\xe5\x50\xff\xeb\x8f\xe3\x7f\x25\x0d\xf5\xbf\xfe\x21\xfe\x37\x99\xfc\x18\x1d\x7c\x09\xf7\x08\xdc\x2b\xc0\xd4\x69\xef\x30\x2a\x46\xb9\xff\x58\x73\x9f\x5a\xbe\xc2\x7c\x1a\xed\x8d\x0b\x70\xaf\xc4\x3d\x53\xee\xbf\xef\x8e\x7a\xff\x4d\x47\x7e\x8c\x0e\xee\xc7\x58\x01\x63\x06\xd4\x1d\xf6\x41\x9b\x18\x51\xfc\x61\xf9\xb2\xb4\xe5\x7b\x55\x1b\xc3\xb3\x18\x33\x61\xec\x84\x31\x14\xf1\xdc\xfb\x40\x5a\xf1\x97\x4e\xf1\xd7\x34\x8a\xd9\x06\x3f\xa9\xe4\x3b\xb6\x48\xb1\xa2\xfa\x12\xc6\x8e\x18\x43\x62\x2c\x89\x31\x25\xc6\x96\x29\xe3\x4f\x6f\x26\xc5\xaa\xf1\x62\x58\x8c\x6d\x31\xc6\x4d\x26\xbf\x7f\x3d\x60\xcc\xac\xce\xa3\x18\x1a\x63\x69\x8c\xa9\xb9\x5b\xc6\xd8\x14\x7f\xe7\xc4\xc4\xdf\x39\xb2\x0e\x63\x73\x8c\x91\x93\xc5\xf0\xc4\x47\x49\x2a\x9f\x30\x68\x0a\xf3\x15\x65\x31\x6b\xaf\x7e\xd3\x3a\x53\xd4\x5b\x67\x8c\x44\xe7\x0f\x49\xc9\xcf\x30\xad\xd6\x59\x27\xa9\xfc\x58\x5d\xf4\x69\x14\x2f\x8d\x95\x67\xab\x51\x9f\xcf\xa6\x5b\x67\xbd\xb4\xe4\x7f\xde\x1f\x7b\x6d\x1c\x66\x2a\x1c\x66\x0c\xe9\x99\xc3\x8c\x4d\xb7\x28\xcf\xa2\x4c\x8b\xd4\x74\xa8\xc5\xa2\x1e\x8b\x02\x16\x99\x8c\xa9\xc0\x48\x90\x6a\xcb\x9d\xc9\x18\x9b\xcd\x18\xfb\xfb\xd8\x3c\xc5\xc3\x9f\xb5\x56\xee\x3e\x77\x9f\x3f\xcc\xc7\xda\x1f\x1f\x16\x5c\x7d\x5b\x70\x75\xa1\xe0\x6a\xc1\xef\x89\x6c\xde\xff\x2a\xb8\xfa\x37\x82\x2b\xb3\x05\x57\x72\x7a\x35\xc6\x0c\x2d\x79\x3e\x29\x0d\xfc\xdf\x16\x5c\x0d\x0a\xae\xc2\x1d\x22\x53\x70\xb5\x53\x70\xf5\x20\xed\xeb\x5c\xc9\xc7\x58\xc3\x48\x33\x3f\x97\x18\xbf\x42\xb1\x57\x5a\x84\x6d\x07\x60\x4a\xd2\x37\x6e\x5b\x97\xfd\x1b\xf5\x56\x2a\xb8\xfa\x24\xec\x67\x4c\xe7\xc3\x1b\x83\x83\x5f\x73\x05\x31\xc6\x0b\xd6\xbe\x45\xe7\xf2\x64\x14\x3c\xbb\x08\x8c\x8a\xa7\xfa\x71\x21\xc6\xc2\x1c\x30\x0e\x7e\x0d\x02\x27\xfe\x81\x78\x20\xe1\x3b\xd6\xc9\x78\x84\x39\xd8\xf5\xe2\x29\x60\x94\xcd\x04\xe1\xcd\x8a\x1d\x47\xa3\xe0\xea\x0b\x7e\xce\x5c\x7a\x8a\xfc\x64\x7c\xfc\x2c\x48\xf1\x75\xb8\x6f\x48\x9c\x37\xf8\x31\xdb\x4a\x64\x9c\x84\xb2\xbd\x19\xe0\x3f\xf2\x17\x60\xb6\xee\x85\x68\xe0\x26\x40\x34\x12\x13\x20\x46\xa8\x0e\xbf\x61\x1b\x6c\x8b\x7d\xf4\xe2\xc9\x10\x6a\x58\x0b\xe1\xcb\x5b\xc1\xa8\xfc\x46\xff\x9c\x70\xb5\x0d\xc7\xd0\xa3\x65\x33\x91\xe6\x3c\x0c\x17\x7f\xa4\xeb\x14\xc5\xb4\xc2\xc3\xe8\x3c\x43\xb1\x65\xb0\x3b\xe5\x98\xb1\x0d\xb6\xc5\x3e\xf2\x7c\x30\x13\x22\xdd\x35\x10\xf5\x5f\xa7\x73\xa9\x28\xbc\xc7\x1e\x43\x93\xe0\xca\x53\x14\x73\x16\xa6\x8e\x89\x86\x83\x9f\xce\x41\x55\xdf\x23\xec\x62\x77\x2e\x9d\xc7\xed\xb3\xa8\x1c\x5c\x10\xa2\xa2\x05\x22\xb7\x3e\x21\xc2\x77\xac\xeb\xff\x1e\xa2\x3e\xd8\x17\x79\x20\xaf\x68\xa8\x87\xda\x84\xea\x97\xcb\xbc\xb3\x1c\x43\xa5\xe0\xea\xe4\x74\x62\xba\xe1\xe0\x0f\x35\xac\x91\x36\xc0\xdd\x10\xac\x7b\xbb\x1f\x7b\x34\x0c\x66\x47\x05\xdd\x3d\xe9\xa5\x8f\x80\x5e\x34\x51\x52\xe9\x23\xf2\x3e\xaa\xa3\x82\xda\xd8\x63\x08\xd6\xfe\x52\xc6\xee\xde\x0c\xe2\x69\x8f\x1d\xcf\x4d\x92\xbf\x1a\x45\x7f\xeb\xf3\x32\x57\x5f\x0a\x3b\x4a\x17\x3f\xea\xd2\x28\x9f\x25\xf5\x76\xe4\x3b\xfd\x79\xad\x70\x1f\x84\xce\xff\x17\xe8\x7b\x26\x59\xfe\xc6\xca\xdb\xdb\x3e\x46\x63\xf4\x0d\xdb\xd8\xbc\xe9\x0c\x78\xe4\x79\xe2\x85\x3c\x69\x9e\x68\x7e\x7b\x21\x50\xfd\x63\x7b\x4d\x5f\x13\x5c\xfd\x7a\xca\x73\x45\x9a\xf8\x69\x7e\xad\xb5\x67\xde\xa8\xea\xd7\x59\xdd\x02\x79\x0f\xc1\xa5\x1f\xe9\xd9\x39\x06\x6e\x6e\xcd\x22\xc2\x77\xb9\x36\x5d\xd4\x06\xdb\xda\xf6\x64\xde\x38\x42\xbc\x90\x27\xf2\x76\x4c\xac\xbb\x86\x72\x44\xd6\x18\xde\xd3\xb9\xcb\x9d\xcc\x1f\xa5\x83\x9f\xf2\x5c\x15\x73\x48\x5f\x81\x4f\x5f\x75\xce\xa8\xe1\xcb\x5b\x1c\x9b\x45\xac\x57\xde\xcd\xa3\x5c\x66\xcd\xd2\x29\x44\xf8\x8e\x75\x72\x1c\x0a\xb5\xc5\x3e\x92\xa9\x49\xbc\x68\x0e\x2a\xe6\x0c\x38\x5b\x87\x2e\x2c\xb1\xce\x86\x6a\xbb\xe0\xea\x57\x93\x9f\x2d\x53\xe3\xa7\x5c\xcf\xae\x6c\xb2\x03\xb3\xb3\x5a\xea\xa9\xaf\x51\x9e\x5d\x35\x06\xdd\xdb\x33\x9c\x9c\x53\x3c\xc2\x6f\xd8\x86\xfc\x4e\xf9\x57\x9c\xfc\x9f\xd9\xf9\xb1\xb4\xbb\xc2\x6c\x30\xdb\xcb\xfb\xf5\xa5\x5f\x95\x79\x41\xb9\x67\x2c\x12\xdc\x9d\xf0\xce\x2a\x1d\xfc\xe4\xdf\xc8\x5f\x7c\x97\x72\xf1\x54\x57\xf3\xef\x64\x17\xbd\x1e\x37\xe5\x6c\x12\x61\xb7\x09\xdb\x60\x5b\xec\x83\x7d\xe5\x00\x0c\xe2\x89\xbc\x51\xc6\x00\x99\x75\x0b\x6d\x1b\x3a\x2e\xb8\x32\x61\xa4\xf8\xf1\x37\xe5\xe9\x35\x06\xa1\x86\x55\x96\x7e\x5a\xc0\x28\x9d\x49\x75\xad\xef\x8d\x1b\x82\xb5\x76\xd9\xc0\x7b\x00\x9b\xb0\xad\xed\xfb\xa3\xfa\x15\x69\x2b\x17\x57\x51\x1d\xca\x88\x95\x8d\xfe\x57\xde\x9f\x29\x3e\xc1\xd5\x6f\x26\xce\x8f\x24\xc7\x1f\xf1\x35\x80\x5e\xf2\x00\xe8\xbb\xf3\x21\x72\xf3\x98\x65\xf7\x1f\x80\xf0\x8e\x81\x9e\x1d\x63\xc8\xc6\x6d\x7c\x35\x4b\xa7\x92\xbd\xdf\xda\x96\x09\x5d\xdb\x33\xa1\x6d\x43\x8e\x73\xbf\x60\xe7\xf9\xb1\x0f\xfa\x48\xdc\x7b\x89\xff\xcd\x6a\xe2\x4d\x79\x65\x5f\xc3\x40\xbd\xfd\xee\x5b\xb6\x0d\x51\x7e\x3b\xde\x3a\x4e\x85\xdf\xec\x38\x44\xb6\x6f\x94\x3d\x46\xff\x67\xc0\x27\x70\xf2\x65\xe2\x7b\xed\xfd\x9c\x98\xbc\xe7\x54\xa9\x5f\x27\x16\x50\x40\xe7\x2a\x5c\xdf\x9c\x4d\xf7\x32\x76\x3b\xec\x83\x7d\x03\x9f\xfc\xa3\xc4\xe9\x6f\x27\xde\x28\x03\x65\x0d\xb0\xa1\x9a\xff\xb0\xf1\x6f\xd3\xb9\x2b\xee\x9d\x69\x2a\xfc\xa4\x6b\x9c\xdf\xc3\xcf\x01\x44\x02\x94\xcb\x35\x2a\x9f\xa6\xba\xa6\xb5\x13\x9c\x7c\x29\xae\x51\x9f\x47\xe6\x93\xf4\xc2\x6c\xd0\x77\x8f\x73\xe2\xcd\xcb\xeb\xf3\x9d\xfc\x2a\xf6\x21\x7e\x87\x9e\x96\x79\xe1\x48\x80\x78\x63\x1d\xca\x1a\x20\xfb\x2a\xb7\xf7\xb3\x6a\xc1\x95\xdc\x91\xe0\x0f\x5d\x58\x26\xfd\xe6\xf1\x97\xa4\xbe\xc4\x65\xca\x5d\xf5\xee\x54\x29\xe7\x6d\xeb\xb5\x0d\xf5\xca\x5d\x60\xec\x9d\x4a\x77\x30\x91\xda\x37\xc0\x5f\x3e\x93\xc6\xd3\xf9\xc1\x58\xb2\x2d\x3b\x4f\xee\xdb\xa9\x12\x0f\xe4\x45\xf3\x79\xfc\x25\x92\x81\xb2\x06\xd8\xee\xcd\x13\xf2\xce\x98\x2b\x4d\x82\xab\x0f\x26\xc9\x91\x3e\x97\x08\x3f\xed\x4f\x3b\x19\x04\x4f\xbd\x2e\x79\xf6\xd4\xd2\xbe\xd3\xbd\x7d\x0c\x9c\xb5\x6c\x1b\xb1\xe1\x7e\xa5\x7b\x55\x08\x1d\xfb\x11\x40\xf3\x5a\x22\xf3\xf4\xcf\x69\x2e\xd0\xff\xa3\xed\x23\x7e\xec\xd3\xbd\xdd\x4d\x31\x74\xa4\xa7\x4e\xca\x38\xf5\xba\x94\x81\xfb\x5b\xac\xef\x10\xcd\x74\xf7\x64\xdd\x79\xcf\x4a\x82\xff\x5b\x42\x63\x86\x71\xf0\xc9\x21\xb1\x24\xf9\x4e\xe4\x5d\x3b\xdf\xd2\xc9\x71\xd0\x8b\xf2\xa1\x6b\x5b\x86\xe3\x63\xb0\xec\xda\x9e\x41\xf7\x6f\x66\xcd\xeb\x00\xcd\x6b\x00\x9a\x56\x43\xb4\x7e\x31\x18\x7b\x27\x83\xcf\x23\xe7\x0a\xf1\x3b\x6d\x8b\xf2\xe9\xff\x3a\x24\xa3\x76\xbe\x94\x31\xc8\x87\x62\x6c\x4a\x31\x8b\xc6\xfa\x04\x57\x9f\x4e\x82\xff\x69\x6c\x43\xb1\x88\xff\xfa\xc8\xf0\x6f\xcb\x00\x7d\x57\x16\x98\x67\x7e\xde\x8f\xff\x42\x01\x18\xc5\x93\x08\xff\xf9\x91\xe0\x0f\x76\x03\xea\x14\x75\x4b\x3a\x4e\x8c\x7f\x16\xe5\x96\xf7\xcd\xa0\x39\x4b\x6e\x3f\x75\x34\xf7\x68\x03\xb6\xfd\xa0\xef\xb9\xb1\x85\xee\xd3\x21\x78\xe4\xcf\xe8\x0e\x10\xed\x27\x7c\xe2\x45\xd0\xbd\x19\xd0\xbd\x23\x83\xda\x0e\xdb\x7e\xc2\x7d\x74\x67\x81\xb6\x4d\x36\x9e\x18\xff\x83\xb8\x46\xf4\xa2\x09\x74\xdf\x31\x92\xf5\x7b\xf5\xb7\xb9\x96\xef\xc9\x81\x60\xd5\x73\x10\xaa\xfe\x3e\xe8\xc5\xf7\x82\xce\x15\xba\xd3\xc2\x31\x9e\xfa\xb5\xbd\x7e\x95\xb4\xd6\xef\x20\xfc\xdf\x8e\x8f\x9f\x72\xeb\xb9\xe4\xa3\x70\x5f\xb9\xca\x07\xf0\x48\xd7\x7f\x9e\x5b\x81\x7b\x53\x46\x7f\xec\x6c\x91\x4f\xeb\x8f\x2f\xb0\x6d\xe3\x30\xfc\x67\xba\xf8\xe5\xde\xa0\x6e\x47\x1e\xc1\x9a\xf9\x03\x78\xa4\xbb\x7f\x21\x35\xad\x1b\x0f\xbd\x9e\x31\xd6\x3d\xa6\x0a\x7d\x9a\x9b\xe6\x05\x75\x3f\x64\xff\x3a\xf9\xb2\xb5\x46\x13\xef\x5f\xe9\xe0\xd7\xed\xbb\x3e\xdc\xa3\x51\x2f\xbf\xfb\xd3\x81\x71\x48\xdc\xf8\x61\x6b\xdc\xf8\xc1\xde\xc7\x30\x6e\x40\x9c\xa8\xeb\x58\xec\xc3\x89\x1f\xd2\xc5\x1f\xb3\x06\xbe\x29\xb8\xd2\xa7\xef\xb9\x0f\x22\x5d\x9f\x0e\xe0\xe1\xc4\x6f\x17\xd3\x8b\xdf\x12\xd1\x70\xe2\xb7\xe1\xe1\xa7\x35\x30\x81\x62\x55\xcd\x45\xe7\xd0\xd8\xe7\xb3\x88\x9f\x87\x85\x5f\x53\x18\xfd\x3f\x95\xab\x8b\xe5\x19\xe3\x71\x3a\x43\x38\x6b\xe0\xf7\x76\x7e\xa9\x8e\x7b\x7e\x19\x2e\xfe\x18\x1b\xc2\xb3\x5a\x07\x9e\xdd\xf0\x0c\xe7\xf0\xb9\x23\xe7\xc7\x9b\xa3\xc4\x4f\x7e\xc8\x8d\x67\x66\x3a\xa7\xef\x7f\x88\xce\xd2\xf6\x93\xf6\xf9\x7d\x47\xcc\xf9\x7d\x47\xb2\xf3\x7b\x55\xdc\xf3\xfb\x48\xf1\xc7\xcc\xc1\xd7\x28\x77\x81\x3e\xae\xfa\xaf\x29\xa7\x01\x43\xf2\x27\xcf\x0f\xcc\x9f\x9c\xbb\x3d\xf9\x93\xd1\xe2\xef\xe3\x2e\xe6\xe3\x4c\x91\x67\x66\x35\x8a\x7e\x8e\xd6\xb2\xa5\xb3\xd4\xf9\xab\x17\xc1\x28\x7d\x04\xf4\x3d\x13\x89\x8c\x44\xf9\xab\xba\xb7\x87\xe6\xaf\x6e\x03\xfe\x98\x39\x98\x2c\xb8\x7a\xc8\xb6\xd9\x50\xfd\x32\x99\x1b\x0c\xf5\x38\xfe\xe2\xf6\xe5\x0f\x13\xe7\x4e\x47\x84\xbf\x50\xb5\xfd\xe9\x1c\x2b\x97\x4a\xb9\x55\xfa\xdf\x50\xe0\x3a\x44\xba\xcf\xc8\xdc\x37\xda\x8a\x9d\xbf\x4d\x82\xc1\xc1\x62\xe5\x6f\xb1\x0f\xe5\x7e\x4b\x1f\xa5\x5c\x70\xd2\x3e\x23\xc0\x4f\x63\xf0\xba\x58\x8f\x27\x1b\xe7\xe1\x05\x99\xd3\x56\x68\xbe\x8d\xca\x6f\xd0\xbe\x19\x6a\x58\xeb\xac\x3d\x27\x7f\xde\x56\x92\x7e\xfe\xbc\xe4\x41\x30\xdb\xf6\xa5\x1e\xf3\x08\xf1\xe3\xa3\x7b\x15\x66\xec\xa2\xff\x6d\xbc\x60\xdd\x2d\x48\xbc\xbb\xb2\xe8\xbf\x31\x7a\xf1\xd4\x24\xf7\x17\xf3\x89\x12\xdd\x5f\x18\x15\x4f\xd1\x1d\x48\xca\x7b\x92\xda\xb7\xac\xff\xda\xba\x86\x8d\x9f\xc6\xc0\x5d\x0c\xbc\xe4\x5b\xd1\x27\x95\x39\x77\x64\x8e\x8f\x19\xcd\xfd\xd1\xb0\xee\xa9\x46\x84\x1f\x1f\x43\x73\xd9\x6b\x7a\xbc\xbc\x6b\xa3\x3b\xb7\x4e\xeb\x0e\xee\x4e\xdd\xf7\x8d\x18\xbf\x33\x0e\x8f\xc2\x7a\x8b\x54\x8c\x35\x72\x04\x57\x67\x0b\xae\xfe\xc4\xba\x0b\x2d\xb8\x03\xf7\xae\x6f\x5b\x77\xbc\x23\xc6\x7f\xf7\xb9\xfb\x7c\x9e\x1e\xb9\x43\x24\x2e\x5b\x18\x63\xcf\x58\x65\x9e\x55\x66\x5a\xa5\x6b\x50\xc9\xec\xb2\xc0\x2a\x9f\x19\x54\x4e\x4f\x50\xe6\x25\x28\x33\x6f\x5f\xd9\x93\xa0\x0c\x24\x28\xcd\x41\x65\xd4\x2a\xc1\x2e\x17\x0e\x2a\x5b\xac\xb2\xc7\x2a\x4d\xab\x4c\xa1\xdf\xff\x0f\x00\x00\xff\xff\xc6\xb9\x24\x2f\xee\x3a\x00\x00" func faviconIcoBytes() ([]byte, error) { return bindataRead( _faviconIco, "favicon.ico", ) } func faviconIco() (*asset, error) { bytes, err := faviconIcoBytes() if err != nil { return nil, err } info := bindataFileInfo{name: "favicon.ico", size: 15086, mode: os.FileMode(438), modTime: time.Unix(1565946440, 0)} a := &asset{bytes: bytes, info: info} return a, nil } var _indexHtml = "\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x64\x91\x3d\x4f\xc3\x30\x10\x86\xf7\x48\xf9\x0f\xd7\x9b\x40\x22\x35\x6c\x0c\x71\x96\x42\x57\x90\x28\x03\xa3\xeb\x5c\x9b\x6b\x1d\x27\xb2\x2f\x69\xfb\xef\x51\x3e\xca\xe7\x64\xdf\xeb\xbb\xc7\x8f\xec\x7c\xf1\xf4\xb2\xda\x7c\xbc\x3e\x43\x25\xb5\x2b\xd2\x24\x1f\x56\x70\xc6\xef\x35\x92\xc7\x22\x4d\x86\x8c\x4c\x59\xa4\x09\x00\x40\x5e\x93\x18\xb0\x95\x09\x91\x44\xe3\xfb\x66\x9d\x3d\xe2\xaf\x33\x6f\x6a\xd2\xd8\x33\x9d\xda\x26\x08\x82\x6d\xbc\x90\x17\x8d\x27\x2e\xa5\xd2\x25\xf5\x6c\x29\x1b\x8b\x3b\x60\xcf\xc2\xc6\x65\xd1\x1a\x47\xfa\x61\x79\xff\xc5\x72\xec\x8f\x10\xc8\x69\x64\xdb\x78\x84\x2a\xd0\x4e\xa3\x6a\xbb\xad\x63\xab\x76\xa6\x1f\xe2\x25\xdb\x06\x41\x2e\x2d\x69\xe4\xda\xec\x49\x9d\xb3\xa9\x5d\xfd\xe7\x44\xb9\x38\x8a\x15\x91\xfc\xa5\xd9\x18\x55\x6d\xd8\x2f\x6d\x8c\x3f\x46\x85\xc5\x51\xb1\x66\x47\xf0\x46\xa1\xa7\x90\xab\x29\x4a\x93\x5c\xcd\x6f\x92\x26\xf9\xb6\x29\x2f\xd7\x11\xf6\x6d\x27\xb3\xd0\xb6\x13\x19\x54\x1a\x6f\x1d\xdb\xa3\xc6\xc6\xaf\x86\xcd\xcd\x2d\x42\x6f\x5c\x47\x1a\xc7\x1a\x6a\x5a\x4c\xb7\xce\x90\x68\x03\xb7\x57\x8a\xd0\x59\xd4\xc1\xf4\x66\x4a\x11\x62\xb0\xdf\xe6\x87\x59\xfc\x10\xb1\xc8\xd5\xd4\x32\xea\xcd\x52\xa3\xe9\xf0\xb3\x9f\x01\x00\x00\xff\xff\x00\xcf\x96\xa8\xe9\x01\x00\x00" func indexHtmlBytes() ([]byte, error) { return bindataRead( _indexHtml, "index.html", ) } func indexHtml() (*asset, error) { bytes, err := indexHtmlBytes() if err != nil { return nil, err } info := bindataFileInfo{name: "index.html", size: 489, mode: os.FileMode(438), modTime: time.Unix(1594886229, 0)} a := &asset{bytes: bytes, info: info} return a, nil } var _jsMainJs = "\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x4a\xce\xcf\x2b\xce\xcf\x49\xd5\xcb\xc9\x4f\xd7\x50\x4a\xad\x48\xcc\x2d\xc8\x49\x55\xd2\xb4\xe6\xe5\xe2\xe5\x4a\x2b\xcd\x4b\x2e\xc9\xcc\xcf\x53\xc8\xcf\x73\xce\xc9\x4c\xce\xd6\xd0\x54\xa8\xe6\xe5\x52\x50\x50\x50\x28\xcf\xcc\x4b\xc9\x2f\xd7\x4b\xcc\x49\x2d\x2a\xd1\x50\x4a\x2a\x2d\x29\xc9\xcf\x53\x48\x06\xa9\x49\x4d\x01\x6b\xae\x05\x04\x00\x00\xff\xff\xa4\xb7\x99\x52\x57\x00\x00\x00" func jsMainJsBytes() ([]byte, error) { return bindataRead( _jsMainJs, "js/main.js", ) } func jsMainJs() (*asset, error) { bytes, err := jsMainJsBytes() if err != nil { return nil, err } info := bindataFileInfo{name: "js/main.js", size: 87, mode: os.FileMode(438), modTime: time.Unix(1594787764, 0)} a := &asset{bytes: bytes, info: info} return a, nil } // Asset loads and returns the asset for the given name. // It returns an error if the asset could not be found or // could not be loaded. func Asset(name string) ([]byte, error) { cannonicalName := strings.Replace(name, "\\", "/", -1) if f, ok := _bindata[cannonicalName]; ok { a, err := f() if err != nil { return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err) } return a.bytes, nil } return nil, fmt.Errorf("Asset %s not found", name) } // MustAsset is like Asset but panics when Asset would return an error. // It simplifies safe initialization of global variables. func MustAsset(name string) []byte { a, err := Asset(name) if err != nil { panic("asset: Asset(" + name + "): " + err.Error()) } return a } // AssetInfo loads and returns the asset info for the given name. // It returns an error if the asset could not be found or // could not be loaded. func AssetInfo(name string) (os.FileInfo, error) { cannonicalName := strings.Replace(name, "\\", "/", -1) if f, ok := _bindata[cannonicalName]; ok { a, err := f() if err != nil { return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err) } return a.info, nil } return nil, fmt.Errorf("AssetInfo %s not found", name) } // AssetNames returns the names of the assets. func AssetNames() []string { names := make([]string, 0, len(_bindata)) for name := range _bindata { names = append(names, name) } return names } // _bindata is a table, holding each asset generator, mapped to its name. var _bindata = map[string]func() (*asset, error){ "app2/app2app3/css/main.css": app2App2app3CssMainCss, "app2/app2app3/dirs/dir1/text.txt": app2App2app3DirsDir1TextTxt, "app2/app2app3/dirs/dir2/text.txt": app2App2app3DirsDir2TextTxt, "app2/app2app3/dirs/text.txt": app2App2app3DirsTextTxt, "app2/app2app3/index.html": app2App2app3IndexHtml, "app2/index.html": app2IndexHtml, "app2/mydir/text.txt": app2MydirTextTxt, "css/main.css": cssMainCss, "favicon.ico": faviconIco, "index.html": indexHtml, "js/main.js": jsMainJs, } // AssetDir returns the file names below a certain // directory embedded in the file by go-bindata. // For example if you run go-bindata on data/... and data contains the // following hierarchy: // // data/ // foo.txt // img/ // a.png // b.png // // then AssetDir("data") would return []string{"foo.txt", "img"} // AssetDir("data/img") would return []string{"a.png", "b.png"} // AssetDir("foo.txt") and AssetDir("notexist") would return an error // AssetDir("") will return []string{"data"}. func AssetDir(name string) ([]string, error) { node := _bintree if len(name) != 0 { cannonicalName := strings.Replace(name, "\\", "/", -1) pathList := strings.Split(cannonicalName, "/") for _, p := range pathList { node = node.Children[p] if node == nil { return nil, fmt.Errorf("Asset %s not found", name) } } } if node.Func != nil { return nil, fmt.Errorf("Asset %s not found", name) } rv := make([]string, 0, len(node.Children)) for childName := range node.Children { rv = append(rv, childName) } return rv, nil } type bintree struct { Func func() (*asset, error) Children map[string]*bintree } var _bintree = &bintree{nil, map[string]*bintree{ "app2": {nil, map[string]*bintree{ "app2app3": {nil, map[string]*bintree{ "css": {nil, map[string]*bintree{ "main.css": {app2App2app3CssMainCss, map[string]*bintree{}}, }}, "dirs": {nil, map[string]*bintree{ "dir1": {nil, map[string]*bintree{ "text.txt": {app2App2app3DirsDir1TextTxt, map[string]*bintree{}}, }}, "dir2": {nil, map[string]*bintree{ "text.txt": {app2App2app3DirsDir2TextTxt, map[string]*bintree{}}, }}, "text.txt": {app2App2app3DirsTextTxt, map[string]*bintree{}}, }}, "index.html": {app2App2app3IndexHtml, map[string]*bintree{}}, }}, "index.html": {app2IndexHtml, map[string]*bintree{}}, "mydir": {nil, map[string]*bintree{ "text.txt": {app2MydirTextTxt, map[string]*bintree{}}, }}, }}, "css": {nil, map[string]*bintree{ "main.css": {cssMainCss, map[string]*bintree{}}, }}, "favicon.ico": {faviconIco, map[string]*bintree{}}, "index.html": {indexHtml, map[string]*bintree{}}, "js": {nil, map[string]*bintree{ "main.js": {jsMainJs, map[string]*bintree{}}, }}, }} // RestoreAsset restores an asset under the given directory func RestoreAsset(dir, name string) error { data, err := Asset(name) if err != nil { return err } info, err := AssetInfo(name) if err != nil { return err } err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755)) if err != nil { return err } err = os.WriteFile(_filePath(dir, name), data, info.Mode()) if err != nil { return err } err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime()) if err != nil { return err } return nil } // RestoreAssets restores an asset under the given directory recursively func RestoreAssets(dir, name string) error { children, err := AssetDir(name) // File if err != nil { return RestoreAsset(dir, name) } // Dir for _, child := range children { err = RestoreAssets(dir, filepath.Join(name, child)) if err != nil { return err } } return nil } func _filePath(dir, name string) string { cannonicalName := strings.Replace(name, "\\", "/", -1) return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...) } ================================================ FILE: _examples/file-server/http2push-embedded/main.go ================================================ package main import ( "regexp" "github.com/kataras/iris/v12" ) // How to run: // $ go install github.com/go-bindata/go-bindata/v3/go-bindata@latesta // $ go-bindata -nomemcopy -fs -prefix "../http2push/assets" ../http2push/assets/... // # OR if the ./assets directory was inside this example foder: // # go-bindata -nomemcopy -refix "assets" ./assets/... // // $ go run . // Physical files are not used, you can delete the "assets" folder and run the example. var opts = iris.DirOptions{ IndexName: "index.html", PushTargetsRegexp: map[string]*regexp.Regexp{ "/": iris.MatchCommonAssets, "/app2/app2app3": iris.MatchCommonAssets, }, Compress: false, ShowList: true, } func main() { app := iris.New() app.HandleDir("/public", AssetFile(), opts) // https://127.0.0.1/public // https://127.0.0.1/public/app2 // https://127.0.0.1/public/app2/app2app3 // https://127.0.0.1/public/app2/app2app3/dirs app.Run(iris.TLS(":443", "../http2push/mycert.crt", "../http2push/mykey.key")) } ================================================ FILE: _examples/file-server/http2push-embedded-gzipped/bindata.go ================================================ // Code generated by go-bindata. (@generated) DO NOT EDIT. // Package main generated by go-bindata.// sources: // ../http2push/assets/app2/app2app3/css/main.css // ../http2push/assets/app2/app2app3/dirs/dir1/text.txt // ../http2push/assets/app2/app2app3/dirs/dir2/text.txt // ../http2push/assets/app2/app2app3/dirs/text.txt // ../http2push/assets/app2/app2app3/index.html // ../http2push/assets/app2/index.html // ../http2push/assets/app2/mydir/text.txt // ../http2push/assets/css/main.css // ../http2push/assets/favicon.ico // ../http2push/assets/index.html // ../http2push/assets/js/main.js package main import ( "bytes" "compress/gzip" "fmt" "io" "net/http" "os" "path/filepath" "strings" "time" ) func bindataRead(data, name string) ([]byte, error) { gz, err := gzip.NewReader(strings.NewReader(data)) if err != nil { return nil, fmt.Errorf("read %q: %v", name, err) } var buf bytes.Buffer _, err = io.Copy(&buf, gz) clErr := gz.Close() if err != nil { return nil, fmt.Errorf("read %q: %v", name, err) } if clErr != nil { return nil, err } return buf.Bytes(), nil } type asset struct { bytes []byte info os.FileInfo } type bindataFileInfo struct { name string size int64 mode os.FileMode modTime time.Time } // Name return file name func (fi bindataFileInfo) Name() string { return fi.name } // Size return file size func (fi bindataFileInfo) Size() int64 { return fi.size } // Mode return file mode func (fi bindataFileInfo) Mode() os.FileMode { return fi.mode } // ModTime return file modify time func (fi bindataFileInfo) ModTime() time.Time { return fi.modTime } // IsDir return file whether a directory func (fi bindataFileInfo) IsDir() bool { return fi.mode&os.ModeDir != 0 } // Sys return file is sys mode func (fi bindataFileInfo) Sys() any { return nil } type assetFile struct { *bytes.Reader name string childInfos []os.FileInfo childInfoOffset int } type assetOperator struct{} // Open implement http.FileSystem interface func (f *assetOperator) Open(name string) (http.File, error) { var err error if len(name) > 0 && name[0] == '/' { name = name[1:] } content, err := Asset(name) if err == nil { return &assetFile{name: name, Reader: bytes.NewReader(content)}, nil } children, err := AssetDir(name) if err == nil { childInfos := make([]os.FileInfo, 0, len(children)) for _, child := range children { childPath := filepath.Join(name, child) info, errInfo := AssetInfo(filepath.Join(name, child)) if errInfo == nil { childInfos = append(childInfos, info) } else { childInfos = append(childInfos, newDirFileInfo(childPath)) } } return &assetFile{name: name, childInfos: childInfos}, nil } else { // If the error is not found, return an error that will // result in a 404 error. Otherwise the server returns // a 500 error for files not found. if strings.Contains(err.Error(), "not found") { return nil, os.ErrNotExist } return nil, err } } // Close no need do anything func (f *assetFile) Close() error { return nil } // Readdir read dir's children file info func (f *assetFile) Readdir(count int) ([]os.FileInfo, error) { if len(f.childInfos) == 0 { return nil, os.ErrNotExist } if count <= 0 { return f.childInfos, nil } if f.childInfoOffset+count > len(f.childInfos) { count = len(f.childInfos) - f.childInfoOffset } offset := f.childInfoOffset f.childInfoOffset += count return f.childInfos[offset : offset+count], nil } // Stat read file info from asset item func (f *assetFile) Stat() (os.FileInfo, error) { if len(f.childInfos) != 0 { return newDirFileInfo(f.name), nil } return AssetInfo(f.name) } // newDirFileInfo return default dir file info func newDirFileInfo(name string) os.FileInfo { return &bindataFileInfo{ name: name, size: 0, mode: os.FileMode(2147484068), // equal os.FileMode(0644)|os.ModeDir modTime: time.Time{}} } // AssetFile return a http.FileSystem instance that data backend by asset func AssetFile() http.FileSystem { return &assetOperator{} } var _app2App2app3CssMainCss = "\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x4a\xca\x4f\xa9\x54\xa8\xe6\xe5\x52\x50\x50\x50\x48\x4a\x4c\xce\x4e\x2f\xca\x2f\xcd\x4b\xd1\x4d\xce\xcf\xc9\x2f\xb2\x52\x48\xca\x29\x4d\xb5\xe6\xe5\xaa\x05\x04\x00\x00\xff\xff\x52\xd7\xbb\x8b\x26\x00\x00\x00" func app2App2app3CssMainCssBytes() ([]byte, error) { return bindataRead( _app2App2app3CssMainCss, "app2/app2app3/css/main.css", ) } func app2App2app3CssMainCss() (*asset, error) { bytes, err := app2App2app3CssMainCssBytes() if err != nil { return nil, err } info := bindataFileInfo{name: "app2/app2app3/css/main.css", size: 38, mode: os.FileMode(438), modTime: time.Unix(1595043712, 0)} a := &asset{bytes: bytes, info: info} return a, nil } var _app2App2app3DirsDir1TextTxt = "\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x4a\x2c\x28\x30\xd2\x07\x11\x89\x05\x05\xc6\xfa\x29\x99\x45\xc5\x20\xc2\x50\xbf\x24\xb5\xa2\x44\xaf\xa4\xa2\x04\x10\x00\x00\xff\xff\x87\xaf\x9d\x00\x20\x00\x00\x00" func app2App2app3DirsDir1TextTxtBytes() ([]byte, error) { return bindataRead( _app2App2app3DirsDir1TextTxt, "app2/app2app3/dirs/dir1/text.txt", ) } func app2App2app3DirsDir1TextTxt() (*asset, error) { bytes, err := app2App2app3DirsDir1TextTxtBytes() if err != nil { return nil, err } info := bindataFileInfo{name: "app2/app2app3/dirs/dir1/text.txt", size: 32, mode: os.FileMode(438), modTime: time.Unix(1594843207, 0)} a := &asset{bytes: bytes, info: info} return a, nil } var _app2App2app3DirsDir2TextTxt = "\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x4a\x2c\x28\x30\xd2\x07\x11\x89\x05\x05\xc6\xfa\x29\x99\x45\xc5\x20\xc2\x48\xbf\x24\xb5\xa2\x44\xaf\xa4\xa2\x04\x10\x00\x00\xff\xff\x84\x14\xaa\xeb\x20\x00\x00\x00" func app2App2app3DirsDir2TextTxtBytes() ([]byte, error) { return bindataRead( _app2App2app3DirsDir2TextTxt, "app2/app2app3/dirs/dir2/text.txt", ) } func app2App2app3DirsDir2TextTxt() (*asset, error) { bytes, err := app2App2app3DirsDir2TextTxtBytes() if err != nil { return nil, err } info := bindataFileInfo{name: "app2/app2app3/dirs/dir2/text.txt", size: 32, mode: os.FileMode(438), modTime: time.Unix(1594843207, 0)} a := &asset{bytes: bytes, info: info} return a, nil } var _app2App2app3DirsTextTxt = "\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x4a\x2c\x28\x30\xd2\x07\x11\x89\x05\x05\xc6\xfa\x29\x99\x45\xc5\xfa\x25\xa9\x15\x25\x7a\x25\x15\x25\x80\x00\x00\x00\xff\xff\x64\xfe\x96\xd6\x1b\x00\x00\x00" func app2App2app3DirsTextTxtBytes() ([]byte, error) { return bindataRead( _app2App2app3DirsTextTxt, "app2/app2app3/dirs/text.txt", ) } func app2App2app3DirsTextTxt() (*asset, error) { bytes, err := app2App2app3DirsTextTxtBytes() if err != nil { return nil, err } info := bindataFileInfo{name: "app2/app2app3/dirs/text.txt", size: 27, mode: os.FileMode(438), modTime: time.Unix(1594843207, 0)} a := &asset{bytes: bytes, info: info} return a, nil } var _app2App2app3IndexHtml = "\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x54\x90\x3f\x4f\xc3\x40\x0c\xc5\xf7\x48\xf9\x0e\xc6\x33\xed\x91\x76\x61\xb8\x8b\x54\xf1\x47\x6c\x30\x94\x81\xf1\x7a\x31\x9c\x85\x73\x39\xe5\x4c\x4b\xbf\x3d\x4a\x68\x90\x58\x6c\x3d\xfb\xbd\x9f\x64\xdb\xab\xfb\xe7\xbb\xfd\xdb\xcb\x03\x44\xed\xa5\xad\x2b\x3b\x75\x10\x9f\x3e\x1c\x52\xc2\xb6\xae\xa6\x19\xf9\xae\xad\x2b\x00\x00\xdb\x93\x7a\x08\xd1\x8f\x85\xd4\xe1\xeb\xfe\x71\x75\x8b\xff\x76\xc9\xf7\xe4\xf0\xc8\x74\xca\xc3\xa8\x08\x61\x48\x4a\x49\x1d\x9e\xb8\xd3\xe8\x3a\x3a\x72\xa0\xd5\x2c\xae\x81\x13\x2b\x7b\x59\x95\xe0\x85\x5c\xb3\xbe\xf9\x63\x09\xa7\x4f\x18\x49\x1c\x16\x3d\x0b\x95\x48\xa4\x08\x71\xa4\x77\x87\x26\x7f\x1d\x84\x83\xf1\x39\x6f\xe6\xe2\x73\xde\x9a\x50\x8a\xe9\x3d\xa7\x75\x28\x05\xc1\x2c\x20\x65\x15\x6a\x77\x39\x6f\x76\x39\x6f\xad\xf9\xd5\x75\x65\xcd\xe5\xac\xba\xb2\x87\xa1\x3b\x2f\xfe\xd8\xb4\x4f\x24\x32\xc0\x12\x01\x4e\x1d\x7d\x5b\x13\x9b\x39\x75\xf1\xce\x80\xe9\x67\x3f\x01\x00\x00\xff\xff\xef\x25\x54\xc8\x43\x01\x00\x00" func app2App2app3IndexHtmlBytes() ([]byte, error) { return bindataRead( _app2App2app3IndexHtml, "app2/app2app3/index.html", ) } func app2App2app3IndexHtml() (*asset, error) { bytes, err := app2App2app3IndexHtmlBytes() if err != nil { return nil, err } info := bindataFileInfo{name: "app2/app2app3/index.html", size: 323, mode: os.FileMode(438), modTime: time.Unix(1595043725, 0)} a := &asset{bytes: bytes, info: info} return a, nil } var _app2IndexHtml = "\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xb2\xc9\x30\xb4\xf3\x48\xcd\xc9\xc9\x57\x70\x2c\x28\x30\x52\xc8\xcc\x4b\x49\xad\xb0\xd1\xcf\x30\xb4\x03\x04\x00\x00\xff\xff\x75\x17\xab\xfa\x19\x00\x00\x00" func app2IndexHtmlBytes() ([]byte, error) { return bindataRead( _app2IndexHtml, "app2/index.html", ) } func app2IndexHtml() (*asset, error) { bytes, err := app2IndexHtmlBytes() if err != nil { return nil, err } info := bindataFileInfo{name: "app2/index.html", size: 25, mode: os.FileMode(438), modTime: time.Unix(1565946440, 0)} a := &asset{bytes: bytes, info: info} return a, nil } var _app2MydirTextTxt = "\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xca\x2a\x2d\x2e\x51\x48\x54\x28\x49\xad\x28\xd1\x03\x04\x00\x00\xff\xff\x2f\xf9\x22\x98\x0c\x00\x00\x00" func app2MydirTextTxtBytes() ([]byte, error) { return bindataRead( _app2MydirTextTxt, "app2/mydir/text.txt", ) } func app2MydirTextTxt() (*asset, error) { bytes, err := app2MydirTextTxtBytes() if err != nil { return nil, err } info := bindataFileInfo{name: "app2/mydir/text.txt", size: 12, mode: os.FileMode(438), modTime: time.Unix(1594787248, 0)} a := &asset{bytes: bytes, info: info} return a, nil } var _cssMainCss = "\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x4a\xca\x4f\xa9\x54\xa8\xe6\xe5\x52\x50\x50\x50\x48\x4a\x4c\xce\x4e\x2f\xca\x2f\xcd\x4b\xd1\x4d\xce\xcf\xc9\x2f\xb2\x52\x48\xca\x49\x4c\xce\xb6\xe6\xe5\xaa\xe5\xe5\x02\x04\x00\x00\xff\xff\x03\x25\x9c\x89\x29\x00\x00\x00" func cssMainCssBytes() ([]byte, error) { return bindataRead( _cssMainCss, "css/main.css", ) } func cssMainCss() (*asset, error) { bytes, err := cssMainCssBytes() if err != nil { return nil, err } info := bindataFileInfo{name: "css/main.css", size: 41, mode: os.FileMode(438), modTime: time.Unix(1565946440, 0)} a := &asset{bytes: bytes, info: info} return a, nil } var _faviconIco = "\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xec\x5a\x0b\x70\x55\xc7\x79\xde\x7b\xce\x45\x12\xb2\x90\xc4\xc3\xe6\x61\x3b\x90\xf8\x31\xc4\x19\x6c\x32\xe3\xc4\x34\xe3\xc6\x34\x6d\xed\xd4\x69\x62\x32\x49\x9a\xa6\x75\xea\x36\x33\xee\xd8\x9e\xb4\x75\xdc\x7a\xa6\xc5\x31\x02\xa6\xd3\x84\x47\x28\x6f\x3b\xc6\x3c\xcc\xeb\x9e\xbd\x08\x21\x04\x92\x28\x12\x0e\x08\x5b\x3c\xec\x02\x92\x78\x08\x21\x09\x41\x25\x24\x10\xe8\x71\xb5\xe7\xdc\xd7\xb9\xf7\xef\xfc\xff\x9e\x73\x74\x25\xdd\x97\x24\x82\x33\x1e\xce\xcc\x3f\x7b\xee\x9e\xdd\xff\xff\xf6\xdf\x7f\xff\xfd\xf7\xdf\xcb\x98\x8b\xa9\x2c\x3f\x1f\xcb\x19\xec\x15\x37\x63\x5f\x67\x8c\xcd\x98\x21\x7f\x6b\xf9\x8c\x6d\x72\x33\x36\x7b\xb6\xf5\xfb\x11\xc6\x9e\xbd\x97\xb1\x99\x8c\xb1\x7c\x6c\xc7\x64\x3d\x3d\x6e\x76\xdb\x1f\xe1\x75\xab\x42\x53\x7e\x22\x3c\x6c\x91\xf0\xb0\x02\x22\xae\x14\x08\x8f\xab\x40\xec\x64\x92\xf0\x5d\x53\xfe\x59\xec\x64\x5f\x15\x1e\xa6\x08\x4f\x7f\x7f\xdf\x7a\x96\xa1\x97\x3c\xb8\x3f\x78\xfa\x0d\x08\x9e\x5d\x08\xc1\xda\x5f\x82\x51\xf6\x65\x30\xca\x67\x41\xf0\xcc\xbf\x11\x19\x07\x1e\x07\xbd\x78\x2a\x18\x65\x8f\xb5\x0b\x4d\x7d\xad\x6f\x07\x73\x0b\xcd\x45\xfd\xbb\x17\xb3\x0c\xa3\x62\xce\xfe\xa8\xd1\x0a\xf8\x98\x6d\x25\x10\x38\xfe\x53\x88\xdc\x3a\x01\x66\x6b\x11\x11\xbe\x07\x8e\xfd\x2d\x84\x2e\x2c\x05\xff\xd1\x79\x3d\xc2\xc3\x7e\x20\x78\x06\x13\x5c\x89\xe9\xdf\x06\xd1\x50\x2f\x04\x4e\xfc\x0c\xcc\x6b\xa5\x60\x1c\xfd\x21\x74\x6c\x1e\x47\x84\xef\x58\x17\xf8\xe4\x65\x30\x6f\x54\x81\x71\xe0\x89\xea\xbe\x2d\x6c\xd2\x80\xfe\xfe\x76\x30\x6f\x1c\x81\x60\xed\x7c\x08\x9e\x5d\x0c\x2d\xeb\xc7\x41\xed\xd2\x29\x50\xbb\x74\x32\xbd\x63\x1d\x7e\xc3\x36\xa1\x73\xff\x19\x14\x1e\x36\x4f\x78\xc7\xc4\xf4\xef\x80\x70\xd3\xfb\x10\x6e\x5c\x0f\xdd\x65\x73\xe1\xdc\xf2\x89\xd0\xb1\x25\x0f\x6e\x7c\x90\x07\xe7\x7f\x33\x81\xea\xf0\x5b\xb8\x79\x13\x61\xd0\x8b\xa7\x2c\x09\x7c\xfc\x57\xac\x7b\x11\xf6\x7f\x6a\x5f\xd4\xb8\x06\xa1\xfa\x15\x10\x6e\x7a\x0f\x3a\xb4\xc7\xa0\x61\x55\x3e\x04\x8f\xfc\x39\x84\x3e\xfa\x4b\x68\x5c\x93\x4f\x75\xf8\x2d\x74\x71\x05\x44\x45\x13\xea\x54\xeb\x7c\x85\xa9\x48\xf8\x1e\x15\xcd\xf4\xcd\xee\x7f\x71\x65\x3e\x04\x3e\x7c\x86\x78\x5c\x5a\x1d\xd3\xbf\x7e\x05\xa0\x2c\x94\x89\xb2\x03\xd5\x3f\x66\x88\x05\x31\x21\x36\x1b\xff\xd9\xe5\x13\xe1\xda\xc6\x5c\x68\xdf\x94\x4b\x63\x71\xf0\x37\xbd\x0f\x38\x56\x1c\x33\x8e\x1d\x75\x80\xba\x40\x9d\x0c\xd6\x5f\xcd\x92\x29\x44\x83\xf5\x87\xba\x76\xfa\x73\x85\xe1\x5c\x18\x07\x9e\x38\x86\x18\x68\x8e\xac\xf9\xbb\xbe\x39\x87\xc8\x99\xbf\x13\x3f\xa3\x39\xc6\xb9\x8e\xed\x4f\xb6\xe0\x61\x3f\x40\xdb\x40\x1b\x41\x5b\x19\x62\x3f\xc7\x7f\x4a\xb6\x85\x0f\xda\x9a\xdd\x9f\xd6\x80\xe6\x62\xd2\x26\xd5\xd7\xd0\x46\xc9\x56\x0f\x3c\xde\x6f\xbf\xe5\xb3\xc8\xa6\xd1\xb6\xc9\xc6\x4f\xbf\x01\x68\xf3\x68\xfb\xce\x3a\xf2\x30\x26\x76\x32\x85\xd6\x08\xae\x95\xc1\xeb\x87\xd6\x14\xb3\x69\x11\xad\x39\xaf\x5b\x1d\xed\xfa\x05\x60\x8c\x65\x32\xc6\x54\x8b\x5c\x31\x64\x3d\x0b\x63\xe8\xb0\x45\x2d\x56\xdf\x99\x96\x8f\x99\x1b\xeb\x67\xf2\x47\x8b\xea\xf3\xf9\x08\xae\x22\xe5\x09\xae\x4e\x17\x5c\xfd\xc2\x28\x69\x8a\xe0\xca\x58\xb4\x3d\xdb\x17\xa6\x29\xff\x5f\x04\x57\x5b\x85\xe6\xba\x92\x90\xb8\x1a\x43\x4a\x4c\xbd\x62\xd7\xb7\x08\xae\xd6\x0b\xae\xfe\x8f\xe0\xea\x9b\x38\x1e\x1f\x1f\x4b\xfe\x34\xb5\x7c\xa5\x40\xec\xca\x02\xbd\x64\x3a\xe8\xfb\xbe\x38\x94\x4a\xa6\x83\xf0\x66\x82\xd0\x14\x10\x9a\x0b\x44\xe1\x38\x5c\x2b\x44\xf8\x4e\x75\x5c\x01\xe1\x75\x03\xf2\x11\x9a\x2b\x2a\xb8\x52\x2b\xb8\x3a\x4f\x68\x8a\x92\x0c\x03\xc9\xf7\xb0\x02\xa3\xfc\x2b\x10\xe9\xae\x85\xa8\x7e\x15\xa2\xfa\x95\x18\xba\x0a\xa1\xb3\x8b\x49\xbe\xbe\xe7\x5e\x08\xfe\xef\x3f\x81\xd9\x51\x09\x91\xbe\x4b\x44\xf8\x8e\x75\xf8\x4d\x70\x37\x04\x3e\xfe\x11\xf9\x14\xbd\x68\x3c\xe2\xe8\x14\x5c\x7d\x49\x78\x99\x2b\x11\x06\x47\xfe\x81\xd9\x10\x0d\xde\x82\xc1\x8f\x79\xe3\x30\xe8\x7b\xa7\x81\x51\xfa\x28\x98\xd7\xca\x00\xa2\xa6\xfc\x80\x65\xcc\x3b\x7e\xc3\x36\xfa\xde\x07\x08\x93\xd9\x5a\x0c\xfa\xfe\x87\x41\x68\xac\x43\x70\xf5\x59\x92\xe3\x1d\xea\x1a\x92\xc9\x8f\x06\x3a\xc1\xff\xe1\x5c\xda\x67\x91\x27\x3e\x91\xde\x0b\xe4\xaf\xfc\x47\x5f\x20\xc2\x77\xac\x23\xac\x1d\x95\xd4\xd6\xff\xe1\x9f\x10\x2f\xfa\x5d\xf2\x05\xc4\x70\x4c\x70\xf5\x7e\x94\x35\x1c\xf9\xa1\xc6\x77\x68\x3e\x43\x17\x57\x4a\xfe\xe8\xc3\xcb\xbe\x8c\xfc\xa0\xcf\x23\x09\xdf\xb1\x0e\xbf\x51\x9f\x8b\x2b\x65\x9f\xc6\x77\xe8\x37\xee\x4f\x62\xd7\x58\xb4\x8f\x05\x3e\x9e\x35\xc4\x1e\x13\xc9\x8f\x06\xbb\xc0\xa8\xfc\x23\xf0\x1f\xfa\x63\x88\x86\xba\x21\x72\xeb\x24\x18\xfb\x1f\x82\xde\x1d\x2e\x68\xdd\x30\x0e\x1a\x56\x4f\x24\xc2\x77\xac\xc3\x6f\xd8\x06\xdb\x62\x1f\xec\x8b\xef\x10\xd6\xc1\xff\xd1\xf7\x11\x67\x83\xe0\xea\x97\x06\xeb\x20\x91\x7c\xd2\xdd\xee\x3c\x08\x37\x6f\x04\x30\x0d\x08\x7c\x34\x8f\xe4\x34\xae\x99\x00\xa7\x7f\x3d\x0d\x4e\xfd\xea\x7e\x22\x7c\xc7\x3a\xfc\x86\x6d\xb0\x6d\xb8\x69\x23\xf5\xb5\xe7\xcc\x6c\x3f\x00\x62\x77\x6e\x54\x68\xae\x57\xad\xf5\x96\x52\x7e\xb0\x6e\x01\xe8\xfb\x66\xd0\x1a\x30\xaf\x95\x83\x28\xcc\x81\x2b\xef\xe6\x92\x3c\x24\x8a\x63\x96\x4d\x71\x7e\xe3\x37\x6c\x83\x6d\xb1\x0f\xf6\x0d\xd6\x15\x48\x5d\x86\x7c\x64\x47\xc2\xc3\xf6\x0a\xae\x66\xc6\xea\x20\xae\xfc\x48\x08\xfc\x55\xdf\x05\x7f\xd5\x77\x00\x22\x41\x08\x7e\xfa\x2a\x74\x6d\x53\xa1\x6e\xf9\x64\x92\x75\x69\xcd\x44\xb8\xb9\x35\x13\x6e\x6d\xcb\x80\xe6\x75\xe3\xa9\x0e\xbf\x61\x1b\x6c\x8b\x7d\xb0\xaf\xbf\xea\x7b\xc4\x8b\xec\xe2\xec\x22\x9c\x83\x66\xc1\xd5\x19\xa9\xe4\xd3\xdc\x1f\x98\x0d\xc1\x33\x6f\x42\x34\xec\x03\x7f\xe5\x1c\x68\xdb\x30\x96\xe4\x9c\xfb\xcd\x7d\xd0\xbd\x63\x0c\xe8\x85\x63\x41\x2f\xbc\x07\x7a\x3d\x6e\xa8\x5f\x39\x89\xbe\x61\x1b\x6c\x8b\x7d\xb0\xaf\xe4\xd9\x65\xd9\x6e\x19\x88\x5d\xd9\xba\xd0\x5c\x73\x53\xca\xd7\xff\x0f\xf4\xfd\x0f\x41\xa8\x61\x35\xf9\x1f\x7d\xdf\x74\x68\x5e\x97\x47\xf3\xdd\xf2\x4e\x1e\xc9\x0d\x9f\xfc\x3b\x30\x4f\xbd\x02\xc6\x9e\x09\xd0\xba\x21\x07\x4e\xfd\x6a\x1a\xb5\xc1\xb6\xe4\xb3\x1a\x56\x13\x0f\xe4\x25\xd7\xed\x39\x5c\x9b\x51\xa1\xb1\x17\xe3\xc8\x7f\x0b\x63\x13\x5c\xef\xd4\xb6\xaf\x91\x7c\x6b\xf8\xf2\x16\x5a\xdb\x62\xcf\x64\xb8\xb4\x7a\x3c\xc9\x68\xdb\x90\x0d\xfe\xb2\x87\x01\x2e\x2d\x07\x68\x5a\x05\x81\xca\x27\xe1\xfa\xe6\x4c\x39\x2f\xab\xc7\x53\xdb\x88\xaf\x9e\xfa\x22\x0f\xe4\xe5\xc4\x47\xa5\x8f\xa2\x0d\xfc\x22\x8e\xfc\x5f\xe0\x37\x3b\x5e\x4f\x2e\xff\x1e\xf0\x97\x7e\x11\xa2\x0d\x4b\x00\x9a\x56\x42\xe0\xe0\x13\x70\x7d\x53\x56\x6a\xf9\x81\x4e\x8a\xbf\x70\xac\x43\xe4\x6b\xec\x45\xd4\x0d\xea\x28\xbe\xfe\x67\x38\xfa\x6f\x5e\x97\x2f\xfd\xcb\xd1\xe7\x21\x74\xec\x87\xa0\xef\xce\x81\xab\xbf\x1d\x67\x7d\xcb\xb3\xd6\xcc\x50\xfd\xe3\xdc\xe2\x1c\xcb\xb3\xd0\x60\xf9\xae\xb9\x68\x1b\xe4\xdf\x53\xd8\x1f\xda\x39\xda\xbe\xee\x55\x41\xf7\xba\xc9\x16\xcf\xaf\xb8\x2f\xa5\xfd\x25\x95\x8f\x6b\x42\x63\xcd\xb8\xcf\x25\x5c\x7f\x5b\xe5\xfa\xc3\x39\xb8\xf0\xdf\xf7\x42\xfb\xc6\x7b\xa0\x63\x53\x36\x5c\x5c\x35\x29\xad\xf5\x97\x42\x7e\x26\xfa\x06\xda\x37\xc2\xbe\xb4\xfc\x0f\xd2\x99\x25\xd3\xd2\xf2\x3f\xc9\xe5\x2b\xf6\x1c\xbc\x86\x3e\x12\x7d\xe5\x50\xff\xeb\x8f\xe3\x7f\x25\x0d\xf5\xbf\xfe\x21\xfe\x37\x99\xfc\x18\x1d\x7c\x09\xf7\x08\xdc\x2b\xc0\xd4\x69\xef\x30\x2a\x46\xb9\xff\x58\x73\x9f\x5a\xbe\xc2\x7c\x1a\xed\x8d\x0b\x70\xaf\xc4\x3d\x53\xee\xbf\xef\x8e\x7a\xff\x4d\x47\x7e\x8c\x0e\xee\xc7\x58\x01\x63\x06\xd4\x1d\xf6\x41\x9b\x18\x51\xfc\x61\xf9\xb2\xb4\xe5\x7b\x55\x1b\xc3\xb3\x18\x33\x61\xec\x84\x31\x14\xf1\xdc\xfb\x40\x5a\xf1\x97\x4e\xf1\xd7\x34\x8a\xd9\x06\x3f\xa9\xe4\x3b\xb6\x48\xb1\xa2\xfa\x12\xc6\x8e\x18\x43\x62\x2c\x89\x31\x25\xc6\x96\x29\xe3\x4f\x6f\x26\xc5\xaa\xf1\x62\x58\x8c\x6d\x31\xc6\x4d\x26\xbf\x7f\x3d\x60\xcc\xac\xce\xa3\x18\x1a\x63\x69\x8c\xa9\xb9\x5b\xc6\xd8\x14\x7f\xe7\xc4\xc4\xdf\x39\xb2\x0e\x63\x73\x8c\x91\x93\xc5\xf0\xc4\x47\x49\x2a\x9f\x30\x68\x0a\xf3\x15\x65\x31\x6b\xaf\x7e\xd3\x3a\x53\xd4\x5b\x67\x8c\x44\xe7\x0f\x49\xc9\xcf\x30\xad\xd6\x59\x27\xa9\xfc\x58\x5d\xf4\x69\x14\x2f\x8d\x95\x67\xab\x51\x9f\xcf\xa6\x5b\x67\xbd\xb4\xe4\x7f\xde\x1f\x7b\x6d\x1c\x66\x2a\x1c\x66\x0c\xe9\x99\xc3\x8c\x4d\xb7\x28\xcf\xa2\x4c\x8b\xd4\x74\xa8\xc5\xa2\x1e\x8b\x02\x16\x99\x8c\xa9\xc0\x48\x90\x6a\xcb\x9d\xc9\x18\x9b\xcd\x18\xfb\xfb\xd8\x3c\xc5\xc3\x9f\xb5\x56\xee\x3e\x77\x9f\x3f\xcc\xc7\xda\x1f\x1f\x16\x5c\x7d\x5b\x70\x75\xa1\xe0\x6a\xc1\xef\x89\x6c\xde\xff\x2a\xb8\xfa\x37\x82\x2b\xb3\x05\x57\x72\x7a\x35\xc6\x0c\x2d\x79\x3e\x29\x0d\xfc\xdf\x16\x5c\x0d\x0a\xae\xc2\x1d\x22\x53\x70\xb5\x53\x70\xf5\x20\xed\xeb\x5c\xc9\xc7\x58\xc3\x48\x33\x3f\x97\x18\xbf\x42\xb1\x57\x5a\x84\x6d\x07\x60\x4a\xd2\x37\x6e\x5b\x97\xfd\x1b\xf5\x56\x2a\xb8\xfa\x24\xec\x67\x4c\xe7\xc3\x1b\x83\x83\x5f\x73\x05\x31\xc6\x0b\xd6\xbe\x45\xe7\xf2\x64\x14\x3c\xbb\x08\x8c\x8a\xa7\xfa\x71\x21\xc6\xc2\x1c\x30\x0e\x7e\x0d\x02\x27\xfe\x81\x78\x20\xe1\x3b\xd6\xc9\x78\x84\x39\xd8\xf5\xe2\x29\x60\x94\xcd\x04\xe1\xcd\x8a\x1d\x47\xa3\xe0\xea\x0b\x7e\xce\x5c\x7a\x8a\xfc\x64\x7c\xfc\x2c\x48\xf1\x75\xb8\x6f\x48\x9c\x37\xf8\x31\xdb\x4a\x64\x9c\x84\xb2\xbd\x19\xe0\x3f\xf2\x17\x60\xb6\xee\x85\x68\xe0\x26\x40\x34\x12\x13\x20\x46\xa8\x0e\xbf\x61\x1b\x6c\x8b\x7d\xf4\xe2\xc9\x10\x6a\x58\x0b\xe1\xcb\x5b\xc1\xa8\xfc\x46\xff\x9c\x70\xb5\x0d\xc7\xd0\xa3\x65\x33\x91\xe6\x3c\x0c\x17\x7f\xa4\xeb\x14\xc5\xb4\xc2\xc3\xe8\x3c\x43\xb1\x65\xb0\x3b\xe5\x98\xb1\x0d\xb6\xc5\x3e\xf2\x7c\x30\x13\x22\xdd\x35\x10\xf5\x5f\xa7\x73\xa9\x28\xbc\xc7\x1e\x43\x93\xe0\xca\x53\x14\x73\x16\xa6\x8e\x89\x86\x83\x9f\xce\x41\x55\xdf\x23\xec\x62\x77\x2e\x9d\xc7\xed\xb3\xa8\x1c\x5c\x10\xa2\xa2\x05\x22\xb7\x3e\x21\xc2\x77\xac\xeb\xff\x1e\xa2\x3e\xd8\x17\x79\x20\xaf\x68\xa8\x87\xda\x84\xea\x97\xcb\xbc\xb3\x1c\x43\xa5\xe0\xea\xe4\x74\x62\xba\xe1\xe0\x0f\x35\xac\x91\x36\xc0\xdd\x10\xac\x7b\xbb\x1f\x7b\x34\x0c\x66\x47\x05\xdd\x3d\xe9\xa5\x8f\x80\x5e\x34\x51\x52\xe9\x23\xf2\x3e\xaa\xa3\x82\xda\xd8\x63\x08\xd6\xfe\x52\xc6\xee\xde\x0c\xe2\x69\x8f\x1d\xcf\x4d\x92\xbf\x1a\x45\x7f\xeb\xf3\x32\x57\x5f\x0a\x3b\x4a\x17\x3f\xea\xd2\x28\x9f\x25\xf5\x76\xe4\x3b\xfd\x79\xad\x70\x1f\x84\xce\xff\x17\xe8\x7b\x26\x59\xfe\xc6\xca\xdb\xdb\x3e\x46\x63\xf4\x0d\xdb\xd8\xbc\xe9\x0c\x78\xe4\x79\xe2\x85\x3c\x69\x9e\x68\x7e\x7b\x21\x50\xfd\x63\x7b\x4d\x5f\x13\x5c\xfd\x7a\xca\x73\x45\x9a\xf8\x69\x7e\xad\xb5\x67\xde\xa8\xea\xd7\x59\xdd\x02\x79\x0f\xc1\xa5\x1f\xe9\xd9\x39\x06\x6e\x6e\xcd\x22\xc2\x77\xb9\x36\x5d\xd4\x06\xdb\xda\xf6\x64\xde\x38\x42\xbc\x90\x27\xf2\x76\x4c\xac\xbb\x86\x72\x44\xd6\x18\xde\xd3\xb9\xcb\x9d\xcc\x1f\xa5\x83\x9f\xf2\x5c\x15\x73\x48\x5f\x81\x4f\x5f\x75\xce\xa8\xe1\xcb\x5b\x1c\x9b\x45\xac\x57\xde\xcd\xa3\x5c\x66\xcd\xd2\x29\x44\xf8\x8e\x75\x72\x1c\x0a\xb5\xc5\x3e\x92\xa9\x49\xbc\x68\x0e\x2a\xe6\x0c\x38\x5b\x87\x2e\x2c\xb1\xce\x86\x6a\xbb\xe0\xea\x57\x93\x9f\x2d\x53\xe3\xa7\x5c\xcf\xae\x6c\xb2\x03\xb3\xb3\x5a\xea\xa9\xaf\x51\x9e\x5d\x35\x06\xdd\xdb\x33\x9c\x9c\x53\x3c\xc2\x6f\xd8\x86\xfc\x4e\xf9\x57\x9c\xfc\x9f\xd9\xf9\xb1\xb4\xbb\xc2\x6c\x30\xdb\xcb\xfb\xf5\xa5\x5f\x95\x79\x41\xb9\x67\x2c\x12\xdc\x9d\xf0\xce\x2a\x1d\xfc\xe4\xdf\xc8\x5f\x7c\x97\x72\xf1\x54\x57\xf3\xef\x64\x17\xbd\x1e\x37\xe5\x6c\x12\x61\xb7\x09\xdb\x60\x5b\xec\x83\x7d\xe5\x00\x0c\xe2\x89\xbc\x51\xc6\x00\x99\x75\x0b\x6d\x1b\x3a\x2e\xb8\x32\x61\xa4\xf8\xf1\x37\xe5\xe9\x35\x06\xa1\x86\x55\x96\x7e\x5a\xc0\x28\x9d\x49\x75\xad\xef\x8d\x1b\x82\xb5\x76\xd9\xc0\x7b\x00\x9b\xb0\xad\xed\xfb\xa3\xfa\x15\x69\x2b\x17\x57\x51\x1d\xca\x88\x95\x8d\xfe\x57\xde\x9f\x29\x3e\xc1\xd5\x6f\x26\xce\x8f\x24\xc7\x1f\xf1\x35\x80\x5e\xf2\x00\xe8\xbb\xf3\x21\x72\xf3\x98\x65\xf7\x1f\x80\xf0\x8e\x81\x9e\x1d\x63\xc8\xc6\x6d\x7c\x35\x4b\xa7\x92\xbd\xdf\xda\x96\x09\x5d\xdb\x33\xa1\x6d\x43\x8e\x73\xbf\x60\xe7\xf9\xb1\x0f\xfa\x48\xdc\x7b\x89\xff\xcd\x6a\xe2\x4d\x79\x65\x5f\xc3\x40\xbd\xfd\xee\x5b\xb6\x0d\x51\x7e\x3b\xde\x3a\x4e\x85\xdf\xec\x38\x44\xb6\x6f\x94\x3d\x46\xff\x67\xc0\x27\x70\xf2\x65\xe2\x7b\xed\xfd\x9c\x98\xbc\xe7\x54\xa9\x5f\x27\x16\x50\x40\xe7\x2a\x5c\xdf\x9c\x4d\xf7\x32\x76\x3b\xec\x83\x7d\x03\x9f\xfc\xa3\xc4\xe9\x6f\x27\xde\x28\x03\x65\x0d\xb0\xa1\x9a\xff\xb0\xf1\x6f\xd3\xb9\x2b\xee\x9d\x69\x2a\xfc\xa4\x6b\x9c\xdf\xc3\xcf\x01\x44\x02\x94\xcb\x35\x2a\x9f\xa6\xba\xa6\xb5\x13\x9c\x7c\x29\xae\x51\x9f\x47\xe6\x93\xf4\xc2\x6c\xd0\x77\x8f\x73\xe2\xcd\xcb\xeb\xf3\x9d\xfc\x2a\xf6\x21\x7e\x87\x9e\x96\x79\xe1\x48\x80\x78\x63\x1d\xca\x1a\x20\xfb\x2a\xb7\xf7\xb3\x6a\xc1\x95\xdc\x91\xe0\x0f\x5d\x58\x26\xfd\xe6\xf1\x97\xa4\xbe\xc4\x65\xca\x5d\xf5\xee\x54\x29\xe7\x6d\xeb\xb5\x0d\xf5\xca\x5d\x60\xec\x9d\x4a\x77\x30\x91\xda\x37\xc0\x5f\x3e\x93\xc6\xd3\xf9\xc1\x58\xb2\x2d\x3b\x4f\xee\xdb\xa9\x12\x0f\xe4\x45\xf3\x79\xfc\x25\x92\x81\xb2\x06\xd8\xee\xcd\x13\xf2\xce\x98\x2b\x4d\x82\xab\x0f\x26\xc9\x91\x3e\x97\x08\x3f\xed\x4f\x3b\x19\x04\x4f\xbd\x2e\x79\xf6\xd4\xd2\xbe\xd3\xbd\x7d\x0c\x9c\xb5\x6c\x1b\xb1\xe1\x7e\xa5\x7b\x55\x08\x1d\xfb\x11\x40\xf3\x5a\x22\xf3\xf4\xcf\x69\x2e\xd0\xff\xa3\xed\x23\x7e\xec\xd3\xbd\xdd\x4d\x31\x74\xa4\xa7\x4e\xca\x38\xf5\xba\x94\x81\xfb\x5b\xac\xef\x10\xcd\x74\xf7\x64\xdd\x79\xcf\x4a\x82\xff\x5b\x42\x63\x86\x71\xf0\xc9\x21\xb1\x24\xf9\x4e\xe4\x5d\x3b\xdf\xd2\xc9\x71\xd0\x8b\xf2\xa1\x6b\x5b\x86\xe3\x63\xb0\xec\xda\x9e\x41\xf7\x6f\x66\xcd\xeb\x00\xcd\x6b\x00\x9a\x56\x43\xb4\x7e\x31\x18\x7b\x27\x83\xcf\x23\xe7\x0a\xf1\x3b\x6d\x8b\xf2\xe9\xff\x3a\x24\xa3\x76\xbe\x94\x31\xc8\x87\x62\x6c\x4a\x31\x8b\xc6\xfa\x04\x57\x9f\x4e\x82\xff\x69\x6c\x43\xb1\x88\xff\xfa\xc8\xf0\x6f\xcb\x00\x7d\x57\x16\x98\x67\x7e\xde\x8f\xff\x42\x01\x18\xc5\x93\x08\xff\xf9\x91\xe0\x0f\x76\x03\xea\x14\x75\x4b\x3a\x4e\x8c\x7f\x16\xe5\x96\xf7\xcd\xa0\x39\x4b\x6e\x3f\x75\x34\xf7\x68\x03\xb6\xfd\xa0\xef\xb9\xb1\x85\xee\xd3\x21\x78\xe4\xcf\xe8\x0e\x10\xed\x27\x7c\xe2\x45\xd0\xbd\x19\xd0\xbd\x23\x83\xda\x0e\xdb\x7e\xc2\x7d\x74\x67\x81\xb6\x4d\x36\x9e\x18\xff\x83\xb8\x46\xf4\xa2\x09\x74\xdf\x31\x92\xf5\x7b\xf5\xb7\xb9\x96\xef\xc9\x81\x60\xd5\x73\x10\xaa\xfe\x3e\xe8\xc5\xf7\x82\xce\x15\xba\xd3\xc2\x31\x9e\xfa\xb5\xbd\x7e\x95\xb4\xd6\xef\x20\xfc\xdf\x8e\x8f\x9f\x72\xeb\xb9\xe4\xa3\x70\x5f\xb9\xca\x07\xf0\x48\xd7\x7f\x9e\x5b\x81\x7b\x53\x46\x7f\xec\x6c\x91\x4f\xeb\x8f\x2f\xb0\x6d\xe3\x30\xfc\x67\xba\xf8\xe5\xde\xa0\x6e\x47\x1e\xc1\x9a\xf9\x03\x78\xa4\xbb\x7f\x21\x35\xad\x1b\x0f\xbd\x9e\x31\xd6\x3d\xa6\x0a\x7d\x9a\x9b\xe6\x05\x75\x3f\x64\xff\x3a\xf9\xb2\xb5\x46\x13\xef\x5f\xe9\xe0\xd7\xed\xbb\x3e\xdc\xa3\x51\x2f\xbf\xfb\xd3\x81\x71\x48\xdc\xf8\x61\x6b\xdc\xf8\xc1\xde\xc7\x30\x6e\x40\x9c\xa8\xeb\x58\xec\xc3\x89\x1f\xd2\xc5\x1f\xb3\x06\xbe\x29\xb8\xd2\xa7\xef\xb9\x0f\x22\x5d\x9f\x0e\xe0\xe1\xc4\x6f\x17\xd3\x8b\xdf\x12\xd1\x70\xe2\xb7\xe1\xe1\xa7\x35\x30\x81\x62\x55\xcd\x45\xe7\xd0\xd8\xe7\xb3\x88\x9f\x87\x85\x5f\x53\x18\xfd\x3f\x95\xab\x8b\xe5\x19\xe3\x71\x3a\x43\x38\x6b\xe0\xf7\x76\x7e\xa9\x8e\x7b\x7e\x19\x2e\xfe\x18\x1b\xc2\xb3\x5a\x07\x9e\xdd\xf0\x0c\xe7\xf0\xb9\x23\xe7\xc7\x9b\xa3\xc4\x4f\x7e\xc8\x8d\x67\x66\x3a\xa7\xef\x7f\x88\xce\xd2\xf6\x93\xf6\xf9\x7d\x47\xcc\xf9\x7d\x47\xb2\xf3\x7b\x55\xdc\xf3\xfb\x48\xf1\xc7\xcc\xc1\xd7\x28\x77\x81\x3e\xae\xfa\xaf\x29\xa7\x01\x43\xf2\x27\xcf\x0f\xcc\x9f\x9c\xbb\x3d\xf9\x93\xd1\xe2\xef\xe3\x2e\xe6\xe3\x4c\x91\x67\x66\x35\x8a\x7e\x8e\xd6\xb2\xa5\xb3\xd4\xf9\xab\x17\xc1\x28\x7d\x04\xf4\x3d\x13\x89\x8c\x44\xf9\xab\xba\xb7\x87\xe6\xaf\x6e\x03\xfe\x98\x39\x98\x2c\xb8\x7a\xc8\xb6\xd9\x50\xfd\x32\x99\x1b\x0c\xf5\x38\xfe\xe2\xf6\xe5\x0f\x13\xe7\x4e\x47\x84\xbf\x50\xb5\xfd\xe9\x1c\x2b\x97\x4a\xb9\x55\xfa\xdf\x50\xe0\x3a\x44\xba\xcf\xc8\xdc\x37\xda\x8a\x9d\xbf\x4d\x82\xc1\xc1\x62\xe5\x6f\xb1\x0f\xe5\x7e\x4b\x1f\xa5\x5c\x70\xd2\x3e\x23\xc0\x4f\x63\xf0\xba\x58\x8f\x27\x1b\xe7\xe1\x05\x99\xd3\x56\x68\xbe\x8d\xca\x6f\xd0\xbe\x19\x6a\x58\xeb\xac\x3d\x27\x7f\xde\x56\x92\x7e\xfe\xbc\xe4\x41\x30\xdb\xf6\xa5\x1e\xf3\x08\xf1\xe3\xa3\x7b\x15\x66\xec\xa2\xff\x6d\xbc\x60\xdd\x2d\x48\xbc\xbb\xb2\xe8\xbf\x31\x7a\xf1\xd4\x24\xf7\x17\xf3\x89\x12\xdd\x5f\x18\x15\x4f\xd1\x1d\x48\xca\x7b\x92\xda\xb7\xac\xff\xda\xba\x86\x8d\x9f\xc6\xc0\x5d\x0c\xbc\xe4\x5b\xd1\x27\x95\x39\x77\x64\x8e\x8f\x19\xcd\xfd\xd1\xb0\xee\xa9\x46\x84\x1f\x1f\x43\x73\xd9\x6b\x7a\xbc\xbc\x6b\xa3\x3b\xb7\x4e\xeb\x0e\xee\x4e\xdd\xf7\x8d\x18\xbf\x33\x0e\x8f\xc2\x7a\x8b\x54\x8c\x35\x72\x04\x57\x67\x0b\xae\xfe\xc4\xba\x0b\x2d\xb8\x03\xf7\xae\x6f\x5b\x77\xbc\x23\xc6\x7f\xf7\xb9\xfb\x7c\x9e\x1e\xb9\x43\x24\x2e\x5b\x18\x63\xcf\x58\x65\x9e\x55\x66\x5a\xa5\x6b\x50\xc9\xec\xb2\xc0\x2a\x9f\x19\x54\x4e\x4f\x50\xe6\x25\x28\x33\x6f\x5f\xd9\x93\xa0\x0c\x24\x28\xcd\x41\x65\xd4\x2a\xc1\x2e\x17\x0e\x2a\x5b\xac\xb2\xc7\x2a\x4d\xab\x4c\xa1\xdf\xff\x0f\x00\x00\xff\xff\xc6\xb9\x24\x2f\xee\x3a\x00\x00" func faviconIcoBytes() ([]byte, error) { return bindataRead( _faviconIco, "favicon.ico", ) } func faviconIco() (*asset, error) { bytes, err := faviconIcoBytes() if err != nil { return nil, err } info := bindataFileInfo{name: "favicon.ico", size: 15086, mode: os.FileMode(438), modTime: time.Unix(1565946440, 0)} a := &asset{bytes: bytes, info: info} return a, nil } var _indexHtml = "\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x64\x91\x3d\x4f\xc3\x30\x10\x86\xf7\x48\xf9\x0f\xd7\x9b\x40\x22\x35\x6c\x0c\x71\x96\x42\x57\x90\x28\x03\xa3\xeb\x5c\x9b\x6b\x1d\x27\xb2\x2f\x69\xfb\xef\x51\x3e\xca\xe7\x64\xdf\xeb\xbb\xc7\x8f\xec\x7c\xf1\xf4\xb2\xda\x7c\xbc\x3e\x43\x25\xb5\x2b\xd2\x24\x1f\x56\x70\xc6\xef\x35\x92\xc7\x22\x4d\x86\x8c\x4c\x59\xa4\x09\x00\x40\x5e\x93\x18\xb0\x95\x09\x91\x44\xe3\xfb\x66\x9d\x3d\xe2\xaf\x33\x6f\x6a\xd2\xd8\x33\x9d\xda\x26\x08\x82\x6d\xbc\x90\x17\x8d\x27\x2e\xa5\xd2\x25\xf5\x6c\x29\x1b\x8b\x3b\x60\xcf\xc2\xc6\x65\xd1\x1a\x47\xfa\x61\x79\xff\xc5\x72\xec\x8f\x10\xc8\x69\x64\xdb\x78\x84\x2a\xd0\x4e\xa3\x6a\xbb\xad\x63\xab\x76\xa6\x1f\xe2\x25\xdb\x06\x41\x2e\x2d\x69\xe4\xda\xec\x49\x9d\xb3\xa9\x5d\xfd\xe7\x44\xb9\x38\x8a\x15\x91\xfc\xa5\xd9\x18\x55\x6d\xd8\x2f\x6d\x8c\x3f\x46\x85\xc5\x51\xb1\x66\x47\xf0\x46\xa1\xa7\x90\xab\x29\x4a\x93\x5c\xcd\x6f\x92\x26\xf9\xb6\x29\x2f\xd7\x11\xf6\x6d\x27\xb3\xd0\xb6\x13\x19\x54\x1a\x6f\x1d\xdb\xa3\xc6\xc6\xaf\x86\xcd\xcd\x2d\x42\x6f\x5c\x47\x1a\xc7\x1a\x6a\x5a\x4c\xb7\xce\x90\x68\x03\xb7\x57\x8a\xd0\x59\xd4\xc1\xf4\x66\x4a\x11\x62\xb0\xdf\xe6\x87\x59\xfc\x10\xb1\xc8\xd5\xd4\x32\xea\xcd\x52\xa3\xe9\xf0\xb3\x9f\x01\x00\x00\xff\xff\x00\xcf\x96\xa8\xe9\x01\x00\x00" func indexHtmlBytes() ([]byte, error) { return bindataRead( _indexHtml, "index.html", ) } func indexHtml() (*asset, error) { bytes, err := indexHtmlBytes() if err != nil { return nil, err } info := bindataFileInfo{name: "index.html", size: 489, mode: os.FileMode(438), modTime: time.Unix(1594886229, 0)} a := &asset{bytes: bytes, info: info} return a, nil } var _jsMainJs = "\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x4a\xce\xcf\x2b\xce\xcf\x49\xd5\xcb\xc9\x4f\xd7\x50\x4a\xad\x48\xcc\x2d\xc8\x49\x55\xd2\xb4\xe6\xe5\xe2\xe5\x4a\x2b\xcd\x4b\x2e\xc9\xcc\xcf\x53\xc8\xcf\x73\xce\xc9\x4c\xce\xd6\xd0\x54\xa8\xe6\xe5\x52\x50\x50\x50\x28\xcf\xcc\x4b\xc9\x2f\xd7\x4b\xcc\x49\x2d\x2a\xd1\x50\x4a\x2a\x2d\x29\xc9\xcf\x53\x48\x06\xa9\x49\x4d\x01\x6b\xae\x05\x04\x00\x00\xff\xff\xa4\xb7\x99\x52\x57\x00\x00\x00" func jsMainJsBytes() ([]byte, error) { return bindataRead( _jsMainJs, "js/main.js", ) } func jsMainJs() (*asset, error) { bytes, err := jsMainJsBytes() if err != nil { return nil, err } info := bindataFileInfo{name: "js/main.js", size: 87, mode: os.FileMode(438), modTime: time.Unix(1594787764, 0)} a := &asset{bytes: bytes, info: info} return a, nil } // Asset loads and returns the asset for the given name. // It returns an error if the asset could not be found or // could not be loaded. func Asset(name string) ([]byte, error) { cannonicalName := strings.Replace(name, "\\", "/", -1) if f, ok := _bindata[cannonicalName]; ok { a, err := f() if err != nil { return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err) } return a.bytes, nil } return nil, fmt.Errorf("Asset %s not found", name) } // MustAsset is like Asset but panics when Asset would return an error. // It simplifies safe initialization of global variables. func MustAsset(name string) []byte { a, err := Asset(name) if err != nil { panic("asset: Asset(" + name + "): " + err.Error()) } return a } // AssetInfo loads and returns the asset info for the given name. // It returns an error if the asset could not be found or // could not be loaded. func AssetInfo(name string) (os.FileInfo, error) { cannonicalName := strings.Replace(name, "\\", "/", -1) if f, ok := _bindata[cannonicalName]; ok { a, err := f() if err != nil { return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err) } return a.info, nil } return nil, fmt.Errorf("AssetInfo %s not found", name) } // AssetNames returns the names of the assets. func AssetNames() []string { names := make([]string, 0, len(_bindata)) for name := range _bindata { names = append(names, name) } return names } // _bindata is a table, holding each asset generator, mapped to its name. var _bindata = map[string]func() (*asset, error){ "app2/app2app3/css/main.css": app2App2app3CssMainCss, "app2/app2app3/dirs/dir1/text.txt": app2App2app3DirsDir1TextTxt, "app2/app2app3/dirs/dir2/text.txt": app2App2app3DirsDir2TextTxt, "app2/app2app3/dirs/text.txt": app2App2app3DirsTextTxt, "app2/app2app3/index.html": app2App2app3IndexHtml, "app2/index.html": app2IndexHtml, "app2/mydir/text.txt": app2MydirTextTxt, "css/main.css": cssMainCss, "favicon.ico": faviconIco, "index.html": indexHtml, "js/main.js": jsMainJs, } // AssetDir returns the file names below a certain // directory embedded in the file by go-bindata. // For example if you run go-bindata on data/... and data contains the // following hierarchy: // // data/ // foo.txt // img/ // a.png // b.png // // then AssetDir("data") would return []string{"foo.txt", "img"} // AssetDir("data/img") would return []string{"a.png", "b.png"} // AssetDir("foo.txt") and AssetDir("notexist") would return an error // AssetDir("") will return []string{"data"}. func AssetDir(name string) ([]string, error) { node := _bintree if len(name) != 0 { cannonicalName := strings.Replace(name, "\\", "/", -1) pathList := strings.Split(cannonicalName, "/") for _, p := range pathList { node = node.Children[p] if node == nil { return nil, fmt.Errorf("Asset %s not found", name) } } } if node.Func != nil { return nil, fmt.Errorf("Asset %s not found", name) } rv := make([]string, 0, len(node.Children)) for childName := range node.Children { rv = append(rv, childName) } return rv, nil } type bintree struct { Func func() (*asset, error) Children map[string]*bintree } var _bintree = &bintree{nil, map[string]*bintree{ "app2": {nil, map[string]*bintree{ "app2app3": {nil, map[string]*bintree{ "css": {nil, map[string]*bintree{ "main.css": {app2App2app3CssMainCss, map[string]*bintree{}}, }}, "dirs": {nil, map[string]*bintree{ "dir1": {nil, map[string]*bintree{ "text.txt": {app2App2app3DirsDir1TextTxt, map[string]*bintree{}}, }}, "dir2": {nil, map[string]*bintree{ "text.txt": {app2App2app3DirsDir2TextTxt, map[string]*bintree{}}, }}, "text.txt": {app2App2app3DirsTextTxt, map[string]*bintree{}}, }}, "index.html": {app2App2app3IndexHtml, map[string]*bintree{}}, }}, "index.html": {app2IndexHtml, map[string]*bintree{}}, "mydir": {nil, map[string]*bintree{ "text.txt": {app2MydirTextTxt, map[string]*bintree{}}, }}, }}, "css": {nil, map[string]*bintree{ "main.css": {cssMainCss, map[string]*bintree{}}, }}, "favicon.ico": {faviconIco, map[string]*bintree{}}, "index.html": {indexHtml, map[string]*bintree{}}, "js": {nil, map[string]*bintree{ "main.js": {jsMainJs, map[string]*bintree{}}, }}, }} // RestoreAsset restores an asset under the given directory func RestoreAsset(dir, name string) error { data, err := Asset(name) if err != nil { return err } info, err := AssetInfo(name) if err != nil { return err } err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755)) if err != nil { return err } err = os.WriteFile(_filePath(dir, name), data, info.Mode()) if err != nil { return err } err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime()) if err != nil { return err } return nil } // RestoreAssets restores an asset under the given directory recursively func RestoreAssets(dir, name string) error { children, err := AssetDir(name) // File if err != nil { return RestoreAsset(dir, name) } // Dir for _, child := range children { err = RestoreAssets(dir, filepath.Join(name, child)) if err != nil { return err } } return nil } func _filePath(dir, name string) string { cannonicalName := strings.Replace(name, "\\", "/", -1) return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...) } ================================================ FILE: _examples/file-server/http2push-embedded-gzipped/main.go ================================================ package main import ( "regexp" "github.com/kataras/iris/v12" ) // How to run: // $ go install github.com/go-bindata/go-bindata/v3/go-bindata@latest // $ go-bindata -nomemcopy -fs -prefix "../http2push/assets" ../http2push/assets/... // $ go run . var opts = iris.DirOptions{ IndexName: "index.html", PushTargetsRegexp: map[string]*regexp.Regexp{ "/": iris.MatchCommonAssets, "/app2/app2app3": iris.MatchCommonAssets, }, ShowList: true, Cache: iris.DirCacheOptions{ Enable: true, CompressIgnore: iris.MatchImagesAssets, // Here, define the encodings that the cached files should be pre-compressed // and served based on client's needs. Encodings: []string{"gzip", "deflate", "br", "snappy"}, CompressMinSize: 50, // files smaller than this size will NOT be compressed. Verbose: 1, }, } func main() { app := iris.New() app.HandleDir("/public", AssetFile(), opts) // https://127.0.0.1/public // https://127.0.0.1/public/app2 // https://127.0.0.1/public/app2/app2app3 // https://127.0.0.1/public/app2/app2app3/dirs app.Run(iris.TLS(":443", "../http2push/mycert.crt", "../http2push/mykey.key")) } ================================================ FILE: _examples/file-server/send-files/main.go ================================================ package main import ( "github.com/kataras/iris/v12" ) func main() { app := iris.New() app.Logger().SetLevel("debug") app.Get("/", download) app.Get("/download", downloadWithRateLimit) app.Listen(":8080") } func download(ctx iris.Context) { src := "./files/first.zip" ctx.SendFile(src, "client.zip") } func downloadWithRateLimit(ctx iris.Context) { // REPLACE THAT WITH A BIG LOCAL FILE OF YOUR OWN. src := "./files/first.zip" dest := "" /* optionally, keep it empty to resolve the filename based on the "src" */ // Limit download speed to ~50Kb/s with a burst of 100KB. limit := 50.0 * iris.KB burst := 100 * iris.KB ctx.SendFileWithRate(src, dest, limit, burst) } ================================================ FILE: _examples/file-server/single-page-application/basic/main.go ================================================ package main import "github.com/kataras/iris/v12" func newApp() *iris.Application { app := iris.New() app.HandleDir("/", iris.Dir("./public"), iris.DirOptions{ IndexName: "index.html", SPA: true, }) return app } func main() { app := newApp() // http://localhost:8080 // http://localhost:8080/about // http://localhost:8080/a_notfound app.Listen(":8080") } ================================================ FILE: _examples/file-server/single-page-application/basic/public/index.html ================================================ Iris SPA Router Example
================================================ FILE: _examples/file-server/single-page-application/basic/public/index.js ================================================ const NotFound = { template: '

Page not found

' } const Home = { template: '

home page

' } const About = { template: '

about page

' } const routes = { '/': Home, '/about': About } const app = new Vue({ el: '#app', data: { currentRoute: window.location.pathname }, computed: { ViewComponent () { return routes[this.currentRoute] || NotFound } }, render (h) { return h(this.ViewComponent) } }) ================================================ FILE: _examples/file-server/single-page-application/embedded-single-page-application/bindata.go ================================================ // Code generated by go-bindata. (@generated) DO NOT EDIT. // Package main generated by go-bindata.// sources: // data/public/app.js // data/public/app2/index.html // data/public/css/main.css // data/views/index.html package main import ( "bytes" "compress/gzip" "fmt" "io" "io/ioutil" "net/http" "os" "path/filepath" "strings" "time" ) func bindataRead(data []byte, name string) ([]byte, error) { gz, err := gzip.NewReader(bytes.NewBuffer(data)) if err != nil { return nil, fmt.Errorf("read %q: %v", name, err) } var buf bytes.Buffer _, err = io.Copy(&buf, gz) clErr := gz.Close() if err != nil { return nil, fmt.Errorf("read %q: %v", name, err) } if clErr != nil { return nil, err } return buf.Bytes(), nil } type asset struct { bytes []byte info os.FileInfo } type bindataFileInfo struct { name string size int64 mode os.FileMode modTime time.Time } // Name return file name func (fi bindataFileInfo) Name() string { return fi.name } // Size return file size func (fi bindataFileInfo) Size() int64 { return fi.size } // Mode return file mode func (fi bindataFileInfo) Mode() os.FileMode { return fi.mode } // ModTime return file modify time func (fi bindataFileInfo) ModTime() time.Time { return fi.modTime } // IsDir return file whether a directory func (fi bindataFileInfo) IsDir() bool { return fi.mode&os.ModeDir != 0 } // Sys return file is sys mode func (fi bindataFileInfo) Sys() any { return nil } type assetFile struct { *bytes.Reader name string childInfos []os.FileInfo childInfoOffset int } type assetOperator struct{} // Open implement http.FileSystem interface func (f *assetOperator) Open(name string) (http.File, error) { var err error if len(name) > 0 && name[0] == '/' { name = name[1:] } content, err := Asset(name) if err == nil { return &assetFile{name: name, Reader: bytes.NewReader(content)}, nil } children, err := AssetDir(name) if err == nil { childInfos := make([]os.FileInfo, 0, len(children)) for _, child := range children { childPath := filepath.Join(name, child) info, errInfo := AssetInfo(filepath.Join(name, child)) if errInfo == nil { childInfos = append(childInfos, info) } else { childInfos = append(childInfos, newDirFileInfo(childPath)) } } return &assetFile{name: name, childInfos: childInfos}, nil } else { // If the error is not found, return an error that will // result in a 404 error. Otherwise the server returns // a 500 error for files not found. if strings.Contains(err.Error(), "not found") { return nil, os.ErrNotExist } return nil, err } } // Close no need do anything func (f *assetFile) Close() error { return nil } // Readdir read dir's children file info func (f *assetFile) Readdir(count int) ([]os.FileInfo, error) { if len(f.childInfos) == 0 { return nil, os.ErrNotExist } if count <= 0 { return f.childInfos, nil } if f.childInfoOffset+count > len(f.childInfos) { count = len(f.childInfos) - f.childInfoOffset } offset := f.childInfoOffset f.childInfoOffset += count return f.childInfos[offset : offset+count], nil } // Stat read file info from asset item func (f *assetFile) Stat() (os.FileInfo, error) { if len(f.childInfos) != 0 { return newDirFileInfo(f.name), nil } return AssetInfo(f.name) } // newDirFileInfo return default dir file info func newDirFileInfo(name string) os.FileInfo { return &bindataFileInfo{ name: name, size: 0, mode: os.FileMode(2147484068), // equal os.FileMode(0644)|os.ModeDir modTime: time.Time{}} } // AssetFile return a http.FileSystem instance that data backend by asset func AssetFile() http.FileSystem { return &assetOperator{} } var _publicAppJs = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x2a\xcf\xcc\x4b\xc9\x2f\xd7\x4b\xcc\x49\x2d\x2a\xd1\x50\x4a\x2c\x28\xd0\xcb\x2a\x56\xc8\xc9\x4f\x4c\x49\x4d\x51\x48\x2b\xca\xcf\x55\x88\x51\xd2\x57\xd2\xb4\x06\x04\x00\x00\xff\xff\xa9\x06\xf7\xa3\x27\x00\x00\x00") func publicAppJsBytes() ([]byte, error) { return bindataRead( _publicAppJs, "public/app.js", ) } func publicAppJs() (*asset, error) { bytes, err := publicAppJsBytes() if err != nil { return nil, err } info := bindataFileInfo{name: "public/app.js", size: 39, mode: os.FileMode(420), modTime: time.Unix(1663416115, 0)} a := &asset{bytes: bytes, info: info} return a, nil } var _publicApp2IndexHtml = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x2c\xcd\x31\x0a\x02\x31\x10\x85\xe1\x7e\x4e\xf1\x4a\x6d\x0c\xbb\xf5\x10\xb0\xdb\x42\x41\xd0\x0b\x44\x33\x9a\x40\xd6\x0c\x32\x85\xde\x5e\x86\x6c\xf9\xe0\xf1\xfd\x5c\x6c\x6d\x91\x88\x8b\xa4\x1c\x09\x00\xd8\xaa\x35\x89\x47\x55\xcc\x1c\xc6\x20\x0e\xe3\x40\x7c\xef\xf9\xb7\x1d\xcb\x14\xb1\xbb\x5a\xb2\xfa\xc0\x72\x3b\x9f\x70\x49\x2f\xd9\x63\x91\xd6\x3a\x9e\x9f\xbe\x22\xa9\xce\xa1\xbe\xb3\x7c\x0f\x1e\x02\x87\x32\x39\x36\x10\x57\x3d\xff\x0f\x00\x00\xff\xff\xdd\xbe\x30\x69\x85\x00\x00\x00") func publicApp2IndexHtmlBytes() ([]byte, error) { return bindataRead( _publicApp2IndexHtml, "public/app2/index.html", ) } func publicApp2IndexHtml() (*asset, error) { bytes, err := publicApp2IndexHtmlBytes() if err != nil { return nil, err } info := bindataFileInfo{name: "public/app2/index.html", size: 133, mode: os.FileMode(420), modTime: time.Unix(1663416115, 0)} a := &asset{bytes: bytes, info: info} return a, nil } var _publicCssMainCss = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x4a\xca\x4f\xa9\x54\xa8\xe6\x52\x50\x50\x50\x48\x4a\x4c\xce\x4e\x2f\xca\x2f\xcd\x4b\xd1\x4d\xce\xcf\xc9\x2f\xb2\x52\x48\xca\x49\x4c\xce\xb6\xe6\xaa\xe5\x02\x04\x00\x00\xff\xff\x96\x97\xac\xb1\x26\x00\x00\x00") func publicCssMainCssBytes() ([]byte, error) { return bindataRead( _publicCssMainCss, "public/css/main.css", ) } func publicCssMainCss() (*asset, error) { bytes, err := publicCssMainCssBytes() if err != nil { return nil, err } info := bindataFileInfo{name: "public/css/main.css", size: 38, mode: os.FileMode(420), modTime: time.Unix(1663416115, 0)} a := &asset{bytes: bytes, info: info} return a, nil } var _viewsIndexHtml = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x2c\xce\xbd\x0e\xc2\x30\x0c\x04\xe0\xdd\x4f\x71\xea\x04\x4b\xa2\xee\xc6\x33\x23\x43\x5f\x20\xb4\x86\x04\xa5\x34\x6a\x22\x7e\x54\xf5\xdd\x51\x14\x46\xeb\x7c\xfa\x8e\x7d\x99\xa3\x10\xb1\x57\x37\x09\x01\x00\x97\x50\xa2\xca\xb6\xc1\x5c\xdc\x5d\xcd\x50\x4f\xec\x3b\xdb\x16\x10\xdb\xf6\x4c\x7c\x5d\xa6\xef\xbf\xe4\x7b\xc1\x61\xd0\x39\x45\x57\xf4\x88\xb3\xc6\xb8\xe0\xb6\x2e\x33\x5e\x41\xdf\xd9\x86\xe7\xa4\x1f\x53\x35\xb0\xf5\xbd\x10\xb5\x5e\x1e\xd7\x90\x0a\xf2\x3a\x9e\x3a\xeb\x52\x32\x8f\xdc\x09\xc0\xb6\x05\x55\x6b\x4a\x65\xeb\xd6\x5f\x00\x00\x00\xff\xff\xd6\xa4\xa5\x16\xb2\x00\x00\x00") func viewsIndexHtmlBytes() ([]byte, error) { return bindataRead( _viewsIndexHtml, "views/index.html", ) } func viewsIndexHtml() (*asset, error) { bytes, err := viewsIndexHtmlBytes() if err != nil { return nil, err } info := bindataFileInfo{name: "views/index.html", size: 178, mode: os.FileMode(420), modTime: time.Unix(1663416115, 0)} a := &asset{bytes: bytes, info: info} return a, nil } // Asset loads and returns the asset for the given name. // It returns an error if the asset could not be found or // could not be loaded. func Asset(name string) ([]byte, error) { cannonicalName := strings.Replace(name, "\\", "/", -1) if f, ok := _bindata[cannonicalName]; ok { a, err := f() if err != nil { return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err) } return a.bytes, nil } return nil, fmt.Errorf("Asset %s not found", name) } // MustAsset is like Asset but panics when Asset would return an error. // It simplifies safe initialization of global variables. func MustAsset(name string) []byte { a, err := Asset(name) if err != nil { panic("asset: Asset(" + name + "): " + err.Error()) } return a } // AssetInfo loads and returns the asset info for the given name. // It returns an error if the asset could not be found or // could not be loaded. func AssetInfo(name string) (os.FileInfo, error) { cannonicalName := strings.Replace(name, "\\", "/", -1) if f, ok := _bindata[cannonicalName]; ok { a, err := f() if err != nil { return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err) } return a.info, nil } return nil, fmt.Errorf("AssetInfo %s not found", name) } // AssetNames returns the names of the assets. func AssetNames() []string { names := make([]string, 0, len(_bindata)) for name := range _bindata { names = append(names, name) } return names } // _bindata is a table, holding each asset generator, mapped to its name. var _bindata = map[string]func() (*asset, error){ "public/app.js": publicAppJs, "public/app2/index.html": publicApp2IndexHtml, "public/css/main.css": publicCssMainCss, "views/index.html": viewsIndexHtml, } // AssetDir returns the file names below a certain // directory embedded in the file by go-bindata. // For example if you run go-bindata on data/... and data contains the // following hierarchy: // // data/ // foo.txt // img/ // a.png // b.png // // then AssetDir("data") would return []string{"foo.txt", "img"} // AssetDir("data/img") would return []string{"a.png", "b.png"} // AssetDir("foo.txt") and AssetDir("notexist") would return an error // AssetDir("") will return []string{"data"}. func AssetDir(name string) ([]string, error) { node := _bintree if len(name) != 0 { cannonicalName := strings.Replace(name, "\\", "/", -1) pathList := strings.Split(cannonicalName, "/") for _, p := range pathList { node = node.Children[p] if node == nil { return nil, fmt.Errorf("Asset %s not found", name) } } } if node.Func != nil { return nil, fmt.Errorf("Asset %s not found", name) } rv := make([]string, 0, len(node.Children)) for childName := range node.Children { rv = append(rv, childName) } return rv, nil } type bintree struct { Func func() (*asset, error) Children map[string]*bintree } var _bintree = &bintree{nil, map[string]*bintree{ "public": {nil, map[string]*bintree{ "app.js": {publicAppJs, map[string]*bintree{}}, "app2": {nil, map[string]*bintree{ "index.html": {publicApp2IndexHtml, map[string]*bintree{}}, }}, "css": {nil, map[string]*bintree{ "main.css": {publicCssMainCss, map[string]*bintree{}}, }}, }}, "views": {nil, map[string]*bintree{ "index.html": {viewsIndexHtml, map[string]*bintree{}}, }}, }} // RestoreAsset restores an asset under the given directory func RestoreAsset(dir, name string) error { data, err := Asset(name) if err != nil { return err } info, err := AssetInfo(name) if err != nil { return err } err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755)) if err != nil { return err } err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode()) if err != nil { return err } err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime()) if err != nil { return err } return nil } // RestoreAssets restores an asset under the given directory recursively func RestoreAssets(dir, name string) error { children, err := AssetDir(name) // File if err != nil { return RestoreAsset(dir, name) } // Dir for _, child := range children { err = RestoreAssets(dir, filepath.Join(name, child)) if err != nil { return err } } return nil } func _filePath(dir, name string) string { cannonicalName := strings.Replace(name, "\\", "/", -1) return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...) } ================================================ FILE: _examples/file-server/single-page-application/embedded-single-page-application/data/public/app.js ================================================ window.alert("app.js loaded from \"/"); ================================================ FILE: _examples/file-server/single-page-application/embedded-single-page-application/data/public/app2/index.html ================================================ App 2

(Static HTML Page) Hello from app2/index.html

================================================ FILE: _examples/file-server/single-page-application/embedded-single-page-application/data/public/css/main.css ================================================ body { background-color: black; } ================================================ FILE: _examples/file-server/single-page-application/embedded-single-page-application/data/views/index.html ================================================ {{ .Page.Title }}

(Template) Hello from views/index.html

================================================ FILE: _examples/file-server/single-page-application/embedded-single-page-application/main.go ================================================ package main import ( "fmt" "github.com/kataras/iris/v12" ) // $ go install github.com/go-bindata/go-bindata/v3/go-bindata@latest // $ go-bindata -prefix "data" -fs ./data/... // $ go run . var page = struct { Title string }{"Welcome"} func newApp() *iris.Application { app := iris.New() app.RegisterView(iris.HTML(AssetFile(), ".html").RootDir("views")) // Using the iris.PrefixDir you can select // which directories to use under a particular file system, // e.g. for views the ./public: // publicFS := iris.PrefixDir("./public", AssetFile()) publicFS := iris.PrefixDir("./public", AssetFile()) app.HandleDir("/", publicFS) app.Get("/", func(ctx iris.Context) { ctx.ViewData("Page", page) if err := ctx.View("index.html"); err != nil { ctx.HTML(fmt.Sprintf("

%s

", err.Error())) return } }) return app } func main() { app := newApp() // http://localhost:8080 // http://localhost:8080/app.js // http://localhost:8080/css/main.css // http://localhost:8080/app2 app.Listen(":8080") } ================================================ FILE: _examples/file-server/single-page-application/embedded-single-page-application/main_test.go ================================================ package main import ( "os" "path/filepath" "runtime" "strings" "testing" "github.com/kataras/iris/v12/httptest" ) type resource string func (r resource) contentType() string { switch filepath.Ext(r.String()) { case ".js": return "text/javascript" case ".css": return "text/css" default: return "text/html" } } func (r resource) String() string { return string(r) } func (r resource) strip(strip string) string { s := r.String() return strings.TrimPrefix(s, strip) } func (r resource) loadFromBase(dir string) string { filename := r.String() if strings.HasSuffix(filename, "/") { filename = filename + "index.html" } fullpath := filepath.Join(dir, filename) b, err := os.ReadFile(fullpath) if err != nil { panic(fullpath + " failed with error: " + err.Error()) } result := string(b) if runtime.GOOS != "windows" { result = strings.ReplaceAll(result, "\n", "\r\n") result = strings.ReplaceAll(result, "\r\r", "") } return result } var urls = []resource{ "/", "/app.js", "/css/main.css", "/app2/", "/app2/index.html", } func TestSPAEmbedded(t *testing.T) { app := newApp() e := httptest.New(t, app) for _, u := range urls { url := u.String() base := "./data/public" if u == "/" || u == "/index.html" { base = "./data/views" } contents := u.loadFromBase(base) contents = strings.Replace(contents, "{{ .Page.Title }}", page.Title, 1) e.GET(url).Expect(). Status(httptest.StatusOK). ContentType(u.contentType(), app.ConfigurationReadOnly().GetCharset()). Body().IsEqual(contents) } e.GET("/index.html").Expect().Status(httptest.StatusNotFound) // only root is served. } ================================================ FILE: _examples/file-server/single-page-application/embedded-single-page-application-with-other-routes/bindata.go ================================================ // Code generated by go-bindata. (@generated) DO NOT EDIT. // Package main generated by go-bindata.// sources: // public/app.js // public/css/main.css // public/index.html package main import ( "bytes" "compress/gzip" "fmt" "io" "net/http" "os" "path/filepath" "strings" "time" ) func bindataRead(data []byte, name string) ([]byte, error) { gz, err := gzip.NewReader(bytes.NewBuffer(data)) if err != nil { return nil, fmt.Errorf("read %q: %v", name, err) } var buf bytes.Buffer _, err = io.Copy(&buf, gz) clErr := gz.Close() if err != nil { return nil, fmt.Errorf("read %q: %v", name, err) } if clErr != nil { return nil, err } return buf.Bytes(), nil } type asset struct { bytes []byte info os.FileInfo } type bindataFileInfo struct { name string size int64 mode os.FileMode modTime time.Time } // Name return file name func (fi bindataFileInfo) Name() string { return fi.name } // Size return file size func (fi bindataFileInfo) Size() int64 { return fi.size } // Mode return file mode func (fi bindataFileInfo) Mode() os.FileMode { return fi.mode } // ModTime return file modify time func (fi bindataFileInfo) ModTime() time.Time { return fi.modTime } // IsDir return file whether a directory func (fi bindataFileInfo) IsDir() bool { return fi.mode&os.ModeDir != 0 } // Sys return file is sys mode func (fi bindataFileInfo) Sys() any { return nil } type assetFile struct { *bytes.Reader name string childInfos []os.FileInfo childInfoOffset int } type assetOperator struct{} // Open implement http.FileSystem interface func (f *assetOperator) Open(name string) (http.File, error) { var err error if len(name) > 0 && name[0] == '/' { name = name[1:] } content, err := Asset(name) if err == nil { return &assetFile{name: name, Reader: bytes.NewReader(content)}, nil } children, err := AssetDir(name) if err == nil { childInfos := make([]os.FileInfo, 0, len(children)) for _, child := range children { childPath := filepath.Join(name, child) info, errInfo := AssetInfo(filepath.Join(name, child)) if errInfo == nil { childInfos = append(childInfos, info) } else { childInfos = append(childInfos, newDirFileInfo(childPath)) } } return &assetFile{name: name, childInfos: childInfos}, nil } else { // If the error is not found, return an error that will // result in a 404 error. Otherwise the server returns // a 500 error for files not found. if strings.Contains(err.Error(), "not found") { return nil, os.ErrNotExist } return nil, err } } // Close no need do anything func (f *assetFile) Close() error { return nil } // Readdir read dir's children file info func (f *assetFile) Readdir(count int) ([]os.FileInfo, error) { if len(f.childInfos) == 0 { return nil, os.ErrNotExist } if count <= 0 { return f.childInfos, nil } if f.childInfoOffset+count > len(f.childInfos) { count = len(f.childInfos) - f.childInfoOffset } offset := f.childInfoOffset f.childInfoOffset += count return f.childInfos[offset : offset+count], nil } // Stat read file info from asset item func (f *assetFile) Stat() (os.FileInfo, error) { if len(f.childInfos) != 0 { return newDirFileInfo(f.name), nil } return AssetInfo(f.name) } // newDirFileInfo return default dir file info func newDirFileInfo(name string) os.FileInfo { return &bindataFileInfo{ name: name, size: 0, mode: os.FileMode(2147484068), // equal os.FileMode(0644)|os.ModeDir modTime: time.Time{}} } // AssetFile return a http.FileSystem instance that data backend by asset func AssetFile() http.FileSystem { return &assetOperator{} } var _appJs = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x2a\xcf\xcc\x4b\xc9\x2f\xd7\x4b\xcc\x49\x2d\x2a\xd1\x50\x4a\x2c\x28\xd0\xcb\x2a\x56\xc8\xc9\x4f\x4c\x49\x4d\x51\x48\x2b\xca\xcf\x55\x88\x51\xd2\x57\xd2\xb4\x06\x04\x00\x00\xff\xff\xa9\x06\xf7\xa3\x27\x00\x00\x00") func appJsBytes() ([]byte, error) { return bindataRead( _appJs, "app.js", ) } func appJs() (*asset, error) { bytes, err := appJsBytes() if err != nil { return nil, err } info := bindataFileInfo{name: "app.js", size: 39, mode: os.FileMode(438), modTime: time.Unix(1565946441, 0)} a := &asset{bytes: bytes, info: info} return a, nil } var _cssMainCss = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x4a\xca\x4f\xa9\x54\xa8\xe6\xe5\x52\x50\x50\x50\x48\x4a\x4c\xce\x4e\x2f\xca\x2f\xcd\x4b\xd1\x4d\xce\xcf\xc9\x2f\xb2\x52\x48\xca\x49\x4c\xce\xb6\xe6\xe5\xaa\xe5\xe5\x02\x04\x00\x00\xff\xff\x03\x25\x9c\x89\x29\x00\x00\x00") func cssMainCssBytes() ([]byte, error) { return bindataRead( _cssMainCss, "css/main.css", ) } func cssMainCss() (*asset, error) { bytes, err := cssMainCssBytes() if err != nil { return nil, err } info := bindataFileInfo{name: "css/main.css", size: 41, mode: os.FileMode(438), modTime: time.Unix(1565946441, 0)} a := &asset{bytes: bytes, info: info} return a, nil } var _indexHtml = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x4c\x8e\x41\x0e\xc2\x20\x10\x45\xf7\x24\xdc\xe1\xa7\x07\x28\xe9\x7e\x64\xed\x35\x10\x46\xc1\x50\x21\x30\x0b\xbd\xbd\x29\xc5\xc4\xf5\x7f\x6f\xde\x50\x94\x3d\x5b\xad\xb4\xa2\xc8\x2e\x58\xad\x00\x80\x24\x49\x66\x7b\xe5\x9c\x0b\xee\xad\xec\xe8\xe2\x24\x79\x54\xf7\x60\x32\xe7\xaa\x15\x99\xe9\x68\x45\xb7\x12\x3e\x3f\x3b\x6e\x16\x7f\x6e\x7a\x05\x7e\xaf\x47\x08\x64\xe2\x36\xf8\x49\x76\xdf\x52\x15\xf4\xe6\x2f\x8b\x71\xb5\xae\xcf\xbe\x58\x80\xcc\x39\x8c\xc6\xbc\x3c\x72\xc7\xb3\xdf\x00\x00\x00\xff\xff\x7e\xad\xd1\x97\xb3\x00\x00\x00") func indexHtmlBytes() ([]byte, error) { return bindataRead( _indexHtml, "index.html", ) } func indexHtml() (*asset, error) { bytes, err := indexHtmlBytes() if err != nil { return nil, err } info := bindataFileInfo{name: "index.html", size: 179, mode: os.FileMode(438), modTime: time.Unix(1565946441, 0)} a := &asset{bytes: bytes, info: info} return a, nil } // Asset loads and returns the asset for the given name. // It returns an error if the asset could not be found or // could not be loaded. func Asset(name string) ([]byte, error) { cannonicalName := strings.Replace(name, "\\", "/", -1) if f, ok := _bindata[cannonicalName]; ok { a, err := f() if err != nil { return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err) } return a.bytes, nil } return nil, fmt.Errorf("Asset %s not found", name) } // MustAsset is like Asset but panics when Asset would return an error. // It simplifies safe initialization of global variables. func MustAsset(name string) []byte { a, err := Asset(name) if err != nil { panic("asset: Asset(" + name + "): " + err.Error()) } return a } // AssetInfo loads and returns the asset info for the given name. // It returns an error if the asset could not be found or // could not be loaded. func AssetInfo(name string) (os.FileInfo, error) { cannonicalName := strings.Replace(name, "\\", "/", -1) if f, ok := _bindata[cannonicalName]; ok { a, err := f() if err != nil { return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err) } return a.info, nil } return nil, fmt.Errorf("AssetInfo %s not found", name) } // AssetNames returns the names of the assets. func AssetNames() []string { names := make([]string, 0, len(_bindata)) for name := range _bindata { names = append(names, name) } return names } // _bindata is a table, holding each asset generator, mapped to its name. var _bindata = map[string]func() (*asset, error){ "app.js": appJs, "css/main.css": cssMainCss, "index.html": indexHtml, } // AssetDir returns the file names below a certain // directory embedded in the file by go-bindata. // For example if you run go-bindata on data/... and data contains the // following hierarchy: // // data/ // foo.txt // img/ // a.png // b.png // // then AssetDir("data") would return []string{"foo.txt", "img"} // AssetDir("data/img") would return []string{"a.png", "b.png"} // AssetDir("foo.txt") and AssetDir("notexist") would return an error // AssetDir("") will return []string{"data"}. func AssetDir(name string) ([]string, error) { node := _bintree if len(name) != 0 { cannonicalName := strings.Replace(name, "\\", "/", -1) pathList := strings.Split(cannonicalName, "/") for _, p := range pathList { node = node.Children[p] if node == nil { return nil, fmt.Errorf("Asset %s not found", name) } } } if node.Func != nil { return nil, fmt.Errorf("Asset %s not found", name) } rv := make([]string, 0, len(node.Children)) for childName := range node.Children { rv = append(rv, childName) } return rv, nil } type bintree struct { Func func() (*asset, error) Children map[string]*bintree } var _bintree = &bintree{nil, map[string]*bintree{ "app.js": {appJs, map[string]*bintree{}}, "css": {nil, map[string]*bintree{ "main.css": {cssMainCss, map[string]*bintree{}}, }}, "index.html": {indexHtml, map[string]*bintree{}}, }} // RestoreAsset restores an asset under the given directory func RestoreAsset(dir, name string) error { data, err := Asset(name) if err != nil { return err } info, err := AssetInfo(name) if err != nil { return err } err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755)) if err != nil { return err } err = os.WriteFile(_filePath(dir, name), data, info.Mode()) if err != nil { return err } err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime()) if err != nil { return err } return nil } // RestoreAssets restores an asset under the given directory recursively func RestoreAssets(dir, name string) error { children, err := AssetDir(name) // File if err != nil { return RestoreAsset(dir, name) } // Dir for _, child := range children { err = RestoreAssets(dir, filepath.Join(name, child)) if err != nil { return err } } return nil } func _filePath(dir, name string) string { cannonicalName := strings.Replace(name, "\\", "/", -1) return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...) } ================================================ FILE: _examples/file-server/single-page-application/embedded-single-page-application-with-other-routes/main.go ================================================ package main import "github.com/kataras/iris/v12" // $ go install github.com/go-bindata/go-bindata/v3/go-bindata@latest // $ go-bindata -fs -prefix "public" ./public/... // $ go run . func newApp() *iris.Application { app := iris.New() app.OnErrorCode(iris.StatusNotFound, func(ctx iris.Context) { ctx.Writef("404 not found here") }) app.HandleDir("/", AssetFile()) // Note: // if you want a dynamic index page then see the file-server/embedded-single-page-application // which is registering a view engine based on bindata as well and a root route. app.Get("/ping", func(ctx iris.Context) { ctx.WriteString("pong") }) app.Get("/.well-known", func(ctx iris.Context) { ctx.WriteString("well-known") }) app.Get(".well-known/ready", func(ctx iris.Context) { ctx.WriteString("ready") }) app.Get(".well-known/live", func(ctx iris.Context) { ctx.WriteString("live") }) app.Get(".well-known/metrics", func(ctx iris.Context) { ctx.Writef("metrics") }) return app } func main() { app := newApp() // http://localhost:8080/index.html // http://localhost:8080/app.js // http://localhost:8080/css/main.css // // http://localhost:8080/ping // http://localhost:8080/.well-known // http://localhost:8080/.well-known/ready // http://localhost:8080/.well-known/live // http://localhost:8080/.well-known/metrics // // Remember: we could use the root wildcard `app.Get("/{param:path}")` and serve the files manually as well. app.Listen(":8080") } ================================================ FILE: _examples/file-server/single-page-application/embedded-single-page-application-with-other-routes/public/app.js ================================================ window.alert("app.js loaded from static page of \"/"); ================================================ FILE: _examples/file-server/single-page-application/embedded-single-page-application-with-other-routes/public/css/main.css ================================================ body { background-color: black; } ================================================ FILE: _examples/file-server/single-page-application/embedded-single-page-application-with-other-routes/public/index.html ================================================ Hello from static page

Hello from index.html

================================================ FILE: _examples/file-server/spa-vue-router/frontend/css/page.css ================================================ .router-link-active { color: red; } ================================================ FILE: _examples/file-server/spa-vue-router/frontend/index.html ================================================ Iris + Vue Router

Hello App!

Go to Foo Go to Bar

================================================ FILE: _examples/file-server/spa-vue-router/frontend/js/app.js ================================================ // 0. If using a module system (e.g. via vue-cli), import Vue and VueRouter // and then call `Vue.use(VueRouter)`. // 1. Define route components. // These can be imported from other files const Foo = { template: '
foo
' } const Bar = { template: '
bar
' } // 2. Define some routes // Each route should map to a component. The "component" can // either be an actual component constructor created via // `Vue.extend()`, or just a component options object. // We'll talk about nested routes later. const routes = [ { path: '/foo', component: Foo }, { path: '/bar', component: Bar } ] // 3. Create the router instance and pass the `routes` option // You can pass in additional options here, but let's // keep it simple for now. const router = new VueRouter({ routes // short for `routes: routes` }) // 4. Create and mount the root instance. // Make sure to inject the router with the router option to make the // whole app router-aware. const app = new Vue({ router }).$mount('#app') // Now the app has started! ================================================ FILE: _examples/file-server/spa-vue-router/main.go ================================================ // Package main simply shows how you can getting started with Iris and Vue Router. // Read more at: https://router.vuejs.org/guide/#html package main import "github.com/kataras/iris/v12" func main() { app := iris.New() app.HandleDir("/", "./frontend") app.Listen(":8080") } /* For those who want to use HTML template as the index page and serve static files in the root request path and use vue router as the main router of the entire application, please follow the below code example: func fullVueRouter() { app := iris.New() app.RegisterView(iris.HTML("./views", ".html")) app.OnAnyErrorCode(index) app.HandleDir("/", "./frontend") app.Get("/", index) app.Listen(":8080") } func index(ctx iris.Context) { if err := ctx.View("index.html"); err != nil { ctx.HTML(fmt.Sprintf("

%s

", err.Error())) return } } */ ================================================ FILE: _examples/file-server/subdomain/assets/app2/app22/just_a_text_no_index.txt ================================================ just a text. ================================================ FILE: _examples/file-server/subdomain/assets/app2/app2app3/index.html ================================================

Hello App2App3 index

================================================ FILE: _examples/file-server/subdomain/assets/app2/index.html ================================================

Hello App2 index

================================================ FILE: _examples/file-server/subdomain/assets/css/main.css ================================================ body { background-color: black; } ================================================ FILE: _examples/file-server/subdomain/assets/index.html ================================================

Hello index

================================================ FILE: _examples/file-server/subdomain/assets/js/jquery-2.1.1.js ================================================ console.log("example"); ================================================ FILE: _examples/file-server/subdomain/hosts ================================================ 127.0.0.1 examle.com 127.0.0.1 v1.examle.com ================================================ FILE: _examples/file-server/subdomain/main.go ================================================ package main import ( "github.com/kataras/iris/v12" ) const ( addr = "example.com:80" subdomain = "v1" ) func newApp() *iris.Application { app := iris.New() app.Favicon("./assets/favicon.ico") v1 := app.Subdomain(subdomain) v1.HandleDir("/", iris.Dir("./assets")) // http://v1.example.com // http://v1.example.com/css/main.css // http://v1.example.com/js/jquery-2.1.1.js // http://v1.example.com/favicon.ico return app } func main() { app := newApp() app.Listen(addr) } ================================================ FILE: _examples/file-server/subdomain/main_test.go ================================================ package main import ( "net" "os" "path/filepath" "testing" "github.com/kataras/iris/v12/httptest" ) type resource string func (r resource) contentType() string { switch filepath.Ext(r.String()) { case ".js": return "text/javascript" case ".css": return "text/css" case ".ico": return "image/x-icon" case ".html", "": return "text/html" default: return "text/plain" } } func (r resource) String() string { return string(r) } func (r resource) loadFromBase(dir string) string { filename := r.String() if filepath.Ext(filename) == "" { // root /. filename = filename + "/index.html" } fullpath := filepath.Join(dir, filename) b, err := os.ReadFile(fullpath) if err != nil { panic(fullpath + " failed with error: " + err.Error()) } result := string(b) return result } func TestFileServerSubdomainBasic(t *testing.T) { urls := []resource{ "/css/main.css", "/js/jquery-2.1.1.js", "/favicon.ico", "/app2", "/app2/app2app3", "/", } app := newApp() e := httptest.New(t, app) host, _, err := net.SplitHostPort(addr) if err != nil { t.Fatal(err) } host = "http://" + subdomain + "." + host for _, u := range urls { url := u.String() contents := u.loadFromBase("./assets") e.GET(url).WithURL(host).Expect(). Status(httptest.StatusOK). ContentType(u.contentType(), app.ConfigurationReadOnly().GetCharset()). Body().IsEqual(contents) } } ================================================ FILE: _examples/file-server/upload-file/main.go ================================================ package main import ( "crypto/md5" "fmt" "io" "path/filepath" "strconv" "time" "github.com/kataras/iris/v12" ) const maxSize = 5 << 20 // 5MB func main() { app := iris.New() app.RegisterView(iris.HTML("./templates", ".html")) // Serve the upload_form.html to the client. app.Get("/upload", func(ctx iris.Context) { // create a token (optionally). now := time.Now().Unix() h := md5.New() io.WriteString(h, strconv.FormatInt(now, 10)) token := fmt.Sprintf("%x", h.Sum(nil)) // render the form with the token for any use you'd like. // ctx.ViewData("", token) // or add second argument to the `View` method. // Token will be passed as {{.}} in the template. if err := ctx.View("upload_form.html", token); err != nil { ctx.HTML(fmt.Sprintf("

%s

", err.Error())) return } }) /* Read before continue. 0. The default post max size is 32MB, you can extend it to read more data using the `iris.WithPostMaxMemory(maxSize)` configurator at `app.Run`, note that this will not be enough for your needs, read below. 1. The faster way to check the size is using the `ctx.GetContentLength()` which returns the whole request's size (plus a logical number like 2MB or even 10MB for the rest of the size like headers). You can create a middleware to adapt this to any necessary handler. myLimiter := func(ctx iris.Context) { if ctx.GetContentLength() > maxSize { // + 2 << 20 { ctx.StatusCode(iris.StatusRequestEntityTooLarge) return } ctx.Next() } app.Post("/upload", myLimiter, myUploadHandler) Most clients will set the "Content-Length" header (like browsers) but it's always better to make sure that any client can't send data that your server can't or doesn't want to handle. This can be happen using the `app.Use(LimitRequestBodySize(maxSize))` (as app or route middleware) or the `ctx.SetMaxRequestBodySize(maxSize)` to limit the request based on a customized logic inside a particular handler, they're the same, read below. 2. You can force-limit the request body size inside a handler using the `ctx.SetMaxRequestBodySize(maxSize)`, this will force the connection to close if the incoming data are larger (most clients will receive it as "connection reset"), use that to make sure that the client will not send data that your server can't or doesn't want to accept, as a fallback. app.Post("/upload", iris.LimitRequestBodySize(maxSize), myUploadHandler) OR app.Post("/upload", func(ctx iris.Context){ ctx.SetMaxRequestBodySize(maxSize) // [...] }) 3. Another way is to receive the data and check the second return value's `Size` value of the `ctx.FormFile`, i.e `info.Size`, this will give you the exact file size, not the whole incoming request data length. app.Post("/", func(ctx iris.Context){ file, info, err := ctx.FormFile("uploadfile") if err != nil { ctx.StatusCode(iris.StatusInternalServerError) ctx.HTML("Error while uploading: " + err.Error() + "") return } defer file.Close() if info.Size > maxSize { ctx.StatusCode(iris.StatusRequestEntityTooLarge) return } // [...] }) */ // Handle the post request from the upload_form.html to the server app.Post("/upload", iris.LimitRequestBodySize(maxSize+1<<20), func(ctx iris.Context) { // Get the file from the request. f, fh, err := ctx.FormFile("uploadfile") if err != nil { ctx.StatusCode(iris.StatusInternalServerError) ctx.HTML("Error while uploading: " + err.Error() + "") return } defer f.Close() _, err = ctx.SaveFormFile(fh, filepath.Join("./uploads", fh.Filename)) if err != nil { ctx.StatusCode(iris.StatusInternalServerError) ctx.HTML("Error while uploading: " + err.Error() + "") return } }) // start the server at http://localhost:8080 with post limit at 5 MB. app.Listen(":8080" /* 0.*/, iris.WithPostMaxMemory(maxSize)) } ================================================ FILE: _examples/file-server/upload-file/templates/upload_form.html ================================================ Upload file
================================================ FILE: _examples/file-server/upload-files/main.go ================================================ package main import ( "crypto/md5" "fmt" "io" "mime/multipart" "strconv" "strings" "time" "github.com/kataras/iris/v12" ) func main() { app := newApp() // start the server at http://localhost:8080 with post limit at 32 MB. app.Listen(":8080", iris.WithPostMaxMemory(32<<20 /* same as 32 * iris.MB */)) } func newApp() *iris.Application { app := iris.New() app.RegisterView(iris.HTML("./templates", ".html")) // Serve the upload_form.html to the client. app.Get("/upload", func(ctx iris.Context) { // create a token (optionally). now := time.Now().Unix() h := md5.New() io.WriteString(h, strconv.FormatInt(now, 10)) token := fmt.Sprintf("%x", h.Sum(nil)) // render the form with the token for any use you'd like. if err := ctx.View("upload_form.html", token); err != nil { ctx.HTML(fmt.Sprintf("

%s

", err.Error())) return } }) // Handle the post request from the upload_form.html to the server. app.Post("/upload", func(ctx iris.Context) { // // UploadFormFiles // uploads any number of incoming files ("multiple" property on the form input). // // second argument is optional, // it can be used to change a file's name based on the request, // at this example we will showcase how to use it // by prefixing the uploaded file with the current user's ip. _, _, err := ctx.UploadFormFiles("./uploads", beforeSave) if err != nil { ctx.StopWithError(iris.StatusBadRequest, err) return } }) app.Post("/upload_manual", func(ctx iris.Context) { // Get the max post value size passed via iris.WithPostMaxMemory. maxSize := ctx.Application().ConfigurationReadOnly().GetPostMaxMemory() err := ctx.Request().ParseMultipartForm(maxSize) if err != nil { ctx.StopWithError(iris.StatusInternalServerError, err) return } form := ctx.Request().MultipartForm files := form.File["files[]"] failures := 0 for _, file := range files { _, err = ctx.SaveFormFile(file, "./uploads/"+file.Filename) if err != nil { failures++ ctx.Writef("failed to upload: %s\n", file.Filename) } } ctx.Writef("%d files uploaded", len(files)-failures) }) return app } func beforeSave(ctx iris.Context, file *multipart.FileHeader) bool { ip := ctx.RemoteAddr() // make sure you format the ip in a way // that can be used for a file name (simple case): ip = strings.ReplaceAll(ip, ".", "_") ip = strings.ReplaceAll(ip, ":", "_") // you can use the time.Now, to prefix or suffix the files // based on the current time as well, as an exercise. // i.e unixTime := time.Now().Unix() // prefix the Filename with the $IP- // no need for more actions, internal uploader will use this // name to save the file into the "./uploads" folder. if ip == "" { return true // don't change the file but continue saving it. } _ = ip // file.Filename = ip + "-" + file.Filename return true } ================================================ FILE: _examples/file-server/upload-files/main_test.go ================================================ package main import ( "os" "testing" "github.com/kataras/iris/v12/httptest" ) func TestUploadFiles(t *testing.T) { app := newApp() e := httptest.New(t, app) // upload the file itself. fh, err := os.Open("main.go") if err != nil { t.Fatal(err) } defer fh.Close() e.POST("/upload").WithMultipart().WithFile("files", "main.go", fh). Expect().Status(httptest.StatusOK) f, err := os.Open("uploads/main.go") if err != nil { t.Fatalf("expected file to get actually uploaded on the system directory but: %v", err) } f.Close() os.Remove(f.Name()) } ================================================ FILE: _examples/file-server/upload-files/templates/upload_form.html ================================================ Upload file
================================================ FILE: _examples/file-server/webdav/main.go ================================================ package main import ( "net/http" "os" "strings" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/middleware/accesslog" "github.com/kataras/iris/v12/middleware/recover" "golang.org/x/net/webdav" ) func main() { app := iris.New() app.Logger().SetLevel("debug") app.Use(recover.New()) app.Use(accesslog.New(os.Stdout).Handler) webdavHandler := &webdav.Handler{ FileSystem: webdav.Dir("./"), LockSystem: webdav.NewMemLS(), Logger: func(r *http.Request, err error) { if err != nil { app.Logger().Error(err) } }, } app.HandleMany(strings.Join(iris.WebDAVMethods, " "), "/{p:path}", iris.FromStd(webdavHandler)) app.Listen(":8080", iris.WithoutServerError(iris.ErrServerClosed, iris.ErrURLQuerySemicolon), iris.WithoutPathCorrection, ) } /* Test with cURL or postman: * List files: curl --location --request PROPFIND 'http://localhost:8080' * Get File: curl --location --request GET 'http://localhost:8080/test.txt' * Upload File: curl --location --request PUT 'http://localhost:8080/newfile.txt' \ --header 'Content-Type: text/plain' \ --data-raw 'This is a new file!' * Copy File: curl --location --request COPY 'http://localhost:8080/test.txt' \ --header 'Destination: newdir/test.txt' * Create New Directory: curl --location --request MKCOL 'http://localhost:8080/anewdir/' And e.t.c. */ ================================================ FILE: _examples/file-server/webdav/newdir/.gitkeep ================================================ ================================================ FILE: _examples/file-server/webdav/test.txt ================================================ Hello, world! ================================================ FILE: _examples/graphql/schema-first/README.md ================================================ # outerbanks-api A graphql api where we can store and get information on characters in Outerbanks. > This example is an updated version (**2023**) of outerbanks-api and it is based on: https://www.apollographql.com/blog/graphql/golang/using-graphql-with-golang. ![](https://www.iris-go.com/images/graphql_playground.png) ## Getting Started ```sh $ go install github.com/99designs/gqlgen@latest ``` Add `gqlgen` to your project's `tools.go` file ```sh $ printf '// +build tools\npackage tools\nimport _ "github.com/99designs/gqlgen"' | gofmt > tools.go $ go get github.com/kataras/iris/v12@latest $ go mod tidy -compat=1.20 ``` Start the graphql server ``` $ go run . ``` ## Mutation Open http://localhost:8080 On the editor panel paste: ```graphql mutation upsertCharacter($input:CharacterInput!){ upsertCharacter(input:$input) { name id } } ``` And in the variables panel below, paste: ```json { "input":{ "name": "kataras", "cliqueType": "POGUES" } } ``` Hit Ctrl+Enter to apply the mutation. ## Query Query: ```graphql query character($id:ID!) { character(id:$id) { id name } } ``` Variables: ```json { "id":1 } ``` ## Re-generate code ```sh $ cd graph $ rm -f graph/schema.resolvers.go $ touch schema.graphql # make your updates here $ gqlgen generate ``` ================================================ FILE: _examples/graphql/schema-first/go.mod ================================================ module github.com/iris-contrib/outerbanks-api go 1.25 require ( github.com/99designs/gqlgen v0.17.86 github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd github.com/vektah/gqlparser/v2 v2.5.31 ) require ( github.com/BurntSushi/toml v1.6.0 // indirect github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect github.com/CloudyKit/jet/v6 v6.3.1 // indirect github.com/Joker/jade v1.1.3 // indirect github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 // indirect github.com/agnivade/levenshtein v1.2.1 // indirect github.com/andybalholm/brotli v1.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/fatih/structs v1.1.0 // indirect github.com/flosch/pongo2/v4 v4.0.2 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/goccy/go-yaml v1.19.2 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/iris-contrib/schema v0.0.6 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/kataras/blocks v0.0.8 // indirect github.com/kataras/golog v0.1.12 // indirect github.com/kataras/pio v0.0.13 // indirect github.com/kataras/sitemap v0.0.6 // indirect github.com/kataras/tunnel v0.0.4 // indirect github.com/klauspost/compress v1.18.2 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mailgun/raymond/v2 v2.0.48 // indirect github.com/mailru/easyjson v0.9.1 // indirect github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/schollz/closestmatch v2.1.0+incompatible // indirect github.com/sirupsen/logrus v1.8.1 // indirect github.com/sosodev/duration v1.3.1 // indirect github.com/tdewolff/minify/v2 v2.24.8 // indirect github.com/tdewolff/parse/v2 v2.8.5 // indirect github.com/urfave/cli/v3 v3.6.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/yosssi/ace v0.0.5 // indirect golang.org/x/crypto v0.47.0 // indirect golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect golang.org/x/mod v0.31.0 // indirect golang.org/x/net v0.49.0 // indirect golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.40.0 // indirect golang.org/x/text v0.33.0 // indirect golang.org/x/time v0.14.0 // indirect golang.org/x/tools v0.40.0 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: _examples/graphql/schema-first/go.sum ================================================ github.com/99designs/gqlgen v0.17.86 h1:C8N3UTa5heXX6twl+b0AJyGkTwYL6dNmFrgZNLRcU6w= github.com/99designs/gqlgen v0.17.86/go.mod h1:KTrPl+vHA1IUzNlh4EYkl7+tcErL3MgKnhHrBcV74Fw= github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= github.com/CloudyKit/jet/v6 v6.3.1 h1:6IAo5Cx21xrHVaR8zzXN5gJatKV/wO7Nf6bfCnCSbUw= github.com/CloudyKit/jet/v6 v6.3.1/go.mod h1:lf8ksdNsxZt7/yH/3n4vJQWA9RUq4wpaHtArHhGVMOw= github.com/Joker/hpp v1.0.0 h1:65+iuJYdRXv/XyN62C1uEmmOx3432rNG/rKlX6V7Kkc= github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= github.com/Joker/jade v1.1.3 h1:Qbeh12Vq6BxURXT1qZBRHsDxeURB8ztcL6f3EXSGeHk= github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKduM= github.com/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw= github.com/PuerkitoBio/goquery v1.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 h1:dG7/gLroJGht/jSQtHiLvT48Hxn+crbmvyItZC8cWOs= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05/go.mod h1:NYezi6wtnJtBm5btoprXc5SvAdqH0XTXWnUup0MptAI= github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM= github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo= github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/flosch/pongo2/v4 v4.0.2 h1:gv+5Pe3vaSVmiJvh/BZa82b7/00YUGm0PIyVVLop0Hw= github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+BWPhwZD8= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a h1:l7A0loSszR5zHd/qK53ZIHMO8b3bBSmENnQ6eKnUT0A= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/iris-contrib/httpexpect/v2 v2.15.2 h1:T9THsdP1woyAqKHwjkEsbCnMefsAFvk8iJJKokcJ3Go= github.com/iris-contrib/httpexpect/v2 v2.15.2/go.mod h1:JLDgIqnFy5loDSUv1OA2j0mb6p/rDhiCqigP22Uq9xE= github.com/iris-contrib/schema v0.0.6 h1:CPSBLyx2e91H2yJzPuhGuifVRnZBBJ3pCOMbOvPZaTw= github.com/iris-contrib/schema v0.0.6/go.mod h1:iYszG0IOsuIsfzjymw1kMzTL8YQcCWlm65f3wX8J5iA= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kataras/blocks v0.0.8 h1:MrpVhoFTCR2v1iOOfGng5VJSILKeZZI+7NGfxEh3SUM= github.com/kataras/blocks v0.0.8/go.mod h1:9Jm5zx6BB+06NwA+OhTbHW1xkMOYxahnqTN5DveZ2Yg= github.com/kataras/golog v0.1.12 h1:Bu7I/G4ilJlbfzjmU39O9N+2uO1pBcMK045fzZ4ytNg= github.com/kataras/golog v0.1.12/go.mod h1:wrGSbOiBqbQSQznleVNX4epWM8rl9SJ/rmEacl0yqy4= github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd h1:oMsQgqKACbUdlaIWnN+EZzzAnHkw90hE/y/R0BSij2U= github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd/go.mod h1:yucjtDl9Vn3OctWM1fi0MuM9OckemC7kEreFa9QtPF8= github.com/kataras/pio v0.0.13 h1:x0rXVX0fviDTXOOLOmr4MUxOabu1InVSTu5itF8CXCM= github.com/kataras/pio v0.0.13/go.mod h1:k3HNuSw+eJ8Pm2lA4lRhg3DiCjVgHlP8hmXApSej3oM= github.com/kataras/sitemap v0.0.6 h1:w71CRMMKYMJh6LR2wTgnk5hSgjVNB9KL60n5e2KHvLY= github.com/kataras/sitemap v0.0.6/go.mod h1:dW4dOCNs896OR1HmG+dMLdT7JjDk7mYBzoIRwuj5jA4= github.com/kataras/tunnel v0.0.4 h1:sCAqWuJV7nPzGrlb0os3j49lk2JhILT0rID38NHNLpA= github.com/kataras/tunnel v0.0.4/go.mod h1:9FkU4LaeifdMWqZu7o20ojmW4B7hdhv2CMLwfnHGpYw= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw= github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18= github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo= github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk= github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4= github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tdewolff/minify/v2 v2.24.8 h1:58/VjsbevI4d5FGV0ZSuBrHMSSkH4MCH0sIz/eKIauE= github.com/tdewolff/minify/v2 v2.24.8/go.mod h1:0Ukj0CRpo/sW/nd8uZ4ccXaV1rEVIWA3dj8U7+Shhfw= github.com/tdewolff/parse/v2 v2.8.5 h1:ZmBiA/8Do5Rpk7bDye0jbbDUpXXbCdc3iah4VeUvwYU= github.com/tdewolff/parse/v2 v2.8.5/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo= github.com/tdewolff/test v1.0.11 h1:FdLbwQVHxqG16SlkGveC0JVyrJN62COWTRyUFzfbtBE= github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= github.com/urfave/cli/v3 v3.6.1 h1:j8Qq8NyUawj/7rTYdBGrxcH7A/j7/G8Q5LhWEW4G3Mo= github.com/urfave/cli/v3 v3.6.1/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/vektah/gqlparser/v2 v2.5.31 h1:YhWGA1mfTjID7qJhd1+Vxhpk5HTgydrGU9IgkWBTJ7k= github.com/vektah/gqlparser/v2 v2.5.31/go.mod h1:c1I28gSOVNzlfc4WuDlqU7voQnsqI6OG2amkBAFmgts= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA= github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0= github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs= moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHcE= ================================================ FILE: _examples/graphql/schema-first/gqlgen.yml ================================================ # Where are all the schema files located? globs are supported eg src/**/*.graphqls schema: - graph/*.graphqls # Where should the generated server code go? exec: filename: graph/generated.go package: graph # Uncomment to enable federation # federation: # filename: graph/federation.go # package: graph # Where should any generated models go? model: filename: graph/model/models_gen.go package: model # Where should the resolver implementations go? resolver: layout: follow-schema dir: graph package: graph # Optional: turn on use ` + "`" + `gqlgen:"fieldName"` + "`" + ` tags in your models # struct_tag: json # Optional: turn on to use []Thing instead of []*Thing # omit_slice_element_pointers: false # Optional: turn off to make struct-type struct fields not use pointers # e.g. type Thing struct { FieldA OtherThing } instead of { FieldA *OtherThing } # struct_fields_always_pointers: true # Optional: turn off to make resolvers return values instead of pointers for structs # resolvers_always_return_pointers: true # Optional: set to speed up generation time by not performing a final validation pass. # skip_validation: true # gqlgen will search for any type names in the schema in these go packages # if they match it will use them, otherwise it will generate them. autobind: # - "github.com/iris-contrib/outerbanks-api/graph/model" # This section declares type mapping between the GraphQL and go type systems # # The first line in each type will be used as defaults for resolver arguments and # modelgen, the others will be allowed when binding to fields. Configure them to # your liking models: ID: model: - github.com/99designs/gqlgen/graphql.ID - github.com/99designs/gqlgen/graphql.Int - github.com/99designs/gqlgen/graphql.Int64 - github.com/99designs/gqlgen/graphql.Int32 Int: model: - github.com/99designs/gqlgen/graphql.Int - github.com/99designs/gqlgen/graphql.Int64 - github.com/99designs/gqlgen/graphql.Int32 ================================================ FILE: _examples/graphql/schema-first/graph/generated.go ================================================ // Code generated by github.com/99designs/gqlgen, DO NOT EDIT. package graph import ( "bytes" "context" "embed" "errors" "fmt" "strconv" "sync" "github.com/99designs/gqlgen/graphql" "github.com/99designs/gqlgen/graphql/introspection" "github.com/iris-contrib/outerbanks-api/graph/model" gqlparser "github.com/vektah/gqlparser/v2" "github.com/vektah/gqlparser/v2/ast" ) // region ************************** generated!.gotpl ************************** // NewExecutableSchema creates an ExecutableSchema from the ResolverRoot interface. func NewExecutableSchema(cfg Config) graphql.ExecutableSchema { return &executableSchema{ resolvers: cfg.Resolvers, directives: cfg.Directives, complexity: cfg.Complexity, } } type Config struct { Resolvers ResolverRoot Directives DirectiveRoot Complexity ComplexityRoot } type ResolverRoot interface { Mutation() MutationResolver Query() QueryResolver } type DirectiveRoot struct { } type ComplexityRoot struct { Character struct { CliqueType func(childComplexity int) int ID func(childComplexity int) int IsHero func(childComplexity int) int Name func(childComplexity int) int } Mutation struct { UpsertCharacter func(childComplexity int, input model.CharacterInput) int } Query struct { Character func(childComplexity int, id string) int Characters func(childComplexity int, cliqueType model.CliqueType) int } } type MutationResolver interface { UpsertCharacter(ctx context.Context, input model.CharacterInput) (*model.Character, error) } type QueryResolver interface { Character(ctx context.Context, id string) (*model.Character, error) Characters(ctx context.Context, cliqueType model.CliqueType) ([]*model.Character, error) } type executableSchema struct { resolvers ResolverRoot directives DirectiveRoot complexity ComplexityRoot } func (e *executableSchema) Schema() *ast.Schema { return parsedSchema } func (e *executableSchema) Complexity(typeName, field string, childComplexity int, rawArgs map[string]any) (int, bool) { ec := executionContext{nil, e} _ = ec switch typeName + "." + field { case "Character.cliqueType": if e.complexity.Character.CliqueType == nil { break } return e.complexity.Character.CliqueType(childComplexity), true case "Character.id": if e.complexity.Character.ID == nil { break } return e.complexity.Character.ID(childComplexity), true case "Character.isHero": if e.complexity.Character.IsHero == nil { break } return e.complexity.Character.IsHero(childComplexity), true case "Character.name": if e.complexity.Character.Name == nil { break } return e.complexity.Character.Name(childComplexity), true case "Mutation.upsertCharacter": if e.complexity.Mutation.UpsertCharacter == nil { break } args, err := ec.field_Mutation_upsertCharacter_args(context.TODO(), rawArgs) if err != nil { return 0, false } return e.complexity.Mutation.UpsertCharacter(childComplexity, args["input"].(model.CharacterInput)), true case "Query.character": if e.complexity.Query.Character == nil { break } args, err := ec.field_Query_character_args(context.TODO(), rawArgs) if err != nil { return 0, false } return e.complexity.Query.Character(childComplexity, args["id"].(string)), true case "Query.characters": if e.complexity.Query.Characters == nil { break } args, err := ec.field_Query_characters_args(context.TODO(), rawArgs) if err != nil { return 0, false } return e.complexity.Query.Characters(childComplexity, args["cliqueType"].(model.CliqueType)), true } return 0, false } func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { rc := graphql.GetOperationContext(ctx) ec := executionContext{rc, e} inputUnmarshalMap := graphql.BuildUnmarshalerMap( ec.unmarshalInputCharacterInput, ) first := true switch rc.Operation.Operation { case ast.Query: return func(ctx context.Context) *graphql.Response { if !first { return nil } first = false ctx = graphql.WithUnmarshalerMap(ctx, inputUnmarshalMap) data := ec._Query(ctx, rc.Operation.SelectionSet) var buf bytes.Buffer data.MarshalGQL(&buf) return &graphql.Response{ Data: buf.Bytes(), } } case ast.Mutation: return func(ctx context.Context) *graphql.Response { if !first { return nil } first = false ctx = graphql.WithUnmarshalerMap(ctx, inputUnmarshalMap) data := ec._Mutation(ctx, rc.Operation.SelectionSet) var buf bytes.Buffer data.MarshalGQL(&buf) return &graphql.Response{ Data: buf.Bytes(), } } default: return graphql.OneShot(graphql.ErrorResponse(ctx, "unsupported GraphQL operation")) } } type executionContext struct { *graphql.OperationContext *executableSchema } func (ec *executionContext) introspectSchema() (*introspection.Schema, error) { if ec.DisableIntrospection { return nil, errors.New("introspection disabled") } return introspection.WrapSchema(parsedSchema), nil } func (ec *executionContext) introspectType(name string) (*introspection.Type, error) { if ec.DisableIntrospection { return nil, errors.New("introspection disabled") } return introspection.WrapTypeFromDef(parsedSchema, parsedSchema.Types[name]), nil } //go:embed "schema.graphqls" var sourcesFS embed.FS func sourceData(filename string) string { data, err := sourcesFS.ReadFile(filename) if err != nil { panic(fmt.Sprintf("codegen problem: %s not available", filename)) } return string(data) } var sources = []*ast.Source{ {Name: "schema.graphqls", Input: sourceData("schema.graphqls"), BuiltIn: false}, } var parsedSchema = gqlparser.MustLoadSchema(sources...) // endregion ************************** generated!.gotpl ************************** // region ***************************** args.gotpl ***************************** func (ec *executionContext) field_Mutation_upsertCharacter_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} var arg0 model.CharacterInput if tmp, ok := rawArgs["input"]; ok { ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("input")) arg0, err = ec.unmarshalNCharacterInput2githubᚗcomᚋirisᚑcontribᚋouterbanksᚑapiᚋgraphᚋgraphᚋmodelᚐCharacterInput(ctx, tmp) if err != nil { return nil, err } } args["input"] = arg0 return args, nil } func (ec *executionContext) field_Query___type_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} var arg0 string if tmp, ok := rawArgs["name"]; ok { ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("name")) arg0, err = ec.unmarshalNString2string(ctx, tmp) if err != nil { return nil, err } } args["name"] = arg0 return args, nil } func (ec *executionContext) field_Query_character_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} var arg0 string if tmp, ok := rawArgs["id"]; ok { ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("id")) arg0, err = ec.unmarshalNID2string(ctx, tmp) if err != nil { return nil, err } } args["id"] = arg0 return args, nil } func (ec *executionContext) field_Query_characters_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} var arg0 model.CliqueType if tmp, ok := rawArgs["cliqueType"]; ok { ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("cliqueType")) arg0, err = ec.unmarshalNCliqueType2githubᚗcomᚋirisᚑcontribᚋouterbanksᚑapiᚋgraphᚋgraphᚋmodelᚐCliqueType(ctx, tmp) if err != nil { return nil, err } } args["cliqueType"] = arg0 return args, nil } func (ec *executionContext) field___Type_enumValues_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} var arg0 bool if tmp, ok := rawArgs["includeDeprecated"]; ok { ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("includeDeprecated")) arg0, err = ec.unmarshalOBoolean2bool(ctx, tmp) if err != nil { return nil, err } } args["includeDeprecated"] = arg0 return args, nil } func (ec *executionContext) field___Type_fields_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} var arg0 bool if tmp, ok := rawArgs["includeDeprecated"]; ok { ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("includeDeprecated")) arg0, err = ec.unmarshalOBoolean2bool(ctx, tmp) if err != nil { return nil, err } } args["includeDeprecated"] = arg0 return args, nil } // endregion ***************************** args.gotpl ***************************** // region ************************** directives.gotpl ************************** // endregion ************************** directives.gotpl ************************** // region **************************** field.gotpl ***************************** func (ec *executionContext) _Character_id(ctx context.Context, field graphql.CollectedField, obj *model.Character) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Character_id(ctx, field) if err != nil { return graphql.Null } ctx = graphql.WithFieldContext(ctx, fc) defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) ret = graphql.Null } }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children return obj.ID, nil }) if err != nil { ec.Error(ctx, err) return graphql.Null } if resTmp == nil { if !graphql.HasFieldError(ctx, fc) { ec.Errorf(ctx, "must not be null") } return graphql.Null } res := resTmp.(string) fc.Result = res return ec.marshalNID2string(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_Character_id(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "Character", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { return nil, errors.New("field of type ID does not have child fields") }, } return fc, nil } func (ec *executionContext) _Character_name(ctx context.Context, field graphql.CollectedField, obj *model.Character) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Character_name(ctx, field) if err != nil { return graphql.Null } ctx = graphql.WithFieldContext(ctx, fc) defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) ret = graphql.Null } }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children return obj.Name, nil }) if err != nil { ec.Error(ctx, err) return graphql.Null } if resTmp == nil { if !graphql.HasFieldError(ctx, fc) { ec.Errorf(ctx, "must not be null") } return graphql.Null } res := resTmp.(string) fc.Result = res return ec.marshalNString2string(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_Character_name(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "Character", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { return nil, errors.New("field of type String does not have child fields") }, } return fc, nil } func (ec *executionContext) _Character_isHero(ctx context.Context, field graphql.CollectedField, obj *model.Character) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Character_isHero(ctx, field) if err != nil { return graphql.Null } ctx = graphql.WithFieldContext(ctx, fc) defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) ret = graphql.Null } }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children return obj.IsHero, nil }) if err != nil { ec.Error(ctx, err) return graphql.Null } if resTmp == nil { if !graphql.HasFieldError(ctx, fc) { ec.Errorf(ctx, "must not be null") } return graphql.Null } res := resTmp.(bool) fc.Result = res return ec.marshalNBoolean2bool(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_Character_isHero(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "Character", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { return nil, errors.New("field of type Boolean does not have child fields") }, } return fc, nil } func (ec *executionContext) _Character_cliqueType(ctx context.Context, field graphql.CollectedField, obj *model.Character) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Character_cliqueType(ctx, field) if err != nil { return graphql.Null } ctx = graphql.WithFieldContext(ctx, fc) defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) ret = graphql.Null } }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children return obj.CliqueType, nil }) if err != nil { ec.Error(ctx, err) return graphql.Null } if resTmp == nil { if !graphql.HasFieldError(ctx, fc) { ec.Errorf(ctx, "must not be null") } return graphql.Null } res := resTmp.(model.CliqueType) fc.Result = res return ec.marshalNCliqueType2githubᚗcomᚋirisᚑcontribᚋouterbanksᚑapiᚋgraphᚋgraphᚋmodelᚐCliqueType(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_Character_cliqueType(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "Character", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { return nil, errors.New("field of type CliqueType does not have child fields") }, } return fc, nil } func (ec *executionContext) _Mutation_upsertCharacter(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Mutation_upsertCharacter(ctx, field) if err != nil { return graphql.Null } ctx = graphql.WithFieldContext(ctx, fc) defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) ret = graphql.Null } }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children return ec.resolvers.Mutation().UpsertCharacter(rctx, fc.Args["input"].(model.CharacterInput)) }) if err != nil { ec.Error(ctx, err) } if resTmp == nil { if !graphql.HasFieldError(ctx, fc) { ec.Errorf(ctx, "must not be null") } return graphql.Null } res := resTmp.(*model.Character) fc.Result = res return ec.marshalNCharacter2ᚖgithubᚗcomᚋirisᚑcontribᚋouterbanksᚑapiᚋgraphᚋgraphᚋmodelᚐCharacter(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_Mutation_upsertCharacter(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "Mutation", Field: field, IsMethod: true, IsResolver: true, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { case "id": return ec.fieldContext_Character_id(ctx, field) case "name": return ec.fieldContext_Character_name(ctx, field) case "isHero": return ec.fieldContext_Character_isHero(ctx, field) case "cliqueType": return ec.fieldContext_Character_cliqueType(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Character", field.Name) }, } defer func() { if r := recover(); r != nil { err = ec.Recover(ctx, r) ec.Error(ctx, err) } }() ctx = graphql.WithFieldContext(ctx, fc) if fc.Args, err = ec.field_Mutation_upsertCharacter_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { ec.Error(ctx, err) return } return fc, nil } func (ec *executionContext) _Query_character(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Query_character(ctx, field) if err != nil { return graphql.Null } ctx = graphql.WithFieldContext(ctx, fc) defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) ret = graphql.Null } }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children return ec.resolvers.Query().Character(rctx, fc.Args["id"].(string)) }) if err != nil { ec.Error(ctx, err) } if resTmp == nil { return graphql.Null } res := resTmp.(*model.Character) fc.Result = res return ec.marshalOCharacter2ᚖgithubᚗcomᚋirisᚑcontribᚋouterbanksᚑapiᚋgraphᚋgraphᚋmodelᚐCharacter(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_Query_character(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "Query", Field: field, IsMethod: true, IsResolver: true, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { case "id": return ec.fieldContext_Character_id(ctx, field) case "name": return ec.fieldContext_Character_name(ctx, field) case "isHero": return ec.fieldContext_Character_isHero(ctx, field) case "cliqueType": return ec.fieldContext_Character_cliqueType(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Character", field.Name) }, } defer func() { if r := recover(); r != nil { err = ec.Recover(ctx, r) ec.Error(ctx, err) } }() ctx = graphql.WithFieldContext(ctx, fc) if fc.Args, err = ec.field_Query_character_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { ec.Error(ctx, err) return } return fc, nil } func (ec *executionContext) _Query_characters(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Query_characters(ctx, field) if err != nil { return graphql.Null } ctx = graphql.WithFieldContext(ctx, fc) defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) ret = graphql.Null } }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children return ec.resolvers.Query().Characters(rctx, fc.Args["cliqueType"].(model.CliqueType)) }) if err != nil { ec.Error(ctx, err) } if resTmp == nil { return graphql.Null } res := resTmp.([]*model.Character) fc.Result = res return ec.marshalOCharacter2ᚕᚖgithubᚗcomᚋirisᚑcontribᚋouterbanksᚑapiᚋgraphᚋgraphᚋmodelᚐCharacterᚄ(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_Query_characters(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "Query", Field: field, IsMethod: true, IsResolver: true, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { case "id": return ec.fieldContext_Character_id(ctx, field) case "name": return ec.fieldContext_Character_name(ctx, field) case "isHero": return ec.fieldContext_Character_isHero(ctx, field) case "cliqueType": return ec.fieldContext_Character_cliqueType(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Character", field.Name) }, } defer func() { if r := recover(); r != nil { err = ec.Recover(ctx, r) ec.Error(ctx, err) } }() ctx = graphql.WithFieldContext(ctx, fc) if fc.Args, err = ec.field_Query_characters_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { ec.Error(ctx, err) return } return fc, nil } func (ec *executionContext) _Query___type(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Query___type(ctx, field) if err != nil { return graphql.Null } ctx = graphql.WithFieldContext(ctx, fc) defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) ret = graphql.Null } }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children return ec.introspectType(fc.Args["name"].(string)) }) if err != nil { ec.Error(ctx, err) } if resTmp == nil { return graphql.Null } res := resTmp.(*introspection.Type) fc.Result = res return ec.marshalO__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_Query___type(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "Query", Field: field, IsMethod: true, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { case "kind": return ec.fieldContext___Type_kind(ctx, field) case "name": return ec.fieldContext___Type_name(ctx, field) case "description": return ec.fieldContext___Type_description(ctx, field) case "fields": return ec.fieldContext___Type_fields(ctx, field) case "interfaces": return ec.fieldContext___Type_interfaces(ctx, field) case "possibleTypes": return ec.fieldContext___Type_possibleTypes(ctx, field) case "enumValues": return ec.fieldContext___Type_enumValues(ctx, field) case "inputFields": return ec.fieldContext___Type_inputFields(ctx, field) case "ofType": return ec.fieldContext___Type_ofType(ctx, field) case "specifiedByURL": return ec.fieldContext___Type_specifiedByURL(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type __Type", field.Name) }, } defer func() { if r := recover(); r != nil { err = ec.Recover(ctx, r) ec.Error(ctx, err) } }() ctx = graphql.WithFieldContext(ctx, fc) if fc.Args, err = ec.field_Query___type_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { ec.Error(ctx, err) return } return fc, nil } func (ec *executionContext) _Query___schema(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Query___schema(ctx, field) if err != nil { return graphql.Null } ctx = graphql.WithFieldContext(ctx, fc) defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) ret = graphql.Null } }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children return ec.introspectSchema() }) if err != nil { ec.Error(ctx, err) } if resTmp == nil { return graphql.Null } res := resTmp.(*introspection.Schema) fc.Result = res return ec.marshalO__Schema2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐSchema(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_Query___schema(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "Query", Field: field, IsMethod: true, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { case "description": return ec.fieldContext___Schema_description(ctx, field) case "types": return ec.fieldContext___Schema_types(ctx, field) case "queryType": return ec.fieldContext___Schema_queryType(ctx, field) case "mutationType": return ec.fieldContext___Schema_mutationType(ctx, field) case "subscriptionType": return ec.fieldContext___Schema_subscriptionType(ctx, field) case "directives": return ec.fieldContext___Schema_directives(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type __Schema", field.Name) }, } return fc, nil } func (ec *executionContext) ___Directive_name(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) { fc, err := ec.fieldContext___Directive_name(ctx, field) if err != nil { return graphql.Null } ctx = graphql.WithFieldContext(ctx, fc) defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) ret = graphql.Null } }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children return obj.Name, nil }) if err != nil { ec.Error(ctx, err) return graphql.Null } if resTmp == nil { if !graphql.HasFieldError(ctx, fc) { ec.Errorf(ctx, "must not be null") } return graphql.Null } res := resTmp.(string) fc.Result = res return ec.marshalNString2string(ctx, field.Selections, res) } func (ec *executionContext) fieldContext___Directive_name(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "__Directive", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { return nil, errors.New("field of type String does not have child fields") }, } return fc, nil } func (ec *executionContext) ___Directive_description(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) { fc, err := ec.fieldContext___Directive_description(ctx, field) if err != nil { return graphql.Null } ctx = graphql.WithFieldContext(ctx, fc) defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) ret = graphql.Null } }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children return obj.Description(), nil }) if err != nil { ec.Error(ctx, err) return graphql.Null } if resTmp == nil { return graphql.Null } res := resTmp.(*string) fc.Result = res return ec.marshalOString2ᚖstring(ctx, field.Selections, res) } func (ec *executionContext) fieldContext___Directive_description(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "__Directive", Field: field, IsMethod: true, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { return nil, errors.New("field of type String does not have child fields") }, } return fc, nil } func (ec *executionContext) ___Directive_locations(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) { fc, err := ec.fieldContext___Directive_locations(ctx, field) if err != nil { return graphql.Null } ctx = graphql.WithFieldContext(ctx, fc) defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) ret = graphql.Null } }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children return obj.Locations, nil }) if err != nil { ec.Error(ctx, err) return graphql.Null } if resTmp == nil { if !graphql.HasFieldError(ctx, fc) { ec.Errorf(ctx, "must not be null") } return graphql.Null } res := resTmp.([]string) fc.Result = res return ec.marshalN__DirectiveLocation2ᚕstringᚄ(ctx, field.Selections, res) } func (ec *executionContext) fieldContext___Directive_locations(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "__Directive", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { return nil, errors.New("field of type __DirectiveLocation does not have child fields") }, } return fc, nil } func (ec *executionContext) ___Directive_args(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) { fc, err := ec.fieldContext___Directive_args(ctx, field) if err != nil { return graphql.Null } ctx = graphql.WithFieldContext(ctx, fc) defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) ret = graphql.Null } }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children return obj.Args, nil }) if err != nil { ec.Error(ctx, err) return graphql.Null } if resTmp == nil { if !graphql.HasFieldError(ctx, fc) { ec.Errorf(ctx, "must not be null") } return graphql.Null } res := resTmp.([]introspection.InputValue) fc.Result = res return ec.marshalN__InputValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValueᚄ(ctx, field.Selections, res) } func (ec *executionContext) fieldContext___Directive_args(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "__Directive", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { case "name": return ec.fieldContext___InputValue_name(ctx, field) case "description": return ec.fieldContext___InputValue_description(ctx, field) case "type": return ec.fieldContext___InputValue_type(ctx, field) case "defaultValue": return ec.fieldContext___InputValue_defaultValue(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type __InputValue", field.Name) }, } return fc, nil } func (ec *executionContext) ___Directive_isRepeatable(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) { fc, err := ec.fieldContext___Directive_isRepeatable(ctx, field) if err != nil { return graphql.Null } ctx = graphql.WithFieldContext(ctx, fc) defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) ret = graphql.Null } }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children return obj.IsRepeatable, nil }) if err != nil { ec.Error(ctx, err) return graphql.Null } if resTmp == nil { if !graphql.HasFieldError(ctx, fc) { ec.Errorf(ctx, "must not be null") } return graphql.Null } res := resTmp.(bool) fc.Result = res return ec.marshalNBoolean2bool(ctx, field.Selections, res) } func (ec *executionContext) fieldContext___Directive_isRepeatable(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "__Directive", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { return nil, errors.New("field of type Boolean does not have child fields") }, } return fc, nil } func (ec *executionContext) ___EnumValue_name(ctx context.Context, field graphql.CollectedField, obj *introspection.EnumValue) (ret graphql.Marshaler) { fc, err := ec.fieldContext___EnumValue_name(ctx, field) if err != nil { return graphql.Null } ctx = graphql.WithFieldContext(ctx, fc) defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) ret = graphql.Null } }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children return obj.Name, nil }) if err != nil { ec.Error(ctx, err) return graphql.Null } if resTmp == nil { if !graphql.HasFieldError(ctx, fc) { ec.Errorf(ctx, "must not be null") } return graphql.Null } res := resTmp.(string) fc.Result = res return ec.marshalNString2string(ctx, field.Selections, res) } func (ec *executionContext) fieldContext___EnumValue_name(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "__EnumValue", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { return nil, errors.New("field of type String does not have child fields") }, } return fc, nil } func (ec *executionContext) ___EnumValue_description(ctx context.Context, field graphql.CollectedField, obj *introspection.EnumValue) (ret graphql.Marshaler) { fc, err := ec.fieldContext___EnumValue_description(ctx, field) if err != nil { return graphql.Null } ctx = graphql.WithFieldContext(ctx, fc) defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) ret = graphql.Null } }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children return obj.Description(), nil }) if err != nil { ec.Error(ctx, err) return graphql.Null } if resTmp == nil { return graphql.Null } res := resTmp.(*string) fc.Result = res return ec.marshalOString2ᚖstring(ctx, field.Selections, res) } func (ec *executionContext) fieldContext___EnumValue_description(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "__EnumValue", Field: field, IsMethod: true, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { return nil, errors.New("field of type String does not have child fields") }, } return fc, nil } func (ec *executionContext) ___EnumValue_isDeprecated(ctx context.Context, field graphql.CollectedField, obj *introspection.EnumValue) (ret graphql.Marshaler) { fc, err := ec.fieldContext___EnumValue_isDeprecated(ctx, field) if err != nil { return graphql.Null } ctx = graphql.WithFieldContext(ctx, fc) defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) ret = graphql.Null } }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children return obj.IsDeprecated(), nil }) if err != nil { ec.Error(ctx, err) return graphql.Null } if resTmp == nil { if !graphql.HasFieldError(ctx, fc) { ec.Errorf(ctx, "must not be null") } return graphql.Null } res := resTmp.(bool) fc.Result = res return ec.marshalNBoolean2bool(ctx, field.Selections, res) } func (ec *executionContext) fieldContext___EnumValue_isDeprecated(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "__EnumValue", Field: field, IsMethod: true, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { return nil, errors.New("field of type Boolean does not have child fields") }, } return fc, nil } func (ec *executionContext) ___EnumValue_deprecationReason(ctx context.Context, field graphql.CollectedField, obj *introspection.EnumValue) (ret graphql.Marshaler) { fc, err := ec.fieldContext___EnumValue_deprecationReason(ctx, field) if err != nil { return graphql.Null } ctx = graphql.WithFieldContext(ctx, fc) defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) ret = graphql.Null } }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children return obj.DeprecationReason(), nil }) if err != nil { ec.Error(ctx, err) return graphql.Null } if resTmp == nil { return graphql.Null } res := resTmp.(*string) fc.Result = res return ec.marshalOString2ᚖstring(ctx, field.Selections, res) } func (ec *executionContext) fieldContext___EnumValue_deprecationReason(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "__EnumValue", Field: field, IsMethod: true, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { return nil, errors.New("field of type String does not have child fields") }, } return fc, nil } func (ec *executionContext) ___Field_name(ctx context.Context, field graphql.CollectedField, obj *introspection.Field) (ret graphql.Marshaler) { fc, err := ec.fieldContext___Field_name(ctx, field) if err != nil { return graphql.Null } ctx = graphql.WithFieldContext(ctx, fc) defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) ret = graphql.Null } }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children return obj.Name, nil }) if err != nil { ec.Error(ctx, err) return graphql.Null } if resTmp == nil { if !graphql.HasFieldError(ctx, fc) { ec.Errorf(ctx, "must not be null") } return graphql.Null } res := resTmp.(string) fc.Result = res return ec.marshalNString2string(ctx, field.Selections, res) } func (ec *executionContext) fieldContext___Field_name(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "__Field", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { return nil, errors.New("field of type String does not have child fields") }, } return fc, nil } func (ec *executionContext) ___Field_description(ctx context.Context, field graphql.CollectedField, obj *introspection.Field) (ret graphql.Marshaler) { fc, err := ec.fieldContext___Field_description(ctx, field) if err != nil { return graphql.Null } ctx = graphql.WithFieldContext(ctx, fc) defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) ret = graphql.Null } }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children return obj.Description(), nil }) if err != nil { ec.Error(ctx, err) return graphql.Null } if resTmp == nil { return graphql.Null } res := resTmp.(*string) fc.Result = res return ec.marshalOString2ᚖstring(ctx, field.Selections, res) } func (ec *executionContext) fieldContext___Field_description(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "__Field", Field: field, IsMethod: true, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { return nil, errors.New("field of type String does not have child fields") }, } return fc, nil } func (ec *executionContext) ___Field_args(ctx context.Context, field graphql.CollectedField, obj *introspection.Field) (ret graphql.Marshaler) { fc, err := ec.fieldContext___Field_args(ctx, field) if err != nil { return graphql.Null } ctx = graphql.WithFieldContext(ctx, fc) defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) ret = graphql.Null } }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children return obj.Args, nil }) if err != nil { ec.Error(ctx, err) return graphql.Null } if resTmp == nil { if !graphql.HasFieldError(ctx, fc) { ec.Errorf(ctx, "must not be null") } return graphql.Null } res := resTmp.([]introspection.InputValue) fc.Result = res return ec.marshalN__InputValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValueᚄ(ctx, field.Selections, res) } func (ec *executionContext) fieldContext___Field_args(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "__Field", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { case "name": return ec.fieldContext___InputValue_name(ctx, field) case "description": return ec.fieldContext___InputValue_description(ctx, field) case "type": return ec.fieldContext___InputValue_type(ctx, field) case "defaultValue": return ec.fieldContext___InputValue_defaultValue(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type __InputValue", field.Name) }, } return fc, nil } func (ec *executionContext) ___Field_type(ctx context.Context, field graphql.CollectedField, obj *introspection.Field) (ret graphql.Marshaler) { fc, err := ec.fieldContext___Field_type(ctx, field) if err != nil { return graphql.Null } ctx = graphql.WithFieldContext(ctx, fc) defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) ret = graphql.Null } }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children return obj.Type, nil }) if err != nil { ec.Error(ctx, err) return graphql.Null } if resTmp == nil { if !graphql.HasFieldError(ctx, fc) { ec.Errorf(ctx, "must not be null") } return graphql.Null } res := resTmp.(*introspection.Type) fc.Result = res return ec.marshalN__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, field.Selections, res) } func (ec *executionContext) fieldContext___Field_type(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "__Field", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { case "kind": return ec.fieldContext___Type_kind(ctx, field) case "name": return ec.fieldContext___Type_name(ctx, field) case "description": return ec.fieldContext___Type_description(ctx, field) case "fields": return ec.fieldContext___Type_fields(ctx, field) case "interfaces": return ec.fieldContext___Type_interfaces(ctx, field) case "possibleTypes": return ec.fieldContext___Type_possibleTypes(ctx, field) case "enumValues": return ec.fieldContext___Type_enumValues(ctx, field) case "inputFields": return ec.fieldContext___Type_inputFields(ctx, field) case "ofType": return ec.fieldContext___Type_ofType(ctx, field) case "specifiedByURL": return ec.fieldContext___Type_specifiedByURL(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type __Type", field.Name) }, } return fc, nil } func (ec *executionContext) ___Field_isDeprecated(ctx context.Context, field graphql.CollectedField, obj *introspection.Field) (ret graphql.Marshaler) { fc, err := ec.fieldContext___Field_isDeprecated(ctx, field) if err != nil { return graphql.Null } ctx = graphql.WithFieldContext(ctx, fc) defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) ret = graphql.Null } }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children return obj.IsDeprecated(), nil }) if err != nil { ec.Error(ctx, err) return graphql.Null } if resTmp == nil { if !graphql.HasFieldError(ctx, fc) { ec.Errorf(ctx, "must not be null") } return graphql.Null } res := resTmp.(bool) fc.Result = res return ec.marshalNBoolean2bool(ctx, field.Selections, res) } func (ec *executionContext) fieldContext___Field_isDeprecated(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "__Field", Field: field, IsMethod: true, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { return nil, errors.New("field of type Boolean does not have child fields") }, } return fc, nil } func (ec *executionContext) ___Field_deprecationReason(ctx context.Context, field graphql.CollectedField, obj *introspection.Field) (ret graphql.Marshaler) { fc, err := ec.fieldContext___Field_deprecationReason(ctx, field) if err != nil { return graphql.Null } ctx = graphql.WithFieldContext(ctx, fc) defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) ret = graphql.Null } }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children return obj.DeprecationReason(), nil }) if err != nil { ec.Error(ctx, err) return graphql.Null } if resTmp == nil { return graphql.Null } res := resTmp.(*string) fc.Result = res return ec.marshalOString2ᚖstring(ctx, field.Selections, res) } func (ec *executionContext) fieldContext___Field_deprecationReason(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "__Field", Field: field, IsMethod: true, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { return nil, errors.New("field of type String does not have child fields") }, } return fc, nil } func (ec *executionContext) ___InputValue_name(ctx context.Context, field graphql.CollectedField, obj *introspection.InputValue) (ret graphql.Marshaler) { fc, err := ec.fieldContext___InputValue_name(ctx, field) if err != nil { return graphql.Null } ctx = graphql.WithFieldContext(ctx, fc) defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) ret = graphql.Null } }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children return obj.Name, nil }) if err != nil { ec.Error(ctx, err) return graphql.Null } if resTmp == nil { if !graphql.HasFieldError(ctx, fc) { ec.Errorf(ctx, "must not be null") } return graphql.Null } res := resTmp.(string) fc.Result = res return ec.marshalNString2string(ctx, field.Selections, res) } func (ec *executionContext) fieldContext___InputValue_name(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "__InputValue", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { return nil, errors.New("field of type String does not have child fields") }, } return fc, nil } func (ec *executionContext) ___InputValue_description(ctx context.Context, field graphql.CollectedField, obj *introspection.InputValue) (ret graphql.Marshaler) { fc, err := ec.fieldContext___InputValue_description(ctx, field) if err != nil { return graphql.Null } ctx = graphql.WithFieldContext(ctx, fc) defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) ret = graphql.Null } }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children return obj.Description(), nil }) if err != nil { ec.Error(ctx, err) return graphql.Null } if resTmp == nil { return graphql.Null } res := resTmp.(*string) fc.Result = res return ec.marshalOString2ᚖstring(ctx, field.Selections, res) } func (ec *executionContext) fieldContext___InputValue_description(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "__InputValue", Field: field, IsMethod: true, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { return nil, errors.New("field of type String does not have child fields") }, } return fc, nil } func (ec *executionContext) ___InputValue_type(ctx context.Context, field graphql.CollectedField, obj *introspection.InputValue) (ret graphql.Marshaler) { fc, err := ec.fieldContext___InputValue_type(ctx, field) if err != nil { return graphql.Null } ctx = graphql.WithFieldContext(ctx, fc) defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) ret = graphql.Null } }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children return obj.Type, nil }) if err != nil { ec.Error(ctx, err) return graphql.Null } if resTmp == nil { if !graphql.HasFieldError(ctx, fc) { ec.Errorf(ctx, "must not be null") } return graphql.Null } res := resTmp.(*introspection.Type) fc.Result = res return ec.marshalN__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, field.Selections, res) } func (ec *executionContext) fieldContext___InputValue_type(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "__InputValue", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { case "kind": return ec.fieldContext___Type_kind(ctx, field) case "name": return ec.fieldContext___Type_name(ctx, field) case "description": return ec.fieldContext___Type_description(ctx, field) case "fields": return ec.fieldContext___Type_fields(ctx, field) case "interfaces": return ec.fieldContext___Type_interfaces(ctx, field) case "possibleTypes": return ec.fieldContext___Type_possibleTypes(ctx, field) case "enumValues": return ec.fieldContext___Type_enumValues(ctx, field) case "inputFields": return ec.fieldContext___Type_inputFields(ctx, field) case "ofType": return ec.fieldContext___Type_ofType(ctx, field) case "specifiedByURL": return ec.fieldContext___Type_specifiedByURL(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type __Type", field.Name) }, } return fc, nil } func (ec *executionContext) ___InputValue_defaultValue(ctx context.Context, field graphql.CollectedField, obj *introspection.InputValue) (ret graphql.Marshaler) { fc, err := ec.fieldContext___InputValue_defaultValue(ctx, field) if err != nil { return graphql.Null } ctx = graphql.WithFieldContext(ctx, fc) defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) ret = graphql.Null } }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children return obj.DefaultValue, nil }) if err != nil { ec.Error(ctx, err) return graphql.Null } if resTmp == nil { return graphql.Null } res := resTmp.(*string) fc.Result = res return ec.marshalOString2ᚖstring(ctx, field.Selections, res) } func (ec *executionContext) fieldContext___InputValue_defaultValue(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "__InputValue", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { return nil, errors.New("field of type String does not have child fields") }, } return fc, nil } func (ec *executionContext) ___Schema_description(ctx context.Context, field graphql.CollectedField, obj *introspection.Schema) (ret graphql.Marshaler) { fc, err := ec.fieldContext___Schema_description(ctx, field) if err != nil { return graphql.Null } ctx = graphql.WithFieldContext(ctx, fc) defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) ret = graphql.Null } }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children return obj.Description(), nil }) if err != nil { ec.Error(ctx, err) return graphql.Null } if resTmp == nil { return graphql.Null } res := resTmp.(*string) fc.Result = res return ec.marshalOString2ᚖstring(ctx, field.Selections, res) } func (ec *executionContext) fieldContext___Schema_description(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "__Schema", Field: field, IsMethod: true, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { return nil, errors.New("field of type String does not have child fields") }, } return fc, nil } func (ec *executionContext) ___Schema_types(ctx context.Context, field graphql.CollectedField, obj *introspection.Schema) (ret graphql.Marshaler) { fc, err := ec.fieldContext___Schema_types(ctx, field) if err != nil { return graphql.Null } ctx = graphql.WithFieldContext(ctx, fc) defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) ret = graphql.Null } }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children return obj.Types(), nil }) if err != nil { ec.Error(ctx, err) return graphql.Null } if resTmp == nil { if !graphql.HasFieldError(ctx, fc) { ec.Errorf(ctx, "must not be null") } return graphql.Null } res := resTmp.([]introspection.Type) fc.Result = res return ec.marshalN__Type2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐTypeᚄ(ctx, field.Selections, res) } func (ec *executionContext) fieldContext___Schema_types(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "__Schema", Field: field, IsMethod: true, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { case "kind": return ec.fieldContext___Type_kind(ctx, field) case "name": return ec.fieldContext___Type_name(ctx, field) case "description": return ec.fieldContext___Type_description(ctx, field) case "fields": return ec.fieldContext___Type_fields(ctx, field) case "interfaces": return ec.fieldContext___Type_interfaces(ctx, field) case "possibleTypes": return ec.fieldContext___Type_possibleTypes(ctx, field) case "enumValues": return ec.fieldContext___Type_enumValues(ctx, field) case "inputFields": return ec.fieldContext___Type_inputFields(ctx, field) case "ofType": return ec.fieldContext___Type_ofType(ctx, field) case "specifiedByURL": return ec.fieldContext___Type_specifiedByURL(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type __Type", field.Name) }, } return fc, nil } func (ec *executionContext) ___Schema_queryType(ctx context.Context, field graphql.CollectedField, obj *introspection.Schema) (ret graphql.Marshaler) { fc, err := ec.fieldContext___Schema_queryType(ctx, field) if err != nil { return graphql.Null } ctx = graphql.WithFieldContext(ctx, fc) defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) ret = graphql.Null } }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children return obj.QueryType(), nil }) if err != nil { ec.Error(ctx, err) return graphql.Null } if resTmp == nil { if !graphql.HasFieldError(ctx, fc) { ec.Errorf(ctx, "must not be null") } return graphql.Null } res := resTmp.(*introspection.Type) fc.Result = res return ec.marshalN__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, field.Selections, res) } func (ec *executionContext) fieldContext___Schema_queryType(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "__Schema", Field: field, IsMethod: true, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { case "kind": return ec.fieldContext___Type_kind(ctx, field) case "name": return ec.fieldContext___Type_name(ctx, field) case "description": return ec.fieldContext___Type_description(ctx, field) case "fields": return ec.fieldContext___Type_fields(ctx, field) case "interfaces": return ec.fieldContext___Type_interfaces(ctx, field) case "possibleTypes": return ec.fieldContext___Type_possibleTypes(ctx, field) case "enumValues": return ec.fieldContext___Type_enumValues(ctx, field) case "inputFields": return ec.fieldContext___Type_inputFields(ctx, field) case "ofType": return ec.fieldContext___Type_ofType(ctx, field) case "specifiedByURL": return ec.fieldContext___Type_specifiedByURL(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type __Type", field.Name) }, } return fc, nil } func (ec *executionContext) ___Schema_mutationType(ctx context.Context, field graphql.CollectedField, obj *introspection.Schema) (ret graphql.Marshaler) { fc, err := ec.fieldContext___Schema_mutationType(ctx, field) if err != nil { return graphql.Null } ctx = graphql.WithFieldContext(ctx, fc) defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) ret = graphql.Null } }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children return obj.MutationType(), nil }) if err != nil { ec.Error(ctx, err) return graphql.Null } if resTmp == nil { return graphql.Null } res := resTmp.(*introspection.Type) fc.Result = res return ec.marshalO__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, field.Selections, res) } func (ec *executionContext) fieldContext___Schema_mutationType(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "__Schema", Field: field, IsMethod: true, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { case "kind": return ec.fieldContext___Type_kind(ctx, field) case "name": return ec.fieldContext___Type_name(ctx, field) case "description": return ec.fieldContext___Type_description(ctx, field) case "fields": return ec.fieldContext___Type_fields(ctx, field) case "interfaces": return ec.fieldContext___Type_interfaces(ctx, field) case "possibleTypes": return ec.fieldContext___Type_possibleTypes(ctx, field) case "enumValues": return ec.fieldContext___Type_enumValues(ctx, field) case "inputFields": return ec.fieldContext___Type_inputFields(ctx, field) case "ofType": return ec.fieldContext___Type_ofType(ctx, field) case "specifiedByURL": return ec.fieldContext___Type_specifiedByURL(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type __Type", field.Name) }, } return fc, nil } func (ec *executionContext) ___Schema_subscriptionType(ctx context.Context, field graphql.CollectedField, obj *introspection.Schema) (ret graphql.Marshaler) { fc, err := ec.fieldContext___Schema_subscriptionType(ctx, field) if err != nil { return graphql.Null } ctx = graphql.WithFieldContext(ctx, fc) defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) ret = graphql.Null } }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children return obj.SubscriptionType(), nil }) if err != nil { ec.Error(ctx, err) return graphql.Null } if resTmp == nil { return graphql.Null } res := resTmp.(*introspection.Type) fc.Result = res return ec.marshalO__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, field.Selections, res) } func (ec *executionContext) fieldContext___Schema_subscriptionType(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "__Schema", Field: field, IsMethod: true, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { case "kind": return ec.fieldContext___Type_kind(ctx, field) case "name": return ec.fieldContext___Type_name(ctx, field) case "description": return ec.fieldContext___Type_description(ctx, field) case "fields": return ec.fieldContext___Type_fields(ctx, field) case "interfaces": return ec.fieldContext___Type_interfaces(ctx, field) case "possibleTypes": return ec.fieldContext___Type_possibleTypes(ctx, field) case "enumValues": return ec.fieldContext___Type_enumValues(ctx, field) case "inputFields": return ec.fieldContext___Type_inputFields(ctx, field) case "ofType": return ec.fieldContext___Type_ofType(ctx, field) case "specifiedByURL": return ec.fieldContext___Type_specifiedByURL(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type __Type", field.Name) }, } return fc, nil } func (ec *executionContext) ___Schema_directives(ctx context.Context, field graphql.CollectedField, obj *introspection.Schema) (ret graphql.Marshaler) { fc, err := ec.fieldContext___Schema_directives(ctx, field) if err != nil { return graphql.Null } ctx = graphql.WithFieldContext(ctx, fc) defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) ret = graphql.Null } }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children return obj.Directives(), nil }) if err != nil { ec.Error(ctx, err) return graphql.Null } if resTmp == nil { if !graphql.HasFieldError(ctx, fc) { ec.Errorf(ctx, "must not be null") } return graphql.Null } res := resTmp.([]introspection.Directive) fc.Result = res return ec.marshalN__Directive2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐDirectiveᚄ(ctx, field.Selections, res) } func (ec *executionContext) fieldContext___Schema_directives(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "__Schema", Field: field, IsMethod: true, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { case "name": return ec.fieldContext___Directive_name(ctx, field) case "description": return ec.fieldContext___Directive_description(ctx, field) case "locations": return ec.fieldContext___Directive_locations(ctx, field) case "args": return ec.fieldContext___Directive_args(ctx, field) case "isRepeatable": return ec.fieldContext___Directive_isRepeatable(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type __Directive", field.Name) }, } return fc, nil } func (ec *executionContext) ___Type_kind(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { fc, err := ec.fieldContext___Type_kind(ctx, field) if err != nil { return graphql.Null } ctx = graphql.WithFieldContext(ctx, fc) defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) ret = graphql.Null } }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children return obj.Kind(), nil }) if err != nil { ec.Error(ctx, err) return graphql.Null } if resTmp == nil { if !graphql.HasFieldError(ctx, fc) { ec.Errorf(ctx, "must not be null") } return graphql.Null } res := resTmp.(string) fc.Result = res return ec.marshalN__TypeKind2string(ctx, field.Selections, res) } func (ec *executionContext) fieldContext___Type_kind(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "__Type", Field: field, IsMethod: true, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { return nil, errors.New("field of type __TypeKind does not have child fields") }, } return fc, nil } func (ec *executionContext) ___Type_name(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { fc, err := ec.fieldContext___Type_name(ctx, field) if err != nil { return graphql.Null } ctx = graphql.WithFieldContext(ctx, fc) defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) ret = graphql.Null } }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children return obj.Name(), nil }) if err != nil { ec.Error(ctx, err) return graphql.Null } if resTmp == nil { return graphql.Null } res := resTmp.(*string) fc.Result = res return ec.marshalOString2ᚖstring(ctx, field.Selections, res) } func (ec *executionContext) fieldContext___Type_name(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "__Type", Field: field, IsMethod: true, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { return nil, errors.New("field of type String does not have child fields") }, } return fc, nil } func (ec *executionContext) ___Type_description(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { fc, err := ec.fieldContext___Type_description(ctx, field) if err != nil { return graphql.Null } ctx = graphql.WithFieldContext(ctx, fc) defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) ret = graphql.Null } }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children return obj.Description(), nil }) if err != nil { ec.Error(ctx, err) return graphql.Null } if resTmp == nil { return graphql.Null } res := resTmp.(*string) fc.Result = res return ec.marshalOString2ᚖstring(ctx, field.Selections, res) } func (ec *executionContext) fieldContext___Type_description(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "__Type", Field: field, IsMethod: true, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { return nil, errors.New("field of type String does not have child fields") }, } return fc, nil } func (ec *executionContext) ___Type_fields(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { fc, err := ec.fieldContext___Type_fields(ctx, field) if err != nil { return graphql.Null } ctx = graphql.WithFieldContext(ctx, fc) defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) ret = graphql.Null } }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children return obj.Fields(fc.Args["includeDeprecated"].(bool)), nil }) if err != nil { ec.Error(ctx, err) return graphql.Null } if resTmp == nil { return graphql.Null } res := resTmp.([]introspection.Field) fc.Result = res return ec.marshalO__Field2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐFieldᚄ(ctx, field.Selections, res) } func (ec *executionContext) fieldContext___Type_fields(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "__Type", Field: field, IsMethod: true, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { case "name": return ec.fieldContext___Field_name(ctx, field) case "description": return ec.fieldContext___Field_description(ctx, field) case "args": return ec.fieldContext___Field_args(ctx, field) case "type": return ec.fieldContext___Field_type(ctx, field) case "isDeprecated": return ec.fieldContext___Field_isDeprecated(ctx, field) case "deprecationReason": return ec.fieldContext___Field_deprecationReason(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type __Field", field.Name) }, } defer func() { if r := recover(); r != nil { err = ec.Recover(ctx, r) ec.Error(ctx, err) } }() ctx = graphql.WithFieldContext(ctx, fc) if fc.Args, err = ec.field___Type_fields_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { ec.Error(ctx, err) return } return fc, nil } func (ec *executionContext) ___Type_interfaces(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { fc, err := ec.fieldContext___Type_interfaces(ctx, field) if err != nil { return graphql.Null } ctx = graphql.WithFieldContext(ctx, fc) defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) ret = graphql.Null } }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children return obj.Interfaces(), nil }) if err != nil { ec.Error(ctx, err) return graphql.Null } if resTmp == nil { return graphql.Null } res := resTmp.([]introspection.Type) fc.Result = res return ec.marshalO__Type2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐTypeᚄ(ctx, field.Selections, res) } func (ec *executionContext) fieldContext___Type_interfaces(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "__Type", Field: field, IsMethod: true, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { case "kind": return ec.fieldContext___Type_kind(ctx, field) case "name": return ec.fieldContext___Type_name(ctx, field) case "description": return ec.fieldContext___Type_description(ctx, field) case "fields": return ec.fieldContext___Type_fields(ctx, field) case "interfaces": return ec.fieldContext___Type_interfaces(ctx, field) case "possibleTypes": return ec.fieldContext___Type_possibleTypes(ctx, field) case "enumValues": return ec.fieldContext___Type_enumValues(ctx, field) case "inputFields": return ec.fieldContext___Type_inputFields(ctx, field) case "ofType": return ec.fieldContext___Type_ofType(ctx, field) case "specifiedByURL": return ec.fieldContext___Type_specifiedByURL(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type __Type", field.Name) }, } return fc, nil } func (ec *executionContext) ___Type_possibleTypes(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { fc, err := ec.fieldContext___Type_possibleTypes(ctx, field) if err != nil { return graphql.Null } ctx = graphql.WithFieldContext(ctx, fc) defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) ret = graphql.Null } }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children return obj.PossibleTypes(), nil }) if err != nil { ec.Error(ctx, err) return graphql.Null } if resTmp == nil { return graphql.Null } res := resTmp.([]introspection.Type) fc.Result = res return ec.marshalO__Type2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐTypeᚄ(ctx, field.Selections, res) } func (ec *executionContext) fieldContext___Type_possibleTypes(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "__Type", Field: field, IsMethod: true, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { case "kind": return ec.fieldContext___Type_kind(ctx, field) case "name": return ec.fieldContext___Type_name(ctx, field) case "description": return ec.fieldContext___Type_description(ctx, field) case "fields": return ec.fieldContext___Type_fields(ctx, field) case "interfaces": return ec.fieldContext___Type_interfaces(ctx, field) case "possibleTypes": return ec.fieldContext___Type_possibleTypes(ctx, field) case "enumValues": return ec.fieldContext___Type_enumValues(ctx, field) case "inputFields": return ec.fieldContext___Type_inputFields(ctx, field) case "ofType": return ec.fieldContext___Type_ofType(ctx, field) case "specifiedByURL": return ec.fieldContext___Type_specifiedByURL(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type __Type", field.Name) }, } return fc, nil } func (ec *executionContext) ___Type_enumValues(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { fc, err := ec.fieldContext___Type_enumValues(ctx, field) if err != nil { return graphql.Null } ctx = graphql.WithFieldContext(ctx, fc) defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) ret = graphql.Null } }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children return obj.EnumValues(fc.Args["includeDeprecated"].(bool)), nil }) if err != nil { ec.Error(ctx, err) return graphql.Null } if resTmp == nil { return graphql.Null } res := resTmp.([]introspection.EnumValue) fc.Result = res return ec.marshalO__EnumValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐEnumValueᚄ(ctx, field.Selections, res) } func (ec *executionContext) fieldContext___Type_enumValues(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "__Type", Field: field, IsMethod: true, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { case "name": return ec.fieldContext___EnumValue_name(ctx, field) case "description": return ec.fieldContext___EnumValue_description(ctx, field) case "isDeprecated": return ec.fieldContext___EnumValue_isDeprecated(ctx, field) case "deprecationReason": return ec.fieldContext___EnumValue_deprecationReason(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type __EnumValue", field.Name) }, } defer func() { if r := recover(); r != nil { err = ec.Recover(ctx, r) ec.Error(ctx, err) } }() ctx = graphql.WithFieldContext(ctx, fc) if fc.Args, err = ec.field___Type_enumValues_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { ec.Error(ctx, err) return } return fc, nil } func (ec *executionContext) ___Type_inputFields(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { fc, err := ec.fieldContext___Type_inputFields(ctx, field) if err != nil { return graphql.Null } ctx = graphql.WithFieldContext(ctx, fc) defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) ret = graphql.Null } }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children return obj.InputFields(), nil }) if err != nil { ec.Error(ctx, err) return graphql.Null } if resTmp == nil { return graphql.Null } res := resTmp.([]introspection.InputValue) fc.Result = res return ec.marshalO__InputValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValueᚄ(ctx, field.Selections, res) } func (ec *executionContext) fieldContext___Type_inputFields(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "__Type", Field: field, IsMethod: true, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { case "name": return ec.fieldContext___InputValue_name(ctx, field) case "description": return ec.fieldContext___InputValue_description(ctx, field) case "type": return ec.fieldContext___InputValue_type(ctx, field) case "defaultValue": return ec.fieldContext___InputValue_defaultValue(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type __InputValue", field.Name) }, } return fc, nil } func (ec *executionContext) ___Type_ofType(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { fc, err := ec.fieldContext___Type_ofType(ctx, field) if err != nil { return graphql.Null } ctx = graphql.WithFieldContext(ctx, fc) defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) ret = graphql.Null } }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children return obj.OfType(), nil }) if err != nil { ec.Error(ctx, err) return graphql.Null } if resTmp == nil { return graphql.Null } res := resTmp.(*introspection.Type) fc.Result = res return ec.marshalO__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, field.Selections, res) } func (ec *executionContext) fieldContext___Type_ofType(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "__Type", Field: field, IsMethod: true, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { case "kind": return ec.fieldContext___Type_kind(ctx, field) case "name": return ec.fieldContext___Type_name(ctx, field) case "description": return ec.fieldContext___Type_description(ctx, field) case "fields": return ec.fieldContext___Type_fields(ctx, field) case "interfaces": return ec.fieldContext___Type_interfaces(ctx, field) case "possibleTypes": return ec.fieldContext___Type_possibleTypes(ctx, field) case "enumValues": return ec.fieldContext___Type_enumValues(ctx, field) case "inputFields": return ec.fieldContext___Type_inputFields(ctx, field) case "ofType": return ec.fieldContext___Type_ofType(ctx, field) case "specifiedByURL": return ec.fieldContext___Type_specifiedByURL(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type __Type", field.Name) }, } return fc, nil } func (ec *executionContext) ___Type_specifiedByURL(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { fc, err := ec.fieldContext___Type_specifiedByURL(ctx, field) if err != nil { return graphql.Null } ctx = graphql.WithFieldContext(ctx, fc) defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) ret = graphql.Null } }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children return obj.SpecifiedByURL(), nil }) if err != nil { ec.Error(ctx, err) return graphql.Null } if resTmp == nil { return graphql.Null } res := resTmp.(*string) fc.Result = res return ec.marshalOString2ᚖstring(ctx, field.Selections, res) } func (ec *executionContext) fieldContext___Type_specifiedByURL(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "__Type", Field: field, IsMethod: true, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { return nil, errors.New("field of type String does not have child fields") }, } return fc, nil } // endregion **************************** field.gotpl ***************************** // region **************************** input.gotpl ***************************** func (ec *executionContext) unmarshalInputCharacterInput(ctx context.Context, obj any) (model.CharacterInput, error) { var it model.CharacterInput asMap := map[string]any{} for k, v := range obj.(map[string]any) { asMap[k] = v } fieldsInOrder := [...]string{"name", "id", "isHero", "cliqueType"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { continue } switch k { case "name": var err error ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("name")) it.Name, err = ec.unmarshalNString2string(ctx, v) if err != nil { return it, err } case "id": var err error ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("id")) it.ID, err = ec.unmarshalOString2ᚖstring(ctx, v) if err != nil { return it, err } case "isHero": var err error ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("isHero")) it.IsHero, err = ec.unmarshalOBoolean2ᚖbool(ctx, v) if err != nil { return it, err } case "cliqueType": var err error ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("cliqueType")) it.CliqueType, err = ec.unmarshalNCliqueType2githubᚗcomᚋirisᚑcontribᚋouterbanksᚑapiᚋgraphᚋgraphᚋmodelᚐCliqueType(ctx, v) if err != nil { return it, err } } } return it, nil } // endregion **************************** input.gotpl ***************************** // region ************************** interface.gotpl *************************** // endregion ************************** interface.gotpl *************************** // region **************************** object.gotpl **************************** var characterImplementors = []string{"Character"} func (ec *executionContext) _Character(ctx context.Context, sel ast.SelectionSet, obj *model.Character) graphql.Marshaler { fields := graphql.CollectFields(ec.OperationContext, sel, characterImplementors) out := graphql.NewFieldSet(fields) var invalids uint32 for i, field := range fields { switch field.Name { case "__typename": out.Values[i] = graphql.MarshalString("Character") case "id": out.Values[i] = ec._Character_id(ctx, field, obj) if out.Values[i] == graphql.Null { invalids++ } case "name": out.Values[i] = ec._Character_name(ctx, field, obj) if out.Values[i] == graphql.Null { invalids++ } case "isHero": out.Values[i] = ec._Character_isHero(ctx, field, obj) if out.Values[i] == graphql.Null { invalids++ } case "cliqueType": out.Values[i] = ec._Character_cliqueType(ctx, field, obj) if out.Values[i] == graphql.Null { invalids++ } default: panic("unknown field " + strconv.Quote(field.Name)) } } out.Dispatch() if invalids > 0 { return graphql.Null } return out } var mutationImplementors = []string{"Mutation"} func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler { fields := graphql.CollectFields(ec.OperationContext, sel, mutationImplementors) ctx = graphql.WithFieldContext(ctx, &graphql.FieldContext{ Object: "Mutation", }) out := graphql.NewFieldSet(fields) for i, field := range fields { innerCtx := graphql.WithRootFieldContext(ctx, &graphql.RootFieldContext{ Object: field.Name, Field: field, }) switch field.Name { case "__typename": out.Values[i] = graphql.MarshalString("Mutation") case "upsertCharacter": out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { return ec._Mutation_upsertCharacter(ctx, field) }) default: panic("unknown field " + strconv.Quote(field.Name)) } } out.Dispatch() return out } var queryImplementors = []string{"Query"} func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler { fields := graphql.CollectFields(ec.OperationContext, sel, queryImplementors) ctx = graphql.WithFieldContext(ctx, &graphql.FieldContext{ Object: "Query", }) out := graphql.NewFieldSet(fields) for i, field := range fields { innerCtx := graphql.WithRootFieldContext(ctx, &graphql.RootFieldContext{ Object: field.Name, Field: field, }) switch field.Name { case "__typename": out.Values[i] = graphql.MarshalString("Query") case "character": field := field innerFunc := func(ctx context.Context) (res graphql.Marshaler) { defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) } }() res = ec._Query_character(ctx, field) return res } rrm := func(ctx context.Context) graphql.Marshaler { return ec.OperationContext.RootResolverMiddleware(ctx, innerFunc) } out.Concurrently(i, func() graphql.Marshaler { return rrm(innerCtx) }) case "characters": field := field innerFunc := func(ctx context.Context) (res graphql.Marshaler) { defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) } }() res = ec._Query_characters(ctx, field) return res } rrm := func(ctx context.Context) graphql.Marshaler { return ec.OperationContext.RootResolverMiddleware(ctx, innerFunc) } out.Concurrently(i, func() graphql.Marshaler { return rrm(innerCtx) }) case "__type": out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { return ec._Query___type(ctx, field) }) case "__schema": out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { return ec._Query___schema(ctx, field) }) default: panic("unknown field " + strconv.Quote(field.Name)) } } out.Dispatch() return out } var __DirectiveImplementors = []string{"__Directive"} func (ec *executionContext) ___Directive(ctx context.Context, sel ast.SelectionSet, obj *introspection.Directive) graphql.Marshaler { fields := graphql.CollectFields(ec.OperationContext, sel, __DirectiveImplementors) out := graphql.NewFieldSet(fields) var invalids uint32 for i, field := range fields { switch field.Name { case "__typename": out.Values[i] = graphql.MarshalString("__Directive") case "name": out.Values[i] = ec.___Directive_name(ctx, field, obj) if out.Values[i] == graphql.Null { invalids++ } case "description": out.Values[i] = ec.___Directive_description(ctx, field, obj) case "locations": out.Values[i] = ec.___Directive_locations(ctx, field, obj) if out.Values[i] == graphql.Null { invalids++ } case "args": out.Values[i] = ec.___Directive_args(ctx, field, obj) if out.Values[i] == graphql.Null { invalids++ } case "isRepeatable": out.Values[i] = ec.___Directive_isRepeatable(ctx, field, obj) if out.Values[i] == graphql.Null { invalids++ } default: panic("unknown field " + strconv.Quote(field.Name)) } } out.Dispatch() if invalids > 0 { return graphql.Null } return out } var __EnumValueImplementors = []string{"__EnumValue"} func (ec *executionContext) ___EnumValue(ctx context.Context, sel ast.SelectionSet, obj *introspection.EnumValue) graphql.Marshaler { fields := graphql.CollectFields(ec.OperationContext, sel, __EnumValueImplementors) out := graphql.NewFieldSet(fields) var invalids uint32 for i, field := range fields { switch field.Name { case "__typename": out.Values[i] = graphql.MarshalString("__EnumValue") case "name": out.Values[i] = ec.___EnumValue_name(ctx, field, obj) if out.Values[i] == graphql.Null { invalids++ } case "description": out.Values[i] = ec.___EnumValue_description(ctx, field, obj) case "isDeprecated": out.Values[i] = ec.___EnumValue_isDeprecated(ctx, field, obj) if out.Values[i] == graphql.Null { invalids++ } case "deprecationReason": out.Values[i] = ec.___EnumValue_deprecationReason(ctx, field, obj) default: panic("unknown field " + strconv.Quote(field.Name)) } } out.Dispatch() if invalids > 0 { return graphql.Null } return out } var __FieldImplementors = []string{"__Field"} func (ec *executionContext) ___Field(ctx context.Context, sel ast.SelectionSet, obj *introspection.Field) graphql.Marshaler { fields := graphql.CollectFields(ec.OperationContext, sel, __FieldImplementors) out := graphql.NewFieldSet(fields) var invalids uint32 for i, field := range fields { switch field.Name { case "__typename": out.Values[i] = graphql.MarshalString("__Field") case "name": out.Values[i] = ec.___Field_name(ctx, field, obj) if out.Values[i] == graphql.Null { invalids++ } case "description": out.Values[i] = ec.___Field_description(ctx, field, obj) case "args": out.Values[i] = ec.___Field_args(ctx, field, obj) if out.Values[i] == graphql.Null { invalids++ } case "type": out.Values[i] = ec.___Field_type(ctx, field, obj) if out.Values[i] == graphql.Null { invalids++ } case "isDeprecated": out.Values[i] = ec.___Field_isDeprecated(ctx, field, obj) if out.Values[i] == graphql.Null { invalids++ } case "deprecationReason": out.Values[i] = ec.___Field_deprecationReason(ctx, field, obj) default: panic("unknown field " + strconv.Quote(field.Name)) } } out.Dispatch() if invalids > 0 { return graphql.Null } return out } var __InputValueImplementors = []string{"__InputValue"} func (ec *executionContext) ___InputValue(ctx context.Context, sel ast.SelectionSet, obj *introspection.InputValue) graphql.Marshaler { fields := graphql.CollectFields(ec.OperationContext, sel, __InputValueImplementors) out := graphql.NewFieldSet(fields) var invalids uint32 for i, field := range fields { switch field.Name { case "__typename": out.Values[i] = graphql.MarshalString("__InputValue") case "name": out.Values[i] = ec.___InputValue_name(ctx, field, obj) if out.Values[i] == graphql.Null { invalids++ } case "description": out.Values[i] = ec.___InputValue_description(ctx, field, obj) case "type": out.Values[i] = ec.___InputValue_type(ctx, field, obj) if out.Values[i] == graphql.Null { invalids++ } case "defaultValue": out.Values[i] = ec.___InputValue_defaultValue(ctx, field, obj) default: panic("unknown field " + strconv.Quote(field.Name)) } } out.Dispatch() if invalids > 0 { return graphql.Null } return out } var __SchemaImplementors = []string{"__Schema"} func (ec *executionContext) ___Schema(ctx context.Context, sel ast.SelectionSet, obj *introspection.Schema) graphql.Marshaler { fields := graphql.CollectFields(ec.OperationContext, sel, __SchemaImplementors) out := graphql.NewFieldSet(fields) var invalids uint32 for i, field := range fields { switch field.Name { case "__typename": out.Values[i] = graphql.MarshalString("__Schema") case "description": out.Values[i] = ec.___Schema_description(ctx, field, obj) case "types": out.Values[i] = ec.___Schema_types(ctx, field, obj) if out.Values[i] == graphql.Null { invalids++ } case "queryType": out.Values[i] = ec.___Schema_queryType(ctx, field, obj) if out.Values[i] == graphql.Null { invalids++ } case "mutationType": out.Values[i] = ec.___Schema_mutationType(ctx, field, obj) case "subscriptionType": out.Values[i] = ec.___Schema_subscriptionType(ctx, field, obj) case "directives": out.Values[i] = ec.___Schema_directives(ctx, field, obj) if out.Values[i] == graphql.Null { invalids++ } default: panic("unknown field " + strconv.Quote(field.Name)) } } out.Dispatch() if invalids > 0 { return graphql.Null } return out } var __TypeImplementors = []string{"__Type"} func (ec *executionContext) ___Type(ctx context.Context, sel ast.SelectionSet, obj *introspection.Type) graphql.Marshaler { fields := graphql.CollectFields(ec.OperationContext, sel, __TypeImplementors) out := graphql.NewFieldSet(fields) var invalids uint32 for i, field := range fields { switch field.Name { case "__typename": out.Values[i] = graphql.MarshalString("__Type") case "kind": out.Values[i] = ec.___Type_kind(ctx, field, obj) if out.Values[i] == graphql.Null { invalids++ } case "name": out.Values[i] = ec.___Type_name(ctx, field, obj) case "description": out.Values[i] = ec.___Type_description(ctx, field, obj) case "fields": out.Values[i] = ec.___Type_fields(ctx, field, obj) case "interfaces": out.Values[i] = ec.___Type_interfaces(ctx, field, obj) case "possibleTypes": out.Values[i] = ec.___Type_possibleTypes(ctx, field, obj) case "enumValues": out.Values[i] = ec.___Type_enumValues(ctx, field, obj) case "inputFields": out.Values[i] = ec.___Type_inputFields(ctx, field, obj) case "ofType": out.Values[i] = ec.___Type_ofType(ctx, field, obj) case "specifiedByURL": out.Values[i] = ec.___Type_specifiedByURL(ctx, field, obj) default: panic("unknown field " + strconv.Quote(field.Name)) } } out.Dispatch() if invalids > 0 { return graphql.Null } return out } // endregion **************************** object.gotpl **************************** // region ***************************** type.gotpl ***************************** func (ec *executionContext) unmarshalNBoolean2bool(ctx context.Context, v any) (bool, error) { res, err := graphql.UnmarshalBoolean(v) return res, graphql.ErrorOnPath(ctx, err) } func (ec *executionContext) marshalNBoolean2bool(ctx context.Context, sel ast.SelectionSet, v bool) graphql.Marshaler { res := graphql.MarshalBoolean(v) if res == graphql.Null { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { ec.Errorf(ctx, "the requested element is null which the schema does not allow") } } return res } func (ec *executionContext) marshalNCharacter2githubᚗcomᚋirisᚑcontribᚋouterbanksᚑapiᚋgraphᚋgraphᚋmodelᚐCharacter(ctx context.Context, sel ast.SelectionSet, v model.Character) graphql.Marshaler { return ec._Character(ctx, sel, &v) } func (ec *executionContext) marshalNCharacter2ᚖgithubᚗcomᚋirisᚑcontribᚋouterbanksᚑapiᚋgraphᚋgraphᚋmodelᚐCharacter(ctx context.Context, sel ast.SelectionSet, v *model.Character) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { ec.Errorf(ctx, "the requested element is null which the schema does not allow") } return graphql.Null } return ec._Character(ctx, sel, v) } func (ec *executionContext) unmarshalNCharacterInput2githubᚗcomᚋirisᚑcontribᚋouterbanksᚑapiᚋgraphᚋgraphᚋmodelᚐCharacterInput(ctx context.Context, v any) (model.CharacterInput, error) { res, err := ec.unmarshalInputCharacterInput(ctx, v) return res, graphql.ErrorOnPath(ctx, err) } func (ec *executionContext) unmarshalNCliqueType2githubᚗcomᚋirisᚑcontribᚋouterbanksᚑapiᚋgraphᚋgraphᚋmodelᚐCliqueType(ctx context.Context, v any) (model.CliqueType, error) { var res model.CliqueType err := res.UnmarshalGQL(v) return res, graphql.ErrorOnPath(ctx, err) } func (ec *executionContext) marshalNCliqueType2githubᚗcomᚋirisᚑcontribᚋouterbanksᚑapiᚋgraphᚋgraphᚋmodelᚐCliqueType(ctx context.Context, sel ast.SelectionSet, v model.CliqueType) graphql.Marshaler { return v } func (ec *executionContext) unmarshalNID2string(ctx context.Context, v any) (string, error) { res, err := graphql.UnmarshalID(v) return res, graphql.ErrorOnPath(ctx, err) } func (ec *executionContext) marshalNID2string(ctx context.Context, sel ast.SelectionSet, v string) graphql.Marshaler { res := graphql.MarshalID(v) if res == graphql.Null { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { ec.Errorf(ctx, "the requested element is null which the schema does not allow") } } return res } func (ec *executionContext) unmarshalNString2string(ctx context.Context, v any) (string, error) { res, err := graphql.UnmarshalString(v) return res, graphql.ErrorOnPath(ctx, err) } func (ec *executionContext) marshalNString2string(ctx context.Context, sel ast.SelectionSet, v string) graphql.Marshaler { res := graphql.MarshalString(v) if res == graphql.Null { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { ec.Errorf(ctx, "the requested element is null which the schema does not allow") } } return res } func (ec *executionContext) marshalN__Directive2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐDirective(ctx context.Context, sel ast.SelectionSet, v introspection.Directive) graphql.Marshaler { return ec.___Directive(ctx, sel, &v) } func (ec *executionContext) marshalN__Directive2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐDirectiveᚄ(ctx context.Context, sel ast.SelectionSet, v []introspection.Directive) graphql.Marshaler { ret := make(graphql.Array, len(v)) var wg sync.WaitGroup isLen1 := len(v) == 1 if !isLen1 { wg.Add(len(v)) } for i := range v { i := i fc := &graphql.FieldContext{ Index: &i, Result: &v[i], } ctx := graphql.WithFieldContext(ctx, fc) f := func(i int) { defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) ret = nil } }() if !isLen1 { defer wg.Done() } ret[i] = ec.marshalN__Directive2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐDirective(ctx, sel, v[i]) } if isLen1 { f(i) } else { go f(i) } } wg.Wait() for _, e := range ret { if e == graphql.Null { return graphql.Null } } return ret } func (ec *executionContext) unmarshalN__DirectiveLocation2string(ctx context.Context, v any) (string, error) { res, err := graphql.UnmarshalString(v) return res, graphql.ErrorOnPath(ctx, err) } func (ec *executionContext) marshalN__DirectiveLocation2string(ctx context.Context, sel ast.SelectionSet, v string) graphql.Marshaler { res := graphql.MarshalString(v) if res == graphql.Null { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { ec.Errorf(ctx, "the requested element is null which the schema does not allow") } } return res } func (ec *executionContext) unmarshalN__DirectiveLocation2ᚕstringᚄ(ctx context.Context, v any) ([]string, error) { var vSlice []any if v != nil { vSlice = graphql.CoerceList(v) } var err error res := make([]string, len(vSlice)) for i := range vSlice { ctx := graphql.WithPathContext(ctx, graphql.NewPathWithIndex(i)) res[i], err = ec.unmarshalN__DirectiveLocation2string(ctx, vSlice[i]) if err != nil { return nil, err } } return res, nil } func (ec *executionContext) marshalN__DirectiveLocation2ᚕstringᚄ(ctx context.Context, sel ast.SelectionSet, v []string) graphql.Marshaler { ret := make(graphql.Array, len(v)) var wg sync.WaitGroup isLen1 := len(v) == 1 if !isLen1 { wg.Add(len(v)) } for i := range v { i := i fc := &graphql.FieldContext{ Index: &i, Result: &v[i], } ctx := graphql.WithFieldContext(ctx, fc) f := func(i int) { defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) ret = nil } }() if !isLen1 { defer wg.Done() } ret[i] = ec.marshalN__DirectiveLocation2string(ctx, sel, v[i]) } if isLen1 { f(i) } else { go f(i) } } wg.Wait() for _, e := range ret { if e == graphql.Null { return graphql.Null } } return ret } func (ec *executionContext) marshalN__EnumValue2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐEnumValue(ctx context.Context, sel ast.SelectionSet, v introspection.EnumValue) graphql.Marshaler { return ec.___EnumValue(ctx, sel, &v) } func (ec *executionContext) marshalN__Field2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐField(ctx context.Context, sel ast.SelectionSet, v introspection.Field) graphql.Marshaler { return ec.___Field(ctx, sel, &v) } func (ec *executionContext) marshalN__InputValue2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValue(ctx context.Context, sel ast.SelectionSet, v introspection.InputValue) graphql.Marshaler { return ec.___InputValue(ctx, sel, &v) } func (ec *executionContext) marshalN__InputValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValueᚄ(ctx context.Context, sel ast.SelectionSet, v []introspection.InputValue) graphql.Marshaler { ret := make(graphql.Array, len(v)) var wg sync.WaitGroup isLen1 := len(v) == 1 if !isLen1 { wg.Add(len(v)) } for i := range v { i := i fc := &graphql.FieldContext{ Index: &i, Result: &v[i], } ctx := graphql.WithFieldContext(ctx, fc) f := func(i int) { defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) ret = nil } }() if !isLen1 { defer wg.Done() } ret[i] = ec.marshalN__InputValue2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValue(ctx, sel, v[i]) } if isLen1 { f(i) } else { go f(i) } } wg.Wait() for _, e := range ret { if e == graphql.Null { return graphql.Null } } return ret } func (ec *executionContext) marshalN__Type2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx context.Context, sel ast.SelectionSet, v introspection.Type) graphql.Marshaler { return ec.___Type(ctx, sel, &v) } func (ec *executionContext) marshalN__Type2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐTypeᚄ(ctx context.Context, sel ast.SelectionSet, v []introspection.Type) graphql.Marshaler { ret := make(graphql.Array, len(v)) var wg sync.WaitGroup isLen1 := len(v) == 1 if !isLen1 { wg.Add(len(v)) } for i := range v { i := i fc := &graphql.FieldContext{ Index: &i, Result: &v[i], } ctx := graphql.WithFieldContext(ctx, fc) f := func(i int) { defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) ret = nil } }() if !isLen1 { defer wg.Done() } ret[i] = ec.marshalN__Type2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, sel, v[i]) } if isLen1 { f(i) } else { go f(i) } } wg.Wait() for _, e := range ret { if e == graphql.Null { return graphql.Null } } return ret } func (ec *executionContext) marshalN__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx context.Context, sel ast.SelectionSet, v *introspection.Type) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { ec.Errorf(ctx, "the requested element is null which the schema does not allow") } return graphql.Null } return ec.___Type(ctx, sel, v) } func (ec *executionContext) unmarshalN__TypeKind2string(ctx context.Context, v any) (string, error) { res, err := graphql.UnmarshalString(v) return res, graphql.ErrorOnPath(ctx, err) } func (ec *executionContext) marshalN__TypeKind2string(ctx context.Context, sel ast.SelectionSet, v string) graphql.Marshaler { res := graphql.MarshalString(v) if res == graphql.Null { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { ec.Errorf(ctx, "the requested element is null which the schema does not allow") } } return res } func (ec *executionContext) unmarshalOBoolean2bool(ctx context.Context, v any) (bool, error) { res, err := graphql.UnmarshalBoolean(v) return res, graphql.ErrorOnPath(ctx, err) } func (ec *executionContext) marshalOBoolean2bool(ctx context.Context, sel ast.SelectionSet, v bool) graphql.Marshaler { res := graphql.MarshalBoolean(v) return res } func (ec *executionContext) unmarshalOBoolean2ᚖbool(ctx context.Context, v any) (*bool, error) { if v == nil { return nil, nil } res, err := graphql.UnmarshalBoolean(v) return &res, graphql.ErrorOnPath(ctx, err) } func (ec *executionContext) marshalOBoolean2ᚖbool(ctx context.Context, sel ast.SelectionSet, v *bool) graphql.Marshaler { if v == nil { return graphql.Null } res := graphql.MarshalBoolean(*v) return res } func (ec *executionContext) marshalOCharacter2ᚕᚖgithubᚗcomᚋirisᚑcontribᚋouterbanksᚑapiᚋgraphᚋgraphᚋmodelᚐCharacterᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.Character) graphql.Marshaler { if v == nil { return graphql.Null } ret := make(graphql.Array, len(v)) var wg sync.WaitGroup isLen1 := len(v) == 1 if !isLen1 { wg.Add(len(v)) } for i := range v { i := i fc := &graphql.FieldContext{ Index: &i, Result: &v[i], } ctx := graphql.WithFieldContext(ctx, fc) f := func(i int) { defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) ret = nil } }() if !isLen1 { defer wg.Done() } ret[i] = ec.marshalNCharacter2ᚖgithubᚗcomᚋirisᚑcontribᚋouterbanksᚑapiᚋgraphᚋgraphᚋmodelᚐCharacter(ctx, sel, v[i]) } if isLen1 { f(i) } else { go f(i) } } wg.Wait() for _, e := range ret { if e == graphql.Null { return graphql.Null } } return ret } func (ec *executionContext) marshalOCharacter2ᚖgithubᚗcomᚋirisᚑcontribᚋouterbanksᚑapiᚋgraphᚋgraphᚋmodelᚐCharacter(ctx context.Context, sel ast.SelectionSet, v *model.Character) graphql.Marshaler { if v == nil { return graphql.Null } return ec._Character(ctx, sel, v) } func (ec *executionContext) unmarshalOString2ᚖstring(ctx context.Context, v any) (*string, error) { if v == nil { return nil, nil } res, err := graphql.UnmarshalString(v) return &res, graphql.ErrorOnPath(ctx, err) } func (ec *executionContext) marshalOString2ᚖstring(ctx context.Context, sel ast.SelectionSet, v *string) graphql.Marshaler { if v == nil { return graphql.Null } res := graphql.MarshalString(*v) return res } func (ec *executionContext) marshalO__EnumValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐEnumValueᚄ(ctx context.Context, sel ast.SelectionSet, v []introspection.EnumValue) graphql.Marshaler { if v == nil { return graphql.Null } ret := make(graphql.Array, len(v)) var wg sync.WaitGroup isLen1 := len(v) == 1 if !isLen1 { wg.Add(len(v)) } for i := range v { i := i fc := &graphql.FieldContext{ Index: &i, Result: &v[i], } ctx := graphql.WithFieldContext(ctx, fc) f := func(i int) { defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) ret = nil } }() if !isLen1 { defer wg.Done() } ret[i] = ec.marshalN__EnumValue2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐEnumValue(ctx, sel, v[i]) } if isLen1 { f(i) } else { go f(i) } } wg.Wait() for _, e := range ret { if e == graphql.Null { return graphql.Null } } return ret } func (ec *executionContext) marshalO__Field2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐFieldᚄ(ctx context.Context, sel ast.SelectionSet, v []introspection.Field) graphql.Marshaler { if v == nil { return graphql.Null } ret := make(graphql.Array, len(v)) var wg sync.WaitGroup isLen1 := len(v) == 1 if !isLen1 { wg.Add(len(v)) } for i := range v { i := i fc := &graphql.FieldContext{ Index: &i, Result: &v[i], } ctx := graphql.WithFieldContext(ctx, fc) f := func(i int) { defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) ret = nil } }() if !isLen1 { defer wg.Done() } ret[i] = ec.marshalN__Field2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐField(ctx, sel, v[i]) } if isLen1 { f(i) } else { go f(i) } } wg.Wait() for _, e := range ret { if e == graphql.Null { return graphql.Null } } return ret } func (ec *executionContext) marshalO__InputValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValueᚄ(ctx context.Context, sel ast.SelectionSet, v []introspection.InputValue) graphql.Marshaler { if v == nil { return graphql.Null } ret := make(graphql.Array, len(v)) var wg sync.WaitGroup isLen1 := len(v) == 1 if !isLen1 { wg.Add(len(v)) } for i := range v { i := i fc := &graphql.FieldContext{ Index: &i, Result: &v[i], } ctx := graphql.WithFieldContext(ctx, fc) f := func(i int) { defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) ret = nil } }() if !isLen1 { defer wg.Done() } ret[i] = ec.marshalN__InputValue2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValue(ctx, sel, v[i]) } if isLen1 { f(i) } else { go f(i) } } wg.Wait() for _, e := range ret { if e == graphql.Null { return graphql.Null } } return ret } func (ec *executionContext) marshalO__Schema2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐSchema(ctx context.Context, sel ast.SelectionSet, v *introspection.Schema) graphql.Marshaler { if v == nil { return graphql.Null } return ec.___Schema(ctx, sel, v) } func (ec *executionContext) marshalO__Type2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐTypeᚄ(ctx context.Context, sel ast.SelectionSet, v []introspection.Type) graphql.Marshaler { if v == nil { return graphql.Null } ret := make(graphql.Array, len(v)) var wg sync.WaitGroup isLen1 := len(v) == 1 if !isLen1 { wg.Add(len(v)) } for i := range v { i := i fc := &graphql.FieldContext{ Index: &i, Result: &v[i], } ctx := graphql.WithFieldContext(ctx, fc) f := func(i int) { defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) ret = nil } }() if !isLen1 { defer wg.Done() } ret[i] = ec.marshalN__Type2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, sel, v[i]) } if isLen1 { f(i) } else { go f(i) } } wg.Wait() for _, e := range ret { if e == graphql.Null { return graphql.Null } } return ret } func (ec *executionContext) marshalO__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx context.Context, sel ast.SelectionSet, v *introspection.Type) graphql.Marshaler { if v == nil { return graphql.Null } return ec.___Type(ctx, sel, v) } // endregion ***************************** type.gotpl ***************************** ================================================ FILE: _examples/graphql/schema-first/graph/model/models_gen.go ================================================ // Code generated by github.com/99designs/gqlgen, DO NOT EDIT. package model import ( "fmt" "io" "strconv" ) type Character struct { ID string `json:"id"` Name string `json:"name"` IsHero bool `json:"isHero"` CliqueType CliqueType `json:"cliqueType"` } type CharacterInput struct { Name string `json:"name"` ID *string `json:"id"` IsHero *bool `json:"isHero"` CliqueType CliqueType `json:"cliqueType"` } type CliqueType string const ( // People who are elite with parents having money CliqueTypeKooks CliqueType = "KOOKS" // People who desperate to move up the social ladder to become new versions of themselves and establish new beginnings CliqueTypePogues CliqueType = "POGUES" ) var AllCliqueType = []CliqueType{ CliqueTypeKooks, CliqueTypePogues, } func (e CliqueType) IsValid() bool { switch e { case CliqueTypeKooks, CliqueTypePogues: return true } return false } func (e CliqueType) String() string { return string(e) } func (e *CliqueType) UnmarshalGQL(v any) error { str, ok := v.(string) if !ok { return fmt.Errorf("enums must be strings") } *e = CliqueType(str) if !e.IsValid() { return fmt.Errorf("%s is not a valid CliqueType", str) } return nil } func (e CliqueType) MarshalGQL(w io.Writer) { fmt.Fprint(w, strconv.Quote(e.String())) } ================================================ FILE: _examples/graphql/schema-first/graph/resolver.go ================================================ package graph import "github.com/iris-contrib/outerbanks-api/graph/model" // This file will not be regenerated automatically. // // It serves as dependency injection for your app, add any dependencies you require here. type Resolver struct { CharacterStore map[string]model.Character } ================================================ FILE: _examples/graphql/schema-first/graph/schema.graphqls ================================================ # GraphQL schema example # # https://gqlgen.com/getting-started/ enum CliqueType { "People who are elite with parents having money" KOOKS "People who desperate to move up the social ladder to become new versions of themselves and establish new beginnings" POGUES } type Character { id: ID! name: String! isHero: Boolean! cliqueType: CliqueType! } input CharacterInput { name: String! id: String isHero: Boolean cliqueType: CliqueType! } type Mutation { upsertCharacter(input: CharacterInput!): Character! } type Query { character(id:ID!): Character characters(cliqueType:CliqueType!): [Character!] } ================================================ FILE: _examples/graphql/schema-first/graph/schema.resolvers.go ================================================ package graph // This file will be automatically regenerated based on the schema, any resolver implementations // will be copied through when generating and any unknown code will be moved to the end. import ( "context" "fmt" "strconv" "github.com/iris-contrib/outerbanks-api/graph/model" ) func (r *mutationResolver) UpsertCharacter(ctx context.Context, input model.CharacterInput) (*model.Character, error) { id := input.ID var character model.Character character.Name = input.Name character.CliqueType = input.CliqueType n := len(r.Resolver.CharacterStore) if n == 0 { r.Resolver.CharacterStore = make(map[string]model.Character) } if id != nil { cs, ok := r.Resolver.CharacterStore[*id] if !ok { return nil, fmt.Errorf("not found") } if input.IsHero != nil { character.IsHero = *input.IsHero } else { character.IsHero = cs.IsHero } r.Resolver.CharacterStore[*id] = character } else { // generate unique id nid := strconv.Itoa(n + 1) character.ID = nid if input.IsHero != nil { character.IsHero = *input.IsHero } r.Resolver.CharacterStore[nid] = character } return &character, nil } func (r *queryResolver) Character(ctx context.Context, id string) (*model.Character, error) { character, ok := r.Resolver.CharacterStore[id] if !ok { return nil, fmt.Errorf("not found") } return &character, nil } func (r *queryResolver) Characters(ctx context.Context, cliqueType model.CliqueType) ([]*model.Character, error) { characters := make([]*model.Character, 0) for idx := range r.Resolver.CharacterStore { character := r.Resolver.CharacterStore[idx] if character.CliqueType == cliqueType { characters = append(characters, &character) } } return characters, nil } // Mutation returns generated.MutationResolver implementation. func (r *Resolver) Mutation() MutationResolver { return &mutationResolver{r} } // Query returns generated.QueryResolver implementation. func (r *Resolver) Query() QueryResolver { return &queryResolver{r} } type mutationResolver struct{ *Resolver } type queryResolver struct{ *Resolver } ================================================ FILE: _examples/graphql/schema-first/server.go ================================================ package main import ( "os" "github.com/iris-contrib/outerbanks-api/graph" "github.com/99designs/gqlgen/graphql/handler" "github.com/99designs/gqlgen/graphql/playground" "github.com/kataras/iris/v12" ) func main() { app := iris.New() graphServer := handler.NewDefaultServer(graph.NewExecutableSchema(graph.Config{Resolvers: &graph.Resolver{}})) playgroundHandler := playground.Handler("GraphQL playground", "/query") app.Get("/", iris.FromStd(playgroundHandler)) // We use iris.FromStd to convert a standard http.Handler to an iris.Handler. app.Any("/query", iris.FromStd(graphServer.ServeHTTP)) // GET, POST, PUT... port := os.Getenv("PORT") if port == "" { port = "8080" } app.Listen(":" + port) } ================================================ FILE: _examples/graphql/schema-first/tools.go ================================================ //go:build tools // +build tools package tools import _ "github.com/99designs/gqlgen" ================================================ FILE: _examples/http-client/weatherapi/client/client.go ================================================ package client import ( "context" "net/url" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/x/client" ) // The BaseURL of our API client. const BaseURL = "https://api.weatherapi.com/v1" type ( Options struct { APIKey string `json:"api_key" yaml:"APIKey" toml:"APIKey"` } Client struct { *client.Client } ) func NewClient(opts Options) *Client { apiKeyParameterSetter := client.RequestParam("key", opts.APIKey) c := client.New( client.Debug, client.BaseURL(BaseURL), client.PersistentRequestOptions(apiKeyParameterSetter), ) return &Client{c} } func (c *Client) GetCurrentByCity(ctx context.Context, city string) (resp Response, err error) { urlpath := "/current.json" // ?q=Athens&aqi=no params := client.RequestQuery(url.Values{ "q": []string{city}, "aqi": []string{"no"}, }) err = c.Client.ReadJSON(ctx, &resp, iris.MethodGet, urlpath, nil, params) return } ================================================ FILE: _examples/http-client/weatherapi/client/response.go ================================================ package client type Response struct { Location struct { Name string `json:"name"` Region string `json:"region"` Country string `json:"country"` Lat float64 `json:"lat"` Lon float64 `json:"lon"` TzID string `json:"tz_id"` LocaltimeEpoch int `json:"localtime_epoch"` Localtime string `json:"localtime"` } `json:"location"` Current struct { LastUpdatedEpoch int `json:"last_updated_epoch"` LastUpdated string `json:"last_updated"` TempC float64 `json:"temp_c"` TempF float64 `json:"temp_f"` IsDay int `json:"is_day"` Condition struct { Text string `json:"text"` Icon string `json:"icon"` Code int `json:"code"` } `json:"condition"` WindMph float64 `json:"wind_mph"` WindKph float64 `json:"wind_kph"` WindDegree int `json:"wind_degree"` WindDir string `json:"wind_dir"` PressureMb float64 `json:"pressure_mb"` PressureIn float64 `json:"pressure_in"` PrecipMm float64 `json:"precip_mm"` PrecipIn float64 `json:"precip_in"` Humidity int `json:"humidity"` Cloud int `json:"cloud"` FeelslikeC float64 `json:"feelslike_c"` FeelslikeF float64 `json:"feelslike_f"` VisKm float64 `json:"vis_km"` VisMiles float64 `json:"vis_miles"` Uv float64 `json:"uv"` GustMph float64 `json:"gust_mph"` GustKph float64 `json:"gust_kph"` } `json:"current"` } ================================================ FILE: _examples/http-client/weatherapi/main.go ================================================ package main import ( "context" "fmt" "github.com/kataras/iris/v12/_examples/http-client/weatherapi/client" ) func main() { c := client.NewClient(client.Options{ APIKey: "{YOUR_API_KEY_HERE}", }) resp, err := c.GetCurrentByCity(context.Background(), "Xanthi/GR") if err != nil { panic(err) } fmt.Printf("Temp: %.2f(C), %.2f(F)\n", resp.Current.TempC, resp.Current.TempF) } ================================================ FILE: _examples/http-server/README.md ================================================ # Hosts ## Listen and Serve You can start the server(s) listening to any type of `net.Listener` or even `http.Server` instance. The method for initialization of the server should be passed at the end, via `Run` function. The most common method that Go developers use to serve their servers are by passing a network address with form of "hostname:ip". With Iris we use the `iris.Addr` which is an `iris.Runner` type ```go // Listening on tcp with network address 0.0.0.0:8080 // app.Listen(":8080") is just a shortcut of: app.Run(iris.Addr(":8080")) ``` Sometimes you have created a standard net/http server somewhere else in your app and want to use that to serve the Iris web app ```go // Same as before but using a custom http.Server which may being used somewhere else too app.Run(iris.Server(&http.Server{Addr:":8080"})) ``` The most advanced usage is to create a custom or a standard `net.Listener` and pass that to `app.Run` ```go // Using a custom net.Listener l, err := net.Listen("tcp4", ":8080") if err != nil { panic(err) } app.Run(iris.Listener(l)) ``` A more complete example, using the unix-only socket files feature ```go package main import ( "os" "net" "github.com/kataras/iris/v12" ) func main() { app := iris.New() // UNIX socket if errOs := os.Remove(socketFile); errOs != nil && !os.IsNotExist(errOs) { app.Logger().Fatal(errOs) } l, err := net.Listen("unix", socketFile) if err != nil { app.Logger().Fatal(err) } if err = os.Chmod(socketFile, mode); err != nil { app.Logger().Fatal(err) } app.Run(iris.Listener(l)) } ``` ### HTTP/2 and Secure If you have signed file keys you can use the `iris.TLS` to serve `https` based on those certification keys ```go // TLS using files app.Run(iris.TLS("127.0.0.1:443", "mycert.cert", "mykey.key")) ``` The method you should use when your app is ready for **production** is the `iris.AutoTLS` which starts a secure server with automated certifications provided by https://letsencrypt.org for **free** ```go // Automatic TLS app.Run(iris.AutoTLS(":443", "example.com", "admin@example.com")) ``` ### Any `iris.Runner` There may be times that you want something very special to listen on, which is not a type of `net.Listener`. You are able to do that by `iris.Raw`, but you're responsible of that method ```go // Using any func() error, // the responsibility of starting up a listener is up to you with this way, // for the sake of simplicity we will use the // ListenAndServe function of the `net/http` package. app.Run(iris.Raw(&http.Server{Addr:":8080"}).ListenAndServe) ``` ## Host configurators All the above forms of listening are accepting a last, variadic argument of `func(*iris.Supervisor)`. This is used to add configurators for that specific host you passed via those functions. For example let's say that we want to add a callback which is fired when the server is shutdown ```go app.Run(iris.Addr(":8080", func(h *iris.Supervisor) { h.RegisterOnShutdown(func() { println("server terminated") }) })) ``` You can even do that before `app.Run` method, but the difference is that these host configurators will be executed to all hosts that you may use to serve your web app (via `app.NewHost` we'll see that in a minute) ```go app := iris.New() app.ConfigureHost(func(h *iris.Supervisor) { h.RegisterOnShutdown(func() { println("server terminated") }) }) app.Listen(":8080") ``` Access to all hosts that serve your application can be provided by the `Application#Hosts` field, after the `Run` method. But the most common scenario is that you may need access to the host before the `app.Run` method, there are two ways of gain access to the host supervisor, read below. We have already saw how to configure all application's hosts by second argument of `app.Run` or `app.ConfigureHost`. There is one more way which suits better for simple scenarios and that is to use the `app.NewHost` to create a new host and use one of its `Serve` or `Listen` functions to start the application via the `iris#Raw` Runner. Note that this way needs an extra import of the `net/http` package. Example Code: ```go h := app.NewHost(&http.Server{Addr:":8080"}) h.RegisterOnShutdown(func(){ println("server terminated") }) app.Run(iris.Raw(h.ListenAndServe)) ``` ## Multi hosts You can serve your Iris web app using more than one server, the `iris.Router` is compatible with the `net/http/Handler` function therefore, as you can understand, it can be used to be adapted at any `net/http` server, however there is an easier way, by using the `app.NewHost` which is also copying all the host configurators and it closes all the hosts attached to the particular web app on `app.Shutdown`. ```go app := iris.New() app.Get("/", indexHandler) // run in different goroutine in order to not block the main "goroutine". go app.Listen(":8080") // start a second server which is listening on tcp 0.0.0.0:9090, // without "go" keyword because we want to block at the last server-run. app.NewHost(&http.Server{Addr:":9090"}).ListenAndServe() ``` ## Shutdown (Gracefully) Let's continue by learning how to catch CONTROL+C/COMMAND+C or unix kill command and shutdown the server gracefully. > Gracefully Shutdown on CONTROL+C/COMMAND+C or when kill command sent is ENABLED BY-DEFAULT. In order to manually manage what to do when app is interrupted, we have to disable the default behavior with the option `WithoutInterruptHandler` and register a new interrupt handler (globally, across all possible hosts). Example code: ```go package main import ( "context" "time" "github.com/kataras/iris/v12" ) func main() { app := iris.New() iris.RegisterOnInterrupt(func() { timeout := 10 * time.Second ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() // close all hosts app.Shutdown(ctx) }) app.Get("/", func(ctx iris.Context) { ctx.HTML("

hi, I just exist in order to see if the server is closed

") }) app.Listen(":8080", iris.WithoutInterruptHandler) } ``` ================================================ FILE: _examples/http-server/custom-httpserver/easy-way/main.go ================================================ package main import ( "net/http" "github.com/kataras/iris/v12" ) func main() { app := iris.New() app.Get("/", func(ctx iris.Context) { ctx.Writef("Hello from the server") }) app.Get("/mypath", func(ctx iris.Context) { ctx.Writef("Hello from %s", ctx.Path()) }) // Any custom fields here. Handler and ErrorLog are set to the server automatically srv := &http.Server{Addr: ":8080"} // http://localhost:8080/ // http://localhost:8080/mypath app.Run(iris.Server(srv)) // same as app.Listen(":8080") // More: // see "multi" if you need to use more than one server at the same app. // // for a custom listener use: iris.Listener(net.Listener) or // iris.TLS(cert,key) or iris.AutoTLS(), see "custom-listener" example for those. } ================================================ FILE: _examples/http-server/custom-httpserver/multi/main.go ================================================ package main import ( "net/http" "github.com/kataras/iris/v12" ) func main() { app := iris.New() app.Get("/", func(ctx iris.Context) { ctx.Writef("Hello from the server") }) app.Get("/mypath", func(ctx iris.Context) { ctx.Writef("Hello from %s", ctx.Path()) }) // Note: It's not needed if the first action is "go app.Run". if err := app.Build(); err != nil { panic(err) } // start a secondary server listening on localhost:9090. // use "go" keyword for Listen functions if you need to use more than one server at the same app. // // http://localhost:9090/ // http://localhost:9090/mypath srv1 := &http.Server{Addr: ":9090", Handler: app} go srv1.ListenAndServe() println("Start a server listening on http://localhost:9090") // start a "second-secondary" server listening on localhost:5050. // // http://localhost:5050/ // http://localhost:5050/mypath srv2 := &http.Server{Addr: ":5050", Handler: app} go srv2.ListenAndServe() println("Start a server listening on http://localhost:5050") // Note: app.Run is totally optional, we have already built the app with app.Build, // you can just make a new http.Server instead. // http://localhost:8080/ // http://localhost:8080/mypath app.Listen(":8080") // Block here. } ================================================ FILE: _examples/http-server/custom-httpserver/std-way/main.go ================================================ package main import ( "net/http" "github.com/kataras/iris/v12" ) func main() { app := iris.New() app.Get("/", func(ctx iris.Context) { ctx.Writef("Hello from the server") }) app.Get("/mypath", func(ctx iris.Context) { ctx.Writef("Hello from %s", ctx.Path()) }) // call .Build before use the 'app' as a http.Handler on a custom http.Server app.Build() // create our custom server and assign the Handler/Router srv := &http.Server{Handler: app, Addr: ":8080"} // you have to set Handler:app and Addr, see "iris-way" which does this automatically. // http://localhost:8080/ // http://localhost:8080/mypath println("Start a server listening on http://localhost:8080") srv.ListenAndServe() // same as app.Listen(":8080") // Notes: // Banner is not shown at all. Same for the Interrupt Handler, even if app's configuration allows them. // // `.Run` is the only one function that cares about those three. // More: // see "multi" if you need to use more than one server at the same app. // // for a custom listener use: iris.Listener(net.Listener) or // iris.TLS(cert,key) or iris.AutoTLS(), see "custom-listener" example for those. } ================================================ FILE: _examples/http-server/custom-listener/main.go ================================================ package main import ( "net" "github.com/kataras/iris/v12" ) func main() { app := iris.New() app.Get("/", func(ctx iris.Context) { ctx.Writef("Hello from the server") }) app.Get("/mypath", func(ctx iris.Context) { ctx.Writef("Hello from %s", ctx.Path()) }) // create any custom tcp listener, unix sock file or tls tcp listener. l, err := net.Listen("tcp4", ":8080") if err != nil { panic(err) } // use of the custom listener app.Run(iris.Listener(l)) } ================================================ FILE: _examples/http-server/graceful-shutdown/custom-notifier/main.go ================================================ package main import ( stdContext "context" "os" "os/signal" "syscall" "time" "github.com/kataras/iris/v12" ) func main() { app := iris.New() app.Get("/", func(ctx iris.Context) { ctx.HTML("

hi, I just exist in order to see if the server is closed

") }) idleConnsClosed := make(chan struct{}) go func() { ch := make(chan os.Signal, 1) signal.Notify(ch, // kill -SIGINT XXXX or Ctrl+c os.Interrupt, syscall.SIGINT, // register that too, it should be ok // os.Kill is equivalent with the syscall.Kill os.Kill, syscall.SIGKILL, // register that too, it should be ok // kill -SIGTERM XXXX syscall.SIGTERM, ) select { case <-ch: println("shutdown...") timeout := 10 * time.Second ctx, cancel := stdContext.WithTimeout(stdContext.Background(), timeout) defer cancel() app.Shutdown(ctx) close(idleConnsClosed) } }() // Start the server and disable the default interrupt handler in order to // handle it clear and simple by our own, without any issues. app.Listen(":8080", iris.WithoutInterruptHandler) <-idleConnsClosed } ================================================ FILE: _examples/http-server/graceful-shutdown/default-notifier/main.go ================================================ package main import ( stdContext "context" "time" "github.com/kataras/iris/v12" ) // Before continue: // // Gracefully Shutdown on control+C/command+C or when kill command sent is ENABLED BY-DEFAULT. // // In order to manually manage what to do when app is interrupted, // We have to disable the default behavior with the option `WithoutInterruptHandler` // and register a new interrupt handler (globally, across all possible hosts). func main() { app := iris.New() idleConnsClosed := make(chan struct{}) iris.RegisterOnInterrupt(func() { timeout := 10 * time.Second ctx, cancel := stdContext.WithTimeout(stdContext.Background(), timeout) defer cancel() // close all hosts app.Shutdown(ctx) close(idleConnsClosed) }) app.Get("/", func(ctx iris.Context) { ctx.HTML("

hi, I just exist in order to see if the server is closed

") }) // http://localhost:8080 app.Listen(":8080", iris.WithoutInterruptHandler, iris.WithoutServerError(iris.ErrServerClosed)) <-idleConnsClosed } ================================================ FILE: _examples/http-server/h2c/go.mod ================================================ module github.com/kataras/iris/_examples/http-server/h2c go 1.25 require ( github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd golang.org/x/net v0.49.0 ) require ( github.com/BurntSushi/toml v1.6.0 // indirect github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect github.com/CloudyKit/jet/v6 v6.3.1 // indirect github.com/Joker/jade v1.1.3 // indirect github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 // indirect github.com/andybalholm/brotli v1.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/fatih/structs v1.1.0 // indirect github.com/flosch/pongo2/v4 v4.0.2 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/iris-contrib/schema v0.0.6 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/kataras/blocks v0.0.8 // indirect github.com/kataras/golog v0.1.12 // indirect github.com/kataras/pio v0.0.13 // indirect github.com/kataras/sitemap v0.0.6 // indirect github.com/kataras/tunnel v0.0.4 // indirect github.com/klauspost/compress v1.18.2 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mailgun/raymond/v2 v2.0.48 // indirect github.com/mailru/easyjson v0.9.1 // indirect github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/schollz/closestmatch v2.1.0+incompatible // indirect github.com/sirupsen/logrus v1.8.1 // indirect github.com/stretchr/testify v1.11.1 // indirect github.com/tdewolff/minify/v2 v2.24.8 // indirect github.com/tdewolff/parse/v2 v2.8.5 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/yosssi/ace v0.0.5 // indirect golang.org/x/crypto v0.47.0 // indirect golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect golang.org/x/sys v0.40.0 // indirect golang.org/x/text v0.33.0 // indirect golang.org/x/time v0.14.0 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: _examples/http-server/h2c/go.sum ================================================ github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= github.com/CloudyKit/jet/v6 v6.3.1 h1:6IAo5Cx21xrHVaR8zzXN5gJatKV/wO7Nf6bfCnCSbUw= github.com/CloudyKit/jet/v6 v6.3.1/go.mod h1:lf8ksdNsxZt7/yH/3n4vJQWA9RUq4wpaHtArHhGVMOw= github.com/Joker/hpp v1.0.0 h1:65+iuJYdRXv/XyN62C1uEmmOx3432rNG/rKlX6V7Kkc= github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= github.com/Joker/jade v1.1.3 h1:Qbeh12Vq6BxURXT1qZBRHsDxeURB8ztcL6f3EXSGeHk= github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKduM= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 h1:dG7/gLroJGht/jSQtHiLvT48Hxn+crbmvyItZC8cWOs= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05/go.mod h1:NYezi6wtnJtBm5btoprXc5SvAdqH0XTXWnUup0MptAI= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/flosch/pongo2/v4 v4.0.2 h1:gv+5Pe3vaSVmiJvh/BZa82b7/00YUGm0PIyVVLop0Hw= github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+BWPhwZD8= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a h1:l7A0loSszR5zHd/qK53ZIHMO8b3bBSmENnQ6eKnUT0A= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/iris-contrib/httpexpect/v2 v2.15.2 h1:T9THsdP1woyAqKHwjkEsbCnMefsAFvk8iJJKokcJ3Go= github.com/iris-contrib/httpexpect/v2 v2.15.2/go.mod h1:JLDgIqnFy5loDSUv1OA2j0mb6p/rDhiCqigP22Uq9xE= github.com/iris-contrib/schema v0.0.6 h1:CPSBLyx2e91H2yJzPuhGuifVRnZBBJ3pCOMbOvPZaTw= github.com/iris-contrib/schema v0.0.6/go.mod h1:iYszG0IOsuIsfzjymw1kMzTL8YQcCWlm65f3wX8J5iA= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kataras/blocks v0.0.8 h1:MrpVhoFTCR2v1iOOfGng5VJSILKeZZI+7NGfxEh3SUM= github.com/kataras/blocks v0.0.8/go.mod h1:9Jm5zx6BB+06NwA+OhTbHW1xkMOYxahnqTN5DveZ2Yg= github.com/kataras/golog v0.1.12 h1:Bu7I/G4ilJlbfzjmU39O9N+2uO1pBcMK045fzZ4ytNg= github.com/kataras/golog v0.1.12/go.mod h1:wrGSbOiBqbQSQznleVNX4epWM8rl9SJ/rmEacl0yqy4= github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd h1:oMsQgqKACbUdlaIWnN+EZzzAnHkw90hE/y/R0BSij2U= github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd/go.mod h1:yucjtDl9Vn3OctWM1fi0MuM9OckemC7kEreFa9QtPF8= github.com/kataras/pio v0.0.13 h1:x0rXVX0fviDTXOOLOmr4MUxOabu1InVSTu5itF8CXCM= github.com/kataras/pio v0.0.13/go.mod h1:k3HNuSw+eJ8Pm2lA4lRhg3DiCjVgHlP8hmXApSej3oM= github.com/kataras/sitemap v0.0.6 h1:w71CRMMKYMJh6LR2wTgnk5hSgjVNB9KL60n5e2KHvLY= github.com/kataras/sitemap v0.0.6/go.mod h1:dW4dOCNs896OR1HmG+dMLdT7JjDk7mYBzoIRwuj5jA4= github.com/kataras/tunnel v0.0.4 h1:sCAqWuJV7nPzGrlb0os3j49lk2JhILT0rID38NHNLpA= github.com/kataras/tunnel v0.0.4/go.mod h1:9FkU4LaeifdMWqZu7o20ojmW4B7hdhv2CMLwfnHGpYw= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw= github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18= github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo= github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk= github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tdewolff/minify/v2 v2.24.8 h1:58/VjsbevI4d5FGV0ZSuBrHMSSkH4MCH0sIz/eKIauE= github.com/tdewolff/minify/v2 v2.24.8/go.mod h1:0Ukj0CRpo/sW/nd8uZ4ccXaV1rEVIWA3dj8U7+Shhfw= github.com/tdewolff/parse/v2 v2.8.5 h1:ZmBiA/8Do5Rpk7bDye0jbbDUpXXbCdc3iah4VeUvwYU= github.com/tdewolff/parse/v2 v2.8.5/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo= github.com/tdewolff/test v1.0.11 h1:FdLbwQVHxqG16SlkGveC0JVyrJN62COWTRyUFzfbtBE= github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA= github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0= github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs= moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHcE= ================================================ FILE: _examples/http-server/h2c/main.go ================================================ package main import ( "net/http" "github.com/kataras/iris/v12" "golang.org/x/net/http2" ) /* Serve HTTP/2 without TLS and keep support for HTTP/1.1 */ // $ go get golang.org/x/net/http2 // # Take a look at the golang.org/x/net/http2/h2c package as well, // # you may want to use it. // $ go run main.go // $ brew install curl-openssl // # Add curl-openssl to the front of your path. // Test with the following commands: // $ curl -v --http2 http://localhost:8080 // $ curl -v --http1.1 http://localhost:8080 func main() { // Initialize Iris Application. app := iris.New() // Build the API. app.Any("/", index) // Finally, listen and serve on port 8080 using h2c. app.Run(iris.Raw(func() error { host := app.NewHost(&http.Server{ Addr: ":8080", }) err := http2.ConfigureServer(host.Server, &http2.Server{ MaxConcurrentStreams: 250, PermitProhibitedCipherSuites: true, }) if err != nil { return err } return host.ListenAndServe() })) } func index(ctx iris.Context) { ctx.WriteString("Hello, World!\n") } ================================================ FILE: _examples/http-server/http3-quic/go.mod ================================================ module github.com/kataras/iris/_examples/http-server/http3-quic go 1.25 require ( github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd github.com/quic-go/quic-go v0.59.0 ) require ( github.com/BurntSushi/toml v1.6.0 // indirect github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect github.com/CloudyKit/jet/v6 v6.3.1 // indirect github.com/Joker/jade v1.1.3 // indirect github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 // indirect github.com/andybalholm/brotli v1.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/fatih/structs v1.1.0 // indirect github.com/flosch/pongo2/v4 v4.0.2 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/iris-contrib/schema v0.0.6 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/kataras/blocks v0.0.8 // indirect github.com/kataras/golog v0.1.12 // indirect github.com/kataras/pio v0.0.13 // indirect github.com/kataras/sitemap v0.0.6 // indirect github.com/kataras/tunnel v0.0.4 // indirect github.com/klauspost/compress v1.18.2 // indirect github.com/mailgun/raymond/v2 v2.0.48 // indirect github.com/mailru/easyjson v0.9.1 // indirect github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/schollz/closestmatch v2.1.0+incompatible // indirect github.com/sirupsen/logrus v1.8.1 // indirect github.com/tdewolff/minify/v2 v2.24.8 // indirect github.com/tdewolff/parse/v2 v2.8.5 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/yosssi/ace v0.0.5 // indirect golang.org/x/crypto v0.47.0 // indirect golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect golang.org/x/net v0.49.0 // indirect golang.org/x/sys v0.40.0 // indirect golang.org/x/text v0.33.0 // indirect golang.org/x/time v0.14.0 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: _examples/http-server/http3-quic/go.sum ================================================ github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= github.com/CloudyKit/jet/v6 v6.3.1 h1:6IAo5Cx21xrHVaR8zzXN5gJatKV/wO7Nf6bfCnCSbUw= github.com/CloudyKit/jet/v6 v6.3.1/go.mod h1:lf8ksdNsxZt7/yH/3n4vJQWA9RUq4wpaHtArHhGVMOw= github.com/Joker/hpp v1.0.0 h1:65+iuJYdRXv/XyN62C1uEmmOx3432rNG/rKlX6V7Kkc= github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= github.com/Joker/jade v1.1.3 h1:Qbeh12Vq6BxURXT1qZBRHsDxeURB8ztcL6f3EXSGeHk= github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKduM= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 h1:dG7/gLroJGht/jSQtHiLvT48Hxn+crbmvyItZC8cWOs= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05/go.mod h1:NYezi6wtnJtBm5btoprXc5SvAdqH0XTXWnUup0MptAI= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/flosch/pongo2/v4 v4.0.2 h1:gv+5Pe3vaSVmiJvh/BZa82b7/00YUGm0PIyVVLop0Hw= github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+BWPhwZD8= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a h1:l7A0loSszR5zHd/qK53ZIHMO8b3bBSmENnQ6eKnUT0A= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/iris-contrib/httpexpect/v2 v2.15.2 h1:T9THsdP1woyAqKHwjkEsbCnMefsAFvk8iJJKokcJ3Go= github.com/iris-contrib/httpexpect/v2 v2.15.2/go.mod h1:JLDgIqnFy5loDSUv1OA2j0mb6p/rDhiCqigP22Uq9xE= github.com/iris-contrib/schema v0.0.6 h1:CPSBLyx2e91H2yJzPuhGuifVRnZBBJ3pCOMbOvPZaTw= github.com/iris-contrib/schema v0.0.6/go.mod h1:iYszG0IOsuIsfzjymw1kMzTL8YQcCWlm65f3wX8J5iA= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kataras/blocks v0.0.8 h1:MrpVhoFTCR2v1iOOfGng5VJSILKeZZI+7NGfxEh3SUM= github.com/kataras/blocks v0.0.8/go.mod h1:9Jm5zx6BB+06NwA+OhTbHW1xkMOYxahnqTN5DveZ2Yg= github.com/kataras/golog v0.1.12 h1:Bu7I/G4ilJlbfzjmU39O9N+2uO1pBcMK045fzZ4ytNg= github.com/kataras/golog v0.1.12/go.mod h1:wrGSbOiBqbQSQznleVNX4epWM8rl9SJ/rmEacl0yqy4= github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd h1:oMsQgqKACbUdlaIWnN+EZzzAnHkw90hE/y/R0BSij2U= github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd/go.mod h1:yucjtDl9Vn3OctWM1fi0MuM9OckemC7kEreFa9QtPF8= github.com/kataras/pio v0.0.13 h1:x0rXVX0fviDTXOOLOmr4MUxOabu1InVSTu5itF8CXCM= github.com/kataras/pio v0.0.13/go.mod h1:k3HNuSw+eJ8Pm2lA4lRhg3DiCjVgHlP8hmXApSej3oM= github.com/kataras/sitemap v0.0.6 h1:w71CRMMKYMJh6LR2wTgnk5hSgjVNB9KL60n5e2KHvLY= github.com/kataras/sitemap v0.0.6/go.mod h1:dW4dOCNs896OR1HmG+dMLdT7JjDk7mYBzoIRwuj5jA4= github.com/kataras/tunnel v0.0.4 h1:sCAqWuJV7nPzGrlb0os3j49lk2JhILT0rID38NHNLpA= github.com/kataras/tunnel v0.0.4/go.mod h1:9FkU4LaeifdMWqZu7o20ojmW4B7hdhv2CMLwfnHGpYw= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw= github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18= github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo= github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk= github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tdewolff/minify/v2 v2.24.8 h1:58/VjsbevI4d5FGV0ZSuBrHMSSkH4MCH0sIz/eKIauE= github.com/tdewolff/minify/v2 v2.24.8/go.mod h1:0Ukj0CRpo/sW/nd8uZ4ccXaV1rEVIWA3dj8U7+Shhfw= github.com/tdewolff/parse/v2 v2.8.5 h1:ZmBiA/8Do5Rpk7bDye0jbbDUpXXbCdc3iah4VeUvwYU= github.com/tdewolff/parse/v2 v2.8.5/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo= github.com/tdewolff/test v1.0.11 h1:FdLbwQVHxqG16SlkGveC0JVyrJN62COWTRyUFzfbtBE= github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA= github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0= github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs= moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHcE= ================================================ FILE: _examples/http-server/http3-quic/localhost.cert ================================================ -----BEGIN CERTIFICATE----- MIIC+zCCAeOgAwIBAgIJAI5gi8BzcdQgMA0GCSqGSIb3DQEBBQUAMBQxEjAQBgNV BAMMCWxvY2FsaG9zdDAeFw0xOTA3MDkxMjExNDBaFw0yOTA3MDYxMjExNDBaMBQx EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC ggEBAM1eSGwoYznwYtt+/hrv9o4FYxJFlxIMX6WN3c2y3rr8uOwExkz2RuU9SzgF qn0ctP1DoIKWNO0/L5j5Bjy2do0k8wHYPbTqb9zG64NGZj1lhkHgYXWyCD9U41DX V1DiJ2JiCRBadowFRRf3/KIPf3xnrCBSCoQwdfIeJJtAF9El2/TnTrGq9N98FJqR dCNyi+zY/iuymcA3aDOyYNjxSiuV//7ONEql5dxvRlhkjCHgrQ/rIbH/lSFAS+NG H/6ksEBX2+Q1LlQBaFiGeEnjVpCymrRfADw7bKyqMav39Nndw4UZ1r5MSG20YtXM dE4lA6VfAKzIZs2n87WF7OO8Y2sCAwEAAaNQME4wHQYDVR0OBBYEFKYItamcYz4Z NiDy3I2zflU4A7ywMB8GA1UdIwQYMBaAFKYItamcYz4ZNiDy3I2zflU4A7ywMAwG A1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBACSoSrEArlgPJ1ftlSMkThUR atTqJ/yB0rSyRxsct0C04qX880VP7avnKc0UhaDruXRjdAVn4X8KI+j6azQSKT40 KRSVBinnonE0D4DBMCUVDFtkBW3FZJXAYyIYdF/6J3Khn/ksm7VDcVxYI1rjg87B U6aJytOkoGA2WGQOB1L0HtnTsarg/SKP/LSDUFT+XK6zTE7uogAUrpbwlpIaxc+8 3jXvgxEdPj9Rq9Nt8/zjCkCGB2EusPPnqxcbqZb5WcGPCIlg3ChKq7vpaQld6KqG 70jT7BZ2fqWSVJ5szRoS8WpKy1SZEx/+AA7VojMzkkw4RLb66zr1v7029b51ol4= -----END CERTIFICATE----- ================================================ FILE: _examples/http-server/http3-quic/localhost.key ================================================ -----BEGIN RSA PRIVATE KEY----- MIIEpAIBAAKCAQEAzV5IbChjOfBi237+Gu/2jgVjEkWXEgxfpY3dzbLeuvy47ATG TPZG5T1LOAWqfRy0/UOggpY07T8vmPkGPLZ2jSTzAdg9tOpv3Mbrg0ZmPWWGQeBh dbIIP1TjUNdXUOInYmIJEFp2jAVFF/f8og9/fGesIFIKhDB18h4km0AX0SXb9OdO sar033wUmpF0I3KL7Nj+K7KZwDdoM7Jg2PFKK5X//s40SqXl3G9GWGSMIeCtD+sh sf+VIUBL40Yf/qSwQFfb5DUuVAFoWIZ4SeNWkLKatF8APDtsrKoxq/f02d3DhRnW vkxIbbRi1cx0TiUDpV8ArMhmzafztYXs47xjawIDAQABAoIBADvaKok7DBAquuT1 keEP5m9lqoX8uhaMfKOnQOleJAOi+9HtYk2zyN2ui2l8XT+xSh41w2XLmQk7zQds LCEtnEduaVQ0TWeYm5lgb+sGbW2fVQ2F82F1zWmHt+grmkr8XjYSFEor0zjjoEtn /rzMf38mR8fzoRT9eqJhnpGQkGBnfo0SyDKIDu9yYFM7yJ5s4KOTVsMGfavjQYgJ ssQm0KQTo8HbYHieS6drEYFRwAgT8U1NFoVq24yU+Voyy7CR5rjfOsO9gSyStVSH nTkePmSIcpeQBfpfT3jh+STSS12eqhFCx1SqAUptUehQpOJhnWAyeSGjVLlrHqDR bCtSWCECgYEA62J2AC8bRQ8omoEOc5h1/kwPTcLYjhOCxwwU8ky7qve173MYZUeT dxWZx3haahccPyUKtsiGBdKYyYKSOIJMSMmwkG4uy6r5nhwNV+btEIL6Npj+XKMe PaATA4gBLRQNwcbUlZWLYc0Y6CXFnPA+atEa7EOEBfdgUqHOym1HsAUCgYEA31rU GPyv8R7Z1UXmxu70M8RwxKS4XhlP7RRg3Zuuqx6WWX4M85Np3wS4UWgQjGKICwoM D4XKZQOya5p+v7a1RUZt/OD6eJ0TjypW5fBmK2yvBUQkQCsVcFjBL3F+yv2Yk1sh KlPky4wXpDcmWXGmesgmyhpKIwL+qXEAJEaL0K8CgYABWqKlI6A7iHfKU726ioD7 QoLABsPqJVCWRoqETk6yEBS62OWmB4Bgqf4leJrEi3d9IYBrRsIGnIyGdDrVGmLH 9GkQm6GnSEeBUlX9UHXCp4467CxiagnNfvM9DPY8xSXDHJqydZbErEJda4I0gelK AgPuogDLa/3g289tuK015QKBgQDKcj9Qrqiiur3jC8rTgX8i9OjptAvQbsz9LL1n 4FZ/j+fjEdeXZ4RMurB+SP7G4ABDUUYBQ9lhmeo8kfpUtryzH9VNonYkoOs7lrrR DAbvUUGKWmspJmP2QtxHrm2ofBexaKY1AXmd7Ur4c2x1IggtvgE6qn2MIojE+EGS n8bWzQKBgQClc/j4GYNNMUYOknxXMH/ec8PBDUtn098YuFL+s7DmRHimtkkjRo1A BtV7F8KpLruWohxXWy4QZ6HsAO255gIJ8DCbEAFCj96EHNx8KADSm3qbslu2fIB0 zCsVaETGNAjV/d5hAEdYmgynCY49gNXV55ehV1UqzFoEfZVM9j5hUg== -----END RSA PRIVATE KEY----- ================================================ FILE: _examples/http-server/http3-quic/main.go ================================================ package main import ( "github.com/kataras/iris/v12" "github.com/quic-go/quic-go/http3" ) /* $ go get github.com/quic-go/quic-go@master */ func main() { app := iris.New() app.Get("/", func(ctx iris.Context) { ctx.Writef("Hello from Index") }) // app.Configure(iris.WithOptimizations, or any other core config here) // app.Build() // http3.ListenAndServe(":443", "./localhost.cert", "./localhost.key", app) // OR: app.Run(iris.Raw(func() error { return http3.ListenAndServe(":443", "./localhost.cert", "./localhost.key", app) })) } ================================================ FILE: _examples/http-server/iris-configurator-and-host-configurator/main.go ================================================ package main import ( "github.com/kataras/iris/v12" ) func main() { app := iris.New() app.ConfigureHost(func(host *iris.Supervisor) { // <- HERE: IMPORTANT // You can control the flow or defer something using some of the host's methods: // host.RegisterOnError // host.RegisterOnServe host.RegisterOnShutdown(func() { app.Logger().Infof("Application shutdown on signal") }) }) app.Get("/", func(ctx iris.Context) { ctx.HTML("

Hello

\n") }) app.Listen(":8080", iris.WithoutServerError(iris.ErrServerClosed)) /* There are more easy ways to notify for global shutdown using the `iris.RegisterOnInterrupt` for default signal interrupt events. You can even go it even further by looking at the: "graceful-shutdown" example. */ } ================================================ FILE: _examples/http-server/listen-addr/main.go ================================================ package main import "github.com/kataras/iris/v12" func main() { app := iris.New() app.Get("/", func(ctx iris.Context) { ctx.HTML("

Hello World!

") }) // http://localhost:8080 // Identical to: app.Run(iris.Addr(":8080")) app.Listen(":8080") // To listen using keep alive tcp connection listener, // set the KeepAlive duration configuration instead: // app.Listen(":8080", iris.WithKeepAlive(3*time.Minute)) } ================================================ FILE: _examples/http-server/listen-addr/omit-server-errors/main.go ================================================ package main import ( "github.com/kataras/iris/v12" ) func main() { app := iris.New() app.Get("/", func(ctx iris.Context) { ctx.HTML("

Hello World!

") }) err := app.Listen(":8080", iris.WithoutServerError(iris.ErrServerClosed)) if err != nil { // do something } // same as: // err := app.Listen(":8080") // import "errors" // if errors.Is(err, iris.ErrServerClosed) { // [...] // } } ================================================ FILE: _examples/http-server/listen-addr/omit-server-errors/main_test.go ================================================ package main import ( "bytes" stdContext "context" "strings" "testing" "time" "github.com/kataras/iris/v12" ) func logger(app *iris.Application) *bytes.Buffer { buf := &bytes.Buffer{} app.Logger().SetOutput(buf) // disable the "Now running at...." in order to have a clean log of the error. // we could attach that on `Run` but better to keep things simple here. app.Configure(iris.WithoutStartupLog) return buf } func TestListenAddr(t *testing.T) { app := iris.New() // we keep the logger running as well but in a controlled way. log := logger(app) // close the server at 3-6 seconds go func() { time.Sleep(3 * time.Second) ctx, cancel := stdContext.WithTimeout(stdContext.TODO(), 3*time.Second) defer cancel() app.Shutdown(ctx) }() err := app.Listen(":9829") // in this case the error should be logged and return as well. if err != iris.ErrServerClosed { t.Fatalf("expecting err to be `iris.ErrServerClosed` but got: %v", err) } expectedMessage := iris.ErrServerClosed.Error() if got := log.String(); !strings.Contains(got, expectedMessage) { t.Fatalf("expecting to log to contains the:\n'%s'\ninstead of:\n'%s'", expectedMessage, got) } } func TestListenAddrWithoutServerErr(t *testing.T) { app := iris.New() // we keep the logger running as well but in a controlled way. log := logger(app) // close the server at 3-6 seconds go func() { time.Sleep(3 * time.Second) ctx, cancel := stdContext.WithTimeout(stdContext.TODO(), 3*time.Second) defer cancel() app.Shutdown(ctx) }() // we disable the ErrServerClosed, so the error should be nil when server is closed by `app.Shutdown` // or by an external issue. err := app.Listen(":9827", iris.WithoutServerError(iris.ErrServerClosed)) if err != nil { t.Fatalf("expecting err to be nil but got: %v", err) } if got := log.String(); got != "" { t.Fatalf("expecting to log nothing but logged: '%s'", got) } } ================================================ FILE: _examples/http-server/listen-addr-public/main.go ================================================ package main import "github.com/kataras/iris/v12" func main() { app := iris.New() app.Get("/", func(ctx iris.Context) { ctx.HTML("

Hello World!

") // Will print the ngrok public domain // that your app is using to be served online. ctx.Writef("From: %s", ctx.Application().ConfigurationReadOnly().GetVHost()) }) app.Listen(":8080", iris.WithTunneling, iris.WithLogLevel("debug")) /* The full configuration can be set as: app.Listen(":8080", iris.WithConfiguration( iris.Configuration{ Tunneling: iris.TunnelingConfiguration{ AuthToken: "my-ngrok-auth-client-token", Bin: "/bin/path/for/ngrok", Region: "eu", WebInterface: "127.0.0.1:4040", Tunnels: []iris.Tunnel{ { Name: "MyApp", Addr: ":8080", Hostname: "your-custom-sub-domain.ngrok.io", // optionally }, }, }, })) */ } ================================================ FILE: _examples/http-server/listen-letsencrypt/main.go ================================================ // Package main provide one-line integration with letsencrypt.org package main import ( "github.com/kataras/iris/v12" ) func main() { app := iris.New() app.Get("/", func(ctx iris.Context) { ctx.Writef("Hello from SECURE SERVER!") }) app.Get("/test2", func(ctx iris.Context) { ctx.Writef("Welcome to secure server from /test2!") }) app.Get("/redirect", func(ctx iris.Context) { ctx.Redirect("/test2") }) // NOTE: This will not work on domains like this, // use real whitelisted domain(or domains split by whitespaces) // and a non-public e-mail instead or edit your hosts file. app.Run(iris.AutoTLS(":443", "example.com", "mail@example.com")) // Note: to disable automatic "http://" to "https://" redirections pass // the `iris.AutoTLSNoRedirect` host configurator to AutoTLS function, example: /* var fallbackServer = func(acme func(http.Handler) http.Handler) *http.Server { // Use any http.Server and Handler, as long as it's wrapped by `acme` one. // In that case we share the application through non-tls users too: srv := &http.Server{Handler: acme(app)} go srv.ListenAndServe() return srv } app.Run(iris.AutoTLS(":443", "example.com myip", "mail@example.com", iris.AutoTLSNoRedirect(fallbackServer))) */ } ================================================ FILE: _examples/http-server/listen-tls/main.go ================================================ package main import ( "github.com/kataras/iris/v12" ) func main() { app := iris.New() app.Get("/", func(ctx iris.Context) { ctx.Writef("Hello from the SECURE server") }) app.Get("/mypath", func(ctx iris.Context) { ctx.Writef("Hello from the SECURE server on path /mypath") }) // Start the server (HTTPS) on port 443, // and a secondary of (HTTP) on port :80 which redirects requests to their HTTPS version. // This is a blocking func. app.Run(iris.TLS("127.0.0.1:443", "mycert.crt", "mykey.key")) // Note: to disable automatic "http://" to "https://" redirections pass the `iris.TLSNoRedirect` // host configurator to TLS function, example: // // app.Run(iris.TLS("127.0.0.1:443", "mycert.crt", "mykey.key", iris.TLSNoRedirect)) } ================================================ FILE: _examples/http-server/listen-tls/mycert.crt ================================================ -----BEGIN CERTIFICATE----- MIIFazCCA1OgAwIBAgIUfwMd9auWixp19UnXOmyxJ9Jkv7IwDQYJKoZIhvcNAQEL BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMDA2MjUwOTUxNDdaFw0yMTA2 MjUwOTUxNDdaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggIiMA0GCSqGSIb3DQEB AQUAA4ICDwAwggIKAoICAQDlVGyGAQ9uyfNbwZyrtYOSjLpxf5NpNToh2OzU7gy2 OexBji5lmWBQ3oYDG+FjAkbHORPzOMNpeMwje+IjGZBw8x6E+8WoGdSzbrEZ6pUV wKJGKEuDlx6g6HEmtv3ZwgGe20gvPjjW+oCO888dwK/mbIHrHTq4nO3o0gAdAJwu amn9BlHU5O4RW7BQ4tLF+j/fBCACWRG1NHXA0AT8eg544GyCdyteAH11oCDsHS8/ DAPsM6t+tZrMCIt9+9dzPdVoOmQNaMMrcz8eJohddRTK6zHe9ixZTt/soayOF7OS QQeekbr3HPYhD450zRVplLMHx7wnph/+O+Po6bqDnUzdnkqAAwwymQapHMuHXZKN rhdfKau3rVo1GeXLIRgeWLUoxFSm4TYshrgt+0AidLRH+dCY7MS9Ngga/sAK3vID gSF75mFgOhY+q7nvY9Ecao6TnoNNRY29hUat4y0VwSyysUy887vHr6lMK5CrAT/l Ch8fuu20HUCoiLwMJvA6+wpivZkuiIvWY7bVGYsEYrrW+bCNN9wCGYTZEyX++os9 v/38wdOqGUT00ewXkjIUFCWbrnxxSr98kF3w3wPf9K4Y40MNxeR90nyX4zjXGF1/ 91msUh+iivsz9mcN9DK83fgTyOsoVLX5cm/L2UBwMacsfjBbN4djOc5IuYMar/VN GQIDAQABo1MwUTAdBgNVHQ4EFgQUtkf+yAvqgZC8f22iJny9hFEDolMwHwYDVR0j BBgwFoAUtkf+yAvqgZC8f22iJny9hFEDolMwDwYDVR0TAQH/BAUwAwEB/zANBgkq hkiG9w0BAQsFAAOCAgEAE2QasBVru618rxupyJgEHw6r4iv7sz1Afz3Q5qJ4oSA9 xVsrVCjr3iHRFSw8Rf670E8Ffk/JjzS65mHw6zeZj/ANBKQWLjRlqzYXeetq5HzG SIgaG7p1RFvvzz3+leFGzjinZ6sKbfB4OB72o2YN+fO8DsDxgGKll0W4KAazizSe HY9Pgu437tWnwF16rFO3IL47n5HzYlRoGIPOpzFoNX5+fyn9GlnKEtONF2QBKTjY rdjvqFRByDiC74d8z/Yx8IiDRn1mTcG90JLR9+c6M7fruha9Y/rJfw+4AhVh5ZDz Bl9rGPjwEs5zwutYvVAJzs7AVcighYP1lHKoJ7DxBDQeyBsYlUNk2l6bmZgLgGUZ +2OyWlqc/jD2GdDsIaZ4i7QqhTI/6aYZIf5zUkblKV1aMSaDulKxRv//OwW28Jax 9EEoV7VaFb3sOkB/tZGhusXeQVtdrhahT3KkZLNwmNXoXWKJ5LjeUlFWJyV6JbDe y/PIWWCwWqyuFCSZS+Cg3RDgAzfSxkI8uVZ+IKKJS3UluDX45lxXtbRrvTQ+oDrA 6ga5c1Vz9C4kn1K5yW4d7QIvg6vPiy7gvl+//sz9oxUM3yswInDBY0HKLgT0Uq9b YzLDh2RSaHsgHMPy2BKqR+q2N+lpg7inAWuJM1Huq6eHFqhiyQkzsfscBd1Dpm8= -----END CERTIFICATE----- ================================================ FILE: _examples/http-server/listen-tls/mykey.key ================================================ -----BEGIN PRIVATE KEY----- MIIJRAIBADANBgkqhkiG9w0BAQEFAASCCS4wggkqAgEAAoICAQDlVGyGAQ9uyfNb wZyrtYOSjLpxf5NpNToh2OzU7gy2OexBji5lmWBQ3oYDG+FjAkbHORPzOMNpeMwj e+IjGZBw8x6E+8WoGdSzbrEZ6pUVwKJGKEuDlx6g6HEmtv3ZwgGe20gvPjjW+oCO 888dwK/mbIHrHTq4nO3o0gAdAJwuamn9BlHU5O4RW7BQ4tLF+j/fBCACWRG1NHXA 0AT8eg544GyCdyteAH11oCDsHS8/DAPsM6t+tZrMCIt9+9dzPdVoOmQNaMMrcz8e JohddRTK6zHe9ixZTt/soayOF7OSQQeekbr3HPYhD450zRVplLMHx7wnph/+O+Po 6bqDnUzdnkqAAwwymQapHMuHXZKNrhdfKau3rVo1GeXLIRgeWLUoxFSm4TYshrgt +0AidLRH+dCY7MS9Ngga/sAK3vIDgSF75mFgOhY+q7nvY9Ecao6TnoNNRY29hUat 4y0VwSyysUy887vHr6lMK5CrAT/lCh8fuu20HUCoiLwMJvA6+wpivZkuiIvWY7bV GYsEYrrW+bCNN9wCGYTZEyX++os9v/38wdOqGUT00ewXkjIUFCWbrnxxSr98kF3w 3wPf9K4Y40MNxeR90nyX4zjXGF1/91msUh+iivsz9mcN9DK83fgTyOsoVLX5cm/L 2UBwMacsfjBbN4djOc5IuYMar/VNGQIDAQABAoICAQCtWx1SSxjkcerxsLEDKApW zOTfiUXgoOjZz0ZwS6b2VWDfyWAPU1r4ps39KaU+F+lzDhWjpYQqhbMjG7G9QMTs bQvkEQLAaQ5duU5NPgQG1oCUsj8rMSBpGGz4jBnm834QHMk7VTjYYbKu3WTyo8cU U2/+UDEkfxRlC+IkCmMFv1FxgMZ5PbktC/eDnYMhP2Pq7Q5ZWAVHymk9IMK0LHwm Kdg842K4A3zTXwGkGwetDCMm+YQpG5TxqX/w82BRcCuTR5h8fnYSsWLEIvKwWyIl ppcjaUnrFPG2yhxLqWUIKPpehuEjjhQMt9rDNoh6MHsJZZY5Dp5eq91EIvLoLQ99 hXBmD4P8LDop4r0jniPZJi/ACsaD0jBooA4525+Kouq7RP28Jp/pek7lVOOcBgRv D3zyESbKfqoaOfyfQ2ff4sILnTAr4V2nq3ekphGEYJrWN0ZoADcLdnr1cZ8L+VBI o/4mi5/3HID/UEDliHSa97hxxGBEqTto0ZuXuNwfwx5ho33uVT6zNwRgiJ62Bgu3 Fhk/wVGuZxWvb1KHUNInG9cvsslhO4Vu9wJvYj91BnRq36rsyKKid5DrU+PNgmog lw3IXQpTojyRCYPuG9TKqEZ6b+so7GTKhBOjiwaupMOletVRGSAdbE81VN6HtxNW aj39+FnxzMAlsieib+PBAQKCAQEA+t1fOYSaZBo7pZUmo2S0zulUEJjrYRGKJlWJ 4psWSwFu/7/3UL4q0RBQaSRew9u/YSpaNlBYfcpnFVOjiLwHq5Hx46Eq0BuKsNlJ 1/qxw9qjHqcrOre6K4/7NaWLPuM9fEmV+3MhFVXgv+WC5BHOowRTlOG30vIcC1J2 L5xsBUsxDDY13cD1bLKRmFcyMFM8y7wMZmo7H/WfVmyoPKQaC43pTcmIXH0Jr2Ws Wsfh18mhjtamaOPEFx5K0x4d0PI8tW5ouiUUkVIDaue27XfS969qEChv768/44eX WeqcekaG9jv2noMClt79rYd3Lne9HkgY6IT9FT+JqXfu+KYwuQKCAQEA6gYzUsGB 9GQO8DE8AYn7JwNOtg1X4zKakXiGxH+nuZb7wJjAeGdYqTHySxPBXg0A2nDwoyz5 4sAdLAr3FZoIvTzo7M5KIKFDzfyDmQDavhroH1mBAEiqKGNniP+RND3nWBBqDK1R qcqbhI3Kj5Ycany6a4nP+hZRBIyT9sfJ0S0YruSY8IGXgDwhlJrZ7bsWMZylrgD/ 1qnPL0KqVBY8YR8msRj88h72IlD5o0kwvisOIvyhA0YgwGBb6lg7A+DifiF03ZlS 2yELbIkKDVr+p3jC7MBh4B+OJY68AMl6wVjAaDM1AZnpjKE5YmZg5+Ks5823zILo PrSB9hn0+DIPYQKCAQEAh9x+JuNmzhHa/dkiHNl8hpadHYQD7gUWwZ4P1/bQAv0a xU2MvmDPRXxFYDv/SqlnI1NRmhq3YiDM5SLv7SyQJt4al4IAcsaHvTFgqaSuw3hU YVR9uAYqwE7w6OPn3r4o3Xfoz05Ru4FP//1nfucZ9vVv4rC/4nGWuJcHRM+9PLy1 KnztfVR0VlL7QPrwRnW99kS4nnqn3K4khiTAlF73cAyCLsuXmydoqGIzDtMzv68G XRpo82NvHmoccevcj/2w3T2XYECWvAEjsrEdQ8xiKBwLIAcWYEOUIUCcumiyKBKs IwzkioI/U8AeuO0lobfdZ1n6i2sCuZA4mNxIQseWmQKCAQEA5YkfXdQeuq5JWJ1x 1bCYfjNoSHfd9CH2KSimRqVOxWGpm8Y3QeFbvNgYZjsCNlVauOZ9oA7FKfp0onY+ 0xk56SKM83eCjW6fKrK6AKAt7LhHZDhNpxGek+6r5luE+FCfUGkJG1YD+x2WW/UW 8K6zQF8GGeQZ8Zlh7axUlIBxGpG43BGrUHpLNqPD7BXWGq6dnhufBYRFay8y34/r sH3+yuPa92ki7/geQppZwCZRgLSKMRbIdoWaKhZZEQlpGOzCOiRmk9OGyRcoNVRU X7UYgPqZdc1cMo/AxGWzULJNjMaYMZvIKcHkqOKZfkIcWlSictn7pMPhN1+k+NWM yMORAQKCAQAyXl02h/c2ihx6cjKlnNeDr2ZfzkoiAvFuKaoAR+KVvb9F9X7ZgKSi wudZyelTglIVCYXeRmG09uX3rNGCzFrweRwgn6x/8DnN5pMRJVZOXFdgR+V9uKep K6F7DYbPyggvLOAsezB+09i9lwxM+XdA2whVpL5NFR1rGfFglnE1EQHcEvNONkcv 0h8x9cNSptJyRDLiTIKI9EhonuzwzkGpvjULQE8MLbT8PbjoLFINcE9ZWhwtyw0V XO32KE8iLKt3KzHz9CfTRCI3M7DwD752AC6zRr8ZS/HXzs+5WTkdVVEtRC7Abd3y W2TzuSMYNDu876twbTVQJED3mwOAQ3J7 -----END PRIVATE KEY----- ================================================ FILE: _examples/http-server/listen-unix/main.go ================================================ package main import ( "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/core/netutil" ) func main() { app := iris.New() l, err := netutil.UNIX("/tmpl/srv.sock", 0666) // see its code to see how you can manually create a new file listener, it's easy. if err != nil { panic(err) } app.Run(iris.Listener(l)) } ================================================ FILE: _examples/http-server/notify-on-shutdown/main.go ================================================ package main import ( "context" "time" "github.com/kataras/iris/v12" ) func main() { app := iris.New() app.Get("/", func(ctx iris.Context) { ctx.HTML("

Hello, try to refresh the page after ~5 secs

") }) app.Logger().Info("Wait 5 seconds and check your terminal again") // simulate a shutdown action here... go func() { <-time.After(5 * time.Second) timeout := 10 * time.Second ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() // close all hosts, this will notify the callback we had register // inside the `configureHost` func. app.Shutdown(ctx) }() // app.ConfigureHost(configureHost) -> or pass "configureHost" as `app.Addr` argument, same result. // start the server as usual, the only difference is that // we're adding a second (optional) function // to configure the just-created host supervisor. // // http://localhost:8080 // wait 10 seconds and check your terminal. app.Run(iris.Addr(":8080", configureHost), iris.WithoutServerError(iris.ErrServerClosed)) time.Sleep(500 * time.Millisecond) // give time to the separate go routine(`onServerShutdown`) to finish. /* See iris.RegisterOnInterrupt(callback) for global catch of the CTRL/CMD+C and OS events. Look at the "graceful-shutdown" example for more. */ } func onServerShutdown() { println("server is closed") } func configureHost(su *iris.Supervisor) { // here we have full access to the host that will be created // inside the `app.Run` function or `NewHost`. // // we're registering a shutdown "event" callback here: su.RegisterOnShutdown(onServerShutdown) // su.RegisterOnError // su.RegisterOnServe } ================================================ FILE: _examples/http-server/socket-sharding/main.go ================================================ package main import ( "time" "github.com/kataras/iris/v12" ) func main() { startup := time.Now() app := iris.New() app.Get("/", func(ctx iris.Context) { s := startup.Format(ctx.Application().ConfigurationReadOnly().GetTimeFormat()) ctx.Writef("This server started at: %s\n", s) }) // This option allows linear scaling server performance on multi-CPU servers. // See https://www.nginx.com/blog/socket-sharding-nginx-release-1-9-1/ for details. app.Listen(":8080", iris.WithSocketSharding) } ================================================ FILE: _examples/http-server/timeout/main.go ================================================ package main import ( "context" "time" "github.com/kataras/iris/v12" ) func main() { app := iris.New() app.Get("/test", func(ctx iris.Context) { w := new(worker) result := w.Work(ctx) ctx.WriteString(result) }) app.Listen(":8080", iris.WithTimeout(4*time.Second)) } type worker struct{} func (w *worker) Work(ctx context.Context) string { t := time.Tick(time.Second) times := 0 for { select { case <-ctx.Done(): println("context.Done: canceled") return "Work canceled" case <-t: times++ println("Doing some work...") if times > 5 { return "Work is done with success" } } } return "nothing to do here" } ================================================ FILE: _examples/i18n/basic/hosts ================================================ # Copyright (c) 1993-2009 Microsoft Corp. # # This is a sample HOSTS file used by Microsoft TCP/IP for Windows. # # This file contains the mappings of IP addresses to host names. Each # entry should be kept on an individual line. The IP address should # be placed in the first column followed by the corresponding host name. # The IP address and the host name should be separated by at least one # space. # # Additionally, comments (such as these) may be inserted on individual # lines or following the machine name denoted by a '#' symbol. # # For example: # # 102.54.94.97 rhino.acme.com # source server # 38.25.63.10 x.acme.com # x client host # localhost name resolution is handled within DNS itself. # 127.0.0.1 localhost # ::1 localhost 127.0.0.1 mydomain.com 127.0.0.1 en.mydomain.com 127.0.0.1 el.mydomain.com 127.0.0.1 el-gr.mydomain.com 127.0.0.1 zh.mydomain.com ================================================ FILE: _examples/i18n/basic/locales/el-GR/locale_el-GR.ini ================================================ hi = γεια, %s userProfilePublicDescription = περιγραφή προφιλ ================================================ FILE: _examples/i18n/basic/locales/el-GR/locale_multi_first_el-GR.yml ================================================ key1: "αυτό είναι μια τιμή από το πρώτο αρχείο: locale_multi_first" ================================================ FILE: _examples/i18n/basic/locales/el-GR/locale_multi_second_el-GR.ini ================================================ key2 = αυτό είναι μια τιμή από το δεύτερο αρχείο μετάφρασης: locale_multi_second ================================================ FILE: _examples/i18n/basic/locales/en-US/locale_en-US.ini ================================================ hi = hello, %s userProfilePublicDescription = profile description ================================================ FILE: _examples/i18n/basic/locales/en-US/locale_multi_first_en-US.yml ================================================ key1: "this is a value from the first file: locale_multi_first" ================================================ FILE: _examples/i18n/basic/locales/en-US/locale_multi_second_en-US.ini ================================================ key2 = this is a value from the second file: locale_multi_second ================================================ FILE: _examples/i18n/basic/locales/zh-CN/locale_zh-CN.ini ================================================ hi = 您好,%s userProfilePublicDescription = 个人资料描述 ================================================ FILE: _examples/i18n/basic/main.go ================================================ package main import ( "fmt" "html/template" "github.com/kataras/iris/v12" ) /* See i18n-template for a more advanced translation key-values. */ func newApp() *iris.Application { app := iris.New() // Configure i18n. // // app.I18n.Subdomain = false to disable resolve lang code from subdomain. // app.I18n.LoadAssets for go-bindata. // Default values: // app.I18n.URLParameter = "lang" // app.I18n.Subdomain = true // // Set to false to disallow path (local) redirects, // see https://github.com/kataras/iris/issues/1369. // app.I18n.PathRedirect = true // // See `app.I18n.ExtractFunc = func(ctx iris.Context) string` or // `ctx.SetLanguage(langCode string)` to change the extracted language from a request. // // Use DefaultMessageFunc to customize the return value of a not found key or lang. // All language inputs fallback to the default locale if not matched. // This is why this one accepts both input and matched languages, // so the caller can be more expressful knowing those. // Defaults to nil. app.I18n.DefaultMessageFunc = func(langInput, langMatched, key string, args ...any) string { msg := fmt.Sprintf("user language input: %s: matched as: %s: not found key: %s: args: %v", langInput, langMatched, key, args) app.Logger().Warn(msg) return msg } // Load i18n when customizations are set in place. // // First parameter: Glob filpath patern, // Second variadic parameter: Optional language tags, the first one is the default/fallback one. err := app.I18n.Load("./locales/*/*", "en-US", "el-GR", "zh-CN") if err != nil { panic(err) } app.Get("/not-matched", func(ctx iris.Context) { text := ctx.Tr("not_found_key", "some", "values", 42) ctx.WriteString(text) // user language input: en-gb: matched as: en-US: not found key: not_found_key: args: [some values 42] }) app.Get("/", func(ctx iris.Context) { hi := ctx.Tr("hi", "iris") locale := ctx.GetLocale() ctx.Writef("From the language %s translated output: %s", locale.Language(), hi) }) app.Get("/some-path", func(ctx iris.Context) { ctx.Writef("%s", ctx.Tr("hi", "iris")) }) app.Get("/other", func(ctx iris.Context) { language := ctx.GetLocale().Language() fromFirstFileValue := ctx.Tr("key1") fromSecondFileValue := ctx.Tr("key2") ctx.Writef("From the language: %s, translated output:\n%s=%s\n%s=%s", language, "key1", fromFirstFileValue, "key2", fromSecondFileValue) }) // using in inside your views: view := iris.HTML("./views", ".html") app.RegisterView(view) app.Get("/templates", func(ctx iris.Context) { if err := ctx.View("index.html", iris.Map{ "tr": ctx.Tr, // word, arguments... {call .tr "hi" "iris"}} "trUnsafe": func(message string, args ...any) template.HTML { return template.HTML(ctx.Tr(message, args...)) }, }); err != nil { ctx.HTML(fmt.Sprintf("

%s

", err.Error())) return } // Note that, // Iris automatically adds a "tr" global template function as well, // the only difference is the way you call it inside your templates and // that it accepts a language code as its first argument: {{ tr "el-GR" "hi" "iris"}} }) // return app } func main() { app := newApp() // go to http://localhost:8080/el-gr/some-path // ^ (by path prefix) // // or http://el.mydomain.com8080/some-path // ^ (by subdomain - test locally with the hosts file) // // or http://localhost:8080/zh-CN/templates // ^ (by path prefix with uppercase) // // or http://localhost:8080/some-path?lang=el-GR // ^ (by url parameter) // // or http://localhost:8080 (default is en-US) // or http://localhost:8080/?lang=zh-CN // // go to http://localhost:8080/other?lang=el-GR // or http://localhost:8080/other (default is en-US) // or http://localhost:8080/other?lang=en-US // // or use cookies to set the language. app.Listen(":8080", iris.WithSitemap("http://localhost:8080")) } ================================================ FILE: _examples/i18n/basic/main_test.go ================================================ package main import ( "fmt" "strings" "testing" "github.com/kataras/iris/v12/httptest" ) func TestI18n(t *testing.T) { app := newApp() const ( expectedf = "From the language %s translated output: %s" enUS = "hello, iris" elGR = "γεια, iris" zhCN = "您好,iris" ) var ( tests = map[string]string{ "en-US": fmt.Sprintf(expectedf, "en-US", enUS), "el-GR": fmt.Sprintf(expectedf, "el-GR", elGR), "zh-CN": fmt.Sprintf(expectedf, "zh-CN", zhCN), } elgrMulti = fmt.Sprintf("From the language: %s, translated output:\n%s=%s\n%s=%s", "el-GR", "key1", "αυτό είναι μια τιμή από το πρώτο αρχείο: locale_multi_first", "key2", "αυτό είναι μια τιμή από το δεύτερο αρχείο μετάφρασης: locale_multi_second") enusMulti = fmt.Sprintf("From the language: %s, translated output:\n%s=%s\n%s=%s", "en-US", "key1", "this is a value from the first file: locale_multi_first", "key2", "this is a value from the second file: locale_multi_second") ) e := httptest.New(t, app) // default should be en-US. e.GET("/").Expect().Status(httptest.StatusOK).Body().IsEqual(tests["en-US"]) for lang, body := range tests { e.GET("/").WithQueryString("lang=" + lang).Expect().Status(httptest.StatusOK). Body().IsEqual(body) // test lowercase. e.GET("/").WithQueryString("lang=" + strings.ToLower(lang)).Expect().Status(httptest.StatusOK). Body().IsEqual(body) // test first part (e.g. en instead of en-US). langFirstPart := strings.Split(lang, "-")[0] e.GET("/").WithQueryString("lang=" + langFirstPart).Expect().Status(httptest.StatusOK). Body().IsEqual(body) // test accept-language header prefix (i18n wrapper). e.GET("/"+lang).WithHeader("Accept-Language", lang).Expect().Status(httptest.StatusOK). Body().IsEqual(body) // test path prefix (i18n router wrapper). e.GET("/" + lang).Expect().Status(httptest.StatusOK). Body().IsEqual(body) // test path prefix with first part. e.GET("/" + langFirstPart).Expect().Status(httptest.StatusOK). Body().IsEqual(body) } e.GET("/other").WithQueryString("lang=el-GR").Expect().Status(httptest.StatusOK). Body().IsEqual(elgrMulti) e.GET("/other").WithQueryString("lang=en-US").Expect().Status(httptest.StatusOK). Body().IsEqual(enusMulti) // test path prefix (i18n router wrapper). e.GET("/el-gr/other").Expect().Status(httptest.StatusOK). Body().IsEqual(elgrMulti) e.GET("/en/other").Expect().Status(httptest.StatusOK). Body().IsEqual(enusMulti) e.GET("/el-GRtemplates").Expect().Status(httptest.StatusNotFound) e.GET("/el-templates").Expect().Status(httptest.StatusNotFound) e.GET("/el/templates").Expect().Status(httptest.StatusOK).Body().Contains(elGR).Contains(zhCN) e.GET("/not-matched").WithQuery("lang", "en-gb").Expect().Status(httptest.StatusOK).Body().IsEqual("user language input: en-gb: matched as: en-US: not found key: not_found_key: args: [some values 42]") } ================================================ FILE: _examples/i18n/basic/views/index.html ================================================

Test translate current locale template function [dynamic] ("word", arguments...)
call .tr "hi" "iris"

{{call .tr "hi" "iris"}}

Test translate of any language template function [static] ("language", "word", arguments...)
tr "zh-CN" "hi" "iris"

{{tr "zh-CN" "hi" "iris"}}

Test HTML link ("word", arguments...)
call .trUnsafe "trUnsafe" "userProfilePublicDescription" "https://iris-go.com"

{{call .trUnsafe "userProfilePublicDescription" "https://iris-go.com"}} ================================================ FILE: _examples/i18n/plurals/locales/en-US/1648.ini ================================================ [message] Encrypted = Encrypted Message = Message EncryptedMessage = {{tr "message.Encrypted"}} {{tr "message.Message"}} HostResult = Store {{tr "message.EncryptedMessage"}} Online ================================================ FILE: _examples/i18n/plurals/locales/en-US/welcome.yml ================================================ # Locale variables # # Unlike normal keys, the variables # have limitations of: no ">x", "zero", "two" and template functions are supported. # This is done to force developers to use small and easy to read variables for easier maintain process. Vars: - Minutes: # possible keys: # one # "=x" - where x is a number # "x" # other "=3": "You have three days and %[2]d ${Minutes} off." # "FreeDay" 3, 15 (plurals + variable pluralization) one: "You have a day off" # "FreeDay", 1 other: "You have %[1]d free days" # "FreeDay", 5 # Sprintf-like raw translation HeIsHome: "%s is home" # Value without plural of its self but variables except pluralization HouseCount: "${Gender} (%[3]s) has %[2]d ${Houses}" # Same as above but with a template instead VarTemplate: (${Gender}) {{tr "HeIsHome" .Name}} # Template and non template with variables in the same plural key VarTemplatePlural: one: "${Gender} is awesome" other: "other (${Gender}) has %[3]d ${Houses}" "=5": "{{call .InlineJoin .Names}} are awesome" TemplatePlural: one: "{{.Name}} is unique" "=5": "{{call .InlineJoin .Names}} are awesome" # Same as above but it takes the variable counting through the map argument TemplateVarTemplatePlural: other: "These {{.PluralCount}} are wonderful, feeding {{.DogsCount}} ${Dogs} in total!" # Local variables and section. LocalVarsHouseCount: Text: "${Gender} has %[2]d ${Houses}" Vars: - Gender: "=3": "She" "=4": "He" - Houses: one: "house" other: "houses" # Sections: root: user: Account nav: home: Home # nav.home user: '{{tr "root.user"}}' # nav.user more: what: "this" # nav.more.what even: more: "yes" # nav.more.even.more aplural: "You are %[1]d ${Minutes} late." # Tr("nav.more.even.aplural", 15) ================================================ FILE: _examples/i18n/plurals/main.go ================================================ package main import ( "strings" "github.com/kataras/iris/v12" ) const ( female = iota + 1 male ) const tableStyle = ` ` /* $ go run . Visit http://localhost:8080 */ func main() { app := iris.New() err := app.I18n.Load("./locales/*/*", "en-US") // ^ here we only use a single locale for the sake of the example, // on a real app you can register as many languages as you want to support. if err != nil { panic(err) } app.Get("/", func(ctx iris.Context) { ctx.HTML("\n") ctx.WriteString(tableStyle) ctx.WriteString(` `) defer ctx.WriteString("
Key Translation Arguments
") tr(ctx, "Classic") tr(ctx, "YouLate", 1) tr(ctx, "YouLate", 2) tr(ctx, "FreeDay", 1) tr(ctx, "FreeDay", 5) tr(ctx, "FreeDay", 3, 15) tr(ctx, "HeIsHome", "Peter") tr(ctx, "HouseCount", female, 2, "Maria") tr(ctx, "HouseCount", male, 1, "Peter") tr(ctx, "nav.home") tr(ctx, "nav.user") tr(ctx, "nav.more.what") tr(ctx, "nav.more.even.more") tr(ctx, "nav.more.even.aplural", 1) tr(ctx, "nav.more.even.aplural", 15) tr(ctx, "VarTemplate", iris.Map{ "Name": "Peter", "GenderCount": male, }) tr(ctx, "VarTemplatePlural", 1, female) tr(ctx, "VarTemplatePlural", 2, female, 1) tr(ctx, "VarTemplatePlural", 2, female, 5) tr(ctx, "VarTemplatePlural", 1, male) tr(ctx, "VarTemplatePlural", 2, male, 1) tr(ctx, "VarTemplatePlural", 2, male, 2) tr(ctx, "VarTemplatePlural", iris.Map{ "PluralCount": 5, "Names": []string{"Makis", "Peter"}, "InlineJoin": func(arr []string) string { return strings.Join(arr, ", ") }, }) tr(ctx, "TemplatePlural", iris.Map{ "PluralCount": 1, "Name": "Peter", }) tr(ctx, "TemplatePlural", iris.Map{ "PluralCount": 5, "Names": []string{"Makis", "Peter"}, "InlineJoin": func(arr []string) string { return strings.Join(arr, ", ") }, }) tr(ctx, "VarTemplatePlural", 2, male, 4) tr(ctx, "TemplateVarTemplatePlural", iris.Map{ "PluralCount": 3, "DogsCount": 5, }) tr(ctx, "message.HostResult") tr(ctx, "LocalVarsHouseCount.Text", 3, 4) }) app.Listen(":8080") } func tr(ctx iris.Context, key string, args ...any) { translation := ctx.Tr(key, args...) ctx.Writef("%s%s%v\n", key, translation, args) } ================================================ FILE: _examples/i18n/plurals/main_test.go ================================================ package main_test import ( "strings" "testing" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/httptest" ) const ( female = iota + 1 male ) func TestI18nPlurals(t *testing.T) { handler := func(ctx iris.Context) { tr(ctx, "Classic") tr(ctx, "YouLate", 1) tr(ctx, "YouLate", 2) tr(ctx, "FreeDay", 1) tr(ctx, "FreeDay", 5) tr(ctx, "FreeDay", 3, 15) tr(ctx, "HeIsHome", "Peter") tr(ctx, "HouseCount", female, 2, "Maria") tr(ctx, "HouseCount", male, 1, "Peter") tr(ctx, "nav.home") tr(ctx, "nav.user") tr(ctx, "nav.more.what") tr(ctx, "nav.more.even.more") tr(ctx, "nav.more.even.aplural", 1) tr(ctx, "nav.more.even.aplural", 15) tr(ctx, "VarTemplate", iris.Map{ "Name": "Peter", "GenderCount": male, }) tr(ctx, "VarTemplatePlural", 1, female) tr(ctx, "VarTemplatePlural", 2, female, 1) tr(ctx, "VarTemplatePlural", 2, female, 5) tr(ctx, "VarTemplatePlural", 1, male) tr(ctx, "VarTemplatePlural", 2, male, 1) tr(ctx, "VarTemplatePlural", 2, male, 2) tr(ctx, "VarTemplatePlural", iris.Map{ "PluralCount": 5, "Names": []string{"Makis", "Peter"}, "InlineJoin": func(arr []string) string { return strings.Join(arr, ", ") }, }) tr(ctx, "TemplatePlural", iris.Map{ "PluralCount": 1, "Name": "Peter", }) tr(ctx, "TemplatePlural", iris.Map{ "PluralCount": 5, "Names": []string{"Makis", "Peter"}, "InlineJoin": func(arr []string) string { return strings.Join(arr, ", ") }, }) tr(ctx, "VarTemplatePlural", 2, male, 4) tr(ctx, "TemplateVarTemplatePlural", iris.Map{ "PluralCount": 3, "DogsCount": 5, }) tr(ctx, "message.HostResult") tr(ctx, "LocalVarsHouseCount.Text", 3, 4) } w := httptest.NewRecorder() r := httptest.NewRequest("GET", "/", nil) defer r.Body.Close() httptest.Do(w, r, handler, func(app *iris.Application) { err := app.I18n.Load("./locales/*/*", "en-US", "el-GR") if err != nil { panic(err) } }) expected := `Classic=classic YouLate=You are 1 minute late. YouLate=You are 2 minutes late. FreeDay=You have a day off FreeDay=You have 5 free days FreeDay=You have three days and 15 minutes off. HeIsHome=Peter is home HouseCount=She (Maria) has 2 houses HouseCount=He (Peter) has 1 house nav.home=Home nav.user=Account nav.more.what=this nav.more.even.more=yes nav.more.even.aplural=You are 1 minute late. nav.more.even.aplural=You are 15 minutes late. VarTemplate=(He) Peter is home VarTemplatePlural=She is awesome VarTemplatePlural=other (She) has 1 house VarTemplatePlural=other (She) has 5 houses VarTemplatePlural=He is awesome VarTemplatePlural=other (He) has 1 house VarTemplatePlural=other (He) has 2 houses VarTemplatePlural=Makis, Peter are awesome TemplatePlural=Peter is unique TemplatePlural=Makis, Peter are awesome VarTemplatePlural=other (He) has 4 houses TemplateVarTemplatePlural=These 3 are wonderful, feeding 5 dogsssss in total! message.HostResult=Store Encrypted Message Online LocalVarsHouseCount.Text=She has 4 houses ` if got := w.Body.String(); expected != got { t.Fatalf("expected:\n'%s'\n\nbut got:\n'%s'", expected, got) } } func tr(ctx iris.Context, key string, args ...any) { translation := ctx.Tr(key, args...) ctx.Writef("%s=%s\n", key, translation) } ================================================ FILE: _examples/i18n/template/locales/el-GR/other.ini ================================================ [nav] User = Λογαριασμός [debug] Title = Μενού προγραμματιστή AccessLog = Πρόσβαση στο αρχείο καταγραφής AccessLogClear = Καθαρισμός {{tr "debug.AccessLog"}} [user.connections] Title = {{tr "nav.User"}} Συνδέσεις ================================================ FILE: _examples/i18n/template/locales/el-GR/user.ini ================================================ [forms] member = μέλος register = Γίνε {{uppercase (tr "forms.member") }} registered = εγγεγραμμένοι ================================================ FILE: _examples/i18n/template/locales/en-US/other.ini ================================================ # just an example of some more nested keys, # see /other endpoint. [nav] User = Account [debug] Title = Developer Menu AccessLog = Access Log AccessLogClear = Clear {{tr "debug.AccessLog"}} [user.connections] Title = {{tr "nav.User"}} Connections ================================================ FILE: _examples/i18n/template/locales/en-US/user.ini ================================================ [forms] member = member register = Become a {{uppercase (tr "forms.member") }} registered = registered ================================================ FILE: _examples/i18n/template/main.go ================================================ package main import ( "strings" "text/template" "github.com/kataras/iris/v12" ) /* Iris I18n supports text/template inside the translation values. Follow this example to learn how to use that feature. */ func main() { app := newApp() app.Listen(":8080") } func newApp() *iris.Application { app := iris.New() // Set custom functions per locale! app.I18n.Loader.Funcs = func(current iris.Locale) template.FuncMap { return template.FuncMap{ "uppercase": func(word string) string { return strings.ToUpper(word) }, } } err := app.I18n.Load("./locales/*/*.ini", "en-US", "el-GR") if err != nil { panic(err) } app.Get("/", func(ctx iris.Context) { text := ctx.Tr("forms.register") // en-US: prints "Become a MEMBER". ctx.WriteString(text) }) app.Get("/title", func(ctx iris.Context) { text := ctx.Tr("user.connections.Title") // en-US: prints "Accounts Connections". ctx.WriteString(text) }) return app } ================================================ FILE: _examples/i18n/template/main_test.go ================================================ package main import ( "testing" "github.com/kataras/iris/v12/httptest" ) func TestI18nLoaderFuncMap(t *testing.T) { app := newApp() e := httptest.New(t, app) e.GET("/").Expect().Status(httptest.StatusOK). Body().IsEqual("Become a MEMBER") e.GET("/title").Expect().Status(httptest.StatusOK). Body().IsEqual("Account Connections") e.GET("/").WithHeader("Accept-Language", "el").Expect().Status(httptest.StatusOK). Body().IsEqual("Γίνε ΜΈΛΟΣ") e.GET("/title").WithHeader("Accept-Language", "el").Expect().Status(httptest.StatusOK). Body().IsEqual("Λογαριασμός Συνδέσεις") } ================================================ FILE: _examples/i18n/template-embedded/embedded/locales/el-GR/other.ini ================================================ [nav] User = Λογαριασμός [debug] Title = Μενού προγραμματιστή AccessLog = Πρόσβαση στο αρχείο καταγραφής AccessLogClear = Καθαρισμός {{ tr "debug.AccessLog" }} [user.connections] Title = {{ tr "nav.User" }} Συνδέσεις ================================================ FILE: _examples/i18n/template-embedded/embedded/locales/el-GR/user.ini ================================================ [forms] member = μέλος register = Γίνε {{ uppercase (tr "forms.member") }} registered = εγγεγραμμένοι ================================================ FILE: _examples/i18n/template-embedded/embedded/locales/en-US/other.ini ================================================ # just an example of some more nested keys, # see /other endpoint. [nav] User = Account [debug] Title = Developer Menu AccessLog = Access Log AccessLogClear = Clear {{ tr "debug.AccessLog" }} [user.connections] Title = {{ tr "nav.User" }} Connections ================================================ FILE: _examples/i18n/template-embedded/embedded/locales/en-US/user.ini ================================================ [forms] member = member register = Become a {{ uppercase (tr "forms.member") }} registered = registered ================================================ FILE: _examples/i18n/template-embedded/main.go ================================================ package main import ( "embed" "strings" "text/template" "github.com/kataras/iris/v12" ) //go:embed embedded/locales/* var embeddedFS embed.FS func main() { app := newApp() // http://localhost:8080 // http://localhost:8080?lang=el // http://localhost:8080?lang=el // http://localhost:8080?lang=el-GR // http://localhost:8080?lang=en // http://localhost:8080?lang=en-US // // http://localhost:8080/title // http://localhost:8080/title?lang=el-GR // ... app.Listen(":8080") } func newApp() *iris.Application { app := iris.New() // Set custom functions per locale! app.I18n.Loader.Funcs = func(current iris.Locale) template.FuncMap { return template.FuncMap{ "uppercase": func(word string) string { return strings.ToUpper(word) }, } } // Instead of: // err := app.I18n.Load("./locales/*/*.ini", "en-US", "el-GR") // apply the below in order to build with embedded locales inside your executable binary. err := app.I18n.LoadFS(embeddedFS, "./embedded/locales/*/*.ini", "en-US", "el-GR") if err != nil { panic(err) } // OR to load all languages by filename: // app.I18n.LoadFS(embeddedFS, "./embedded/locales/*/*.ini") // Then set the default language using: // app.I18n.SetDefault("en-US") app.Get("/", func(ctx iris.Context) { text := ctx.Tr("forms.register") // en-US: prints "Become a MEMBER". ctx.WriteString(text) }) app.Get("/title", func(ctx iris.Context) { text := ctx.Tr("user.connections.Title") // en-US: prints "Accounts Connections". ctx.WriteString(text) }) return app } ================================================ FILE: _examples/i18n/template-embedded/main_test.go ================================================ package main import ( "testing" "github.com/kataras/iris/v12/httptest" ) func TestI18nLoaderFuncMap(t *testing.T) { app := newApp() e := httptest.New(t, app) e.GET("/").Expect().Status(httptest.StatusOK). Body().IsEqual("Become a MEMBER") e.GET("/title").Expect().Status(httptest.StatusOK). Body().IsEqual("Account Connections") e.GET("/").WithHeader("Accept-Language", "el").Expect().Status(httptest.StatusOK). Body().IsEqual("Γίνε ΜΈΛΟΣ") e.GET("/title").WithHeader("Accept-Language", "el").Expect().Status(httptest.StatusOK). Body().IsEqual("Λογαριασμός Συνδέσεις") } ================================================ FILE: _examples/kafka-api/Dockerfile ================================================ # docker build -t myapp . # docker run --rm -it -p 8080:8080 myapp:latest FROM golang:latest AS builder RUN apt-get update ENV GO111MODULE=on \ CGO_ENABLED=0 \ GOOS=linux \ GOARCH=amd64 WORKDIR /go/src/app COPY go.mod . RUN go mod download COPY . . RUN go install FROM scratch COPY --from=builder /go/bin/myapp . ENTRYPOINT ["./myapp"] ================================================ FILE: _examples/kafka-api/README.md ================================================ # Writing an API for Apache Kafka with Iris Read the [code](main.go). ## Docker 1. Open [docker-compose.yml](docker-compose.yml) and replace `KAFKA_ADVERTISED_HOST_NAME` with your own local address 2. Install [Docker](https://www.docker.com/) 3. Execute the command below to start kafka stack and the go application: ```sh $ docker-compose up ``` ### Troubleshooting On windows, if you get an error of `An attempt was made to access a socket in a way forbidden by its access permissions` Solution: 1. Stop Docker 2. Open CMD with Administrator privileges and execute the following commands: ```sh $ dism.exe /Online /Disable-Feature:Microsoft-Hyper-V $ netsh int ipv4 add excludedportrange protocol=tcp startport=2181 numberofports=1 $ dism.exe /Online /Enable-Feature:Microsoft-Hyper-V /All $ docker-compose up --build ``` ## Manually Install & run Kafka and Zookeper locally and then: ```sh go run main.go ``` ## Screens ![](0_docs.png) ![](1_create_topic.png) ![](2_list_topics.png) ![](3_store_to_topic.png) ![](4_retrieve_from_topic_real_time.png) ================================================ FILE: _examples/kafka-api/docker-compose.yml ================================================ version: '3.1' services: zookeeper: image: wurstmeister/zookeeper ports: - 2181:2181 kafka: image: wurstmeister/kafka ports: - 9092:9092 environment: KAFKA_ADVERTISED_HOST_NAME: 10.122.1.142 # replace that with your own local ipv4 addr. KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 # kafka: # image: confluentinc/cp-kafka:5.5.0 # hostname: kafka # ports: # - 9092:9092 # environment: # KAFKA_ADVERTISED_LISTENERS: LISTENER_DOCKER_INTERNAL://kafka:19092,LISTENER_DOCKER_EXTERNAL://${DOCKER_HOST_IP:-127.0.0.1}:9092 app: build: . ports: - 8080:8080 environment: KAFKA_1: kafka:9092 depends_on: - kafka ================================================ FILE: _examples/kafka-api/go.mod ================================================ module myapp go 1.25 require ( github.com/IBM/sarama v1.46.3 github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd ) require ( github.com/BurntSushi/toml v1.6.0 // indirect github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect github.com/CloudyKit/jet/v6 v6.3.1 // indirect github.com/Joker/jade v1.1.3 // indirect github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 // indirect github.com/andybalholm/brotli v1.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/eapache/go-resiliency v1.7.0 // indirect github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 // indirect github.com/eapache/queue v1.1.0 // indirect github.com/fatih/structs v1.1.0 // indirect github.com/flosch/pongo2/v4 v4.0.2 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect github.com/iris-contrib/schema v0.0.6 // indirect github.com/jcmturner/aescts/v2 v2.0.0 // indirect github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect github.com/jcmturner/gofork v1.7.6 // indirect github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect github.com/jcmturner/rpc/v2 v2.0.3 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/kataras/blocks v0.0.8 // indirect github.com/kataras/golog v0.1.12 // indirect github.com/kataras/pio v0.0.13 // indirect github.com/kataras/sitemap v0.0.6 // indirect github.com/kataras/tunnel v0.0.4 // indirect github.com/klauspost/compress v1.18.2 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mailgun/raymond/v2 v2.0.48 // indirect github.com/mailru/easyjson v0.9.1 // indirect github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/pierrec/lz4/v4 v4.1.22 // indirect github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/schollz/closestmatch v2.1.0+incompatible // indirect github.com/sirupsen/logrus v1.8.1 // indirect github.com/tdewolff/minify/v2 v2.24.8 // indirect github.com/tdewolff/parse/v2 v2.8.5 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/yosssi/ace v0.0.5 // indirect golang.org/x/crypto v0.47.0 // indirect golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect golang.org/x/net v0.49.0 // indirect golang.org/x/sys v0.40.0 // indirect golang.org/x/text v0.33.0 // indirect golang.org/x/time v0.14.0 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: _examples/kafka-api/go.sum ================================================ github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= github.com/CloudyKit/jet/v6 v6.3.1 h1:6IAo5Cx21xrHVaR8zzXN5gJatKV/wO7Nf6bfCnCSbUw= github.com/CloudyKit/jet/v6 v6.3.1/go.mod h1:lf8ksdNsxZt7/yH/3n4vJQWA9RUq4wpaHtArHhGVMOw= github.com/IBM/sarama v1.46.3 h1:njRsX6jNlnR+ClJ8XmkO+CM4unbrNr/2vB5KK6UA+IE= github.com/IBM/sarama v1.46.3/go.mod h1:GTUYiF9DMOZVe3FwyGT+dtSPceGFIgA+sPc5u6CBwko= github.com/Joker/hpp v1.0.0 h1:65+iuJYdRXv/XyN62C1uEmmOx3432rNG/rKlX6V7Kkc= github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= github.com/Joker/jade v1.1.3 h1:Qbeh12Vq6BxURXT1qZBRHsDxeURB8ztcL6f3EXSGeHk= github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKduM= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 h1:dG7/gLroJGht/jSQtHiLvT48Hxn+crbmvyItZC8cWOs= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05/go.mod h1:NYezi6wtnJtBm5btoprXc5SvAdqH0XTXWnUup0MptAI= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/eapache/go-resiliency v1.7.0 h1:n3NRTnBn5N0Cbi/IeOHuQn9s2UwVUH7Ga0ZWcP+9JTA= github.com/eapache/go-resiliency v1.7.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho= github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 h1:Oy0F4ALJ04o5Qqpdz8XLIpNA3WM/iSIXqxtqo7UGVws= github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0= github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/flosch/pongo2/v4 v4.0.2 h1:gv+5Pe3vaSVmiJvh/BZa82b7/00YUGm0PIyVVLop0Hw= github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+BWPhwZD8= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a h1:l7A0loSszR5zHd/qK53ZIHMO8b3bBSmENnQ6eKnUT0A= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/iris-contrib/httpexpect/v2 v2.15.2 h1:T9THsdP1woyAqKHwjkEsbCnMefsAFvk8iJJKokcJ3Go= github.com/iris-contrib/httpexpect/v2 v2.15.2/go.mod h1:JLDgIqnFy5loDSUv1OA2j0mb6p/rDhiCqigP22Uq9xE= github.com/iris-contrib/schema v0.0.6 h1:CPSBLyx2e91H2yJzPuhGuifVRnZBBJ3pCOMbOvPZaTw= github.com/iris-contrib/schema v0.0.6/go.mod h1:iYszG0IOsuIsfzjymw1kMzTL8YQcCWlm65f3wX8J5iA= github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kataras/blocks v0.0.8 h1:MrpVhoFTCR2v1iOOfGng5VJSILKeZZI+7NGfxEh3SUM= github.com/kataras/blocks v0.0.8/go.mod h1:9Jm5zx6BB+06NwA+OhTbHW1xkMOYxahnqTN5DveZ2Yg= github.com/kataras/golog v0.1.12 h1:Bu7I/G4ilJlbfzjmU39O9N+2uO1pBcMK045fzZ4ytNg= github.com/kataras/golog v0.1.12/go.mod h1:wrGSbOiBqbQSQznleVNX4epWM8rl9SJ/rmEacl0yqy4= github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd h1:oMsQgqKACbUdlaIWnN+EZzzAnHkw90hE/y/R0BSij2U= github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd/go.mod h1:yucjtDl9Vn3OctWM1fi0MuM9OckemC7kEreFa9QtPF8= github.com/kataras/pio v0.0.13 h1:x0rXVX0fviDTXOOLOmr4MUxOabu1InVSTu5itF8CXCM= github.com/kataras/pio v0.0.13/go.mod h1:k3HNuSw+eJ8Pm2lA4lRhg3DiCjVgHlP8hmXApSej3oM= github.com/kataras/sitemap v0.0.6 h1:w71CRMMKYMJh6LR2wTgnk5hSgjVNB9KL60n5e2KHvLY= github.com/kataras/sitemap v0.0.6/go.mod h1:dW4dOCNs896OR1HmG+dMLdT7JjDk7mYBzoIRwuj5jA4= github.com/kataras/tunnel v0.0.4 h1:sCAqWuJV7nPzGrlb0os3j49lk2JhILT0rID38NHNLpA= github.com/kataras/tunnel v0.0.4/go.mod h1:9FkU4LaeifdMWqZu7o20ojmW4B7hdhv2CMLwfnHGpYw= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw= github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18= github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg= github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo= github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk= github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tdewolff/minify/v2 v2.24.8 h1:58/VjsbevI4d5FGV0ZSuBrHMSSkH4MCH0sIz/eKIauE= github.com/tdewolff/minify/v2 v2.24.8/go.mod h1:0Ukj0CRpo/sW/nd8uZ4ccXaV1rEVIWA3dj8U7+Shhfw= github.com/tdewolff/parse/v2 v2.8.5 h1:ZmBiA/8Do5Rpk7bDye0jbbDUpXXbCdc3iah4VeUvwYU= github.com/tdewolff/parse/v2 v2.8.5/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo= github.com/tdewolff/test v1.0.11 h1:FdLbwQVHxqG16SlkGveC0JVyrJN62COWTRyUFzfbtBE= github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA= github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0= github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs= moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHcE= ================================================ FILE: _examples/kafka-api/main.go ================================================ package main import ( "encoding/json" "fmt" "os" "strings" "time" "github.com/IBM/sarama" "github.com/kataras/iris/v12" ) /* First of all, read about Apache Kafka, install and run it, if you didn't already: https://kafka.apache.org/quickstart Secondly, install your favourite Go library for Apache Kafka communication. I have chosen the shopify's one although I really loved the `segmentio/kafka-go` as well but it needs more to be done there and you will be bored to read all the necessary code required to get started with it, so: $ go get -u github.com/IBM/sarama The minimum Apache Kafka broker(s) version required is 0.10.0.0 but 0.11.x+ is recommended (tested with 2.5.0). Resources: - https://github.com/apache/kafka - https://github.com/IBM/sarama/blob/master/examples/http_server/http_server.go - DIY */ // package-level variables for the sake of the example // but you can define them inside your main func // and pass around this config whenever you need to create a client or a producer or a consumer or use a cluster. var ( // The Kafka brokers to connect to, as a comma separated list. brokers = []string{getenv("KAFKA_1", "localhost:9092")} // The config which makes our live easier when passing around, it pre-mades a lot of things for us. config *sarama.Config ) func getenv(key string, def string) string { if value := os.Getenv(key); value != "" { return value } return def } func init() { config = sarama.NewConfig() config.ClientID = "iris-example-client" config.Version = sarama.V0_11_0_2 // config.Producer.RequiredAcks = sarama.WaitForAll // Wait for all in-sync replicas to ack the message. config.Producer.Compression = sarama.CompressionSnappy config.Producer.Flush.Frequency = 500 * time.Millisecond config.Producer.Retry.Max = 10 // Retry up to 10 times to produce the message. config.Producer.Return.Successes = true // for SASL/basic plain text authentication: config.Net.SASL. // config.Net.SASL.Enable = true // config.Net.SASL.Handshake = false // config.Net.SASL.User = "myuser" // config.Net.SASL.Password = "mypass" config.Consumer.Return.Errors = true } func main() { app := iris.New() app.OnErrorCode(iris.StatusNotFound, handleNotFound) v1 := app.Party("/api/v1") { topicsAPI := v1.Party("/topics") { topicsAPI.Post("/", postTopicsHandler) // create a topic. topicsAPI.Get("/", getTopicsHandler) // list all topics. topicsAPI.Post("/{topic}/produce", postTopicProduceHandler) // store to a topic. topicsAPI.Get("/{topic}/consume", getTopicConsumeSSEHandler) // retrieve all messages from a topic. } } app.Get("/", docsHandler) app.Logger().Infof("Brokers: %s", strings.Join(brokers, ", ")) // GET : http://localhost:8080 // POST, GET: http://localhost:8080/api/v1/topics // POST : http://localhost:8080/api/v1/topics/{topic}/produce?key=my-key // GET : http://localhost:8080/api/v1/topics/{topic}/consume?partition=0&offset=0 app.Listen(":8080") } // simple use-case, you can use templates and views obviously, see the "_examples/views" examples. func docsHandler(ctx iris.Context) { ctx.ContentType("text/html") // or ctx.HTML(fmt.Sprintf(...)) ctx.Writef(` `) defer ctx.Writef("") ctx.Writef("") defer ctx.Writef("") ctx.Writef(` `) defer ctx.Writef(`
Method Path Handler
`) registeredRoutes := ctx.Application().GetRoutesReadOnly() for _, r := range registeredRoutes { if r.Path() == "/" { // don't list the root, current one. continue } ctx.Writef(` %s %s%s %s `, r.Method(), ctx.Host(), r.Path(), r.MainHandlerName()) } } type httpError struct { Code int `json:"code"` Reason string `json:"reason,omitempty"` } func (h httpError) Error() string { return fmt.Sprintf("Status Code: %d\nReason: %s", h.Code, h.Reason) } func fail(ctx iris.Context, statusCode int, format string, a ...any) { reason := "unspecified" if format != "" { reason = fmt.Sprintf(format, a...) } err := httpError{ Code: statusCode, Reason: reason, } ctx.StopWithJSON(statusCode, err) } func handleNotFound(ctx iris.Context) { suggestPaths := ctx.FindClosest(3) if len(suggestPaths) == 0 { ctx.WriteString("not found") return } ctx.HTML("Did you mean?
    ") for _, s := range suggestPaths { ctx.HTML(`
  • %s
  • `, s, s) } ctx.HTML("
") } // Topic the payload for a kafka topic creation. type Topic struct { Topic string `json:"topic"` Partitions int32 `json:"partitions"` ReplicationFactor int16 `json:"replication"` Configs []kv `json:"configs,omitempty"` } type kv struct { Key string `json:"key"` Value string `json:"value"` } func createKafkaTopic(t Topic) error { cluster, err := sarama.NewClusterAdmin(brokers, config) if err != nil { return err } defer cluster.Close() topicName := t.Topic topicDetail := sarama.TopicDetail{ NumPartitions: t.Partitions, ReplicationFactor: t.ReplicationFactor, } if len(t.Configs) > 0 { topicDetail.ConfigEntries = make(map[string]*string, len(t.Configs)) for _, c := range t.Configs { topicDetail.ConfigEntries[c.Key] = &c.Value // generate a ptr, or fill a new(string) with it and use that. } } return cluster.CreateTopic(topicName, &topicDetail, false) } func postTopicsHandler(ctx iris.Context) { var t Topic err := ctx.ReadJSON(&t) if err != nil { fail(ctx, iris.StatusBadRequest, "received invalid topic payload: %v", err) return } // try to create the topic inside kafka. err = createKafkaTopic(t) if err != nil { fail(ctx, iris.StatusInternalServerError, "unable to create topic: %v", err) return } ctx.StatusCode(iris.StatusCreated) ctx.Writef("Topic %q created", t.Topic) } func getKafkaTopics() ([]string, error) { client, err := sarama.NewClient(brokers, config) if err != nil { return nil, err } defer client.Close() return client.Topics() } func getTopicsHandler(ctx iris.Context) { topics, err := getKafkaTopics() if err != nil { fail(ctx, iris.StatusInternalServerError, "unable to retrieve topics: %v", err) return } ctx.JSON(topics) } func produceKafkaMessage(toTopic string, key string, value []byte) (partition int32, offset int64, err error) { // On the broker side, you may want to change the following settings to get // stronger consistency guarantees: // - For your broker, set `unclean.leader.election.enable` to false // - For the topic, you could increase `min.insync.replicas`. producer, err := sarama.NewSyncProducer(brokers, config) if err != nil { return -1, -1, err } defer producer.Close() // We are not setting a message key, which means that all messages will // be distributed randomly over the different partitions. return producer.SendMessage(&sarama.ProducerMessage{ Topic: toTopic, Key: sarama.StringEncoder(key), Value: sarama.ByteEncoder(value), }) } func postTopicProduceHandler(ctx iris.Context) { topicName := ctx.Params().Get("topic") key := ctx.URLParamDefault("key", "default") // read the request data and store them as they are (not recommended in production ofcourse, do your own checks here). body, err := ctx.GetBody() if err != nil { fail(ctx, iris.StatusUnprocessableEntity, "unable to read your data: %v", err) return } partition, offset, err := produceKafkaMessage(topicName, key, body) if err != nil { fail(ctx, iris.StatusInternalServerError, "failed to store your data: %v", err) return } // The tuple (topic, partition, offset) can be used as a unique identifier // for a message in a Kafka cluster. ctx.Writef("Your data is stored with unique identifier: %s/%d/%d", topicName, partition, offset) } type message struct { Time time.Time `json:"time"` Key string `json:"key"` // Value []byte/json.RawMessage(if you are sure that you are sending only JSON) `json:"value"` // or: Value string `json:"value"` // for simple key-value storage. } func getTopicConsumeSSEHandler(ctx iris.Context) { flusher, ok := ctx.ResponseWriter().Flusher() if !ok { ctx.StopWithText(iris.StatusHTTPVersionNotSupported, "streaming unsupported") return } ctx.ContentType("application/json, text/event-stream") ctx.Header("Cache-Control", "no-cache") ctx.Header("Connection", "keep-alive") master, err := sarama.NewConsumer(brokers, config) if err != nil { fail(ctx, iris.StatusInternalServerError, "unable to start master consumer: %v", err) return } fromTopic := ctx.Params().Get("topic") // take the partition, defaults to the first found if not url query parameter "partition" passed. var partition int32 partitions, err := master.Partitions(fromTopic) if err != nil { master.Close() fail(ctx, iris.StatusInternalServerError, "unable to get partitions for topic: '%s': %v", fromTopic, err) return } if len(partitions) > 0 { partition = partitions[0] } partition = ctx.URLParamInt32Default("partition", partition) offset := ctx.URLParamInt64Default("offset", sarama.OffsetOldest) consumer, err := master.ConsumePartition(fromTopic, partition, offset) if err != nil { ctx.Application().Logger().Error(err) master.Close() // close the master here to avoid any leaks, we will exit. fail(ctx, iris.StatusInternalServerError, "unable to start partition consumer: %v", err) return } // `OnClose` fires when the request is finally done (all data read and handler exits) or interrupted by the user. ctx.OnClose(func(_ iris.Context) { ctx.Application().Logger().Warnf("a client left") // Close shuts down the consumer. It must be called after all child // PartitionConsumers have already been closed. <-- That is what // godocs says but it doesn't work like this. // if err = consumer.Close(); err != nil { // ctx.Application().Logger().Errorf("[%s] unable to close partition consumer: %v", ctx.RemoteAddr(), err) // } // so close the master only and omit the first ^ consumer.Close: if err = master.Close(); err != nil { ctx.Application().Logger().Errorf("[%s] unable to close master consumer: %v", ctx.RemoteAddr(), err) } }) for { select { case consumerErr, ok := <-consumer.Errors(): if !ok { return } ctx.Writef("data: error: {\"reason\": \"%s\"}\n\n", consumerErr.Error()) flusher.Flush() case incoming, ok := <-consumer.Messages(): if !ok { return } msg := message{ Time: incoming.Timestamp, Key: string(incoming.Key), Value: string(incoming.Value), } b, err := json.Marshal(msg) if err != nil { ctx.Application().Logger().Error(err) continue } ctx.Writef("data: %s\n\n", b) flusher.Flush() } } } ================================================ FILE: _examples/kafka-api/postman_collection.json ================================================ { "info": { "_postman_id": "8b135d95-ea8c-4dd5-a127-4b83cb735504", "name": "iris-kafka-postman", "description": "Postman API Requests for Iris + Kafka example", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, "item": [ { "name": "Create Topic", "request": { "method": "POST", "header": [], "body": { "mode": "raw", "raw": "{\r\n \"topic\":\"mytopic\",\r\n \"partitions\": 1,\r\n \"replication\":1\r\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "http://localhost:8080/api/v1/topics", "protocol": "http", "host": [ "localhost" ], "port": "8080", "path": [ "api", "v1", "topics" ] }, "description": "Create a new kafka topic" }, "response": [] }, { "name": "List all Topics", "request": { "method": "GET", "header": [], "url": { "raw": "http://localhost:8080/api/v1/topics", "protocol": "http", "host": [ "localhost" ], "port": "8080", "path": [ "api", "v1", "topics" ] }, "description": "List all topics" }, "response": [] }, { "name": "Store data to Topic", "request": { "method": "POST", "header": [], "body": { "mode": "raw", "raw": "{\r\n \"username\":\"kataras\",\r\n \"repo\":\"iris\"\r\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "http://localhost:8080/api/v1/topics/mytopic/produce?key=mykey", "protocol": "http", "host": [ "localhost" ], "port": "8080", "path": [ "api", "v1", "topics", "mytopic", "produce" ], "query": [ { "key": "key", "value": "mykey" } ] }, "description": "Produce some data to a Topic" }, "response": [] }, { "name": "(Open in Browser) Consume data from a Topic", "request": { "method": "GET", "header": [], "url": { "raw": "http://localhost:8080/api/v1/topics/mytopic/consume?partition=0&offset=0", "protocol": "http", "host": [ "localhost" ], "port": "8080", "path": [ "api", "v1", "topics", "mytopic", "consume" ], "query": [ { "key": "partition", "value": "0" }, { "key": "offset", "value": "0" } ] }, "description": "Note that, you have to open this one at your browser. Postman does not support SSE testing, see: https://github.com/postmanlabs/postman-app-support/issues/6682" }, "response": [] } ], "protocolProfileBehavior": {} } ================================================ FILE: _examples/logging/file-logger/main.go ================================================ package main import ( "os" "time" "github.com/kataras/iris/v12" ) func main() { f := newLogFile() defer f.Close() app := iris.New() // Attach the file as logger, remember, iris' app logger is just an io.Writer. // Use the following code if you need to write the logs to file and console at the same time. // app.Logger().SetOutput(io.MultiWriter(f, os.Stdout)) app.Logger().SetOutput(f) app.Get("/ping", func(ctx iris.Context) { // for the sake of simplicity, in order see the logs at the ./_today_.txt ctx.Application().Logger().Infof("Request path: %s", ctx.Path()) ctx.WriteString("pong") }) // Navigate to http://localhost:8080/ping // and open the ./logs{TODAY}.txt file. if err := app.Listen(":8080", iris.WithoutBanner); err != nil { app.Logger().Warn("Shutdown with error: " + err.Error()) } } // Get a filename based on the date, just for the sugar. func todayFilename() string { today := time.Now().Format("Jan 02 2006") return today + ".txt" } func newLogFile() *os.File { filename := todayFilename() // Open the file, this will append to the today's file if server restarted. f, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) if err != nil { panic(err) } return f } ================================================ FILE: _examples/logging/json-logger/main.go ================================================ package main import ( "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/middleware/requestid" "github.com/kataras/golog" ) func main() { app := iris.New() app.Logger().SetLevel("debug") app.Logger().SetFormat("json", " ") // to register a custom Formatter: // app.Logger().RegisterFormatter(golog.Formatter...) // Also, see app.Logger().SetLevelOutput(level string, w io.Writer) // to set a custom writer for a specific level. app.Use(requestid.New()) /* Example Output: { "timestamp": 1591422944, "level": "debug", "message": "This is a message with data", "fields": { "username": "kataras" }, "stacktrace": [ { "function": "main.main", "source": "C:/mygopath/src/github.com/kataras/iris/_examples/logging/json-logger/main.go:16" } ] } */ app.Logger().Debugf("This is a %s with data (debug prints the stacktrace too)", "message", golog.Fields{ "username": "kataras", }) /* Example Output: { "timestamp": 1591422944, "level": "info", "message": "An info message", "fields": { "home": "https://iris-go.com" } } */ app.Logger().Infof("An info message", golog.Fields{"home": "https://iris-go.com"}) app.Get("/ping", ping) // Navigate to http://localhost:8080/ping. app.Listen(":8080" /*, iris.WithoutBanner*/) } func ping(ctx iris.Context) { /* Example Output: { "timestamp": 1591423046, "level": "debug", "message": "Request path: /ping", "fields": { "request_id": "fc12d88a-a338-4bb9-aa5e-126f2104365c" }, "stacktrace": [ { "function": "main.ping", "source": "C:/mygopath/src/github.com/kataras/iris/_examples/logging/json-logger/main.go:82" }, ... ] } */ ctx.Application().Logger().Debugf("Request path: %s", ctx.Path(), golog.Fields{ "request_id": ctx.GetID(), }) ctx.WriteString("pong") } ================================================ FILE: _examples/logging/json-logger/main_test.go ================================================ package main import ( "bytes" "encoding/json" "fmt" "path" "runtime" "strings" "sync" "testing" "github.com/kataras/golog" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/httptest" ) func TestJSONLogger(t *testing.T) { iters := 500 out := new(bytes.Buffer) app := iris.New() app.Logger().SetTimeFormat("") // disable timestamps. app.Logger().SetStacktraceLimit(1) // limit debug stacktrace to 1, show only the first caller. app.Logger().SetOutput(out) app.Logger().Handle(func(l *golog.Log) bool { enc := json.NewEncoder(l.Logger.Printer) // you can change the output to a file as well. err := enc.Encode(l) return err == nil }) app.Get("/ping", ping) expectedSourceDir := getSourceDirPath() expectedLogStr := fmt.Sprintf(`{"level":"debug","message":"Request path: /ping","fields":{"request_id":null},"stacktrace":[{"function":"json-logger/ping","source":"%s/main.go:78"}]}`, expectedSourceDir) // gh actions-specific. e := httptest.New(t, app, httptest.LogLevel("debug")) wg := new(sync.WaitGroup) wg.Add(iters) for i := 0; i < iters; i++ { go func() { e.GET("/ping").Expect().Status(httptest.StatusOK).Body().IsEqual("pong") wg.Done() }() } wg.Wait() expected := "" for i := 0; i < iters; i++ { expected += expectedLogStr + "\n" } got := out.String() got = got[strings.Index(got, "{"):] // take only the json we care and after. if expected != got { if !strings.HasSuffix(got, expected) { // C:/mygopath vs /home/travis vs any file system, // pure check but it does the job. t.Fatalf("expected:\n%s\nbut got:\n%s", expected, got) } } } func getSourceDirPath() string { _, file, _, ok := runtime.Caller(1) // get the caller's file. if !ok { return "unknown source" } return path.Dir(file) // get the directory of the file (delimiter: /). } ================================================ FILE: _examples/logging/request-logger/accesslog/access.log.sample ================================================ 2020-09-13 13:37:42 2.0057ms 200 POST /read_body ::1 61 B 94 B {"name":"John","email":"example@example.com"} 2020-09-13 13:37:50 0s 400 POST /read_body ::1 0 B 0 B error(invalid character '\r' in string literal) 2020-09-13 13:38:03 0s 404 GET /favicon.ico ::1 0 B 9 B 2020-09-13 13:38:07 0s 404 GET /public ::1 0 B 9 B 2020-09-13 13:38:09 1.0021ms 200 GET /user/kataras ::1 username=kataras 0 B 15 B 2020-09-13 13:38:14 0s 200 GET /user/kataras ::1 username=kataras a_query_parameter=name 0 B 15 B 2020-09-13 13:38:18 0s 401 GET /admin ::1 0 B 0 B 2020-09-13 13:38:19 0s 200 GET /admin ::1 auth=admin:admin 0 B 48 B 2020-09-13 13:38:22 0s 200 GET /session ::1 session_id=23fe763f-c9d5-4d65-9e1a-2cc8d23d1aa3 session_test_key=session_test_value auth=admin:admin 0 B 2 B 2020-09-13 13:38:25 2.0001204s 200 GET /fields ::1 job_latency=2s auth=admin:admin user=user-id:user-name 0 B 2 B ================================================ FILE: _examples/logging/request-logger/accesslog/main.go ================================================ package main // See https://github.com/kataras/iris/issues/1601 import ( "bufio" "fmt" "time" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/middleware/accesslog" "github.com/kataras/iris/v12/middleware/basicauth" "github.com/kataras/iris/v12/middleware/requestid" "github.com/kataras/iris/v12/sessions" rotatelogs "github.com/iproj/file-rotatelogs" ) // Default line format: // Time|Latency|Code|Method|Path|IP|Path Params Query Fields|Bytes Received|Bytes Sent|Request|Response| // // Read the example and its comments carefully. func makeAccessLog() *accesslog.AccessLog { // Optionally, let's Go with log rotation. pathToAccessLog := "./access_log.%Y%m%d%H%M" w, err := rotatelogs.New( pathToAccessLog, rotatelogs.WithMaxAge(24*time.Hour), rotatelogs.WithRotationTime(time.Hour)) if err != nil { panic(err) } // Initialize a new access log middleware. // Accepts an `io.Writer`. ac := accesslog.New(bufio.NewWriter(w)) ac.Delim = ' ' // change the separator from '|' to space. // ac.TimeFormat = "2006-01-02 15:04:05" // default // Example of adding more than one field to the logger. // Here we logging all the session values this request has. // // You can also add fields per request handler, // look below to the `fieldsHandler` function. // Note that this method can override a key stored by a handler's fields. ac.AddFields(func(ctx iris.Context, fields *accesslog.Fields) { if sess := sessions.Get(ctx); sess != nil { fields.Set("session_id", sess.ID()) sess.Visit(func(k string, v any) { fields.Set(k, v) }) } }) // Add a custom field of "auth" when basic auth is available. ac.AddFields(func(ctx iris.Context, fields *accesslog.Fields) { if username, password, ok := ctx.Request().BasicAuth(); ok { fields.Set("auth", username+":"+password) } }) return ac /* Use a file directly: ac := accesslog.File("./access.log") Log after the response was sent (defaults to false): ac.Async = true Force-protect writer with locks. On this example this is not required: ac.LockWriter = true" // To disable request and response calculations // (enabled by default but slows down the whole operation if Async is false): ac.RequestBody = false ac.ResponseBody = false ac.BytesReceived = false ac.BytesSent = false ac.BytesReceivedBody = false ac.BytesSentBody = false Add second output: ac.AddOutput(app.Logger().Printer) OR: accesslog.New(io.MultiWriter(w, os.Stdout)) Change format (after output was set): ac.SetFormatter(&accesslog.JSON{Indent: " "}) Modify the output format and customize the order with the Template formatter: ac.SetFormatter(&accesslog.Template{ Text: "{{.Now.Format .TimeFormat}}|{{.Latency}}|{{.Code}}|{{.Method}}|{{.Path}}|{{.IP}}|{{.RequestValuesLine}}|{{.BytesReceivedLine}}|{{.BytesSentLine}}|{{.Request}}|{{.Response}}|\n", // Default ^ }) */ } func main() { ac := makeAccessLog() defer ac.Close() app := iris.New() // Register the middleware (UseRouter to catch http errors too). app.UseRouter(ac.Handler) // // Register other middlewares... app.UseRouter(requestid.New()) // Register some routes... app.HandleDir("/", iris.Dir("./public")) app.Get("/user/{username}", userHandler) app.Post("/read_body", readBodyHandler) app.Get("/html_response", htmlResponse) basicAuth := basicauth.Default(map[string]string{ "admin": "admin", }) app.Get("/admin", basicAuth, adminHandler) sess := sessions.New(sessions.Config{Cookie: "my_session_id", AllowReclaim: true}) app.Get("/session", sess.Handler(), sessionHandler) app.Get("/fields", fieldsHandler) // app.Listen(":8080") } func readBodyHandler(ctx iris.Context) { var request any if err := ctx.ReadBody(&request); err != nil { ctx.StopWithPlainError(iris.StatusBadRequest, err) return } ctx.JSON(iris.Map{"message": "OK", "data": request}) } func userHandler(ctx iris.Context) { ctx.Writef("Hello, %s!", ctx.Params().Get("username")) } func htmlResponse(ctx iris.Context) { ctx.HTML("

HTML Response

") } func adminHandler(ctx iris.Context) { username, password, _ := ctx.Request().BasicAuth() // of course you don't want that in production: ctx.HTML(fmt.Sprintf("

Username: %s

Password: %s

", username, password)) } func sessionHandler(ctx iris.Context) { sess := sessions.Get(ctx) sess.Set("session_test_key", "session_test_value") ctx.WriteString("OK") } type user struct { ID string Username string } // Log custom structs, they can implement the fmt.Stringer interface too. func (u user) String() string { return u.ID + ":" + u.Username } func fieldsHandler(ctx iris.Context) { start := time.Now() // simulate a heavy job... time.Sleep(2 * time.Second) end := time.Since(start) // Get the current fields instance // and use it to set custom log values. logFields := accesslog.GetFields(ctx) logFields.Set("job_latency", end.Round(time.Second)) // Simulate a database fetch or anything // to get a "user" and log it: u := user{ ID: "user-id", Username: "user-name", } logFields.Set("user", u) ctx.WriteString("OK") } ================================================ FILE: _examples/logging/request-logger/accesslog/public/index.html ================================================

Hello index

================================================ FILE: _examples/logging/request-logger/accesslog-broker/main.go ================================================ package main import ( "fmt" "os" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/middleware/accesslog" "github.com/kataras/iris/v12/middleware/recover" ) func main() { /* On this example we will make use of the logs broker. A handler will listen for any incoming logs and render those logs as chunks of JSON to the client (e.g. browser) at real-time. Note that this ^ can be done with Server-Sent Events but for the sake of the example we'll do it using Transfer-Encoding: chunked. */ ac := accesslog.File("./access.log") defer ac.Close() ac.AddOutput(os.Stdout) ac.RequestBody = true // Set to false to print errors as one line: // ac.KeepMultiLineError = false // Set the "depth" of a panic trace: ac.PanicLog = accesslog.LogHandler // or LogCallers or LogStack // Optionally run logging after response has sent: // ac.Async = true broker := ac.Broker() // <- IMPORTANT app := iris.New() app.UseRouter(ac.Handler) app.UseRouter(recover.New()) app.OnErrorCode(iris.StatusNotFound, notFoundHandler) app.Get("/panic", testPanic) app.Get("/", indexHandler) app.Get("/profile/{username}", profileHandler) app.Post("/read_body", readBodyHandler) // register the /logs route, // registers a listener and prints the incoming logs. // Optionally, skip logging this handler. app.Get("/logs", accesslog.SkipHandler, logsHandler(broker)) // http://localhost:8080/logs to see the logs at real-time. app.Listen(":8080") } func notFoundHandler(ctx iris.Context) { // ctx.Application().Logger().Infof("Not Found Handler for: %s", ctx.Path()) suggestPaths := ctx.FindClosest(3) if len(suggestPaths) == 0 { ctx.WriteString("The page you're looking does not exist.") return } ctx.HTML("Did you mean?
    ") for _, s := range suggestPaths { ctx.HTML(fmt.Sprintf(`
  • %s
  • `, s, s)) } ctx.HTML("
") } func indexHandler(ctx iris.Context) { ctx.HTML("

Index

") } func profileHandler(ctx iris.Context) { username := ctx.Params().Get("username") ctx.HTML(fmt.Sprintf("Hello, %s!", username)) } func readBodyHandler(ctx iris.Context) { var request any if err := ctx.ReadBody(&request); err != nil { ctx.StopWithPlainError(iris.StatusBadRequest, err) return } ctx.JSON(iris.Map{"message": "OK", "data": request}) } func testPanic(ctx iris.Context) { panic("PANIC HERE") } func logsHandler(b *accesslog.Broker) iris.Handler { return func(ctx iris.Context) { // accesslog.Skip(ctx) // or inline skip. logs := b.NewListener() // <- IMPORTANT ctx.Header("Transfer-Encoding", "chunked") notifyClose := ctx.Request().Context().Done() for { select { case <-notifyClose: b.CloseListener(logs) // <- IMPORTANT err := ctx.Request().Context().Err() ctx.Application().Logger().Infof("Listener closed [%v], loop end.", err) return case log := <-logs: // <- IMPORTANT ctx.JSON(log, iris.JSON{Indent: " ", UnescapeHTML: true}) ctx.ResponseWriter().Flush() } } } } ================================================ FILE: _examples/logging/request-logger/accesslog-csv/access_log.csv.sample ================================================ Timestamp,Latency,Code,Method,Path,IP,Req Values,In,Out,Request,Response 1599996265254,0s,200,GET,/,::1,a=1 b=2,0,5,,Index 1599996266138,0s,200,GET,/,::1,sleep=32ms,0,5,,Index 1599996266778,1s,200,GET,/,::1,sleep=1s,0,5,,Index 1599996267780,1s,200,GET,/,::1,sleep=1s,0,5,,Index ================================================ FILE: _examples/logging/request-logger/accesslog-csv/main.go ================================================ package main import ( "time" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/middleware/accesslog" ) func main() { app := iris.New() ac := accesslog.File("access_log.csv") ac.ResponseBody = true ac.LatencyRound = time.Second ac.SetFormatter(&accesslog.CSV{ Header: true, // DateScript: "FROM_UNIX", }) app.UseRouter(ac.Handler) app.Get("/", index) app.Listen(":8080") } func index(ctx iris.Context) { if sleepDur := ctx.URLParam("sleep"); sleepDur != "" { if d, err := time.ParseDuration(sleepDur); err == nil { time.Sleep(d) } } ctx.WriteString("Index") } ================================================ FILE: _examples/logging/request-logger/accesslog-formatter/main.go ================================================ // Package main shows how to create a quite fast custom Log Formatter. // Note that, this example requires a little more knowledge about Go. package main import ( "bytes" "fmt" "io" "strconv" "sync" "time" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/middleware/accesslog" "github.com/kataras/iris/v12/middleware/requestid" ) func logFields(ctx iris.Context, fields *accesslog.Fields) { fields.Set("reqid", ctx.GetID()) } func main() { app := iris.New() ac := accesslog.File("./access.log"). AddFields(logFields). SetFormatter(newCustomFormatter(' ', "-\t\t\t\t\t")) ac.RequestBody = false ac.BytesReceivedBody = false ac.BytesSentBody = false defer ac.Close() app.UseRouter(ac.Handler) app.UseRouter(requestid.New()) app.OnErrorCode(iris.StatusNotFound, notFound) app.Get("/", index) app.Listen(":8080") } func notFound(ctx iris.Context) { ctx.WriteString("The page you're looking for does not exist!") } func index(ctx iris.Context) { ctx.WriteString("OK Index") } type customFormatter struct { w io.Writer bufPool *sync.Pool delim byte blank string } var _ accesslog.Formatter = (*customFormatter)(nil) func newCustomFormatter(delim byte, blank string) *customFormatter { return &customFormatter{delim: delim, blank: blank} } func (f *customFormatter) SetOutput(dest io.Writer) { f.w = dest f.bufPool = &sync.Pool{ New: func() any { return new(bytes.Buffer) }, } if f.delim == 0 { f.delim = ' ' } } const newLine = '\n' func (f *customFormatter) Format(log *accesslog.Log) (bool, error) { buf := f.bufPool.Get().(*bytes.Buffer) buf.WriteString(log.Now.Format(log.TimeFormat)) buf.WriteByte(f.delim) reqid := log.Fields.GetString("reqid") f.writeTextOrBlank(buf, reqid) buf.WriteString(uniformDuration(log.Latency)) buf.WriteByte(f.delim) buf.WriteString(log.IP) buf.WriteByte(f.delim) buf.WriteString(strconv.Itoa(log.Code)) buf.WriteByte(f.delim) buf.WriteString(log.Method) buf.WriteByte(f.delim) buf.WriteString(log.Path) buf.WriteByte(newLine) // _, err := buf.WriteTo(f.w) // or (to make sure that it resets on errors too): _, err := f.w.Write(buf.Bytes()) buf.Reset() f.bufPool.Put(buf) return true, err } func (f *customFormatter) writeTextOrBlank(buf *bytes.Buffer, s string) { if len(s) == 0 { if len(f.blank) == 0 { return } buf.WriteString(f.blank) } else { buf.WriteString(s) } buf.WriteByte(f.delim) } func uniformDuration(t time.Duration) string { return fmt.Sprintf("%*s", 12, t.String()) } ================================================ FILE: _examples/logging/request-logger/accesslog-proxy/main.go ================================================ /* Package main is a proxy + accesslog example. In this example we will make a small proxy which listens requests on "/proxy/+path". With two accesslog instances, one for the main application and one for the /proxy/ requests. Of cource, you could a single accesslog for the whole application, but for the sake of the example let's log them separately. We will make use of iris.StripPrefix and host.ProxyHandler. */ package main import ( "net/url" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/core/host" "github.com/kataras/iris/v12/middleware/accesslog" "github.com/kataras/iris/v12/middleware/recover" ) func main() { app := iris.New() app.Get("/", index) ac := accesslog.File("access.log") defer ac.Close() ac.Async = true ac.RequestBody = true ac.ResponseBody = true ac.BytesReceived = false ac.BytesSent = false app.UseRouter(ac.Handler) app.UseRouter(recover.New()) proxy := app.Party("/proxy") { acProxy := accesslog.File("proxy_access.log") defer acProxy.Close() acProxy.Async = true acProxy.RequestBody = true acProxy.ResponseBody = true acProxy.BytesReceived = false acProxy.BytesSent = false // Unlike Use, the UseRouter method replaces any duplications automatically. // (see UseOnce for the same behavior on Use). // Therefore, this statement removes the parent's accesslog and registers this new one. proxy.UseRouter(acProxy.Handler) proxy.UseRouter(recover.New()) proxy.Use(func(ctx iris.Context) { ctx.CompressReader(true) ctx.Next() }) /* Listen for specific proxy paths: // Listen on "/proxy" for "http://localhost:9090/read-write" proxy.Any("/", iris.StripPrefix("/proxy", newProxyHandler("http://localhost:9090/read-write"))) */ // You can register an access log only for proxied requests, e.g. proxy_access.log: // proxy.UseRouter(ac2.Handler) // Listen for any proxy path. // Proxies the "/proxy/+$path" to "http://localhost:9090/$path". proxy.Any("/{p:path}", iris.StripPrefix("/proxy", newProxyHandler("http://localhost:9090"))) } // $ go run target/main.go // open new terminal // $ go run main.go app.Listen(":8080") } func index(ctx iris.Context) { ctx.WriteString("OK") } func newProxyHandler(proxyURL string) iris.Handler { target, err := url.Parse(proxyURL) if err != nil { panic(err) } reverseProxy := host.ProxyHandler(target, nil) return iris.FromStd(reverseProxy) } ================================================ FILE: _examples/logging/request-logger/accesslog-proxy/target/main.go ================================================ package main import "github.com/kataras/iris/v12" // The target server, can be written using any programming language and any web framework, of course. func main() { app := iris.New() app.Logger().SetLevel("debug") // Just a test route which reads some data and responds back with json. app.Post("/read-write", readWriteHandler) app.Get("/get", getHandler) // The target ip:port. app.Listen(":9090") } func readWriteHandler(ctx iris.Context) { var req any ctx.ReadBody(&req) ctx.JSON(iris.Map{ "message": "OK", "request": req, }) } func getHandler(ctx iris.Context) { // ctx.CompressWriter(true) ctx.WriteString("Compressed data") } ================================================ FILE: _examples/logging/request-logger/accesslog-simple/access.log.sample ================================================ {"timestamp":1599993744664,"latency":0,"code":404,"method":"GET","path":"/favicon.ico","ip":"::1","request":"","bytes_sent":9} {"timestamp":1599993774018,"latency":0,"code":200,"method":"GET","path":"/","ip":"::1","query":[{"key":"a","value":"1"},{"key":"b","value":"2"}],"request":"","bytes_sent":2} ================================================ FILE: _examples/logging/request-logger/accesslog-simple/main.go ================================================ package main import ( "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/middleware/accesslog" ) // Read the example and its comments carefully. func makeAccessLog() *accesslog.AccessLog { // Initialize a new access log middleware. ac := accesslog.File("./access.log") // The default configuration: ac.Delim = '|' ac.TimeFormat = "2006-01-02 15:04:05" ac.Async = false ac.IP = true ac.BytesReceivedBody = true ac.BytesSentBody = true ac.BytesReceived = false ac.BytesSent = false ac.BodyMinify = true ac.RequestBody = true ac.ResponseBody = false ac.KeepMultiLineError = true ac.PanicLog = accesslog.LogHandler // Default line format if formatter is missing: // Time|Latency|Code|Method|Path|IP|Path Params Query Fields|Bytes Received|Bytes Sent|Request|Response| // // Set Custom Formatter: ac.SetFormatter(&accesslog.JSON{ Indent: " ", HumanTime: true, }) // ac.SetFormatter(&accesslog.CSV{}) // ac.SetFormatter(&accesslog.Template{Text: "{{.Code}}"}) return ac } func main() { ac := makeAccessLog() defer ac.Close() // Close the underline file. app := iris.New() // Register the middleware (UseRouter to catch http errors too). app.UseRouter(ac.Handler) app.Get("/", indexHandler) app.Listen(":8080") } func indexHandler(ctx iris.Context) { ctx.WriteString("OK") } ================================================ FILE: _examples/logging/request-logger/accesslog-slack/go.mod ================================================ module github.com/kataras/iris/_examples/logging/accesslog-slack go 1.25 require ( github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd github.com/slack-go/slack v0.17.3 ) require ( github.com/BurntSushi/toml v1.6.0 // indirect github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect github.com/CloudyKit/jet/v6 v6.3.1 // indirect github.com/Joker/jade v1.1.3 // indirect github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 // indirect github.com/andybalholm/brotli v1.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/fatih/structs v1.1.0 // indirect github.com/flosch/pongo2/v4 v4.0.2 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/iris-contrib/schema v0.0.6 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/kataras/blocks v0.0.8 // indirect github.com/kataras/golog v0.1.12 // indirect github.com/kataras/pio v0.0.13 // indirect github.com/kataras/sitemap v0.0.6 // indirect github.com/kataras/tunnel v0.0.4 // indirect github.com/klauspost/compress v1.18.2 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mailgun/raymond/v2 v2.0.48 // indirect github.com/mailru/easyjson v0.9.1 // indirect github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/schollz/closestmatch v2.1.0+incompatible // indirect github.com/sirupsen/logrus v1.8.1 // indirect github.com/stretchr/testify v1.11.1 // indirect github.com/tdewolff/minify/v2 v2.24.8 // indirect github.com/tdewolff/parse/v2 v2.8.5 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/yosssi/ace v0.0.5 // indirect golang.org/x/crypto v0.47.0 // indirect golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect golang.org/x/net v0.49.0 // indirect golang.org/x/sys v0.40.0 // indirect golang.org/x/text v0.33.0 // indirect golang.org/x/time v0.14.0 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: _examples/logging/request-logger/accesslog-slack/go.sum ================================================ github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= github.com/CloudyKit/jet/v6 v6.3.1 h1:6IAo5Cx21xrHVaR8zzXN5gJatKV/wO7Nf6bfCnCSbUw= github.com/CloudyKit/jet/v6 v6.3.1/go.mod h1:lf8ksdNsxZt7/yH/3n4vJQWA9RUq4wpaHtArHhGVMOw= github.com/Joker/hpp v1.0.0 h1:65+iuJYdRXv/XyN62C1uEmmOx3432rNG/rKlX6V7Kkc= github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= github.com/Joker/jade v1.1.3 h1:Qbeh12Vq6BxURXT1qZBRHsDxeURB8ztcL6f3EXSGeHk= github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKduM= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 h1:dG7/gLroJGht/jSQtHiLvT48Hxn+crbmvyItZC8cWOs= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05/go.mod h1:NYezi6wtnJtBm5btoprXc5SvAdqH0XTXWnUup0MptAI= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/flosch/pongo2/v4 v4.0.2 h1:gv+5Pe3vaSVmiJvh/BZa82b7/00YUGm0PIyVVLop0Hw= github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+BWPhwZD8= github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a h1:l7A0loSszR5zHd/qK53ZIHMO8b3bBSmENnQ6eKnUT0A= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/iris-contrib/httpexpect/v2 v2.15.2 h1:T9THsdP1woyAqKHwjkEsbCnMefsAFvk8iJJKokcJ3Go= github.com/iris-contrib/httpexpect/v2 v2.15.2/go.mod h1:JLDgIqnFy5loDSUv1OA2j0mb6p/rDhiCqigP22Uq9xE= github.com/iris-contrib/schema v0.0.6 h1:CPSBLyx2e91H2yJzPuhGuifVRnZBBJ3pCOMbOvPZaTw= github.com/iris-contrib/schema v0.0.6/go.mod h1:iYszG0IOsuIsfzjymw1kMzTL8YQcCWlm65f3wX8J5iA= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kataras/blocks v0.0.8 h1:MrpVhoFTCR2v1iOOfGng5VJSILKeZZI+7NGfxEh3SUM= github.com/kataras/blocks v0.0.8/go.mod h1:9Jm5zx6BB+06NwA+OhTbHW1xkMOYxahnqTN5DveZ2Yg= github.com/kataras/golog v0.1.12 h1:Bu7I/G4ilJlbfzjmU39O9N+2uO1pBcMK045fzZ4ytNg= github.com/kataras/golog v0.1.12/go.mod h1:wrGSbOiBqbQSQznleVNX4epWM8rl9SJ/rmEacl0yqy4= github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd h1:oMsQgqKACbUdlaIWnN+EZzzAnHkw90hE/y/R0BSij2U= github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd/go.mod h1:yucjtDl9Vn3OctWM1fi0MuM9OckemC7kEreFa9QtPF8= github.com/kataras/pio v0.0.13 h1:x0rXVX0fviDTXOOLOmr4MUxOabu1InVSTu5itF8CXCM= github.com/kataras/pio v0.0.13/go.mod h1:k3HNuSw+eJ8Pm2lA4lRhg3DiCjVgHlP8hmXApSej3oM= github.com/kataras/sitemap v0.0.6 h1:w71CRMMKYMJh6LR2wTgnk5hSgjVNB9KL60n5e2KHvLY= github.com/kataras/sitemap v0.0.6/go.mod h1:dW4dOCNs896OR1HmG+dMLdT7JjDk7mYBzoIRwuj5jA4= github.com/kataras/tunnel v0.0.4 h1:sCAqWuJV7nPzGrlb0os3j49lk2JhILT0rID38NHNLpA= github.com/kataras/tunnel v0.0.4/go.mod h1:9FkU4LaeifdMWqZu7o20ojmW4B7hdhv2CMLwfnHGpYw= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw= github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18= github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo= github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk= github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/slack-go/slack v0.17.3 h1:zV5qO3Q+WJAQ/XwbGfNFrRMaJ5T/naqaonyPV/1TP4g= github.com/slack-go/slack v0.17.3/go.mod h1:X+UqOufi3LYQHDnMG1vxf0J8asC6+WllXrVrhl8/Prk= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tdewolff/minify/v2 v2.24.8 h1:58/VjsbevI4d5FGV0ZSuBrHMSSkH4MCH0sIz/eKIauE= github.com/tdewolff/minify/v2 v2.24.8/go.mod h1:0Ukj0CRpo/sW/nd8uZ4ccXaV1rEVIWA3dj8U7+Shhfw= github.com/tdewolff/parse/v2 v2.8.5 h1:ZmBiA/8Do5Rpk7bDye0jbbDUpXXbCdc3iah4VeUvwYU= github.com/tdewolff/parse/v2 v2.8.5/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo= github.com/tdewolff/test v1.0.11 h1:FdLbwQVHxqG16SlkGveC0JVyrJN62COWTRyUFzfbtBE= github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA= github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0= github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs= moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHcE= ================================================ FILE: _examples/logging/request-logger/accesslog-slack/main.go ================================================ package main import ( "os" "time" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/middleware/accesslog" ) var ( // https://api.slack.com/apps/your_app_id/oauth token = os.Getenv("SLACK_BOT_TOKEN") // on slack app: right click on the channel -> view channel details -> on bottom, copy the channel id. channelID = os.Getenv("SLACK_CHANNEL_ID") ) // $ go run . func main() { app := iris.New() ac := accesslog.New(os.Stdout) // or app.Logger().Printer ac.LatencyRound = time.Second ac.SetFormatter(&Slack{ Token: token, ChannelIDs: []string{channelID}, HandleMessage: true, }) app.UseRouter(ac.Handler) app.Get("/", index) app.Listen(":8080") } func index(ctx iris.Context) { if sleepDur := ctx.URLParam("sleep"); sleepDur != "" { if d, err := time.ParseDuration(sleepDur); err == nil { time.Sleep(d) } } ctx.WriteString("Index") } ================================================ FILE: _examples/logging/request-logger/accesslog-slack/slack_formatter.go ================================================ package main import ( "io" "github.com/kataras/iris/v12/middleware/accesslog" "github.com/slack-go/slack" ) type Slack struct { // Client is the underline slack slack. // This or Token are required. Client *slack.Client // Token is the oauth slack client token. // Read more at: https://api.slack.com/web#authentication. // // If Client is not null then this is used to initialize the Slack.Client field. Token string // ChannelIDs specifies one or more channel the request logs // will be printed to. ChannelIDs []string // HandleMessage set to true whether the slack formatter // should just send the log to the slack channel(s) and // stop printing the log to the accesslog's io.Writer output. HandleMessage bool // Template is the underline text template format of the logs. // Set to a custom one if you want to customize the template (how the text is rended). Template *accesslog.Template } func (f *Slack) SetOutput(dest io.Writer) { if f.Client == nil && f.Token == "" { panic("client or token fields must be provided") } if len(f.ChannelIDs) == 0 { panic("channel ids field is required") } if f.Token != "" { c := slack.New(f.Token) f.Client = c } if f.Template == nil { f.Template = &accesslog.Template{} } f.Template.SetOutput(dest) } func (f *Slack) Format(log *accesslog.Log) (bool, error) { text, err := f.Template.LogText(log) if err != nil { return false, err } for _, channelID := range f.ChannelIDs { _, _, err := f.Client.PostMessage( channelID, slack.MsgOptionText(text, false), slack.MsgOptionAsUser(true), ) if err != nil { return false, err } } return f.HandleMessage, nil } ================================================ FILE: _examples/logging/request-logger/accesslog-template/main.go ================================================ package main import ( "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/middleware/accesslog" "github.com/kataras/iris/v12/middleware/requestid" ) func main() { /* This example will show you how you can register custom fields and log them separately with a custom format through the Template formatter. */ app := iris.New() ac := accesslog.File("./access.log").AddOutput(app.Logger().Printer) defer ac.Close() // 1. Register a field. ac.AddFields(func(ctx iris.Context, fields *accesslog.Fields) { fields.Set("Request ID", ctx.GetID()) }) // 2. Use Template formatter's `Text` value // to define a log line format. ac.SetFormatter(&accesslog.Template{ Text: `{{.Now.Format .TimeFormat}} {{.Path}} {{.Code}} {{.IP}} {{.Fields.Get "Request ID" }} `, /* 2020-09-11 09:30:10 / 200 ::1 050a0979-c5e4-4c2b-9f08-cb456628edb1 */ }) // 3. Register the middleware. That's all. app.UseRouter(ac.Handler) // Register the request id middleware, after the logger, this maps the Context.GetID(). // Remember: the accesslog runs the next handlers before itself to provide some fields. app.UseRouter(requestid.New()) app.Get("/", index) app.Listen(":8080") } func index(ctx iris.Context) { ctx.WriteString("Index") } /* Use a custom *template.Template: // 2.1 The log line format: text := `{{.Now.Format .TimeFormat}} {{.Path}} {{.Code}} {{.IP}} {{.Fields.Get "Request ID" }} ` // // 2.2 Parse the template, optionally using custom Template Functions. tmpl := template.Must(template.New("").Funcs(template.FuncMap{ // Custom functions you may want to use inside "text", // e.g. prefixFields .Fields "my_prefix" // to get a slice of fields starts with "my_prefix" // and later, in the template, loop through them and render their values. // "key": func(input) string { return ... } }).Parse(text)) // // 3. Use Template formatter's `Text` value // or the `Tmpl` field to customize the look & feel of a log. ac.SetFormatter(&accesslog.Template{ Tmpl: tmpl, }) */ ================================================ FILE: _examples/logging/rollbar/go.mod ================================================ module github.com/kataras/iris/examples/logging/rollbar go 1.25 require ( github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd github.com/rollbar/rollbar-go v1.4.8 ) require ( github.com/BurntSushi/toml v1.6.0 // indirect github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect github.com/CloudyKit/jet/v6 v6.3.1 // indirect github.com/Joker/jade v1.1.3 // indirect github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 // indirect github.com/andybalholm/brotli v1.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/fatih/structs v1.1.0 // indirect github.com/flosch/pongo2/v4 v4.0.2 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/iris-contrib/schema v0.0.6 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/kataras/blocks v0.0.8 // indirect github.com/kataras/golog v0.1.12 // indirect github.com/kataras/pio v0.0.13 // indirect github.com/kataras/sitemap v0.0.6 // indirect github.com/kataras/tunnel v0.0.4 // indirect github.com/klauspost/compress v1.18.2 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mailgun/raymond/v2 v2.0.48 // indirect github.com/mailru/easyjson v0.9.1 // indirect github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/schollz/closestmatch v2.1.0+incompatible // indirect github.com/sirupsen/logrus v1.8.1 // indirect github.com/stretchr/testify v1.11.1 // indirect github.com/tdewolff/minify/v2 v2.24.8 // indirect github.com/tdewolff/parse/v2 v2.8.5 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/yosssi/ace v0.0.5 // indirect golang.org/x/crypto v0.47.0 // indirect golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect golang.org/x/net v0.49.0 // indirect golang.org/x/sys v0.40.0 // indirect golang.org/x/text v0.33.0 // indirect golang.org/x/time v0.14.0 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: _examples/logging/rollbar/go.sum ================================================ github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= github.com/CloudyKit/jet/v6 v6.3.1 h1:6IAo5Cx21xrHVaR8zzXN5gJatKV/wO7Nf6bfCnCSbUw= github.com/CloudyKit/jet/v6 v6.3.1/go.mod h1:lf8ksdNsxZt7/yH/3n4vJQWA9RUq4wpaHtArHhGVMOw= github.com/Joker/hpp v1.0.0 h1:65+iuJYdRXv/XyN62C1uEmmOx3432rNG/rKlX6V7Kkc= github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= github.com/Joker/jade v1.1.3 h1:Qbeh12Vq6BxURXT1qZBRHsDxeURB8ztcL6f3EXSGeHk= github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKduM= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 h1:dG7/gLroJGht/jSQtHiLvT48Hxn+crbmvyItZC8cWOs= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05/go.mod h1:NYezi6wtnJtBm5btoprXc5SvAdqH0XTXWnUup0MptAI= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/flosch/pongo2/v4 v4.0.2 h1:gv+5Pe3vaSVmiJvh/BZa82b7/00YUGm0PIyVVLop0Hw= github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+BWPhwZD8= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a h1:l7A0loSszR5zHd/qK53ZIHMO8b3bBSmENnQ6eKnUT0A= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/iris-contrib/httpexpect/v2 v2.15.2 h1:T9THsdP1woyAqKHwjkEsbCnMefsAFvk8iJJKokcJ3Go= github.com/iris-contrib/httpexpect/v2 v2.15.2/go.mod h1:JLDgIqnFy5loDSUv1OA2j0mb6p/rDhiCqigP22Uq9xE= github.com/iris-contrib/schema v0.0.6 h1:CPSBLyx2e91H2yJzPuhGuifVRnZBBJ3pCOMbOvPZaTw= github.com/iris-contrib/schema v0.0.6/go.mod h1:iYszG0IOsuIsfzjymw1kMzTL8YQcCWlm65f3wX8J5iA= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kataras/blocks v0.0.8 h1:MrpVhoFTCR2v1iOOfGng5VJSILKeZZI+7NGfxEh3SUM= github.com/kataras/blocks v0.0.8/go.mod h1:9Jm5zx6BB+06NwA+OhTbHW1xkMOYxahnqTN5DveZ2Yg= github.com/kataras/golog v0.1.12 h1:Bu7I/G4ilJlbfzjmU39O9N+2uO1pBcMK045fzZ4ytNg= github.com/kataras/golog v0.1.12/go.mod h1:wrGSbOiBqbQSQznleVNX4epWM8rl9SJ/rmEacl0yqy4= github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd h1:oMsQgqKACbUdlaIWnN+EZzzAnHkw90hE/y/R0BSij2U= github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd/go.mod h1:yucjtDl9Vn3OctWM1fi0MuM9OckemC7kEreFa9QtPF8= github.com/kataras/pio v0.0.13 h1:x0rXVX0fviDTXOOLOmr4MUxOabu1InVSTu5itF8CXCM= github.com/kataras/pio v0.0.13/go.mod h1:k3HNuSw+eJ8Pm2lA4lRhg3DiCjVgHlP8hmXApSej3oM= github.com/kataras/sitemap v0.0.6 h1:w71CRMMKYMJh6LR2wTgnk5hSgjVNB9KL60n5e2KHvLY= github.com/kataras/sitemap v0.0.6/go.mod h1:dW4dOCNs896OR1HmG+dMLdT7JjDk7mYBzoIRwuj5jA4= github.com/kataras/tunnel v0.0.4 h1:sCAqWuJV7nPzGrlb0os3j49lk2JhILT0rID38NHNLpA= github.com/kataras/tunnel v0.0.4/go.mod h1:9FkU4LaeifdMWqZu7o20ojmW4B7hdhv2CMLwfnHGpYw= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw= github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18= github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rollbar/rollbar-go v1.4.8 h1:SAKy97CHXSFZjxQUxmuBnQmfzCjX54kvQGEQZHEqwuQ= github.com/rollbar/rollbar-go v1.4.8/go.mod h1:I/jSI5yHNj7Uy8oxntmCeBSZ1ILvypqRKlFQvZTINgA= github.com/rollbar/rollbar-go/errors v1.0.0/go.mod h1:Ie0xEc1Cyj+T4XMO8s0Vf7pMfvSAAy1sb4AYc8aJsao= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo= github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk= github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tdewolff/minify/v2 v2.24.8 h1:58/VjsbevI4d5FGV0ZSuBrHMSSkH4MCH0sIz/eKIauE= github.com/tdewolff/minify/v2 v2.24.8/go.mod h1:0Ukj0CRpo/sW/nd8uZ4ccXaV1rEVIWA3dj8U7+Shhfw= github.com/tdewolff/parse/v2 v2.8.5 h1:ZmBiA/8Do5Rpk7bDye0jbbDUpXXbCdc3iah4VeUvwYU= github.com/tdewolff/parse/v2 v2.8.5/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo= github.com/tdewolff/test v1.0.11 h1:FdLbwQVHxqG16SlkGveC0JVyrJN62COWTRyUFzfbtBE= github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA= github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0= github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs= moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHcE= ================================================ FILE: _examples/logging/rollbar/main.go ================================================ package main import ( "errors" "fmt" "os" "runtime/debug" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/middleware/requestid" "github.com/rollbar/rollbar-go" ) // * https://rollbar.com/signup // * https://docs.rollbar.com/docs/go func init() { token := os.Getenv("ROLLBAR_TOKEN") // replace that with your token. if token == "" { panic("ROLLBAR_TOKEN is missing") } // rb := rollbar.NewAsync(token, "production", "", hostname, "github.com/kataras/iris") // Or use the package-level instance: rollbar.SetToken(token) // defaults to "development" rollbar.SetEnvironment("production") // optional Git hash/branch/tag (required for GitHub integration) // rollbar.SetCodeVersion("v2") // optional override; defaults to hostname // rollbar.SetServerHost("web.1") // path of project (required for GitHub integration and non-project stacktrace collapsing) rollbar.SetServerRoot("github.com/kataras/iris") } func main() { app := iris.New() // A middleware which sets the ctx.GetID (or requestid.Get(ctx)). app.Use(requestid.New()) // A recover middleware which sends the error trace to the rollbar. app.Use(func(ctx iris.Context) { defer func() { if r := recover(); r != nil { debug.PrintStack() file, line := ctx.HandlerFileLine() // the failed handler's source code position. // cause other info rollbar.Critical(errors.New(fmt.Sprint(r)), iris.Map{ "request_id": ctx.GetID(), "request_ip": ctx.RemoteAddr(), "request_uri": ctx.FullRequestURI(), "handler": iris.Map{ "name": ctx.HandlerName(), // the handler which failed. "file": fmt.Sprintf("%s:%d", file, line), }, }) ctx.StopWithStatus(iris.StatusInternalServerError) } }() ctx.Next() }) app.Get("/", index) app.Get("/panic", panicMe) // http://localhost:8080 should add an info message to the rollbar's "Items" dashboard. // http://localhost:8080/panic should add a critical message to the rollbar's "Items" dashboard, // with the corresponding information appending on its "Occurrences" tab item, e.g: // Timestamp (PDT) // * 2020-06-08 04:47 pm // // server.host // * DESKTOP-HOSTNAME // // trace_chain.0.exception.message // * a critical error message here // // custom.handler.file // * C:/mygopath/src/github.com/kataras/iris/_examples/logging/rollbar/main.go:76 // // custom.handler.name // * main.panicMe // // custom.request_id // * cce61665-0c1b-4fb5-8547-06a3537e477c // // custom.request_ip // * ::1 // // custom.request_uri // * http://localhost:8080/panic app.Listen(":8080") } func index(ctx iris.Context) { rollbar.Info(fmt.Sprintf("Index page requested by %s", ctx.RemoteAddr())) ctx.HTML("

Index Page

") } func panicMe(ctx iris.Context) { panic("a critical error message here") } ================================================ FILE: _examples/monitor/monitor-middleware/main.go ================================================ package main import ( "time" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/middleware/monitor" ) func main() { app := iris.New() // Initialize and start the monitor middleware. m := monitor.New(monitor.Options{ RefreshInterval: 2 * time.Second, ViewRefreshInterval: 2 * time.Second, ViewTitle: "MyServer Monitor", }) // Manually stop monitoring on CMD/CTRL+C. iris.RegisterOnInterrupt(m.Stop) // Serve the actual server's process and operating system statistics as JSON. app.Post("/monitor", m.Stats) // Render with the default page. app.Get("/monitor", m.View) /* You can protect the /monitor under an /admin group of routes with basic authentication or any type authorization and authentication system. Example Code: app.Post("/monitor", myProtectMiddleware, m.Stats) app.Get("/monitor", myProtectMiddleware, m.View) */ /* You can also get the OS statistics using the Holder.GetStats method. Example Code: for { stats := m.Holder.GetStats() fmt.Printf("%#+v\n", stats) time.Sleep(time.Second) } Note that the same stats are also stored in the expvar metrics: - pid_cpu - pid_ram - pid_conns - os_cpu - os_ram - os_total_ram - os_load_avg - os_conns Check https://github.com/iris-contrib/middleware/tree/master/expmetric which can be integrated with datadog or other platforms. */ app.Get("/", handler) app.Listen(":8080") } func handler(ctx iris.Context) { ctx.WriteString("Test Index Handler") } ================================================ FILE: _examples/monitor/statsviz/go.mod ================================================ module github.com/kataras/iris/_examples/statsviz go 1.25 require ( github.com/arl/statsviz v0.8.0 github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd ) require ( github.com/BurntSushi/toml v1.6.0 // indirect github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect github.com/CloudyKit/jet/v6 v6.3.1 // indirect github.com/Joker/jade v1.1.3 // indirect github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 // indirect github.com/andybalholm/brotli v1.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/fatih/structs v1.1.0 // indirect github.com/flosch/pongo2/v4 v4.0.2 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/iris-contrib/schema v0.0.6 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/kataras/blocks v0.0.8 // indirect github.com/kataras/golog v0.1.12 // indirect github.com/kataras/pio v0.0.13 // indirect github.com/kataras/sitemap v0.0.6 // indirect github.com/kataras/tunnel v0.0.4 // indirect github.com/klauspost/compress v1.18.2 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mailgun/raymond/v2 v2.0.48 // indirect github.com/mailru/easyjson v0.9.1 // indirect github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/schollz/closestmatch v2.1.0+incompatible // indirect github.com/sirupsen/logrus v1.8.1 // indirect github.com/stretchr/testify v1.11.1 // indirect github.com/tdewolff/minify/v2 v2.24.8 // indirect github.com/tdewolff/parse/v2 v2.8.5 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/yosssi/ace v0.0.5 // indirect golang.org/x/crypto v0.47.0 // indirect golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect golang.org/x/net v0.49.0 // indirect golang.org/x/sys v0.40.0 // indirect golang.org/x/text v0.33.0 // indirect golang.org/x/time v0.14.0 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: _examples/monitor/statsviz/go.sum ================================================ github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= github.com/CloudyKit/jet/v6 v6.3.1 h1:6IAo5Cx21xrHVaR8zzXN5gJatKV/wO7Nf6bfCnCSbUw= github.com/CloudyKit/jet/v6 v6.3.1/go.mod h1:lf8ksdNsxZt7/yH/3n4vJQWA9RUq4wpaHtArHhGVMOw= github.com/Joker/hpp v1.0.0 h1:65+iuJYdRXv/XyN62C1uEmmOx3432rNG/rKlX6V7Kkc= github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= github.com/Joker/jade v1.1.3 h1:Qbeh12Vq6BxURXT1qZBRHsDxeURB8ztcL6f3EXSGeHk= github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKduM= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 h1:dG7/gLroJGht/jSQtHiLvT48Hxn+crbmvyItZC8cWOs= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05/go.mod h1:NYezi6wtnJtBm5btoprXc5SvAdqH0XTXWnUup0MptAI= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/arl/statsviz v0.8.0 h1:O6GjjVxEDxcByAucOSl29HaGYLXsuwA3ujJw8H9E7/U= github.com/arl/statsviz v0.8.0/go.mod h1:XlrbiT7xYT03xaW9JMMfD8KFUhBOESJwfyNJu83PbB0= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/flosch/pongo2/v4 v4.0.2 h1:gv+5Pe3vaSVmiJvh/BZa82b7/00YUGm0PIyVVLop0Hw= github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+BWPhwZD8= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a h1:l7A0loSszR5zHd/qK53ZIHMO8b3bBSmENnQ6eKnUT0A= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/iris-contrib/httpexpect/v2 v2.15.2 h1:T9THsdP1woyAqKHwjkEsbCnMefsAFvk8iJJKokcJ3Go= github.com/iris-contrib/httpexpect/v2 v2.15.2/go.mod h1:JLDgIqnFy5loDSUv1OA2j0mb6p/rDhiCqigP22Uq9xE= github.com/iris-contrib/schema v0.0.6 h1:CPSBLyx2e91H2yJzPuhGuifVRnZBBJ3pCOMbOvPZaTw= github.com/iris-contrib/schema v0.0.6/go.mod h1:iYszG0IOsuIsfzjymw1kMzTL8YQcCWlm65f3wX8J5iA= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kataras/blocks v0.0.8 h1:MrpVhoFTCR2v1iOOfGng5VJSILKeZZI+7NGfxEh3SUM= github.com/kataras/blocks v0.0.8/go.mod h1:9Jm5zx6BB+06NwA+OhTbHW1xkMOYxahnqTN5DveZ2Yg= github.com/kataras/golog v0.1.12 h1:Bu7I/G4ilJlbfzjmU39O9N+2uO1pBcMK045fzZ4ytNg= github.com/kataras/golog v0.1.12/go.mod h1:wrGSbOiBqbQSQznleVNX4epWM8rl9SJ/rmEacl0yqy4= github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd h1:oMsQgqKACbUdlaIWnN+EZzzAnHkw90hE/y/R0BSij2U= github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd/go.mod h1:yucjtDl9Vn3OctWM1fi0MuM9OckemC7kEreFa9QtPF8= github.com/kataras/pio v0.0.13 h1:x0rXVX0fviDTXOOLOmr4MUxOabu1InVSTu5itF8CXCM= github.com/kataras/pio v0.0.13/go.mod h1:k3HNuSw+eJ8Pm2lA4lRhg3DiCjVgHlP8hmXApSej3oM= github.com/kataras/sitemap v0.0.6 h1:w71CRMMKYMJh6LR2wTgnk5hSgjVNB9KL60n5e2KHvLY= github.com/kataras/sitemap v0.0.6/go.mod h1:dW4dOCNs896OR1HmG+dMLdT7JjDk7mYBzoIRwuj5jA4= github.com/kataras/tunnel v0.0.4 h1:sCAqWuJV7nPzGrlb0os3j49lk2JhILT0rID38NHNLpA= github.com/kataras/tunnel v0.0.4/go.mod h1:9FkU4LaeifdMWqZu7o20ojmW4B7hdhv2CMLwfnHGpYw= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw= github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18= github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo= github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk= github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tdewolff/minify/v2 v2.24.8 h1:58/VjsbevI4d5FGV0ZSuBrHMSSkH4MCH0sIz/eKIauE= github.com/tdewolff/minify/v2 v2.24.8/go.mod h1:0Ukj0CRpo/sW/nd8uZ4ccXaV1rEVIWA3dj8U7+Shhfw= github.com/tdewolff/parse/v2 v2.8.5 h1:ZmBiA/8Do5Rpk7bDye0jbbDUpXXbCdc3iah4VeUvwYU= github.com/tdewolff/parse/v2 v2.8.5/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo= github.com/tdewolff/test v1.0.11 h1:FdLbwQVHxqG16SlkGveC0JVyrJN62COWTRyUFzfbtBE= github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA= github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0= github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs= moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHcE= ================================================ FILE: _examples/monitor/statsviz/main.go ================================================ package main import ( "strings" "time" "github.com/kataras/iris/v12" "github.com/arl/statsviz" ) // $ go get github.com/arl/statsviz func main() { app := iris.New() // Register a router wrapper for this one. statsvizPath := "/debug/statsviz" serveRoot := statsviz.IndexAtRoot(statsvizPath) serveWS := statsviz.NewWsHandler(time.Second) app.UseRouter(func(ctx iris.Context) { // You can optimize this if branch, I leave it to you as an exercise. if strings.HasPrefix(ctx.Path(), statsvizPath+"/ws") { serveWS(ctx.ResponseWriter(), ctx.Request()) } else if strings.HasPrefix(ctx.Path(), statsvizPath) { serveRoot(ctx.ResponseWriter(), ctx.Request()) } else { ctx.Next() } }) // // Register other routes. app.Get("/", index) // Navigate to: http://localhost:8080/debug/statsviz/ app.Listen(":8080") } func index(ctx iris.Context) { ctx.WriteString("Hello, World!") } ================================================ FILE: _examples/mvc/authenticated-controller/main.go ================================================ // Package main shows how to use a dependency to check if a user is logged in // using a special custom Go type `Authenticated`, which when, // present on a controller's method or a field then // it limits the visibility to "authenticated" users only. // // The same result could be done through a middleware as well, however using a static type // any person reads your code can see that an "X" controller or method // should only be used by "authenticated" users. package main import ( "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/mvc" "github.com/kataras/iris/v12/sessions" ) func main() { app := newApp() // app.UseRouter(iris.Compression) app.Logger().SetLevel("debug") // Open a client, e.g. Postman and visit the below endpoints. // GET: http://localhost:8080/user (UnauthenticatedUserController.Get) // POST: http://localhost:8080/user/login (UnauthenticatedUserController.PostLogin) // GET: http://localhost:8080/user (UserController.Get) // POST: http://localhost:8080/user/logout (UserController.PostLogout) app.Listen(":8080") } func newApp() *iris.Application { app := iris.New() sess := sessions.New(sessions.Config{ Cookie: "myapp_session_id", AllowReclaim: true, }) app.Use(sess.Handler()) userRouter := app.Party("/user") { // Use that in order to be able to register a route twice, // last one will be executed if the previous route's handler(s) stopped and the response can be reset-ed. // See core/router/route_register_rule_test.go#TestRegisterRuleOverlap. userRouter.SetRegisterRule(iris.RouteOverlap) // Initialize a new MVC application on top of the "userRouter". userApp := mvc.New(userRouter) // Register Dependencies. userApp.Register(authDependency) // Register Controllers. userApp.Handle(new(UserController)) userApp.Handle(new(UnauthenticatedUserController)) } return app } // Authenticated is a custom type used as "annotation" for resources that requires authentication, // its value should be the logged user ID. type Authenticated uint64 func authDependency(ctx iris.Context, session *sessions.Session) Authenticated { userID := session.GetUint64Default("user_id", 0) if userID == 0 { // If execution was stopped // any controller's method will not be executed at all. // // Note that, the below will not fire the error to the user: // ctx.StopWithStatus(iris.StatusUnauthorized) // because of the imaginary: // UnauthenticatedUserController.Get() (string, int) { // return "...", iris.StatusOK // } // // OR // If you don't want to set a status code at all: ctx.StopExecution() return 0 } return Authenticated(userID) } // UnauthenticatedUserController serves the "public" Unauthorized User API. type UnauthenticatedUserController struct{} // Get registers a route that will be executed when authentication is not passed // (see UserController.Get) too. func (c *UnauthenticatedUserController) Get() string { return "custom action to redirect on authentication page" } // PostLogin serves // POST: /user/login func (c *UnauthenticatedUserController) PostLogin(session *sessions.Session) mvc.Response { session.Set("user_id", 1) // Redirect (you can still use the Context.Redirect if you want so). return mvc.Response{ Path: "/user", Code: iris.StatusFound, } } // UserController serves the "public" User API. type UserController struct { CurrentUserID Authenticated } // Get returns a message for the sake of the example. // GET: /user func (c *UserController) Get() string { return `UserController.Get: The Authenticated type can be used to secure a controller's method too.` } // PostLogout serves // POST: /user/logout func (c *UserController) PostLogout(ctx iris.Context) { sessions.Get(ctx).Man.Destroy(ctx) } ================================================ FILE: _examples/mvc/authenticated-controller/main_test.go ================================================ package main import ( "testing" "github.com/kataras/iris/v12/httptest" ) func TestMVCOverlapping(t *testing.T) { app := newApp() e := httptest.New(t, app, httptest.URL("http://example.com")) // unauthenticated. e.GET("/user").Expect().Status(httptest.StatusOK).Body().IsEqual("custom action to redirect on authentication page") // login. e.POST("/user/login").Expect().Status(httptest.StatusOK) // authenticated. e.GET("/user").Expect().Status(httptest.StatusOK).Body().IsEqual(`UserController.Get: The Authenticated type can be used to secure a controller's method too.`) // logout. e.POST("/user/logout").Expect().Status(httptest.StatusOK) // unauthenticated. e.GET("/user").Expect().Status(httptest.StatusOK).Body().IsEqual("custom action to redirect on authentication page") } ================================================ FILE: _examples/mvc/basic/main.go ================================================ package main import ( "fmt" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/middleware/accesslog" "github.com/kataras/iris/v12/middleware/recover" "github.com/kataras/iris/v12/sessions" "github.com/kataras/iris/v12/mvc" ) func main() { app := iris.New() app.Logger().SetLevel("debug") basic := app.Party("/basic") { // Register middlewares to run under the /basic path prefix. ac := accesslog.File("./basic_access.log") defer ac.Close() basic.UseRouter(ac.Handler) basic.UseRouter(recover.New()) mvc.Configure(basic, basicMVC) } app.Listen(":8080") } func basicMVC(app *mvc.Application) { // Disable verbose logging of controllers for this and its children mvc apps // when the log level is "debug": app.SetControllersNoLog(true) // You can still register middlewares at MVC apps of course. // The app.Router returns the Party that this MVC // was registered on. // app.Router.UseRouter/Use/.... // Register dependencies which will be binding to the controller(s), // can be either a function which accepts an iris.Context and returns a single value (dynamic binding) // or a static struct value (service). app.Register( sessions.New(sessions.Config{}).Start, &prefixedLogger{prefix: "DEV"}, accesslog.GetFields, // Set custom fields through a controller or controller's methods. ) // GET: http://localhost:8080/basic // GET: http://localhost:8080/basic/custom // GET: http://localhost:8080/basic/custom2 app.Handle(new(basicController)) // All dependencies of the parent *mvc.Application // are cloned to this new child, // thefore it has access to the same session as well. // GET: http://localhost:8080/basic/sub app.Party("/sub"). Handle(new(basicSubController)) } // If controller's fields (or even its functions) expecting an interface // but a struct value is binded then it will check // if that struct value implements // the interface and if true then it will add this to the // available bindings, as expected, before the server ran of course, // remember? Iris always uses the best possible way to reduce load // on serving web resources. type LoggerService interface { Log(string) } type prefixedLogger struct { prefix string } func (s *prefixedLogger) Log(msg string) { fmt.Printf("%s: %s\n", s.prefix, msg) } type basicController struct { Logger LoggerService // the static logger service attached to this app. Session *sessions.Session // current HTTP session. LogFields *accesslog.Fields // accesslog middleware custom fields. } func (c *basicController) BeforeActivation(b mvc.BeforeActivation) { b.HandleMany("GET", "/custom /custom2", "Custom") } func (c *basicController) AfterActivation(a mvc.AfterActivation) { if a.Singleton() { panic("basicController should be stateless, a request-scoped, we have a 'Session' which depends on the context.") } } func (c *basicController) Get() string { count := c.Session.Increment("count", 1) c.LogFields.Set("count", count) body := fmt.Sprintf("Hello from basicController\nTotal visits from you: %d", count) c.Logger.Log(body) return body } func (c *basicController) Custom() string { return "custom" } type basicSubController struct { Session *sessions.Session } func (c *basicSubController) Get() string { count := c.Session.GetIntDefault("count", 1) return fmt.Sprintf("Hello from basicSubController.\nRead-only visits count: %d", count) } ================================================ FILE: _examples/mvc/basic/wildcard/main.go ================================================ package main import ( "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/mvc" ) func main() { app := iris.New() usersRouter := app.Party("/users") mvc.New(usersRouter).Handle(new(myController)) // Same as: // usersRouter.Get("/{p:path}", func(ctx iris.Context) { // wildcardPathParameter := ctx.Params().Get("p") // ctx.JSON(response{ // Message: "The path parameter is: " + wildcardPathParameter, // }) // }) /* curl --location --request GET 'http://localhost:8080/users/path_segment_1/path_segment_2' Expected Output: { "message": "The wildcard is: path_segment_1/path_segment_2" } */ app.Listen(":8080") } type myController struct{} type response struct { Message string `json:"message"` } func (c *myController) GetByWildcard(wildcardPathParameter string) response { return response{ Message: "The path parameter is: " + wildcardPathParameter, } } ================================================ FILE: _examples/mvc/error-handler/main.go ================================================ package main import ( "fmt" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/mvc" ) func main() { app := iris.New() app.Logger().SetLevel("debug") mvcApp := mvc.New(app) // To all controllers, it can optionally be overridden per-controller // if the controller contains the `HandleError(ctx iris.Context, err error)` function. // mvcApp.HandleError(func(ctx iris.Context, err error) { ctx.HTML(fmt.Sprintf("%s", err.Error())) }) // mvcApp.Handle(new(myController)) // http://localhost:8080 app.Listen(":8080") } type myController struct { } // overriddes the mvcApp.HandleError function. func (c *myController) HandleError(ctx iris.Context, err error) { ctx.HTML(fmt.Sprintf("%s", err.Error())) } func (c *myController) Get() error { return fmt.Errorf("error here") } ================================================ FILE: _examples/mvc/error-handler-custom-result/main.go ================================================ package main import ( "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/mvc" ) func main() { app := iris.New() app.RegisterView(iris.HTML("./views", ".html")) m := mvc.New(app) m.Handle(new(controller)) app.Listen(":8080") } type errorResponse struct { Code int Message string } /* // Note: if a struct implements the standard go error, so it's an error // and its Error() is not empty, then its text will be rendered instead, // override any Dispatch method. func (e errorResponse) Error() string { return e.Message } */ // implements mvc.Result. func (e errorResponse) Dispatch(ctx iris.Context) { // If u want to use mvc.Result on any method without an output return value // go for it: // view := mvc.View{Code: e.Code, Data: e} // use Code and Message as the template data. switch e.Code { case iris.StatusNotFound: view.Name = "404" default: view.Name = "500" } view.Dispatch(ctx) // Otherwise use ctx methods: // // ctx.StatusCode(e.Code) // switch e.Code { // case iris.StatusNotFound: // // use Code and Message as the template data. // if err := ctx.View("404.html", e) // default: // if err := ctx.View("500.html", e) // } } type controller struct{} type user struct { ID uint64 `json:"id"` } func (c *controller) GetBy(userid uint64) mvc.Result { if userid != 1 { return errorResponse{ Code: iris.StatusNotFound, Message: "User Not Found", } } return mvc.Response{ Object: user{ID: userid}, } } ================================================ FILE: _examples/mvc/error-handler-custom-result/views/404.html ================================================ Client Error Page

{{.Code}}

{{.Message}}

================================================ FILE: _examples/mvc/error-handler-custom-result/views/500.html ================================================ Server Error Page

{{.Code}}

{{.Message}}

================================================ FILE: _examples/mvc/error-handler-hijack/main.go ================================================ package main import ( "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/mvc" ) func main() { app := iris.New() app.RegisterView(iris.HTML("./views", ".html")) // Hijack each output value of a method (can be used per-party too). app.ConfigureContainer(). UseResultHandler(func(next iris.ResultHandler) iris.ResultHandler { return func(ctx iris.Context, v any) error { switch val := v.(type) { case errorResponse: return next(ctx, errorView(val)) default: return next(ctx, v) } } }) m := mvc.New(app) m.Handle(new(controller)) app.Listen(":8080") } func errorView(e errorResponse) mvc.Result { switch e.Code { case iris.StatusNotFound: return mvc.View{Code: e.Code, Name: "404.html", Data: e} default: return mvc.View{Code: e.Code, Name: "500.html", Data: e} } } type errorResponse struct { Code int Message string } type controller struct{} type user struct { ID uint64 `json:"id"` } func (c *controller) GetBy(userid uint64) any { if userid != 1 { return errorResponse{ Code: iris.StatusNotFound, Message: "User Not Found", } } return user{ID: userid} } ================================================ FILE: _examples/mvc/error-handler-hijack/views/404.html ================================================ Client Error Page

{{.Code}}

{{.Message}}

================================================ FILE: _examples/mvc/error-handler-hijack/views/500.html ================================================ Server Error Page

{{.Code}}

{{.Message}}

================================================ FILE: _examples/mvc/error-handler-http/main.go ================================================ package main import ( "fmt" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/mvc" ) func main() { app := newApp() app.Logger().SetLevel("debug") app.Listen(":8080") } func newApp() *iris.Application { app := iris.New() app.RegisterView(iris.HTML("./views", ".html")) m := mvc.New(app) m.Handle(new(controller)) return app } type controller struct{} func (c *controller) Get() string { return "Hello!" } func (c *controller) GetError() mvc.Result { return mvc.View{ // Map to mvc.Code and mvc.Err respectfully on HandleHTTPError method. Code: iris.StatusBadRequest, Err: fmt.Errorf("custom error"), } } // The input parameter of mvc.Code is optional but a good practise to follow. // You could register a Context and get its error code through ctx.GetStatusCode(). // // This can accept dependencies and output values like any other Controller Method, // however be careful if your registered dependencies depend only on successful(200...) requests. // // Also note that, if you register more than one controller.HandleHTTPError // in the same Party, you need to use the RouteOverlap feature as shown // in the "authenticated-controller" example, and a dependency on // a controller's field (or method's input argument) is required // to select which, between those two controllers, is responsible // to handle http errors. func (c *controller) HandleHTTPError(statusCode mvc.Code, err mvc.Err) mvc.View { if err != nil { // Do something with that error, // e.g. view.Data = MyPageData{Message: err.Error()} } code := int(statusCode) // cast it to int. view := mvc.View{ Code: code, Name: "unexpected-error.html", } switch code { case 404: view.Name = "404.html" // [...] case 500: view.Name = "500.html" } return view } ================================================ FILE: _examples/mvc/error-handler-http/main_test.go ================================================ package main import ( "testing" "github.com/kataras/iris/v12/httptest" ) func TestControllerHandleHTTPError(t *testing.T) { const ( expectedIndex = "Hello!" expectedNotFound = "

Not Found Custom Page Rendered through Controller's HandleHTTPError

" ) app := newApp() e := httptest.New(t, app) e.GET("/").Expect().Status(httptest.StatusOK).Body().IsEqual(expectedIndex) e.GET("/a_notefound").Expect().Status(httptest.StatusNotFound).ContentType("text/html").Body().IsEqual(expectedNotFound) } ================================================ FILE: _examples/mvc/error-handler-http/views/404.html ================================================

Not Found Custom Page Rendered through Controller's HandleHTTPError

================================================ FILE: _examples/mvc/error-handler-http/views/500.html ================================================

Internal Server Err

================================================ FILE: _examples/mvc/error-handler-http/views/unexpected-error.html ================================================

Unexpected Error

================================================ FILE: _examples/mvc/error-handler-preflight/main.go ================================================ package main import ( "fmt" "time" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/mvc" ) func main() { app := iris.New() app.RegisterView(iris.HTML("./views", ".html")) m := mvc.New(app) m.Handle(new(controller)) app.Listen(":8080") } type controller struct{} // Generic response type for JSON results. type response struct { ID uint64 `json:"id,omitempty"` Data any `json:"data,omitempty"` // {data: result } on fetch actions. Code int `json:"code,omitempty"` Message string `json:"message,omitempty"` Timestamp int64 `json:"timestamp,omitempty"` } func (r *response) Preflight(ctx iris.Context) error { if r.ID > 0 { r.Timestamp = time.Now().Unix() } if code := r.Code; code > 0 { // You can call ctx.View or mvc.View{...}.Dispatch // to render HTML on Code != 200 // but in order to not proceed with the response resulting // as JSON you MUST return the iris.ErrStopExecution error. // Example: if code != 200 { mvc.View{ /* calls the ctx.StatusCode */ Code: code, /* use any r.Data as the template data OR the whole "response" as its data. */ Data: r, /* automatically pick the template per error (just for the sake of the example) */ Name: fmt.Sprintf("%d", code), }.Dispatch(ctx) return iris.ErrStopExecution } ctx.StatusCode(r.Code) } return nil } type user struct { ID uint64 `json:"id"` } func (c *controller) GetBy(userid uint64) *response { if userid != 1 { return &response{ Code: iris.StatusNotFound, Message: "User Not Found", } } return &response{ ID: userid, Data: user{ID: userid}, } } /* You can use that `response` structure on non-mvc applications too, using handlers: c := app.ConfigureContainer() c.Get("/{id:uint64}", getUserByID) func getUserByID(id uint64) response { if userid != 1 { return response{ Code: iris.StatusNotFound, Message: "User Not Found", } } return response{ ID: userid, Data: user{ID: userid}, } } */ ================================================ FILE: _examples/mvc/error-handler-preflight/views/404.html ================================================ Client Error Page

{{.Code}}

{{.Message}}

================================================ FILE: _examples/mvc/error-handler-preflight/views/500.html ================================================ Server Error Page

{{.Code}}

{{.Message}}

================================================ FILE: _examples/mvc/grpc-compatible/README.md ================================================ # gRPC Iris Example ## Generate TLS Keys ```sh $ openssl genrsa -out server.key 2048 $ openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650 ``` ## Install the protoc Go plugin ```sh $ go get -u github.com/golang/protobuf/protoc-gen-go ``` ## Generate proto ```sh $ protoc -I helloworld/ helloworld/helloworld.proto --go_out=plugins=grpc:helloworld ``` ================================================ FILE: _examples/mvc/grpc-compatible/grpc-client/main.go ================================================ // Package main implements a client for Greeter service. package main import ( "context" "crypto/tls" "log" "os" "time" pb "github.com/kataras/iris/v12/_examples/mvc/grpc-compatible/helloworld" "google.golang.org/grpc" "google.golang.org/grpc/credentials" ) const ( address = "localhost:443" defaultName = "world" ) func main() { // Set up a connection to the server. // cred, err := credentials.NewClientTLSFromFile("../server.crt", "localhost") // if err != nil { // log.Fatal(err) // } cred := credentials.NewTLS(&tls.Config{ InsecureSkipVerify: true, Renegotiation: tls.RenegotiateNever, }) conn, err := grpc.Dial(address, grpc.WithTransportCredentials(cred), grpc.WithBlock()) if err != nil { log.Fatalf("did not connect: %v", err) } defer conn.Close() c := pb.NewGreeterClient(conn) // Contact the server and print out its response. name := defaultName if len(os.Args) > 1 { name = os.Args[1] } ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() r, err := c.SayHello(ctx, &pb.HelloRequest{Name: name}) if err != nil { log.Fatalf("could not greet: %v", err) } log.Printf("Greeting: %s", r.GetMessage()) } ================================================ FILE: _examples/mvc/grpc-compatible/helloworld/README.md ================================================ # Helloworld gRPC Example https://github.com/grpc/grpc-go/tree/master/examples/helloworld ================================================ FILE: _examples/mvc/grpc-compatible/helloworld/helloworld.pb.go ================================================ // Copyright 2015 gRPC authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.21.0 // protoc v3.11.1 // source: helloworld.proto package helloworld import ( context "context" grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" ) const ( // Verify that this generated code is sufficiently up-to-date. _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) // Verify that runtime/protoimpl is sufficiently up-to-date. _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) // The request message containing the user's name. type HelloRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` } func (x *HelloRequest) Reset() { *x = HelloRequest{} if protoimpl.UnsafeEnabled { mi := &file_helloworld_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *HelloRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*HelloRequest) ProtoMessage() {} func (x *HelloRequest) ProtoReflect() protoreflect.Message { mi := &file_helloworld_proto_msgTypes[0] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use HelloRequest.ProtoReflect.Descriptor instead. func (*HelloRequest) Descriptor() ([]byte, []int) { return file_helloworld_proto_rawDescGZIP(), []int{0} } func (x *HelloRequest) GetName() string { if x != nil { return x.Name } return "" } // The response message containing the greetings type HelloReply struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` } func (x *HelloReply) Reset() { *x = HelloReply{} if protoimpl.UnsafeEnabled { mi := &file_helloworld_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *HelloReply) String() string { return protoimpl.X.MessageStringOf(x) } func (*HelloReply) ProtoMessage() {} func (x *HelloReply) ProtoReflect() protoreflect.Message { mi := &file_helloworld_proto_msgTypes[1] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use HelloReply.ProtoReflect.Descriptor instead. func (*HelloReply) Descriptor() ([]byte, []int) { return file_helloworld_proto_rawDescGZIP(), []int{1} } func (x *HelloReply) GetMessage() string { if x != nil { return x.Message } return "" } var File_helloworld_proto protoreflect.FileDescriptor var file_helloworld_proto_rawDesc = []byte{ 0x0a, 0x10, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0a, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x22, 0x22, 0x0a, 0x0c, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x26, 0x0a, 0x0a, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x32, 0x49, 0x0a, 0x07, 0x47, 0x72, 0x65, 0x65, 0x74, 0x65, 0x72, 0x12, 0x3e, 0x0a, 0x08, 0x53, 0x61, 0x79, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x12, 0x18, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, 0x42, 0x30, 0x0a, 0x1b, 0x69, 0x6f, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x73, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x42, 0x0f, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x57, 0x6f, 0x72, 0x6c, 0x64, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( file_helloworld_proto_rawDescOnce sync.Once file_helloworld_proto_rawDescData = file_helloworld_proto_rawDesc ) func file_helloworld_proto_rawDescGZIP() []byte { file_helloworld_proto_rawDescOnce.Do(func() { file_helloworld_proto_rawDescData = protoimpl.X.CompressGZIP(file_helloworld_proto_rawDescData) }) return file_helloworld_proto_rawDescData } var file_helloworld_proto_msgTypes = make([]protoimpl.MessageInfo, 2) var file_helloworld_proto_goTypes = []any{ (*HelloRequest)(nil), // 0: helloworld.HelloRequest (*HelloReply)(nil), // 1: helloworld.HelloReply } var file_helloworld_proto_depIdxs = []int32{ 0, // 0: helloworld.Greeter.SayHello:input_type -> helloworld.HelloRequest 1, // 1: helloworld.Greeter.SayHello:output_type -> helloworld.HelloReply 1, // [1:2] is the sub-list for method output_type 0, // [0:1] is the sub-list for method input_type 0, // [0:0] is the sub-list for extension type_name 0, // [0:0] is the sub-list for extension extendee 0, // [0:0] is the sub-list for field type_name } func init() { file_helloworld_proto_init() } func file_helloworld_proto_init() { if File_helloworld_proto != nil { return } if !protoimpl.UnsafeEnabled { file_helloworld_proto_msgTypes[0].Exporter = func(v any, i int) any { switch v := v.(*HelloRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_helloworld_proto_msgTypes[1].Exporter = func(v any, i int) any { switch v := v.(*HelloReply); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_helloworld_proto_rawDesc, NumEnums: 0, NumMessages: 2, NumExtensions: 0, NumServices: 1, }, GoTypes: file_helloworld_proto_goTypes, DependencyIndexes: file_helloworld_proto_depIdxs, MessageInfos: file_helloworld_proto_msgTypes, }.Build() File_helloworld_proto = out.File file_helloworld_proto_rawDesc = nil file_helloworld_proto_goTypes = nil file_helloworld_proto_depIdxs = nil } // Reference imports to suppress errors if they are not otherwise used. var _ context.Context var _ grpc.ClientConnInterface // This is a compile-time assertion to ensure that this generated file // is compatible with the grpc package it is being compiled against. const _ = grpc.SupportPackageIsVersion6 // GreeterClient is the client API for Greeter service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. type GreeterClient interface { // Sends a greeting SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) } type greeterClient struct { cc grpc.ClientConnInterface } func NewGreeterClient(cc grpc.ClientConnInterface) GreeterClient { return &greeterClient{cc} } func (c *greeterClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) { out := new(HelloReply) err := c.cc.Invoke(ctx, "/helloworld.Greeter/SayHello", in, out, opts...) if err != nil { return nil, err } return out, nil } // GreeterServer is the server API for Greeter service. type GreeterServer interface { // Sends a greeting SayHello(context.Context, *HelloRequest) (*HelloReply, error) } // UnimplementedGreeterServer can be embedded to have forward compatible implementations. type UnimplementedGreeterServer struct { } func (*UnimplementedGreeterServer) SayHello(context.Context, *HelloRequest) (*HelloReply, error) { return nil, status.Errorf(codes.Unimplemented, "method SayHello not implemented") } func RegisterGreeterServer(s *grpc.Server, srv GreeterServer) { s.RegisterService(&_Greeter_serviceDesc, srv) } func _Greeter_SayHello_Handler(srv any, ctx context.Context, dec func(any) error, interceptor grpc.UnaryServerInterceptor) (any, error) { in := new(HelloRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(GreeterServer).SayHello(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/helloworld.Greeter/SayHello", } handler := func(ctx context.Context, req any) (any, error) { return srv.(GreeterServer).SayHello(ctx, req.(*HelloRequest)) } return interceptor(ctx, in, info, handler) } var _Greeter_serviceDesc = grpc.ServiceDesc{ ServiceName: "helloworld.Greeter", HandlerType: (*GreeterServer)(nil), Methods: []grpc.MethodDesc{ { MethodName: "SayHello", Handler: _Greeter_SayHello_Handler, }, }, Streams: []grpc.StreamDesc{}, Metadata: "helloworld.proto", } ================================================ FILE: _examples/mvc/grpc-compatible/helloworld/helloworld.proto ================================================ // Copyright 2015 gRPC authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. syntax = "proto3"; package helloworld; // The greeting service definition. service Greeter { // Sends a greeting rpc SayHello (HelloRequest) returns (HelloReply) {} } // The request message containing the user's name. message HelloRequest { string name = 1; } // The response message containing the greetings message HelloReply { string message = 1; } ================================================ FILE: _examples/mvc/grpc-compatible/http-client/main.go ================================================ package main import ( "bytes" "crypto/tls" "crypto/x509" "encoding/json" "log" "net/http" "os" pb "github.com/kataras/iris/v12/_examples/mvc/grpc-compatible/helloworld" ) func main() { b, err := os.ReadFile("../server.crt") if err != nil { log.Fatal(err) } cp := x509.NewCertPool() if !cp.AppendCertsFromPEM(b) { log.Fatal("credentials: failed to append certificates") } transport := &http.Transport{ TLSClientConfig: &tls.Config{ RootCAs: cp, }, } client := http.Client{Transport: transport} buf := new(bytes.Buffer) err = json.NewEncoder(buf).Encode(pb.HelloRequest{Name: "world"}) if err != nil { log.Fatal(err) } resp, err := client.Post("https://localhost/helloworld.Greeter/SayHello", "application/json", buf) if err != nil { log.Fatal(err) } defer resp.Body.Close() var reply pb.HelloReply err = json.NewDecoder(resp.Body).Decode(&reply) if err != nil { log.Fatal(err) } log.Printf("Greeting: %s", reply.GetMessage()) } ================================================ FILE: _examples/mvc/grpc-compatible/main.go ================================================ package main import ( "context" "log" pb "github.com/kataras/iris/v12/_examples/mvc/grpc-compatible/helloworld" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/mvc" "google.golang.org/grpc" ) // See https://github.com/kataras/iris/issues/1449 // Iris automatically binds the standard "context" context.Context to `iris.Context.Request().Context()` // and any other structure that is not mapping to a registered dependency // as a payload depends on the request, e.g XML, YAML, Query, Form, JSON. // // Useful to use gRPC services as Iris controllers fast and without wrappers. func main() { app := newApp() app.Logger().SetLevel("debug") // The Iris server should ran under TLS (it's a gRPC requirement). // POST: https://localhost:443/helloworld.Greeter/SayHello // with request data: {"name": "John"} // and expected output: {"message": "Hello John"} app.Run(iris.TLS(":443", "server.crt", "server.key")) } func newApp() *iris.Application { app := iris.New() // app.Configure(iris.WithLowercaseRouting) // OPTIONAL. app.Get("/", func(ctx iris.Context) { ctx.HTML("

Index Page

") }) ctrl := &myController{} // Register gRPC server. grpcServer := grpc.NewServer() pb.RegisterGreeterServer(grpcServer, ctrl) // serviceName := pb.File_helloworld_proto.Services().Get(0).FullName() // Register MVC application controller for gRPC services. // You can bind as many mvc gRpc services in the same Party or app, // as the ServiceName differs. mvc.New(app). Register(new(myService)). Handle(ctrl, mvc.GRPC{ Server: grpcServer, // Required. ServiceName: "helloworld.Greeter", // Required. Strict: false, }) return app } type service interface { DoSomething() error } type myService struct{} func (s *myService) DoSomething() error { log.Println("service: DoSomething") return nil } type myController struct { // Ctx iris.Context SingletonDependency service } // SayHello implements helloworld.GreeterServer. // See https://github.com/kataras/iris/issues/1449#issuecomment-625570442 // for the comments below (https://github.com/iris-contrib/swagger). // // @Description greet service // @Accept json // @Produce json // @Success 200 {string} string "Hello {name}" // @Router /helloworld.Greeter/SayHello [post] func (c *myController) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) { err := c.SingletonDependency.DoSomething() if err != nil { return nil, err } return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil } ================================================ FILE: _examples/mvc/grpc-compatible/main_test.go ================================================ package main import ( "testing" "github.com/kataras/iris/v12/httptest" ) func TestGRPCCompatible(t *testing.T) { app := newApp() e := httptest.New(t, app) e.POST("/helloworld.Greeter/SayHello").WithJSON(map[string]string{"name": "makis"}).Expect(). Status(httptest.StatusOK). JSON().IsEqual(map[string]string{"message": "Hello makis"}) } ================================================ FILE: _examples/mvc/grpc-compatible/server.crt ================================================ -----BEGIN CERTIFICATE----- MIIDKjCCAhICCQDpz77z0oyjCDANBgkqhkiG9w0BAQsFADBXMQswCQYDVQQGEwJH UjEPMA0GA1UECAwGQXRoZW5zMQ8wDQYDVQQHDAZBdGhlbnMxJjAkBgkqhkiG9w0B CQEWF2thdGFyYXMyMDA2QGhvdG1haWwuY29tMB4XDTIyMDMwMzExMjczM1oXDTMy MDIyOTExMjczM1owVzELMAkGA1UEBhMCR1IxDzANBgNVBAgMBkF0aGVuczEPMA0G A1UEBwwGQXRoZW5zMSYwJAYJKoZIhvcNAQkBFhdrYXRhcmFzMjAwNkBob3RtYWls LmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALu4Uqpcf0AnRDwW QGDh//v2wDNQ2EP/jn2Y4YxYJXtvWZ7dWcoX4fPA03mOAEmEPXWGxkhe8DYFh8PY 0zpZW5sFY7ae0AcpcjvlyxjNvHsqjhnh7M1gKZuhfyxvvLv7afC5Gs64tyg+f4C3 EqvwKHV+fjC8eKiASs4FsEEi89uv2AZdD9Vp0Zvbz6/1mRQQWBMpsPiIO7/WsaW4 DPq/zUZVY15WaX5B0Fg57O5Uv8aHyV4A+AQN7XzadCpYBp8XUb/cVcN9Azj2F6d3 WODyYT7y/RMuuih9HJQMxuAYPNB5UAkW6syTdfjbdyqbXe45z5iKPcotE4KaXYJf hTkj6osCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAsb3QGQUGNGs8zv2y4QmsDCB9 +mkzwH9BpqSo40Zs19kT35nH0u//UuXiNG3U8d2WH1J7kk64cdrETuVay5TwW9jJ EagdJl4zAZbfLQuDUCI+cVpjbywbO//GXzi8Q/aO7viboU0OR+MPbabubtKE7f+X THIjnMj3OhFM4zzrFyEL8gsRMLzRRCV18wTsiCl9bi4bZ1Ssvr6MwqyXHs3uK+fx JpxQVb3MckoIgUKwwU4MlXwufDkOmslGFsRg4e1iiwlkJ0HgRdf5lSuDmnBjH4T+ fWvRqCeHDTjOI09+7IAK1EF7rw4TZjHv+RbPl5+b1TJcanUC9EDg4z4/rR0XOg== -----END CERTIFICATE----- ================================================ FILE: _examples/mvc/grpc-compatible/server.key ================================================ -----BEGIN RSA PRIVATE KEY----- MIIEpQIBAAKCAQEAu7hSqlx/QCdEPBZAYOH/+/bAM1DYQ/+OfZjhjFgle29Znt1Z yhfh88DTeY4ASYQ9dYbGSF7wNgWHw9jTOllbmwVjtp7QBylyO+XLGM28eyqOGeHs zWApm6F/LG+8u/tp8Lkazri3KD5/gLcSq/AodX5+MLx4qIBKzgWwQSLz26/YBl0P 1WnRm9vPr/WZFBBYEymw+Ig7v9axpbgM+r/NRlVjXlZpfkHQWDns7lS/xofJXgD4 BA3tfNp0KlgGnxdRv9xVw30DOPYXp3dY4PJhPvL9Ey66KH0clAzG4Bg80HlQCRbq zJN1+Nt3Kptd7jnPmIo9yi0Tgppdgl+FOSPqiwIDAQABAoIBAQCg0XM4cc+uVTV2 yJVUqqjT4fucusjb0EbxQJUR174ctjMwD2/J25X+bhZ9z3JdiQXh9plODM97aFd8 J/glx8Hb180p+Xo8eHxd5iqNUEwFtFpSwCNPeu+KXduGZR9qaCPFT78wlDyNJKW0 zqIXXMI8jiZreDtiF65+O49Y7im97Lqwhw8fEABcr3rpOpjK79nPHkf0X3CKj1TC IhW5Av4j41uv8+1BgObjVkeYFHK4O+vRWiSId9OiXzZT9ZaG56ntGOap9KuugTym 2ngbIBm2DdUDmbY3vL9eUzhfY75VIjFrgnf/EYN+7kymbymo3vnv5OmB3OByX7iw bXTmxQgxAoGBAOLLx6EzcQJ83g85Gtd3WNiNW414ynDPyIB/XAEcyygZGYqrVfsH HfGhxI787WqzjIZxl987HTPOkrX5Xc/GhHNeTVa1+neyUUqx46DxessU3/s53Vg4 X1AmnmL6+Wl1qrlTJiUQINDyOZkHsbycR8okDlrzSS1rwO90wW6K5snDAoGBANPk a/NH2t+KrAboPtXZk2DCUMkCoyDhzXnPU4HARO0BRa3Qhul8keV8u54MtNsej1y+ TcatUQYpuT4Pgpl1kdtyHrLNiEhd0ThBgy5RmS/rLk9XnuWgfeth5bGxbEIlHXMN HvoxL34UhGxwWFCHKr5vV2OYVcPLkbdDBhLneweZAoGAaL6tCGp1uyxocqdxGipo wjsnGYO8G7YbaB1qJKljurU88qqHH1T+I2cPHOr7y9f5Au7bsaHfrtmtMJZnGVsa OR5Ioc+SSk309YaLFv3wNHMDr0feTqxaeO4dIKHBJ0/M9aLNbzivr1DwARlooS+c iGN2rdLG7U9i4DUQUTmdtXkCgYEAllEJM8DZyJN7jjrbuKFtJ8sxvCeeygjl12/4 8acQPoIUiEXSL3krlv1xq6Gf+4ImeciXLEZvoEuhGiGuqGb7Xg4LMRUVhSDo91ui UA2a+p+AbtDd7FB6g60jYXdYMWRbC+9W9m5GHs83UiYwwI/jBs291O2QiiGz8aoe ePK2GKECgYEAg7PYp1uOWEuA39gqrKmuLA5f/i2bGgBrajk90m+9kgNwZ9RqeZJG V97hRwO86EwBBqhMUjaFv/zwSjKhCZUE8vOt5DF4sH5Ppa9j/kqrzsZjlKjajj/U tH1iFSXz/+PwQVeNssfjFMUPwWWsGSN5pvqx/5Ru/DIvPLHOg3YbEQc= -----END RSA PRIVATE KEY----- ================================================ FILE: _examples/mvc/grpc-compatible-bidirectional-stream/client/main.go ================================================ package main import ( "context" "time" pb "grpcexample/helloworld" "google.golang.org/grpc" "google.golang.org/grpc/credentials" ) func main() { // Set up a connection to the server. cred, err := credentials.NewClientTLSFromFile("../server.crt", "localhost") if err != nil { panic(err) } conn, err := grpc.Dial("localhost:443", grpc.WithTransportCredentials(cred), grpc.WithBlock()) defer conn.Close() client := pb.NewGreeterBidirectionalStreamClient(conn) stream, err := client.SayHello(context.Background()) if err != nil { panic(err) } waitCh := make(chan struct{}) // Implement the send channel. // As an exercise you can implement the read channel one (reading from server, see the server/main.go). go func() { for { println("Sleeping for 2 seconds...") time.Sleep(2 * time.Second) println("Sending a msg...") msg := &pb.HelloRequest{Name: "test"} err = stream.Send(msg) if err != nil { panic("stream.Send: " + err.Error()) } } }() <-waitCh stream.CloseSend() } ================================================ FILE: _examples/mvc/grpc-compatible-bidirectional-stream/go.mod ================================================ module grpcexample go 1.25 require ( github.com/golang/protobuf v1.5.4 github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd google.golang.org/grpc v1.78.0 google.golang.org/protobuf v1.36.11 ) require ( github.com/BurntSushi/toml v1.6.0 // indirect github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect github.com/CloudyKit/jet/v6 v6.3.1 // indirect github.com/Joker/jade v1.1.3 // indirect github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 // indirect github.com/andybalholm/brotli v1.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/fatih/structs v1.1.0 // indirect github.com/flosch/pongo2/v4 v4.0.2 // indirect github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/ws v1.4.0 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/iris-contrib/schema v0.0.6 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/kataras/blocks v0.0.8 // indirect github.com/kataras/golog v0.1.12 // indirect github.com/kataras/neffos v0.0.24 // indirect github.com/kataras/pio v0.0.13 // indirect github.com/kataras/sitemap v0.0.6 // indirect github.com/kataras/tunnel v0.0.4 // indirect github.com/klauspost/compress v1.18.2 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mailgun/raymond/v2 v2.0.48 // indirect github.com/mailru/easyjson v0.9.1 // indirect github.com/mediocregopher/radix/v3 v3.8.1 // indirect github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/nats-io/nats.go v1.40.1 // indirect github.com/nats-io/nkeys v0.4.9 // indirect github.com/nats-io/nuid v1.0.1 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/schollz/closestmatch v2.1.0+incompatible // indirect github.com/sirupsen/logrus v1.8.1 // indirect github.com/stretchr/testify v1.11.1 // indirect github.com/tdewolff/minify/v2 v2.24.8 // indirect github.com/tdewolff/parse/v2 v2.8.5 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/yosssi/ace v0.0.5 // indirect golang.org/x/crypto v0.47.0 // indirect golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect golang.org/x/net v0.49.0 // indirect golang.org/x/sys v0.40.0 // indirect golang.org/x/text v0.33.0 // indirect golang.org/x/time v0.14.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: _examples/mvc/grpc-compatible-bidirectional-stream/go.sum ================================================ github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= github.com/CloudyKit/jet/v6 v6.3.1 h1:6IAo5Cx21xrHVaR8zzXN5gJatKV/wO7Nf6bfCnCSbUw= github.com/CloudyKit/jet/v6 v6.3.1/go.mod h1:lf8ksdNsxZt7/yH/3n4vJQWA9RUq4wpaHtArHhGVMOw= github.com/Joker/hpp v1.0.0 h1:65+iuJYdRXv/XyN62C1uEmmOx3432rNG/rKlX6V7Kkc= github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= github.com/Joker/jade v1.1.3 h1:Qbeh12Vq6BxURXT1qZBRHsDxeURB8ztcL6f3EXSGeHk= github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKduM= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 h1:dG7/gLroJGht/jSQtHiLvT48Hxn+crbmvyItZC8cWOs= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05/go.mod h1:NYezi6wtnJtBm5btoprXc5SvAdqH0XTXWnUup0MptAI= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/flosch/pongo2/v4 v4.0.2 h1:gv+5Pe3vaSVmiJvh/BZa82b7/00YUGm0PIyVVLop0Hw= github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+BWPhwZD8= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a h1:l7A0loSszR5zHd/qK53ZIHMO8b3bBSmENnQ6eKnUT0A= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/iris-contrib/httpexpect/v2 v2.15.2 h1:T9THsdP1woyAqKHwjkEsbCnMefsAFvk8iJJKokcJ3Go= github.com/iris-contrib/httpexpect/v2 v2.15.2/go.mod h1:JLDgIqnFy5loDSUv1OA2j0mb6p/rDhiCqigP22Uq9xE= github.com/iris-contrib/schema v0.0.6 h1:CPSBLyx2e91H2yJzPuhGuifVRnZBBJ3pCOMbOvPZaTw= github.com/iris-contrib/schema v0.0.6/go.mod h1:iYszG0IOsuIsfzjymw1kMzTL8YQcCWlm65f3wX8J5iA= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kataras/blocks v0.0.8 h1:MrpVhoFTCR2v1iOOfGng5VJSILKeZZI+7NGfxEh3SUM= github.com/kataras/blocks v0.0.8/go.mod h1:9Jm5zx6BB+06NwA+OhTbHW1xkMOYxahnqTN5DveZ2Yg= github.com/kataras/golog v0.1.12 h1:Bu7I/G4ilJlbfzjmU39O9N+2uO1pBcMK045fzZ4ytNg= github.com/kataras/golog v0.1.12/go.mod h1:wrGSbOiBqbQSQznleVNX4epWM8rl9SJ/rmEacl0yqy4= github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd h1:oMsQgqKACbUdlaIWnN+EZzzAnHkw90hE/y/R0BSij2U= github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd/go.mod h1:yucjtDl9Vn3OctWM1fi0MuM9OckemC7kEreFa9QtPF8= github.com/kataras/neffos v0.0.24 h1:S3lHqJopCfXN285VdlbGeOj+Id83u4xdQKToa+w1vW0= github.com/kataras/neffos v0.0.24/go.mod h1:/3K9zQ0yEC5/xUiSQx46ToWa3xneGfUo/nMit/F5g+U= github.com/kataras/pio v0.0.13 h1:x0rXVX0fviDTXOOLOmr4MUxOabu1InVSTu5itF8CXCM= github.com/kataras/pio v0.0.13/go.mod h1:k3HNuSw+eJ8Pm2lA4lRhg3DiCjVgHlP8hmXApSej3oM= github.com/kataras/sitemap v0.0.6 h1:w71CRMMKYMJh6LR2wTgnk5hSgjVNB9KL60n5e2KHvLY= github.com/kataras/sitemap v0.0.6/go.mod h1:dW4dOCNs896OR1HmG+dMLdT7JjDk7mYBzoIRwuj5jA4= github.com/kataras/tunnel v0.0.4 h1:sCAqWuJV7nPzGrlb0os3j49lk2JhILT0rID38NHNLpA= github.com/kataras/tunnel v0.0.4/go.mod h1:9FkU4LaeifdMWqZu7o20ojmW4B7hdhv2CMLwfnHGpYw= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw= github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18= github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mediocregopher/radix/v3 v3.8.1 h1:rOkHflVuulFKlwsLY01/M2cM2tWCjDoETcMqKbAWu1M= github.com/mediocregopher/radix/v3 v3.8.1/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/nats-io/nats.go v1.40.1 h1:MLjDkdsbGUeCMKFyCFoLnNn/HDTqcgVa3EQm+pMNDPk= github.com/nats-io/nats.go v1.40.1/go.mod h1:wV73x0FSI/orHPSYoyMeJB+KajMDoWyXmFaRrrYaaTo= github.com/nats-io/nkeys v0.4.9 h1:qe9Faq2Gxwi6RZnZMXfmGMZkg3afLLOtrU+gDZJ35b0= github.com/nats-io/nkeys v0.4.9/go.mod h1:jcMqs+FLG+W5YO36OX6wFIFcmpdAns+w1Wm6D3I/evE= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo= github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk= github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tdewolff/minify/v2 v2.24.8 h1:58/VjsbevI4d5FGV0ZSuBrHMSSkH4MCH0sIz/eKIauE= github.com/tdewolff/minify/v2 v2.24.8/go.mod h1:0Ukj0CRpo/sW/nd8uZ4ccXaV1rEVIWA3dj8U7+Shhfw= github.com/tdewolff/parse/v2 v2.8.5 h1:ZmBiA/8Do5Rpk7bDye0jbbDUpXXbCdc3iah4VeUvwYU= github.com/tdewolff/parse/v2 v2.8.5/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo= github.com/tdewolff/test v1.0.11 h1:FdLbwQVHxqG16SlkGveC0JVyrJN62COWTRyUFzfbtBE= github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA= github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0= github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda h1:i/Q+bfisr7gq6feoJnS/DlpdwEL4ihp41fvRiM3Ork0= google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs= moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHcE= ================================================ FILE: _examples/mvc/grpc-compatible-bidirectional-stream/helloworld/helloworld.pb.go ================================================ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.25.0 // protoc v3.12.3 // source: helloworld.proto package helloworld import ( proto "github.com/golang/protobuf/proto" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" ) const ( // Verify that this generated code is sufficiently up-to-date. _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) // Verify that runtime/protoimpl is sufficiently up-to-date. _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) // This is a compile-time assertion that a sufficiently up-to-date version // of the legacy proto package is being used. const _ = proto.ProtoPackageIsVersion4 // The request message containing the user's name. type HelloRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` } func (x *HelloRequest) Reset() { *x = HelloRequest{} if protoimpl.UnsafeEnabled { mi := &file_helloworld_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *HelloRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*HelloRequest) ProtoMessage() {} func (x *HelloRequest) ProtoReflect() protoreflect.Message { mi := &file_helloworld_proto_msgTypes[0] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use HelloRequest.ProtoReflect.Descriptor instead. func (*HelloRequest) Descriptor() ([]byte, []int) { return file_helloworld_proto_rawDescGZIP(), []int{0} } func (x *HelloRequest) GetName() string { if x != nil { return x.Name } return "" } // The response message containing the greetings type HelloReply struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` } func (x *HelloReply) Reset() { *x = HelloReply{} if protoimpl.UnsafeEnabled { mi := &file_helloworld_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *HelloReply) String() string { return protoimpl.X.MessageStringOf(x) } func (*HelloReply) ProtoMessage() {} func (x *HelloReply) ProtoReflect() protoreflect.Message { mi := &file_helloworld_proto_msgTypes[1] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use HelloReply.ProtoReflect.Descriptor instead. func (*HelloReply) Descriptor() ([]byte, []int) { return file_helloworld_proto_rawDescGZIP(), []int{1} } func (x *HelloReply) GetMessage() string { if x != nil { return x.Message } return "" } var File_helloworld_proto protoreflect.FileDescriptor var file_helloworld_proto_rawDesc = []byte{ 0x0a, 0x10, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0a, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x22, 0x22, 0x0a, 0x0c, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x26, 0x0a, 0x0a, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x32, 0x49, 0x0a, 0x07, 0x47, 0x72, 0x65, 0x65, 0x74, 0x65, 0x72, 0x12, 0x3e, 0x0a, 0x08, 0x53, 0x61, 0x79, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x12, 0x18, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, 0x32, 0x5c, 0x0a, 0x18, 0x47, 0x72, 0x65, 0x65, 0x74, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x69, 0x64, 0x65, 0x53, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x40, 0x0a, 0x08, 0x53, 0x61, 0x79, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x12, 0x18, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, 0x30, 0x01, 0x32, 0x5b, 0x0a, 0x17, 0x47, 0x72, 0x65, 0x65, 0x74, 0x65, 0x72, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x69, 0x64, 0x65, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x40, 0x0a, 0x08, 0x53, 0x61, 0x79, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x12, 0x18, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, 0x28, 0x01, 0x32, 0x60, 0x0a, 0x1a, 0x47, 0x72, 0x65, 0x65, 0x74, 0x65, 0x72, 0x42, 0x69, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x42, 0x0a, 0x08, 0x53, 0x61, 0x79, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x12, 0x18, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, 0x28, 0x01, 0x30, 0x01, 0x42, 0x0e, 0x5a, 0x0c, 0x2e, 0x3b, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( file_helloworld_proto_rawDescOnce sync.Once file_helloworld_proto_rawDescData = file_helloworld_proto_rawDesc ) func file_helloworld_proto_rawDescGZIP() []byte { file_helloworld_proto_rawDescOnce.Do(func() { file_helloworld_proto_rawDescData = protoimpl.X.CompressGZIP(file_helloworld_proto_rawDescData) }) return file_helloworld_proto_rawDescData } var file_helloworld_proto_msgTypes = make([]protoimpl.MessageInfo, 2) var file_helloworld_proto_goTypes = []any{ (*HelloRequest)(nil), // 0: helloworld.HelloRequest (*HelloReply)(nil), // 1: helloworld.HelloReply } var file_helloworld_proto_depIdxs = []int32{ 0, // 0: helloworld.Greeter.SayHello:input_type -> helloworld.HelloRequest 0, // 1: helloworld.GreeterServerSideSStream.SayHello:input_type -> helloworld.HelloRequest 0, // 2: helloworld.GreeterClientSideStream.SayHello:input_type -> helloworld.HelloRequest 0, // 3: helloworld.GreeterBidirectionalStream.SayHello:input_type -> helloworld.HelloRequest 1, // 4: helloworld.Greeter.SayHello:output_type -> helloworld.HelloReply 1, // 5: helloworld.GreeterServerSideSStream.SayHello:output_type -> helloworld.HelloReply 1, // 6: helloworld.GreeterClientSideStream.SayHello:output_type -> helloworld.HelloReply 1, // 7: helloworld.GreeterBidirectionalStream.SayHello:output_type -> helloworld.HelloReply 4, // [4:8] is the sub-list for method output_type 0, // [0:4] is the sub-list for method input_type 0, // [0:0] is the sub-list for extension type_name 0, // [0:0] is the sub-list for extension extendee 0, // [0:0] is the sub-list for field type_name } func init() { file_helloworld_proto_init() } func file_helloworld_proto_init() { if File_helloworld_proto != nil { return } if !protoimpl.UnsafeEnabled { file_helloworld_proto_msgTypes[0].Exporter = func(v any, i int) any { switch v := v.(*HelloRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_helloworld_proto_msgTypes[1].Exporter = func(v any, i int) any { switch v := v.(*HelloReply); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_helloworld_proto_rawDesc, NumEnums: 0, NumMessages: 2, NumExtensions: 0, NumServices: 4, }, GoTypes: file_helloworld_proto_goTypes, DependencyIndexes: file_helloworld_proto_depIdxs, MessageInfos: file_helloworld_proto_msgTypes, }.Build() File_helloworld_proto = out.File file_helloworld_proto_rawDesc = nil file_helloworld_proto_goTypes = nil file_helloworld_proto_depIdxs = nil } ================================================ FILE: _examples/mvc/grpc-compatible-bidirectional-stream/helloworld/helloworld_grpc.pb.go ================================================ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. package helloworld import ( context "context" grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" ) // This is a compile-time assertion to ensure that this generated file // is compatible with the grpc package it is being compiled against. const _ = grpc.SupportPackageIsVersion7 // GreeterClient is the client API for Greeter service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. type GreeterClient interface { // Sends a greeting SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) } type greeterClient struct { cc grpc.ClientConnInterface } func NewGreeterClient(cc grpc.ClientConnInterface) GreeterClient { return &greeterClient{cc} } func (c *greeterClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) { out := new(HelloReply) err := c.cc.Invoke(ctx, "/helloworld.Greeter/SayHello", in, out, opts...) if err != nil { return nil, err } return out, nil } // GreeterServer is the server API for Greeter service. // All implementations must embed UnimplementedGreeterServer // for forward compatibility type GreeterServer interface { // Sends a greeting SayHello(context.Context, *HelloRequest) (*HelloReply, error) mustEmbedUnimplementedGreeterServer() } // UnimplementedGreeterServer must be embedded to have forward compatible implementations. type UnimplementedGreeterServer struct { } func (UnimplementedGreeterServer) SayHello(context.Context, *HelloRequest) (*HelloReply, error) { return nil, status.Errorf(codes.Unimplemented, "method SayHello not implemented") } func (UnimplementedGreeterServer) mustEmbedUnimplementedGreeterServer() {} // UnsafeGreeterServer may be embedded to opt out of forward compatibility for this service. // Use of this interface is not recommended, as added methods to GreeterServer will // result in compilation errors. type UnsafeGreeterServer interface { mustEmbedUnimplementedGreeterServer() } func RegisterGreeterServer(s grpc.ServiceRegistrar, srv GreeterServer) { s.RegisterService(&_Greeter_serviceDesc, srv) } func _Greeter_SayHello_Handler(srv any, ctx context.Context, dec func(any) error, interceptor grpc.UnaryServerInterceptor) (any, error) { in := new(HelloRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(GreeterServer).SayHello(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/helloworld.Greeter/SayHello", } handler := func(ctx context.Context, req any) (any, error) { return srv.(GreeterServer).SayHello(ctx, req.(*HelloRequest)) } return interceptor(ctx, in, info, handler) } var _Greeter_serviceDesc = grpc.ServiceDesc{ ServiceName: "helloworld.Greeter", HandlerType: (*GreeterServer)(nil), Methods: []grpc.MethodDesc{ { MethodName: "SayHello", Handler: _Greeter_SayHello_Handler, }, }, Streams: []grpc.StreamDesc{}, Metadata: "helloworld.proto", } // GreeterServerSideSStreamClient is the client API for GreeterServerSideSStream service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. type GreeterServerSideSStreamClient interface { // Sends a greeting SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (GreeterServerSideSStream_SayHelloClient, error) } type greeterServerSideSStreamClient struct { cc grpc.ClientConnInterface } func NewGreeterServerSideSStreamClient(cc grpc.ClientConnInterface) GreeterServerSideSStreamClient { return &greeterServerSideSStreamClient{cc} } func (c *greeterServerSideSStreamClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (GreeterServerSideSStream_SayHelloClient, error) { stream, err := c.cc.NewStream(ctx, &_GreeterServerSideSStream_serviceDesc.Streams[0], "/helloworld.GreeterServerSideSStream/SayHello", opts...) if err != nil { return nil, err } x := &greeterServerSideSStreamSayHelloClient{stream} if err := x.ClientStream.SendMsg(in); err != nil { return nil, err } if err := x.ClientStream.CloseSend(); err != nil { return nil, err } return x, nil } type GreeterServerSideSStream_SayHelloClient interface { Recv() (*HelloReply, error) grpc.ClientStream } type greeterServerSideSStreamSayHelloClient struct { grpc.ClientStream } func (x *greeterServerSideSStreamSayHelloClient) Recv() (*HelloReply, error) { m := new(HelloReply) if err := x.ClientStream.RecvMsg(m); err != nil { return nil, err } return m, nil } // GreeterServerSideSStreamServer is the server API for GreeterServerSideSStream service. // All implementations must embed UnimplementedGreeterServerSideSStreamServer // for forward compatibility type GreeterServerSideSStreamServer interface { // Sends a greeting SayHello(*HelloRequest, GreeterServerSideSStream_SayHelloServer) error mustEmbedUnimplementedGreeterServerSideSStreamServer() } // UnimplementedGreeterServerSideSStreamServer must be embedded to have forward compatible implementations. type UnimplementedGreeterServerSideSStreamServer struct { } func (UnimplementedGreeterServerSideSStreamServer) SayHello(*HelloRequest, GreeterServerSideSStream_SayHelloServer) error { return status.Errorf(codes.Unimplemented, "method SayHello not implemented") } func (UnimplementedGreeterServerSideSStreamServer) mustEmbedUnimplementedGreeterServerSideSStreamServer() { } // UnsafeGreeterServerSideSStreamServer may be embedded to opt out of forward compatibility for this service. // Use of this interface is not recommended, as added methods to GreeterServerSideSStreamServer will // result in compilation errors. type UnsafeGreeterServerSideSStreamServer interface { mustEmbedUnimplementedGreeterServerSideSStreamServer() } func RegisterGreeterServerSideSStreamServer(s grpc.ServiceRegistrar, srv GreeterServerSideSStreamServer) { s.RegisterService(&_GreeterServerSideSStream_serviceDesc, srv) } func _GreeterServerSideSStream_SayHello_Handler(srv any, stream grpc.ServerStream) error { m := new(HelloRequest) if err := stream.RecvMsg(m); err != nil { return err } return srv.(GreeterServerSideSStreamServer).SayHello(m, &greeterServerSideSStreamSayHelloServer{stream}) } type GreeterServerSideSStream_SayHelloServer interface { Send(*HelloReply) error grpc.ServerStream } type greeterServerSideSStreamSayHelloServer struct { grpc.ServerStream } func (x *greeterServerSideSStreamSayHelloServer) Send(m *HelloReply) error { return x.ServerStream.SendMsg(m) } var _GreeterServerSideSStream_serviceDesc = grpc.ServiceDesc{ ServiceName: "helloworld.GreeterServerSideSStream", HandlerType: (*GreeterServerSideSStreamServer)(nil), Methods: []grpc.MethodDesc{}, Streams: []grpc.StreamDesc{ { StreamName: "SayHello", Handler: _GreeterServerSideSStream_SayHello_Handler, ServerStreams: true, }, }, Metadata: "helloworld.proto", } // GreeterClientSideStreamClient is the client API for GreeterClientSideStream service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. type GreeterClientSideStreamClient interface { // Sends a greeting SayHello(ctx context.Context, opts ...grpc.CallOption) (GreeterClientSideStream_SayHelloClient, error) } type greeterClientSideStreamClient struct { cc grpc.ClientConnInterface } func NewGreeterClientSideStreamClient(cc grpc.ClientConnInterface) GreeterClientSideStreamClient { return &greeterClientSideStreamClient{cc} } func (c *greeterClientSideStreamClient) SayHello(ctx context.Context, opts ...grpc.CallOption) (GreeterClientSideStream_SayHelloClient, error) { stream, err := c.cc.NewStream(ctx, &_GreeterClientSideStream_serviceDesc.Streams[0], "/helloworld.GreeterClientSideStream/SayHello", opts...) if err != nil { return nil, err } x := &greeterClientSideStreamSayHelloClient{stream} return x, nil } type GreeterClientSideStream_SayHelloClient interface { Send(*HelloRequest) error CloseAndRecv() (*HelloReply, error) grpc.ClientStream } type greeterClientSideStreamSayHelloClient struct { grpc.ClientStream } func (x *greeterClientSideStreamSayHelloClient) Send(m *HelloRequest) error { return x.ClientStream.SendMsg(m) } func (x *greeterClientSideStreamSayHelloClient) CloseAndRecv() (*HelloReply, error) { if err := x.ClientStream.CloseSend(); err != nil { return nil, err } m := new(HelloReply) if err := x.ClientStream.RecvMsg(m); err != nil { return nil, err } return m, nil } // GreeterClientSideStreamServer is the server API for GreeterClientSideStream service. // All implementations must embed UnimplementedGreeterClientSideStreamServer // for forward compatibility type GreeterClientSideStreamServer interface { // Sends a greeting SayHello(GreeterClientSideStream_SayHelloServer) error mustEmbedUnimplementedGreeterClientSideStreamServer() } // UnimplementedGreeterClientSideStreamServer must be embedded to have forward compatible implementations. type UnimplementedGreeterClientSideStreamServer struct { } func (UnimplementedGreeterClientSideStreamServer) SayHello(GreeterClientSideStream_SayHelloServer) error { return status.Errorf(codes.Unimplemented, "method SayHello not implemented") } func (UnimplementedGreeterClientSideStreamServer) mustEmbedUnimplementedGreeterClientSideStreamServer() { } // UnsafeGreeterClientSideStreamServer may be embedded to opt out of forward compatibility for this service. // Use of this interface is not recommended, as added methods to GreeterClientSideStreamServer will // result in compilation errors. type UnsafeGreeterClientSideStreamServer interface { mustEmbedUnimplementedGreeterClientSideStreamServer() } func RegisterGreeterClientSideStreamServer(s grpc.ServiceRegistrar, srv GreeterClientSideStreamServer) { s.RegisterService(&_GreeterClientSideStream_serviceDesc, srv) } func _GreeterClientSideStream_SayHello_Handler(srv any, stream grpc.ServerStream) error { return srv.(GreeterClientSideStreamServer).SayHello(&greeterClientSideStreamSayHelloServer{stream}) } type GreeterClientSideStream_SayHelloServer interface { SendAndClose(*HelloReply) error Recv() (*HelloRequest, error) grpc.ServerStream } type greeterClientSideStreamSayHelloServer struct { grpc.ServerStream } func (x *greeterClientSideStreamSayHelloServer) SendAndClose(m *HelloReply) error { return x.ServerStream.SendMsg(m) } func (x *greeterClientSideStreamSayHelloServer) Recv() (*HelloRequest, error) { m := new(HelloRequest) if err := x.ServerStream.RecvMsg(m); err != nil { return nil, err } return m, nil } var _GreeterClientSideStream_serviceDesc = grpc.ServiceDesc{ ServiceName: "helloworld.GreeterClientSideStream", HandlerType: (*GreeterClientSideStreamServer)(nil), Methods: []grpc.MethodDesc{}, Streams: []grpc.StreamDesc{ { StreamName: "SayHello", Handler: _GreeterClientSideStream_SayHello_Handler, ClientStreams: true, }, }, Metadata: "helloworld.proto", } // GreeterBidirectionalStreamClient is the client API for GreeterBidirectionalStream service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. type GreeterBidirectionalStreamClient interface { // Sends a greeting SayHello(ctx context.Context, opts ...grpc.CallOption) (GreeterBidirectionalStream_SayHelloClient, error) } type greeterBidirectionalStreamClient struct { cc grpc.ClientConnInterface } func NewGreeterBidirectionalStreamClient(cc grpc.ClientConnInterface) GreeterBidirectionalStreamClient { return &greeterBidirectionalStreamClient{cc} } func (c *greeterBidirectionalStreamClient) SayHello(ctx context.Context, opts ...grpc.CallOption) (GreeterBidirectionalStream_SayHelloClient, error) { stream, err := c.cc.NewStream(ctx, &_GreeterBidirectionalStream_serviceDesc.Streams[0], "/helloworld.GreeterBidirectionalStream/SayHello", opts...) if err != nil { return nil, err } x := &greeterBidirectionalStreamSayHelloClient{stream} return x, nil } type GreeterBidirectionalStream_SayHelloClient interface { Send(*HelloRequest) error Recv() (*HelloReply, error) grpc.ClientStream } type greeterBidirectionalStreamSayHelloClient struct { grpc.ClientStream } func (x *greeterBidirectionalStreamSayHelloClient) Send(m *HelloRequest) error { return x.ClientStream.SendMsg(m) } func (x *greeterBidirectionalStreamSayHelloClient) Recv() (*HelloReply, error) { m := new(HelloReply) if err := x.ClientStream.RecvMsg(m); err != nil { return nil, err } return m, nil } // GreeterBidirectionalStreamServer is the server API for GreeterBidirectionalStream service. // All implementations must embed UnimplementedGreeterBidirectionalStreamServer // for forward compatibility type GreeterBidirectionalStreamServer interface { // Sends a greeting SayHello(GreeterBidirectionalStream_SayHelloServer) error mustEmbedUnimplementedGreeterBidirectionalStreamServer() } // UnimplementedGreeterBidirectionalStreamServer must be embedded to have forward compatible implementations. type UnimplementedGreeterBidirectionalStreamServer struct { } func (UnimplementedGreeterBidirectionalStreamServer) SayHello(GreeterBidirectionalStream_SayHelloServer) error { return status.Errorf(codes.Unimplemented, "method SayHello not implemented") } func (UnimplementedGreeterBidirectionalStreamServer) mustEmbedUnimplementedGreeterBidirectionalStreamServer() { } // UnsafeGreeterBidirectionalStreamServer may be embedded to opt out of forward compatibility for this service. // Use of this interface is not recommended, as added methods to GreeterBidirectionalStreamServer will // result in compilation errors. type UnsafeGreeterBidirectionalStreamServer interface { mustEmbedUnimplementedGreeterBidirectionalStreamServer() } func RegisterGreeterBidirectionalStreamServer(s grpc.ServiceRegistrar, srv GreeterBidirectionalStreamServer) { s.RegisterService(&_GreeterBidirectionalStream_serviceDesc, srv) } func _GreeterBidirectionalStream_SayHello_Handler(srv any, stream grpc.ServerStream) error { return srv.(GreeterBidirectionalStreamServer).SayHello(&greeterBidirectionalStreamSayHelloServer{stream}) } type GreeterBidirectionalStream_SayHelloServer interface { Send(*HelloReply) error Recv() (*HelloRequest, error) grpc.ServerStream } type greeterBidirectionalStreamSayHelloServer struct { grpc.ServerStream } func (x *greeterBidirectionalStreamSayHelloServer) Send(m *HelloReply) error { return x.ServerStream.SendMsg(m) } func (x *greeterBidirectionalStreamSayHelloServer) Recv() (*HelloRequest, error) { m := new(HelloRequest) if err := x.ServerStream.RecvMsg(m); err != nil { return nil, err } return m, nil } var _GreeterBidirectionalStream_serviceDesc = grpc.ServiceDesc{ ServiceName: "helloworld.GreeterBidirectionalStream", HandlerType: (*GreeterBidirectionalStreamServer)(nil), Methods: []grpc.MethodDesc{}, Streams: []grpc.StreamDesc{ { StreamName: "SayHello", Handler: _GreeterBidirectionalStream_SayHello_Handler, ServerStreams: true, ClientStreams: true, }, }, Metadata: "helloworld.proto", } ================================================ FILE: _examples/mvc/grpc-compatible-bidirectional-stream/helloworld.proto ================================================ syntax = "proto3"; package helloworld; option go_package = ".;helloworld"; // The request message containing the user's name. message HelloRequest { string name = 1; } // The response message containing the greetings message HelloReply { string message = 1; } // The greeting service definition. service Greeter { // Sends a greeting rpc SayHello (HelloRequest) returns (HelloReply) {} } // The greeting service definition. (Server-side streaming RPC) service GreeterServerSideSStream { // Sends a greeting rpc SayHello (HelloRequest) returns (stream HelloReply) {} } // The greeting service definition. (Client-side streaming RPC) service GreeterClientSideStream { // Sends a greeting rpc SayHello (stream HelloRequest) returns (HelloReply) {} } // The greeting service definition. (Bidirectional streaming RPC) service GreeterBidirectionalStream { // Sends a greeting rpc SayHello (stream HelloRequest) returns (stream HelloReply) {} } ================================================ FILE: _examples/mvc/grpc-compatible-bidirectional-stream/server/main.go ================================================ package main import ( "io" pb "grpcexample/helloworld" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/mvc" "google.golang.org/grpc" ) type Greeter struct { pb.UnimplementedGreeterBidirectionalStreamServer } // SayHello implements the proto Bidirectional Stream Greeter service. func (g *Greeter) SayHello(stream pb.GreeterBidirectionalStream_SayHelloServer) error { for { in, err := stream.Recv() if err == io.EOF { return nil } if err != nil { return err } println("Received input: " + in.Name) // On client side you can implement the 'read' operation too. stream.Send(&pb.HelloReply{Message: "Hello " + in.Name}) } } func main() { app := iris.New() grpcServer := grpc.NewServer() myService := &Greeter{} pb.RegisterGreeterBidirectionalStreamServer(grpcServer, myService) rootApp := mvc.New(app) rootApp.Handle(myService, mvc.GRPC{ Server: grpcServer, // Required. ServiceName: "helloworld.GreeterBidirectionalStream", // Required. Strict: true, // Set it to true on gRPC streaming. }) app.Run(iris.TLS(":443", "../server.crt", "../server.key")) } ================================================ FILE: _examples/mvc/grpc-compatible-bidirectional-stream/server.crt ================================================ -----BEGIN CERTIFICATE----- MIICwzCCAaugAwIBAgIJANAsSlhuqzW/MA0GCSqGSIb3DQEBBQUAMBQxEjAQBgNV BAMTCWxvY2FsaG9zdDAeFw0yMDEwMzEwMjM4MTNaFw0zMDEwMjkwMjM4MTNaMBQx EjAQBgNVBAMTCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC ggEBAKY8mFh0nvXa8V6Vx5TVVbfq9Zx81hiIKeXN977JaQGrPfCMRU61smaDJAzR MBEFjHT37Zj5sZr6qvQmt3Dfst8bNcFBsZ7s3bBknuevD3gPJBy1r5KFwSkPZekH T+0DF0Drq1/iIZQXE13udWzDnzxABUBOmIzEmn+rfp2j3xJJGLoO1O4Wu2YpXhtj yNJfswoztGGie6u61mAN7Rw4JQUkDKa3hAKkYtfOoGHZBs6Y0RIKZQikjxlVpMMc fCvUMHkY51GP8ycgZ8+0n/cjm13KkqfvoyRZ1rQqCd24YNIEl18t0FClpUAHkxD/ aUPCqV3bqpFBbDPQcYybIGjfNIMCAwEAAaMYMBYwFAYDVR0RBA0wC4IJbG9jYWxo b3N0MA0GCSqGSIb3DQEBBQUAA4IBAQAd6F6tG/jcUu9pm/5XeH1Iof5PB9tFuBvv w9HFjBI5S7ItnTepneoOP26lx2iOE0GwkvrhseZcHkGDZPdncBn12PuCHUbcO2Ux bEDyDMsAHooKLvmAfAWeiOmeJAdFl8DOTgD/lTHK1n/mEwCUOtGozmfm/Y0pnfc7 nMgbQ+yNsj1X7HVJHRUdOfOCGjiWofo6v7V7YyZJC2jpn3K7O4126jic2ibWYKQl fLLjgS0N5Wcun117e1mRYV41jtPOkeAvxJbJGf0IP7Os0VfG1UQhodwjkV4s8GcG naB5qC4Y508cB7Lq58kbGgZ0nfMCcpnrKobliIZVEpH7aa0Po1lm -----END CERTIFICATE----- ================================================ FILE: _examples/mvc/grpc-compatible-bidirectional-stream/server.key ================================================ -----BEGIN RSA PRIVATE KEY----- MIIEpAIBAAKCAQEApjyYWHSe9drxXpXHlNVVt+r1nHzWGIgp5c33vslpAas98IxF TrWyZoMkDNEwEQWMdPftmPmxmvqq9Ca3cN+y3xs1wUGxnuzdsGSe568PeA8kHLWv koXBKQ9l6QdP7QMXQOurX+IhlBcTXe51bMOfPEAFQE6YjMSaf6t+naPfEkkYug7U 7ha7ZileG2PI0l+zCjO0YaJ7q7rWYA3tHDglBSQMpreEAqRi186gYdkGzpjREgpl CKSPGVWkwxx8K9QweRjnUY/zJyBnz7Sf9yObXcqSp++jJFnWtCoJ3bhg0gSXXy3Q UKWlQAeTEP9pQ8KpXduqkUFsM9BxjJsgaN80gwIDAQABAoIBACDVNwHBhuPoKmQU ESdEO3nn3jraLS8LNbs9wwDbpvG9cK5iBg5VtLaqkCQ37NZv0h4IGdVs+7cwazNt si2JATsvlJ5m6z4IaoC8XuZDnTqJQwiomdTGti/16prr5s1ZHu6jnWWCtD8bj6et wWOJ/5lWy7K300l6S0mMBaX9B8IEfGR+YPGF/bfsIlf+9iuVulBjH5NPIozwl4c9 VUafESEZX0DM/KDHqFvvWdMaw9e+3QNi+l0t3VJz6JuEcUfRztT6lbzYbRGBYqKr DmUNxYUThd717NJklxr9D+GMm5FnfrzY8vcJ16SILBQyDthsz1s6gdT0yeRFgjJf afDObAECgYEA1WQdayAeqT7sE0T0jHAWHdVhRd9sGUTZ4LjVuGbMjbJWJVUPn/fz ahBQh77xutmNCbDSUD0srT+zHtRRJUVhl+ojFS1CMyyL/UAfH24gDwUnXIyq/DPA heXKD8NwGoTPbHKjdmy+89qVbes3oxBc5tDKpzAuBoPY4iBMVwfqSmECgYEAx24Y i1lhxuG+g2YSjiIhmD/ytZSXamP3+KmnqWWXbEjJd2oYmCPr9yBCfdk9jCUXZJsg tYsRduMajTjmTNkYmGyZNVtmjXfrq/vaJgf7bZhhLOFxWoNmLlKQfmbuJW5fkV1b hY8oid+YbBob4xseFzKvm5j0aKwufi3v7XLUEWMCgYEAs7rqKFNaX9SWhDhc/Xhe uGwDzRU8eCAMnwEvSWyUN3iQpEr7qRHvXFM3cM47zdP0vcfHrDuKSLXRSVMssYa5 h3l2aRzAmFeZ5Qk/7XoU2HHP0FzOmzN/oYeE5DgJUNyx1DbORS2cu8lMeNNX/ikH BoWvWpfy/BvK7dKkWd1Z0aECgYEAm1+QKcjqX5ty5UZ6AFhhGhAAVS2+Rfo6sHXl FRn8PjX7GFkFbkrWRUPR6eB9jhk7v3sIocgGRDytbAc/jfG5ss8xEhvyqxcZ+nUO QYEIhxsn4mKGAMHMsxxKTOB+e5UhScyVSFn/eGNGijpRLb/r0qD/pdcl3AMBefbq LXG//QcCgYBrYTCaKYDg/TSMt6fTSZgJRaui8PvvlLjZy63vrVCexVUibiCpbUt9 sHe9gbSDPr/uCyNgKnZjDbVabmZboVYqHBDQnXPUYYx9dBSL2DzE3JXA6Q8RVnOY zwCPzVxYbE9h02ishAgc6k9w61XUaDnASmBZAnlxt9/8cB7+IZAlfg== -----END RSA PRIVATE KEY----- ================================================ FILE: _examples/mvc/hello-world/main.go ================================================ package main import ( "strings" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/middleware/logger" "github.com/kataras/iris/v12/middleware/recover" "github.com/kataras/iris/v12/mvc" ) // This example is equivalent to the // https://github.com/kataras/iris/blob/main/_examples/hello-world/main.go // // It seems that additional code you // have to write doesn't worth it // but remember that, this example // does not make use of iris mvc features like // the Model, Persistence or the View engine neither the Session, // it's very simple for learning purposes, // probably you'll never use such // as simple controller anywhere in your app. // // The cost we have on this example for using MVC // on the "/hello" path which serves JSON // is ~2MB per 20MB throughput on my personal laptop, // it's tolerated for the majority of the applications // but you can choose // what suits you best with Iris, low-level handlers: performance // or high-level controllers: easier to maintain and smaller codebase on large applications. // Of course you can put all these to main func, it's just a separate function // for the main_test.go. func newApp() *iris.Application { app := iris.New() // Optionally, add two builtin handlers // that can recover from any http-relative panics // and log the requests to the terminal. app.Use(recover.New()) app.Use(logger.New()) // Serve a controller based on the root Router, "/". mvc.New(app).Handle(new(ExampleController)) // Add custom path func mvc.New(app).SetCustomPathWordFunc(func(path, w string, wordIndex int) string { if wordIndex == 0 { w = strings.ToLower(w) } path += w return path }).Handle(new(ExampleControllerCustomPath)) return app } func main() { app := newApp() // http://localhost:8080 // http://localhost:8080/ping // http://localhost:8080/hello // http://localhost:8080/custom_path app.Listen(":8080") } // ExampleController serves the "/", "/ping" and "/hello". type ExampleController struct{} // Get serves // Method: GET // Resource: http://localhost:8080 func (c *ExampleController) Get() mvc.Result { return mvc.Response{ ContentType: "text/html", Text: "

Welcome

", } } // GetPing serves // Method: GET // Resource: http://localhost:8080/ping func (c *ExampleController) GetPing() string { return "pong" } // GetHello serves // Method: GET // Resource: http://localhost:8080/hello func (c *ExampleController) GetHello() any { return map[string]string{"message": "Hello Iris!"} } // GetHelloWorld serves // Method: GET // Resource: http://localhost:8080/hello/world func (c *ExampleController) GetHelloWorld() any { return map[string]string{"message": "Hello Iris! DefaultPath"} } // BeforeActivation called once, before the controller adapted to the main application // and of course before the server ran. // After version 9 you can also add custom routes for a specific controller's methods. // Here you can register custom method's handlers // use the standard router with `ca.Router` to do something that you can do without mvc as well, // and add dependencies that will be binded to a controller's fields or method function's input arguments. func (c *ExampleController) BeforeActivation(b mvc.BeforeActivation) { anyMiddlewareHere := func(ctx iris.Context) { ctx.Application().Logger().Warnf("Inside /custom_path") ctx.Next() } b.Handle("GET", "/custom_path", "CustomHandlerWithoutFollowingTheNamingGuide", anyMiddlewareHere) // or even add a global middleware based on this controller's router, // which in this example is the root "/": // b.Router().Use(myMiddleware) } // CustomHandlerWithoutFollowingTheNamingGuide serves // Method: GET // Resource: http://localhost:8080/custom_path func (c *ExampleController) CustomHandlerWithoutFollowingTheNamingGuide() string { return "hello from the custom handler without following the naming guide" } type ExampleControllerCustomPath struct{} // GetHelloWorld serves // Method: GET // Resource: http://localhost:8080/helloWorld func (c *ExampleControllerCustomPath) GetHelloWorld() any { return map[string]string{"message": "Hello Iris! CustomPath"} } // GetUserBy serves // Method: GET // Resource: http://localhost:8080/user/{username:string} // By is a reserved "keyword" to tell the framework that you're going to // bind path parameters in the function's input arguments, and it also // helps to have "Get" and "GetBy" in the same controller. // // func (c *ExampleController) GetUserBy(username string) mvc.Result { // return mvc.View{ // Name: "user/username.html", // Data: username, // } // } /* Can use more than one, the factory will make sure that the correct http methods are being registered for each route for this controller, uncomment these if you want: func (c *ExampleController) Post() {} func (c *ExampleController) Put() {} func (c *ExampleController) Delete() {} func (c *ExampleController) Connect() {} func (c *ExampleController) Head() {} func (c *ExampleController) Patch() {} func (c *ExampleController) Options() {} func (c *ExampleController) Trace() {} */ /* func (c *ExampleController) All() {} // OR func (c *ExampleController) Any() {} func (c *ExampleController) BeforeActivation(b mvc.BeforeActivation) { // 1 -> the HTTP Method // 2 -> the route's path // 3 -> this controller's method name that should be handler for that route. b.Handle("GET", "/mypath/{param}", "DoIt", optionalMiddlewareHere...) } // After activation, all dependencies are set-ed - so read only access on them // but still possible to add custom controller or simple standard handlers. func (c *ExampleController) AfterActivation(a mvc.AfterActivation) {} */ ================================================ FILE: _examples/mvc/hello-world/main_test.go ================================================ package main import ( "testing" "github.com/kataras/iris/v12/httptest" ) func TestMVCHelloWorld(t *testing.T) { e := httptest.New(t, newApp()) e.GET("/").Expect().Status(httptest.StatusOK). ContentType("text/html", "utf-8").Body().IsEqual("

Welcome

") e.GET("/ping").Expect().Status(httptest.StatusOK). Body().IsEqual("pong") e.GET("/hello").Expect().Status(httptest.StatusOK). JSON().Object().Value("message").Equal("Hello Iris!") e.GET("/custom_path").Expect().Status(httptest.StatusOK). Body().IsEqual("hello from the custom handler without following the naming guide") } ================================================ FILE: _examples/mvc/login/datamodels/user.go ================================================ package datamodels import ( "time" "golang.org/x/crypto/bcrypt" ) // User is our User example model. // Keep note that the tags for public-use (for our web app) // should be kept in other file like "web/viewmodels/user.go" // which could wrap by embedding the datamodels.User or // define completely new fields instead but for the sake // of the example, we will use this datamodel // as the only one User model in our application. type User struct { ID int64 `json:"id" form:"id"` Firstname string `json:"firstname" form:"firstname"` Username string `json:"username" form:"username"` HashedPassword []byte `json:"-" form:"-"` CreatedAt time.Time `json:"created_at" form:"created_at"` } // IsValid can do some very very simple "low-level" data validations. func (u User) IsValid() bool { return u.ID > 0 } // GeneratePassword will generate a hashed password for us based on the // user's input. func GeneratePassword(userPassword string) ([]byte, error) { return bcrypt.GenerateFromPassword([]byte(userPassword), bcrypt.DefaultCost) } // ValidatePassword will check if passwords are matched. func ValidatePassword(userPassword string, hashed []byte) (bool, error) { if err := bcrypt.CompareHashAndPassword(hashed, []byte(userPassword)); err != nil { return false, err } return true, nil } ================================================ FILE: _examples/mvc/login/datasource/users.go ================================================ // file: datasource/users.go package datasource import ( "errors" "github.com/kataras/iris/v12/_examples/mvc/login/datamodels" ) // Engine is from where to fetch the data, in this case the users. type Engine uint32 const ( // Memory stands for simple memory location; // map[int64] datamodels.User ready to use, it's our source in this example. Memory Engine = iota // Bolt for boltdb source location. Bolt // MySQL for mysql-compatible source location. MySQL ) // LoadUsers returns all users(empty map) from the memory, for the sake of simplicty. func LoadUsers(engine Engine) (map[int64]datamodels.User, error) { if engine != Memory { return nil, errors.New("for the sake of simplicity we're using a simple map as the data source") } return make(map[int64]datamodels.User), nil } ================================================ FILE: _examples/mvc/login/main.go ================================================ // file: main.go package main import ( "fmt" "time" "github.com/kataras/iris/v12/_examples/mvc/login/datasource" "github.com/kataras/iris/v12/_examples/mvc/login/repositories" "github.com/kataras/iris/v12/_examples/mvc/login/services" "github.com/kataras/iris/v12/_examples/mvc/login/web/controllers" "github.com/kataras/iris/v12/_examples/mvc/login/web/middleware" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/mvc" "github.com/kataras/iris/v12/sessions" ) func main() { app := iris.New() // You got full debug messages, useful when using MVC and you want to make // sure that your code is aligned with the Iris' MVC Architecture. app.Logger().SetLevel("debug") // Load the template files. tmpl := iris.HTML("./web/views", ".html"). Layout("shared/layout.html"). Reload(true) app.RegisterView(tmpl) app.HandleDir("/public", iris.Dir("./web/public")) app.OnAnyErrorCode(func(ctx iris.Context) { ctx.ViewData("Message", ctx.Values(). GetStringDefault("message", "The page you're looking for doesn't exist")) if err := ctx.View("shared/error.html"); err != nil { ctx.HTML(fmt.Sprintf("

%s

", err.Error())) return } }) // ---- Serve our controllers. ---- // Prepare our repositories and services. db, err := datasource.LoadUsers(datasource.Memory) if err != nil { app.Logger().Fatalf("error while loading the users: %v", err) return } repo := repositories.NewUserRepository(db) userService := services.NewUserService(repo) // "/users" based mvc application. users := mvc.New(app.Party("/users")) // Add the basic authentication(admin:password) middleware // for the /users based requests. users.Router.Use(middleware.BasicAuth) // Bind the "userService" to the UserController's Service (interface) field. users.Register(userService) users.Handle(new(controllers.UsersController)) // "/user" based mvc application. sessManager := sessions.New(sessions.Config{ Cookie: "sessioncookiename", Expires: 24 * time.Hour, }) user := mvc.New(app.Party("/user")) user.Register( userService, sessManager.Start, ) user.Handle(new(controllers.UserController)) // http://localhost:8080/noexist // and all controller's methods like // http://localhost:8080/users/1 // http://localhost:8080/user/register // http://localhost:8080/user/login // http://localhost:8080/user/me // http://localhost:8080/user/logout // basic auth: "admin", "password", see "./middleware/basicauth.go" source file. // Starts the web server at localhost:8080 // Enables faster json serialization and more. app.Listen(":8080", iris.WithOptimizations) } ================================================ FILE: _examples/mvc/login/repositories/user_repository.go ================================================ package repositories import ( "errors" "sync" "github.com/kataras/iris/v12/_examples/mvc/login/datamodels" ) // Query represents the visitor and action queries. type Query func(datamodels.User) bool // UserRepository handles the basic operations of a user entity/model. // It's an interface in order to be testable, i.e a memory user repository or // a connected to an sql database. type UserRepository interface { Exec(query Query, action Query, limit int, mode int) (ok bool) Select(query Query) (user datamodels.User, found bool) SelectMany(query Query, limit int) (results []datamodels.User) InsertOrUpdate(user datamodels.User) (updatedUser datamodels.User, err error) Delete(query Query, limit int) (deleted bool) } // NewUserRepository returns a new user memory-based repository, // the one and only repository type in our example. func NewUserRepository(source map[int64]datamodels.User) UserRepository { return &userMemoryRepository{source: source} } // userMemoryRepository is a "UserRepository" // which manages the users using the memory data source (map). type userMemoryRepository struct { source map[int64]datamodels.User mu sync.RWMutex } const ( // ReadOnlyMode will RLock(read) the data . ReadOnlyMode = iota // ReadWriteMode will Lock(read/write) the data. ReadWriteMode ) func (r *userMemoryRepository) Exec(query Query, action Query, actionLimit int, mode int) (ok bool) { loops := 0 if mode == ReadOnlyMode { r.mu.RLock() defer r.mu.RUnlock() } else { r.mu.Lock() defer r.mu.Unlock() } for _, user := range r.source { ok = query(user) if ok { if action(user) { loops++ if actionLimit >= loops { break // break } } } } return } // Select receives a query function // which is fired for every single user model inside // our imaginary data source. // When that function returns true then it stops the iteration. // // It returns the query's return last known boolean value // and the last known user model // to help callers to reduce the LOC. // // It's actually a simple but very clever prototype function // I'm using everywhere since I firstly think of it, // hope you'll find it very useful as well. func (r *userMemoryRepository) Select(query Query) (user datamodels.User, found bool) { found = r.Exec(query, func(m datamodels.User) bool { user = m return true }, 1, ReadOnlyMode) // set an empty datamodels.User if not found at all. if !found { user = datamodels.User{} } return } // SelectMany same as Select but returns one or more datamodels.User as a slice. // If limit <=0 then it returns everything. func (r *userMemoryRepository) SelectMany(query Query, limit int) (results []datamodels.User) { r.Exec(query, func(m datamodels.User) bool { results = append(results, m) return true }, limit, ReadOnlyMode) return } // InsertOrUpdate adds or updates a user to the (memory) storage. // // Returns the new user and an error if any. func (r *userMemoryRepository) InsertOrUpdate(user datamodels.User) (datamodels.User, error) { id := user.ID if id == 0 { // Create new action var lastID int64 // find the biggest ID in order to not have duplications // in productions apps you can use a third-party // library to generate a UUID as string. r.mu.RLock() for _, item := range r.source { if item.ID > lastID { lastID = item.ID } } r.mu.RUnlock() id = lastID + 1 user.ID = id // map-specific thing r.mu.Lock() r.source[id] = user r.mu.Unlock() return user, nil } // Update action based on the user.ID, // here we will allow updating the poster and genre if not empty. // Alternatively we could do pure replace instead: // r.source[id] = user // and comment the code below; current, exists := r.Select(func(m datamodels.User) bool { return m.ID == id }) if !exists { // ID is not a real one, return an error. return datamodels.User{}, errors.New("failed to update a nonexistent user") } // or comment these and r.source[id] = user for pure replace if user.Username != "" { current.Username = user.Username } if user.Firstname != "" { current.Firstname = user.Firstname } // map-specific thing r.mu.Lock() r.source[id] = current r.mu.Unlock() return user, nil } func (r *userMemoryRepository) Delete(query Query, limit int) bool { return r.Exec(query, func(m datamodels.User) bool { delete(r.source, m.ID) return true }, limit, ReadWriteMode) } ================================================ FILE: _examples/mvc/login/services/user_service.go ================================================ package services import ( "errors" "github.com/kataras/iris/v12/_examples/mvc/login/datamodels" "github.com/kataras/iris/v12/_examples/mvc/login/repositories" ) // UserService handles CRUID operations of a user datamodel, // it depends on a user repository for its actions. // It's here to decouple the data source from the higher level compoments. // As a result a different repository type can be used with the same logic without any aditional changes. // It's an interface and it's used as interface everywhere // because we may need to change or try an experimental different domain logic at the future. type UserService interface { GetAll() []datamodels.User GetByID(id int64) (datamodels.User, bool) GetByUsernameAndPassword(username, userPassword string) (datamodels.User, bool) DeleteByID(id int64) bool Update(id int64, user datamodels.User) (datamodels.User, error) UpdatePassword(id int64, newPassword string) (datamodels.User, error) UpdateUsername(id int64, newUsername string) (datamodels.User, error) Create(userPassword string, user datamodels.User) (datamodels.User, error) } // NewUserService returns the default user service. func NewUserService(repo repositories.UserRepository) UserService { return &userService{ repo: repo, } } type userService struct { repo repositories.UserRepository } // GetAll returns all users. func (s *userService) GetAll() []datamodels.User { return s.repo.SelectMany(func(_ datamodels.User) bool { return true }, -1) } // GetByID returns a user based on its id. func (s *userService) GetByID(id int64) (datamodels.User, bool) { return s.repo.Select(func(m datamodels.User) bool { return m.ID == id }) } // GetByUsernameAndPassword returns a user based on its username and password, // used for authentication. func (s *userService) GetByUsernameAndPassword(username, userPassword string) (datamodels.User, bool) { if username == "" || userPassword == "" { return datamodels.User{}, false } return s.repo.Select(func(m datamodels.User) bool { if m.Username == username { hashed := m.HashedPassword if ok, _ := datamodels.ValidatePassword(userPassword, hashed); ok { return true } } return false }) } // Update updates every field from an existing User, // it's not safe to be used via public API, // however we will use it on the web/controllers/user_controller.go#PutBy // in order to show you how it works. func (s *userService) Update(id int64, user datamodels.User) (datamodels.User, error) { user.ID = id return s.repo.InsertOrUpdate(user) } // UpdatePassword updates a user's password. func (s *userService) UpdatePassword(id int64, newPassword string) (datamodels.User, error) { // update the user and return it. hashed, err := datamodels.GeneratePassword(newPassword) if err != nil { return datamodels.User{}, err } return s.Update(id, datamodels.User{ HashedPassword: hashed, }) } // UpdateUsername updates a user's username. func (s *userService) UpdateUsername(id int64, newUsername string) (datamodels.User, error) { return s.Update(id, datamodels.User{ Username: newUsername, }) } // Create inserts a new User, // the userPassword is the client-typed password // it will be hashed before the insertion to our repository. func (s *userService) Create(userPassword string, user datamodels.User) (datamodels.User, error) { if user.ID > 0 || userPassword == "" || user.Firstname == "" || user.Username == "" { return datamodels.User{}, errors.New("unable to create this user") } hashed, err := datamodels.GeneratePassword(userPassword) if err != nil { return datamodels.User{}, err } user.HashedPassword = hashed return s.repo.InsertOrUpdate(user) } // DeleteByID deletes a user by its id. // // Returns true if deleted otherwise false. func (s *userService) DeleteByID(id int64) bool { return s.repo.Delete(func(m datamodels.User) bool { return m.ID == id }, 1) } ================================================ FILE: _examples/mvc/login/web/controllers/user_controller.go ================================================ // file: controllers/user_controller.go package controllers import ( "github.com/kataras/iris/v12/_examples/mvc/login/datamodels" "github.com/kataras/iris/v12/_examples/mvc/login/services" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/mvc" "github.com/kataras/iris/v12/sessions" ) // UserController is our /user controller. // UserController is responsible to handle the following requests: // GET /user/register // POST /user/register // GET /user/login // POST /user/login // GET /user/me // All HTTP Methods /user/logout type UserController struct { // context is auto-binded by Iris on each request, // remember that on each incoming request iris creates a new UserController each time, // so all fields are request-scoped by-default, only dependency injection is able to set // custom fields like the Service which is the same for all requests (static binding) // and the Session which depends on the current context (dynamic binding). Ctx iris.Context // Our UserService, it's an interface which // is binded from the main application. Service services.UserService // Session, binded using dependency injection from the main.go. Session *sessions.Session } const userIDKey = "UserID" func (c *UserController) getCurrentUserID() int64 { userID := c.Session.GetInt64Default(userIDKey, 0) return userID } func (c *UserController) isLoggedIn() bool { return c.getCurrentUserID() > 0 } func (c *UserController) logout() { c.Session.Destroy() } var registerStaticView = mvc.View{ Name: "user/register.html", Data: iris.Map{"Title": "User Registration"}, } // GetRegister handles GET: http://localhost:8080/user/register. func (c *UserController) GetRegister() mvc.Result { if c.isLoggedIn() { c.logout() } return registerStaticView } // PostRegister handles POST: http://localhost:8080/user/register. func (c *UserController) PostRegister() mvc.Result { // get firstname, username and password from the form. var ( firstname = c.Ctx.FormValue("firstname") username = c.Ctx.FormValue("username") password = c.Ctx.FormValue("password") ) // create the new user, the password will be hashed by the service. u, err := c.Service.Create(password, datamodels.User{ Username: username, Firstname: firstname, }) // set the user's id to this session even if err != nil, // the zero id doesn't matters because .getCurrentUserID() checks for that. // If err != nil then it will be shown, see below on mvc.Response.Err: err. c.Session.Set(userIDKey, u.ID) return mvc.Response{ // if not nil then this error will be shown instead. Err: err, // redirect to /user/me. Path: "/user/me", // When redirecting from POST to GET request you -should- use this HTTP status code, // however there're some (complicated) alternatives if you // search online or even the HTTP RFC. // Status "See Other" RFC 7231, however iris can automatically fix that // but it's good to know you can set a custom code; // Code: 303, } } var loginStaticView = mvc.View{ Name: "user/login.html", Data: iris.Map{"Title": "User Login"}, } // GetLogin handles GET: http://localhost:8080/user/login. func (c *UserController) GetLogin() mvc.Result { if c.isLoggedIn() { // if it's already logged in then destroy the previous session. c.logout() } return loginStaticView } // PostLogin handles POST: http://localhost:8080/user/register. func (c *UserController) PostLogin() mvc.Result { var ( username = c.Ctx.FormValue("username") password = c.Ctx.FormValue("password") ) u, found := c.Service.GetByUsernameAndPassword(username, password) if !found { return mvc.Response{ Path: "/user/register", } } c.Session.Set(userIDKey, u.ID) return mvc.Response{ Path: "/user/me", } } // GetMe handles GET: http://localhost:8080/user/me. func (c *UserController) GetMe() mvc.Result { if !c.isLoggedIn() { // if it's not logged in then redirect user to the login page. return mvc.Response{Path: "/user/login"} } u, found := c.Service.GetByID(c.getCurrentUserID()) if !found { // if the session exists but for some reason the user doesn't exist in the "database" // then logout and re-execute the function, it will redirect the client to the // /user/login page. c.logout() return c.GetMe() } return mvc.View{ Name: "user/me.html", Data: iris.Map{ "Title": "Profile of " + u.Username, "User": u, }, } } // AnyLogout handles All/Any HTTP Methods for: http://localhost:8080/user/logout. func (c *UserController) AnyLogout() { if c.isLoggedIn() { c.logout() } c.Ctx.Redirect("/user/login") } ================================================ FILE: _examples/mvc/login/web/controllers/users_controller.go ================================================ package controllers import ( "github.com/kataras/iris/v12/_examples/mvc/login/datamodels" "github.com/kataras/iris/v12/_examples/mvc/login/services" "github.com/kataras/iris/v12" ) // UsersController is our /users API controller. // GET /users | get all // GET /users/{id:int64} | get by id // PUT /users/{id:int64} | update by id // DELETE /users/{id:int64} | delete by id // Requires basic authentication. type UsersController struct { // Optionally: context is auto-binded by Iris on each request, // remember that on each incoming request iris creates a new UserController each time, // so all fields are request-scoped by-default, only dependency injection is able to set // custom fields like the Service which is the same for all requests (static binding). Ctx iris.Context // Our UserService, it's an interface which // is binded from the main application. Service services.UserService } // Get returns list of the users. // Demo: // curl -i -u admin:password http://localhost:8080/users // // The correct way if you have sensitive data: // // func (c *UsersController) Get() (results []viewmodels.User) { // data := c.Service.GetAll() // // for _, user := range data { // results = append(results, viewmodels.User{user}) // } // return // } // // otherwise just return the datamodels. func (c *UsersController) Get() (results []datamodels.User) { return c.Service.GetAll() } // GetBy returns a user. // Demo: // curl -i -u admin:password http://localhost:8080/users/1 func (c *UsersController) GetBy(id int64) (user datamodels.User, found bool) { u, found := c.Service.GetByID(id) if !found { // this message will be binded to the // main.go -> app.OnAnyErrorCode -> NotFound -> shared/error.html -> .Message text. c.Ctx.Values().Set("message", "User couldn't be found!") } return u, found // it will throw/emit 404 if found == false. } // PutBy updates a user. // Demo: // curl -i -X PUT -u admin:password -F "username=kataras" // -F "password=rawPasswordIsNotSafeIfOrNotHTTPs_You_Should_Use_A_client_side_lib_for_hash_as_well" // http://localhost:8080/users/1 func (c *UsersController) PutBy(id int64) (datamodels.User, error) { // username := c.Ctx.FormValue("username") // password := c.Ctx.FormValue("password") u := datamodels.User{} if err := c.Ctx.ReadForm(&u); err != nil { return u, err } return c.Service.Update(id, u) } // DeleteBy deletes a user. // Demo: // curl -i -X DELETE -u admin:password http://localhost:8080/users/1 func (c *UsersController) DeleteBy(id int64) any { wasDel := c.Service.DeleteByID(id) if wasDel { // return the deleted user's ID return map[string]any{"deleted": id} } // right here we can see that a method function // can return any of those two types(map or int), // we don't have to specify the return type to a specific type. return iris.StatusBadRequest // same as 400. } ================================================ FILE: _examples/mvc/login/web/middleware/basicauth.go ================================================ // file: middleware/basicauth.go package middleware import "github.com/kataras/iris/v12/middleware/basicauth" // BasicAuth middleware sample. var BasicAuth = basicauth.Default(map[string]string{ "admin": "password", }) ================================================ FILE: _examples/mvc/login/web/public/css/site.css ================================================ /* Bordered form */ form { border: 3px solid #f1f1f1; } /* Full-width inputs */ input[type=text], input[type=password] { width: 100%; padding: 12px 20px; margin: 8px 0; display: inline-block; border: 1px solid #ccc; box-sizing: border-box; } /* Set a style for all buttons */ button { background-color: #4CAF50; color: white; padding: 14px 20px; margin: 8px 0; border: none; cursor: pointer; width: 100%; } /* Add a hover effect for buttons */ button:hover { opacity: 0.8; } /* Extra style for the cancel button (red) */ .cancelbtn { width: auto; padding: 10px 18px; background-color: #f44336; } /* Center the container */ /* Add padding to containers */ .container { padding: 16px; } /* The "Forgot password" text */ span.psw { float: right; padding-top: 16px; } /* Change styles for span and cancel button on extra small screens */ @media screen and (max-width: 300px) { span.psw { display: block; float: none; } .cancelbtn { width: 100%; } } ================================================ FILE: _examples/mvc/login/web/viewmodels/README.md ================================================ # View Models There should be the view models, the structure that the client will be able to see. Example: ```go import ( "github.com/kataras/iris/v12/_examples/mvc/login/datamodels" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/context" ) type User struct { datamodels.User } func (m User) IsValid() bool { /* do some checks and return true if it's valid... */ return m.ID > 0 } ``` Iris is able to convert any custom data Structure into an HTTP Response Dispatcher, so theoretically, something like the following is permitted if it's really necessary; ```go // Dispatch completes the `kataras/iris/mvc#Result` interface. // Sends a `User` as a controlled http response. // If its ID is zero or less then it returns a 404 not found error // else it returns its json representation, // (just like the controller's functions do for custom types by default). // // Don't overdo it, the application's logic should not be here. // It's just one more step of validation before the response, // simple checks can be added here. // // It's just a showcase, // imagine the potentials this feature gives when designing a bigger application. // // This is called where the return value from a controller's method functions // is type of `User`. // For example the `controllers/user_controller.go#GetBy`. func (m User) Dispatch(ctx iris.Context) { if !m.IsValid() { ctx.NotFound() return } ctx.JSON(m, context.JSON{Indent: " "}) } ``` However, we will use the "datamodels" as the only one models package because User structure doesn't contain any sensitive data, clients are able to see all of its fields and we don't need any extra functionality or validation inside it. ================================================ FILE: _examples/mvc/login/web/views/shared/error.html ================================================

Error.

An error occurred while processing your request.

{{.Message}}

================================================ FILE: _examples/mvc/login/web/views/shared/layout.html ================================================ {{.Title}} {{ yield . }} ================================================ FILE: _examples/mvc/login/web/views/user/login.html ================================================
================================================ FILE: _examples/mvc/login/web/views/user/me.html ================================================

Welcome back {{.User.Firstname}}!

================================================ FILE: _examples/mvc/login/web/views/user/register.html ================================================
================================================ FILE: _examples/mvc/login-mvc-single-responsibility/main.go ================================================ package main import ( "time" "github.com/kataras/iris/v12/_examples/mvc/login-mvc-single-responsibility/user" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/mvc" "github.com/kataras/iris/v12/sessions" ) func main() { app := iris.New() // You got full debug messages, useful when using MVC and you want to make // sure that your code is aligned with the Iris' MVC Architecture. app.Logger().SetLevel("debug") app.RegisterView(iris.HTML("./views", ".html").Layout("shared/layout.html")) app.HandleDir("/public", iris.Dir("./public")) userRouter := app.Party("/user") { manager := sessions.New(sessions.Config{ Cookie: "sessioncookiename", Expires: 24 * time.Hour, }) userRouter.Use(manager.Handler()) mvc.Configure(userRouter, configureUserMVC) } // http://localhost:8080/user/register // http://localhost:8080/user/login // http://localhost:8080/user/me // http://localhost:8080/user/logout // http://localhost:8080/user/1 app.Listen(":8080", configure) } func configureUserMVC(userApp *mvc.Application) { userApp.Register( user.NewDataSource(), ) userApp.Handle(new(user.Controller)) } func configure(app *iris.Application) { app.Configure( iris.WithOptimizations, iris.WithFireMethodNotAllowed, iris.WithLowercaseRouting, iris.WithPathIntelligence, iris.WithTunneling, ) } ================================================ FILE: _examples/mvc/login-mvc-single-responsibility/public/css/site.css ================================================ /* Bordered form */ form { border: 3px solid #f1f1f1; } /* Full-width inputs */ input[type=text], input[type=password] { width: 100%; padding: 12px 20px; margin: 8px 0; display: inline-block; border: 1px solid #ccc; box-sizing: border-box; } /* Set a style for all buttons */ button { background-color: #4CAF50; color: white; padding: 14px 20px; margin: 8px 0; border: none; cursor: pointer; width: 100%; } /* Add a hover effect for buttons */ button:hover { opacity: 0.8; } /* Extra style for the cancel button (red) */ .cancelbtn { width: auto; padding: 10px 18px; background-color: #f44336; } /* Center the container */ /* Add padding to containers */ .container { padding: 16px; } /* The "Forgot password" text */ span.psw { float: right; padding-top: 16px; } /* Change styles for span and cancel button on extra small screens */ @media screen and (max-width: 300px) { span.psw { display: block; float: none; } .cancelbtn { width: 100%; } } ================================================ FILE: _examples/mvc/login-mvc-single-responsibility/user/auth.go ================================================ package user import ( "errors" "strconv" "strings" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/mvc" "github.com/kataras/iris/v12/sessions" ) const sessionIDKey = "UserID" // paths var ( PathLogin = mvc.Response{Path: "/user/login"} PathLogout = mvc.Response{Path: "/user/logout"} ) // AuthController is the user authentication controller, a custom shared controller. type AuthController struct { // context is auto-binded if struct depends on this, // in this controller we don't we do everything with mvc-style, // and that's neither the 30% of its features. // Ctx iris.Context Source *DataSource Session *sessions.Session // the whole controller is request-scoped because we already depend on Session, so // this will be new for each new incoming request, BeginRequest sets that based on the session. UserID int64 } // BeginRequest saves login state to the context, the user id. func (c *AuthController) BeginRequest(ctx iris.Context) { c.UserID, _ = c.Session.GetInt64(sessionIDKey) } // EndRequest is here just to complete the BaseController // in order to be tell iris to call the `BeginRequest` before the main method. func (c *AuthController) EndRequest(ctx iris.Context) {} func (c *AuthController) fireError(err error) mvc.View { return mvc.View{ Code: iris.StatusBadRequest, Name: "shared/error.html", Data: iris.Map{"Title": "User Error", "Message": strings.ToUpper(err.Error())}, } } func (c *AuthController) redirectTo(id int64) mvc.Response { return mvc.Response{Path: "/user/" + strconv.Itoa(int(id))} } func (c *AuthController) createOrUpdate(firstname, username, password string) (user Model, err error) { username = strings.Trim(username, " ") if username == "" || password == "" || firstname == "" { return user, errors.New("empty firstname, username or/and password") } userToInsert := Model{ Firstname: firstname, Username: username, password: password, } // password is hashed by the Source. newUser, err := c.Source.InsertOrUpdate(userToInsert) if err != nil { return user, err } return newUser, nil } func (c *AuthController) isLoggedIn() bool { // we don't search by session, we have the user id // already by the `BeginRequest` middleware. return c.UserID > 0 } func (c *AuthController) verify(username, password string) (user Model, err error) { if username == "" || password == "" { return user, errors.New("please fill both username and password fields") } u, found := c.Source.GetByUsername(username) if !found { // if user found with that username not found at all. return user, errors.New("user with that username does not exist") } if ok, err := ValidatePassword(password, u.HashedPassword); err != nil || !ok { // if user found but an error occurred or the password is not valid. return user, errors.New("please try to login with valid credentials") } return u, nil } // if logged in then destroy the session // and redirect to the login page // otherwise redirect to the registration page. func (c *AuthController) logout() mvc.Response { if c.isLoggedIn() { c.Session.Destroy() } return PathLogin } ================================================ FILE: _examples/mvc/login-mvc-single-responsibility/user/controller.go ================================================ package user import ( "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/mvc" ) var ( // About Code: iris.StatusSeeOther -> // When redirecting from POST to GET request you -should- use this HTTP status code, // however there're some (complicated) alternatives if you // search online or even the HTTP RFC. // "See Other" RFC 7231 pathMyProfile = mvc.Response{Path: "/user/me", Code: iris.StatusSeeOther} pathRegister = mvc.Response{Path: "/user/register"} ) // Controller is responsible to handle the following requests: // GET /user/register // POST /user/register // GET /user/login // POST /user/login // GET /user/me // GET /user/{id:int64} // All HTTP Methods /user/logout type Controller struct { AuthController } type formValue func(string) string // BeforeActivation called once before the server start // and before the controller's registration, here you can add // dependencies, to this controller and only, that the main caller may skip. func (c *Controller) BeforeActivation(b mvc.BeforeActivation) { // bind the context's `FormValue` as well in order to be // acceptable on the controller or its methods' input arguments (NEW feature as well). b.Dependencies().Register(func(ctx iris.Context) formValue { return ctx.FormValue }) } type page struct { Title string } // GetRegister handles GET:/user/register. // mvc.Result can accept any struct which contains a `Dispatch(ctx iris.Context)` method. // Both mvc.Response and mvc.View are mvc.Result. func (c *Controller) GetRegister() mvc.Result { if c.isLoggedIn() { return c.logout() } // You could just use it as a variable to win some time in serve-time, // this is an exersise for you :) return mvc.View{ Name: pathRegister.Path + ".html", Data: page{"User Registration"}, } } // PostRegister handles POST:/user/register. func (c *Controller) PostRegister(form formValue) mvc.Result { // we can either use the `c.Ctx.ReadForm` or read values one by one. var ( firstname = form("firstname") username = form("username") password = form("password") ) user, err := c.createOrUpdate(firstname, username, password) if err != nil { return c.fireError(err) } // setting a session value was never easier. c.Session.Set(sessionIDKey, user.ID) // succeed, nothing more to do here, just redirect to the /user/me. return pathMyProfile } // with these static views, // you can use variables-- that are initialized before server start // so you can win some time on serving. // You can do it else where as well but I let them as pracise for you, // essentially you can understand by just looking below. var userLoginView = mvc.View{ Name: PathLogin.Path + ".html", Data: page{"User Login"}, } // GetLogin handles GET:/user/login. func (c *Controller) GetLogin() mvc.Result { if c.isLoggedIn() { return c.logout() } return userLoginView } // PostLogin handles POST:/user/login. func (c *Controller) PostLogin(form formValue) mvc.Result { var ( username = form("username") password = form("password") ) user, err := c.verify(username, password) if err != nil { return c.fireError(err) } c.Session.Set(sessionIDKey, user.ID) return pathMyProfile } // AnyLogout handles any method on path /user/logout. func (c *Controller) AnyLogout() { c.logout() } // GetMe handles GET:/user/me. func (c *Controller) GetMe() mvc.Result { id, err := c.Session.GetInt64(sessionIDKey) if err != nil || id <= 0 { // when not already logged in, redirect to login. return PathLogin } u, found := c.Source.GetByID(id) if !found { // if the session exists but for some reason the user doesn't exist in the "database" // then logout him and redirect to the register page. return c.logout() } // set the model and render the view template. return mvc.View{ Name: pathMyProfile.Path + ".html", Data: iris.Map{ "Title": "Profile of " + u.Username, "User": u, }, } } func (c *Controller) renderNotFound(id int64) mvc.View { return mvc.View{ Code: iris.StatusNotFound, Name: "user/notfound.html", Data: iris.Map{ "Title": "User Not Found", "ID": id, }, } } // Dispatch completes the `mvc.Result` interface // in order to be able to return a type of `Model` // as mvc.Result. // If this function didn't exist then // we should explicit set the output result to that Model or to an any. func (u Model) Dispatch(ctx iris.Context) { ctx.JSON(u) } // GetBy handles GET:/user/{id:int64}, // i.e http://localhost:8080/user/1 func (c *Controller) GetBy(userID int64) mvc.Result { // we have /user/{id} // fetch and render user json. user, found := c.Source.GetByID(userID) if !found { // not user found with that ID. return c.renderNotFound(userID) } // Q: how the hell Model can be return as mvc.Result? // A: I told you before on some comments and the docs, // any struct that has a `Dispatch(ctx iris.Context)` // can be returned as an mvc.Result(see ~20 lines above), // therefore we are able to combine many type of results in the same method. // For example, here, we return either an mvc.View to render a not found custom template // either a user which returns the Model as JSON via its Dispatch. // // We could also return just a struct value that is not an mvc.Result, // if the output result of the `GetBy` was that struct's type or an any // and iris would render that with JSON as well, but here we can't do that without complete the `Dispatch` // function, because we may return an mvc.View which is an mvc.Result. return user } ================================================ FILE: _examples/mvc/login-mvc-single-responsibility/user/datasource.go ================================================ package user import ( "errors" "sync" "time" ) // IDGenerator would be our user ID generator // but here we keep the order of users by their IDs // so we will use numbers that can be easly written // to the browser to get results back from the REST API. // var IDGenerator = func() string { // return uuid.NewV4().String() // } // DataSource is our data store example. type DataSource struct { Users map[int64]Model mu sync.RWMutex } // NewDataSource returns a new user data source. func NewDataSource() *DataSource { return &DataSource{ Users: make(map[int64]Model), } } // GetBy receives a query function // which is fired for every single user model inside // our imaginary database. // When that function returns true then it stops the iteration. // // It returns the query's return last known boolean value // and the last known user model // to help callers to reduce the loc. // // But be carefully, the caller should always check for the "found" // because it may be false but the user model has actually real data inside it. // // It's actually a simple but very clever prototype function // I'm think of and using everywhere since then, // hope you find it very useful too. func (d *DataSource) GetBy(query func(Model) bool) (user Model, found bool) { d.mu.RLock() for _, user = range d.Users { found = query(user) if found { break } } d.mu.RUnlock() return } // GetByID returns a user model based on its ID. func (d *DataSource) GetByID(id int64) (Model, bool) { return d.GetBy(func(u Model) bool { return u.ID == id }) } // GetByUsername returns a user model based on the Username. func (d *DataSource) GetByUsername(username string) (Model, bool) { return d.GetBy(func(u Model) bool { return u.Username == username }) } func (d *DataSource) getLastID() (lastID int64) { d.mu.RLock() for id := range d.Users { if id > lastID { lastID = id } } d.mu.RUnlock() return lastID } // InsertOrUpdate adds or updates a user to the (memory) storage. func (d *DataSource) InsertOrUpdate(user Model) (Model, error) { // no matter what we will update the password hash // for both update and insert actions. hashedPassword, err := GeneratePassword(user.password) if err != nil { return user, err } user.HashedPassword = hashedPassword // update if id := user.ID; id > 0 { _, found := d.GetByID(id) if !found { return user, errors.New("ID should be zero or a valid one that maps to an existing User") } d.mu.Lock() d.Users[id] = user d.mu.Unlock() return user, nil } // insert id := d.getLastID() + 1 user.ID = id d.mu.Lock() user.CreatedAt = time.Now() d.Users[id] = user d.mu.Unlock() return user, nil } ================================================ FILE: _examples/mvc/login-mvc-single-responsibility/user/model.go ================================================ package user import ( "time" "golang.org/x/crypto/bcrypt" ) // Model is our User example model. type Model struct { ID int64 `json:"id"` Firstname string `json:"firstname"` Username string `json:"username"` // password is the client-given password // which will not be stored anywhere in the server. // It's here only for actions like registration and update password, // because we caccept a Model instance // inside the `DataSource#InsertOrUpdate` function. password string HashedPassword []byte `json:"-"` CreatedAt time.Time `json:"created_at"` } // GeneratePassword will generate a hashed password for us based on the // user's input. func GeneratePassword(userPassword string) ([]byte, error) { return bcrypt.GenerateFromPassword([]byte(userPassword), bcrypt.DefaultCost) } // ValidatePassword will check if passwords are matched. func ValidatePassword(userPassword string, hashed []byte) (bool, error) { if err := bcrypt.CompareHashAndPassword(hashed, []byte(userPassword)); err != nil { return false, err } return true, nil } ================================================ FILE: _examples/mvc/login-mvc-single-responsibility/views/shared/error.html ================================================

Error.

An error occurred while processing your request.

{{.Message}}

================================================ FILE: _examples/mvc/login-mvc-single-responsibility/views/shared/layout.html ================================================ {{.Title}} {{ yield . }} ================================================ FILE: _examples/mvc/login-mvc-single-responsibility/views/user/login.html ================================================
================================================ FILE: _examples/mvc/login-mvc-single-responsibility/views/user/me.html ================================================

Welcome back {{.User.Firstname}}!

================================================ FILE: _examples/mvc/login-mvc-single-responsibility/views/user/notfound.html ================================================

User with ID {{.ID}} does not exist.

================================================ FILE: _examples/mvc/login-mvc-single-responsibility/views/user/register.html ================================================
================================================ FILE: _examples/mvc/middleware/main.go ================================================ // Package main shows how you can add middleware to an mvc Application, simply // by using its `Router` which is a sub router(an iris.Party) of the main iris app. package main import ( "time" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/cache" "github.com/kataras/iris/v12/mvc" ) var cacheHandler = cache.Handler(10 * time.Second) func main() { app := iris.New() app.Logger().SetLevel("debug") mvc.Configure(app, configure) // http://localhost:8080 // http://localhost:8080/other // // refresh every 10 seconds and you'll see different time output. app.Listen(":8080") } func configure(m *mvc.Application) { m.Router.Use(cacheHandler) m.Handle(&exampleController{ timeFormat: "Mon, Jan 02 2006 15:04:05", } /* ,mvc.IgnoreEmbedded --- Can be used to ignore any embedded struct method handlers */) } type exampleController struct { timeFormat string } func (c *exampleController) Get() string { now := time.Now().Format(c.timeFormat) return "last time executed without cache: " + now } func (c *exampleController) GetOther() string { now := time.Now().Format(c.timeFormat) return "/other: " + now } ================================================ FILE: _examples/mvc/middleware/per-method/main.go ================================================ /* If you want to use it as middleware for the entire controller you can use its router which is just a sub router to add it as you normally do with standard API: I'll show you 4 different methods for adding a middleware into an mvc application, all of those 4 do exactly the same thing, select what you prefer, I prefer the last code-snippet when I need the middleware to be registered somewhere else as well, otherwise I am going with the first one: ```go // 1 mvc.Configure(app.Party("/user"), func(m *mvc.Application) { m.Router.Use(cache.Handler(10*time.Second)) }) ``` ```go // 2 // same: userRouter := app.Party("/user") userRouter.Use(cache.Handler(10*time.Second)) mvc.Configure(userRouter, ...) ``` ```go // 3 // same: userRouter := app.Party("/user", cache.Handler(10*time.Second)) mvc.Configure(userRouter, ...) ``` ```go // 4 // same: app.PartyFunc("/user", func(r iris.Party){ r.Use(cache.Handler(10*time.Second)) mvc.Configure(r, ...) }) ``` If you want to use a middleware for a single route, for a single controller's method that is already registered by the engine and not by custom `Handle` (which you can add the middleware there on the last parameter) and it's not depend on the `Next Handler` to do its job then you just call it on the method: ```go var myMiddleware := myMiddleware.New(...) // this should return an iris/context.Handler type UserController struct{} func (c *UserController) GetSomething(ctx iris.Context) { // ctx.Proceed checks if myMiddleware called `ctx.Next()` // inside it and returns true if so, otherwise false. nextCalled := ctx.Proceed(myMiddleware) if !nextCalled { return } // else do the job here, it's allowed } ``` And last, if you want to add a middleware on a specific method and it depends on the next and the whole chain then you have to do it using the `AfterActivation` like the example below: */ package main import ( "time" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/cache" "github.com/kataras/iris/v12/mvc" ) var cacheHandler = cache.Handler(10 * time.Second) func main() { app := iris.New() // You don't have to use .Configure if you do it all in the main func // mvc.Configure and mvc.New(...).Configure() are just helpers to split // your code better, here we use the simplest form: m := mvc.New(app) m.Handle(&exampleController{}) app.Listen(":8080") } type exampleController struct{} func (c *exampleController) AfterActivation(a mvc.AfterActivation) { // select the route based on the method name you want to // modify. index := a.GetRoute("Get") // just prepend the handler(s) as middleware(s) you want to use. // or append for "done" handlers. index.Handlers = append([]iris.Handler{cacheHandler}, index.Handlers...) } func (c *exampleController) Get() string { // refresh every 10 seconds and you will see different time output. now := time.Now().Format("Mon, Jan 02 2006 15:04:05") return "last time executed without cache: " + now } ================================================ FILE: _examples/mvc/middleware/without-ctx-next/main.go ================================================ /* Package main shows how to add done handlers in an MVC application without the necessity of `ctx.Next()` inside the controller's methods. When we want the `Done` handlers of that specific mvc app's `Party` to be executed but we don't want to add `ctx.Next()` on the `exampleController#EndRequest` */ package main import ( "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/mvc" ) func main() { app := iris.New() app.Get("/", func(ctx iris.Context) { ctx.Redirect("/example") }) exampleRouter := app.Party("/example") { exampleRouter.SetExecutionRules(iris.ExecutionRules{ Done: iris.ExecutionOptions{Force: true}, }) exampleRouter.Done(doneHandler) m := mvc.New(exampleRouter) m.Handle(&exampleController{}) } app.Listen(":8080") } func doneHandler(ctx iris.Context) { ctx.WriteString("\nFrom Done Handler") } type exampleController struct{} func (c *exampleController) Get() string { return "From Main Handler" // Note that here we don't binding the `Context`, and we don't call its `Next()` // function in order to call the `doneHandler`, // this is done automatically for us because we changed the execution rules with the // `SetExecutionRules`. // // Therefore the final output is: // From Main Handler // From Done Handler } ================================================ FILE: _examples/mvc/overview/Dockerfile ================================================ # docker build -t app . # docker run --rm -it -p 8080:8080 app:latest FROM golang:latest AS builder RUN apt-get update ENV GO111MODULE=on \ CGO_ENABLED=0 \ GOOS=linux \ GOARCH=amd64 WORKDIR /go/src/app COPY go.mod . RUN go mod download COPY . . RUN go install FROM scratch COPY --from=builder /go/bin/app . ENTRYPOINT ["./app"] ================================================ FILE: _examples/mvc/overview/README.md ================================================ # Quick start The following guide is just a simple example of usage of some of the **Iris MVC** features. You are not limited to that data structure or code flow. Create a folder, let's assume its path is `app`. The structure should look like that: ``` │ main.go │ go.mod │ go.sum └───environment │ environment.go └───model │ request.go │ response.go └───database │ database.go │ mysql.go │ sqlite.go └───service │ greet_service.go └───controller greet_controller.go ``` Navigate to that `app` folder and execute the following command: ```sh $ go init app $ go get github.com/kataras/iris/v12@main # or @latest for the latest official release. ``` ## Environment Let's start by defining the available environments that our web-application can behave on. We'll just work on two available environments, the "development" and the "production", as they define the two most common scenarios. The `ReadEnv` will read from the `Env` type of a system's environment variable (see `main.go` in the end of the page). Create a `environment/environment.go` file and put the following contents: ```go package environment import ( "os" "strings" ) const ( PROD Env = "production" DEV Env = "development" ) type Env string func (e Env) String() string { return string(e) } func ReadEnv(key string, def Env) Env { v := Getenv(key, def.String()) if v == "" { return def } env := Env(strings.ToLower(v)) switch env { case PROD, DEV: // allowed. default: panic("unexpected environment " + v) } return env } func Getenv(key string, def string) string { if v := os.Getenv(key); v != "" { return v } return def } ``` ## Database We will use two database management systems, the `MySQL` and the `SQLite`. The first one for "production" use and the other for "development". Create a `database/database.go` file and copy-paste the following: ```go package database import "app/environment" type DB interface { Exec(q string) error } func NewDB(env environment.Env) DB { switch env { case environment.PROD: return &mysql{} case environment.DEV: return &sqlite{} default: panic("unknown environment") } } ``` Let's simulate our MySQL and SQLite `DB` instances. Create a `database/mysql.go` file which looks like the following one: ```go package database import "fmt" type mysql struct{} func (db *mysql) Exec(q string) error { return fmt.Errorf("mysql: not implemented <%s>", q) } ``` And a `database/sqlite.go` file. ```go package database type sqlite struct{} func (db *sqlite) Exec(q string) error { return nil } ``` The `DB` depends on the `Environment. > A practical and operational database example, including Docker images, can be found at the following guide: https://github.com/kataras/iris/tree/main/_examples/database/mysql ## Service We'll need a service that will communicate with a database instance in behalf of our Controller(s). In our case we will only need a single service, the Greet Service. For the sake of the example, let's use two implementations of a greet service based on the `Environment`. The `GreetService` interface contains a single method of `Say(input string) (output string, err error)`. Create a `./service/greet_service.go` file and write the following code: ```go package service import ( "fmt" "app/database" "app/environment" ) // GreetService example service. type GreetService interface { Say(input string) (string, error) } // NewGreetService returns a service backed with a "db" based on "env". func NewGreetService(env environment.Env, db database.DB) GreetService { service := &greeter{db: db, prefix: "Hello"} switch env { case environment.PROD: return service case environment.DEV: return &greeterWithLogging{service} default: panic("unknown environment") } } type greeter struct { prefix string db database.DB } func (s *greeter) Say(input string) (string, error) { if err := s.db.Exec("simulate a query..."); err != nil { return "", err } result := s.prefix + " " + input return result, nil } type greeterWithLogging struct { *greeter } func (s *greeterWithLogging) Say(input string) (string, error) { result, err := s.greeter.Say(input) fmt.Printf("result: %s\nerror: %v\n", result, err) return result, err } ``` The `greeter` will be used on "production" and the `greeterWithLogging` on "development". The `GreetService` depends on the `Environment` and the `DB`. ## Models Continue by creating our HTTP request and response models. Create a `model/request.go` file and copy-paste the following code: ```go package model type Request struct { Name string `url:"name"` } ``` Same for the `model/response.go` file. ```go package model type Response struct { Message string `json:"msg"` } ``` The server will accept a URL Query Parameter of `name` (e.g. `/greet?name=kataras`) and will reply back with a JSON message. ## Controller MVC Controllers are responsible for controlling the flow of the application execution. When you make a request (means request a page) to MVC Application, a controller is responsible for returning the response to that request. We will only need the `GreetController` for our mini web-application. Create a file at `controller/greet_controller.go` which looks like that: ```go package controller import ( "app/model" "app/service" ) type GreetController struct { Service service.GreetService // Ctx iris.Context } func (c *GreetController) Get(req model.Request) (model.Response, error) { message, err := c.Service.Say(req.Name) if err != nil { return model.Response{}, err } return model.Response{Message: message}, nil } ``` The `GreetController` depends on the `GreetService`. It serves the `GET: /greet` index path through its `Get` method. The `Get` method accepts a `model.Request` which contains a single field name of `Name` which will be extracted from the `URL Query Parameter 'name'` (because it's a `GET` requst and its `url:"name"` struct field). ## Wrap up ```sh +-------------------+ | Env (DEV, PROD) | +---------+---------+ | | | | | | | | | DEV | | | PROD -------------------+---------------------+ | +----------------------+------------------- | | | | | | +---+-----+ +----------------v------------------+ +----+----+ | sqlite | | NewDB(Env) DB | | mysql | +---+-----+ +----------------+---+--------------+ +----+----+ | | | | | | | | | | | | +--------------+-----+ +-------------------v---v-----------------+ +----+------+ | greeterWithLogging | | NewGreetService(Env, DB) GreetService | | greeter | +--------------+-----+ +---------------------------+-------------+ +----+------+ | | | | | | | +-----------------------------------------+ | | | GreetController | | | | | | | | | | - Service GreetService <-- | | | | | | | +-------------------+---------------------+ | | | | | | | | | | | +-----------+-----------+ | | | HTTP Request | | | +-----------------------+ | | | /greet?name=kataras | | | +-----------+-----------+ | | | | +------------------+--------+ +------------+------------+ +-------+------------------+ | model.Response (JSON) | | Response (JSON, error) | | Bad Request | +---------------------------+ +-------------------------+ +--------------------------+ | { | | mysql: not implemented | | "msg": "Hello kataras" | +--------------------------+ | } | +---------------------------+ ``` Now it's the time to wrap all the above into our `main.go` file. Copy-paste the following code: ```go package main import ( "app/controller" "app/database" "app/environment" "app/service" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/mvc" ) func main() { app := iris.New() app.Get("/ping", pong).Describe("healthcheck") mvc.Configure(app.Party("/greet"), setup) // http://localhost:8080/greet?name=kataras app.Listen(":8080", iris.WithLogLevel("debug")) } func pong(ctx iris.Context) { ctx.WriteString("pong") } func setup(app *mvc.Application) { // Register Dependencies. app.Register( environment.DEV, // DEV, PROD database.NewDB, // sqlite, mysql service.NewGreetService, // greeterWithLogging, greeter ) // Register Controllers. app.Handle(new(controller.GreetController)) } ``` The `mvc.Application.Register` method registers one more dependencies, dependencies can depend on previously registered dependencies too. Thats the reason we pass, first, the `Environment(DEV)`, then the `NewDB` which depends on that `Environment`, following by the `NewGreetService` function which depends on both the `Environment(DEV)` and the `DB`. The `mvc.Application.Handle` registers a new controller, which depends on the `GreetService`, for the targeted sub-router of `Party("/greet")`. ## Run Install [Go](https://golang.org/dl) and run the application with: ```sh go run main.go ```
Docker Download the [Dockerfile](https://raw.githubusercontent.com/kataras/iris/9b93c0dbb491dcedf49c91e89ca13bab884d116f/_examples/mvc/overview/Dockerfile) and [docker-compose.yml](https://raw.githubusercontent.com/kataras/iris/9b93c0dbb491dcedf49c91e89ca13bab884d116f/_examples/mvc/overview/docker-compose.yml) files to the `app` folder. Install [Docker](https://www.docker.com/) and execute the following command: ```sh $ docker-compose up ```
Visit http://localhost:8080?name=kataras. Optionally, replace the `main.go`'s `app.Register(environment.DEV` with `environment.PROD`, restart the application and refresh. You will see that a new database (`sqlite`) and another service of (`greeterWithLogging`) will be binded to the `GreetController`. With **a single change** you achieve to completety change the result. ================================================ FILE: _examples/mvc/overview/controller/greet_controller.go ================================================ package controller import ( "app/model" "app/service" ) // GreetController handles the index. type GreetController struct { Service service.GreetService // Ctx iris.Context } // Get serves [GET] /. // Query: name func (c *GreetController) Get(req model.Request) (model.Response, error) { message, err := c.Service.Say(req.Name) if err != nil { return model.Response{}, err } return model.Response{Message: message}, nil } ================================================ FILE: _examples/mvc/overview/database/database.go ================================================ package database import "app/environment" // DB example database interface. type DB interface { Exec(q string) error } // NewDB returns a database based on "env". func NewDB(env environment.Env) DB { switch env { case environment.PROD: return &mysql{} case environment.DEV: return &sqlite{} default: panic("unknown environment") } } ================================================ FILE: _examples/mvc/overview/database/mysql.go ================================================ package database import "fmt" type mysql struct{} func (db *mysql) Exec(q string) error { // simulate an error response. return fmt.Errorf("mysql: not implemented <%s>", q) } ================================================ FILE: _examples/mvc/overview/database/sqlite.go ================================================ package database type sqlite struct{} func (db *sqlite) Exec(q string) error { return nil } ================================================ FILE: _examples/mvc/overview/docker-compose.yml ================================================ version: '3.1' services: app: build: . ports: - 8080:8080 environment: PORT: 8080 ENVIRONMENT: development restart: on-failure healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/ping"] interval: 30s timeout: 10s retries: 5 ================================================ FILE: _examples/mvc/overview/environment/environment.go ================================================ package environment import ( "os" "strings" ) // Available environments example. const ( PROD Env = "production" DEV Env = "development" ) // Env is the environment type. type Env string // String just returns the string representation of the Env. func (e Env) String() string { return string(e) } // ReadEnv returns the environment of the system environment variable of "key". // Returns the "def" if not found. // Reports a panic message if the environment variable found // but the Env is unknown. func ReadEnv(key string, def Env) Env { v := Getenv(key, def.String()) if v == "" { return def } env := Env(strings.ToLower(v)) switch env { case PROD, DEV: // allowed. default: panic("unexpected environment " + v) } return env } // Getenv returns the value of a system environment variable "key". // Defaults to "def" if not found. func Getenv(key string, def string) string { if v := os.Getenv(key); v != "" { return v } return def } ================================================ FILE: _examples/mvc/overview/go.mod ================================================ module app go 1.25 require github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd require ( github.com/BurntSushi/toml v1.6.0 // indirect github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect github.com/CloudyKit/jet/v6 v6.3.1 // indirect github.com/Joker/jade v1.1.3 // indirect github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 // indirect github.com/andybalholm/brotli v1.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/fatih/structs v1.1.0 // indirect github.com/flosch/pongo2/v4 v4.0.2 // indirect github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/ws v1.4.0 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/iris-contrib/schema v0.0.6 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/kataras/blocks v0.0.8 // indirect github.com/kataras/golog v0.1.12 // indirect github.com/kataras/neffos v0.0.24 // indirect github.com/kataras/pio v0.0.13 // indirect github.com/kataras/sitemap v0.0.6 // indirect github.com/kataras/tunnel v0.0.4 // indirect github.com/klauspost/compress v1.18.2 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mailgun/raymond/v2 v2.0.48 // indirect github.com/mailru/easyjson v0.9.1 // indirect github.com/mediocregopher/radix/v3 v3.8.1 // indirect github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/nats-io/nats.go v1.40.1 // indirect github.com/nats-io/nkeys v0.4.9 // indirect github.com/nats-io/nuid v1.0.1 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/schollz/closestmatch v2.1.0+incompatible // indirect github.com/sirupsen/logrus v1.8.1 // indirect github.com/stretchr/testify v1.11.1 // indirect github.com/tdewolff/minify/v2 v2.24.8 // indirect github.com/tdewolff/parse/v2 v2.8.5 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/yosssi/ace v0.0.5 // indirect golang.org/x/crypto v0.47.0 // indirect golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect golang.org/x/net v0.49.0 // indirect golang.org/x/sys v0.40.0 // indirect golang.org/x/text v0.33.0 // indirect golang.org/x/time v0.14.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: _examples/mvc/overview/go.sum ================================================ github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= github.com/CloudyKit/jet/v6 v6.3.1 h1:6IAo5Cx21xrHVaR8zzXN5gJatKV/wO7Nf6bfCnCSbUw= github.com/CloudyKit/jet/v6 v6.3.1/go.mod h1:lf8ksdNsxZt7/yH/3n4vJQWA9RUq4wpaHtArHhGVMOw= github.com/Joker/hpp v1.0.0 h1:65+iuJYdRXv/XyN62C1uEmmOx3432rNG/rKlX6V7Kkc= github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= github.com/Joker/jade v1.1.3 h1:Qbeh12Vq6BxURXT1qZBRHsDxeURB8ztcL6f3EXSGeHk= github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKduM= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 h1:dG7/gLroJGht/jSQtHiLvT48Hxn+crbmvyItZC8cWOs= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05/go.mod h1:NYezi6wtnJtBm5btoprXc5SvAdqH0XTXWnUup0MptAI= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/flosch/pongo2/v4 v4.0.2 h1:gv+5Pe3vaSVmiJvh/BZa82b7/00YUGm0PIyVVLop0Hw= github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+BWPhwZD8= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a h1:l7A0loSszR5zHd/qK53ZIHMO8b3bBSmENnQ6eKnUT0A= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/iris-contrib/httpexpect/v2 v2.15.2 h1:T9THsdP1woyAqKHwjkEsbCnMefsAFvk8iJJKokcJ3Go= github.com/iris-contrib/httpexpect/v2 v2.15.2/go.mod h1:JLDgIqnFy5loDSUv1OA2j0mb6p/rDhiCqigP22Uq9xE= github.com/iris-contrib/schema v0.0.6 h1:CPSBLyx2e91H2yJzPuhGuifVRnZBBJ3pCOMbOvPZaTw= github.com/iris-contrib/schema v0.0.6/go.mod h1:iYszG0IOsuIsfzjymw1kMzTL8YQcCWlm65f3wX8J5iA= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kataras/blocks v0.0.8 h1:MrpVhoFTCR2v1iOOfGng5VJSILKeZZI+7NGfxEh3SUM= github.com/kataras/blocks v0.0.8/go.mod h1:9Jm5zx6BB+06NwA+OhTbHW1xkMOYxahnqTN5DveZ2Yg= github.com/kataras/golog v0.1.12 h1:Bu7I/G4ilJlbfzjmU39O9N+2uO1pBcMK045fzZ4ytNg= github.com/kataras/golog v0.1.12/go.mod h1:wrGSbOiBqbQSQznleVNX4epWM8rl9SJ/rmEacl0yqy4= github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd h1:oMsQgqKACbUdlaIWnN+EZzzAnHkw90hE/y/R0BSij2U= github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd/go.mod h1:yucjtDl9Vn3OctWM1fi0MuM9OckemC7kEreFa9QtPF8= github.com/kataras/neffos v0.0.24 h1:S3lHqJopCfXN285VdlbGeOj+Id83u4xdQKToa+w1vW0= github.com/kataras/neffos v0.0.24/go.mod h1:/3K9zQ0yEC5/xUiSQx46ToWa3xneGfUo/nMit/F5g+U= github.com/kataras/pio v0.0.13 h1:x0rXVX0fviDTXOOLOmr4MUxOabu1InVSTu5itF8CXCM= github.com/kataras/pio v0.0.13/go.mod h1:k3HNuSw+eJ8Pm2lA4lRhg3DiCjVgHlP8hmXApSej3oM= github.com/kataras/sitemap v0.0.6 h1:w71CRMMKYMJh6LR2wTgnk5hSgjVNB9KL60n5e2KHvLY= github.com/kataras/sitemap v0.0.6/go.mod h1:dW4dOCNs896OR1HmG+dMLdT7JjDk7mYBzoIRwuj5jA4= github.com/kataras/tunnel v0.0.4 h1:sCAqWuJV7nPzGrlb0os3j49lk2JhILT0rID38NHNLpA= github.com/kataras/tunnel v0.0.4/go.mod h1:9FkU4LaeifdMWqZu7o20ojmW4B7hdhv2CMLwfnHGpYw= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw= github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18= github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mediocregopher/radix/v3 v3.8.1 h1:rOkHflVuulFKlwsLY01/M2cM2tWCjDoETcMqKbAWu1M= github.com/mediocregopher/radix/v3 v3.8.1/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/nats-io/nats.go v1.40.1 h1:MLjDkdsbGUeCMKFyCFoLnNn/HDTqcgVa3EQm+pMNDPk= github.com/nats-io/nats.go v1.40.1/go.mod h1:wV73x0FSI/orHPSYoyMeJB+KajMDoWyXmFaRrrYaaTo= github.com/nats-io/nkeys v0.4.9 h1:qe9Faq2Gxwi6RZnZMXfmGMZkg3afLLOtrU+gDZJ35b0= github.com/nats-io/nkeys v0.4.9/go.mod h1:jcMqs+FLG+W5YO36OX6wFIFcmpdAns+w1Wm6D3I/evE= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo= github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk= github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tdewolff/minify/v2 v2.24.8 h1:58/VjsbevI4d5FGV0ZSuBrHMSSkH4MCH0sIz/eKIauE= github.com/tdewolff/minify/v2 v2.24.8/go.mod h1:0Ukj0CRpo/sW/nd8uZ4ccXaV1rEVIWA3dj8U7+Shhfw= github.com/tdewolff/parse/v2 v2.8.5 h1:ZmBiA/8Do5Rpk7bDye0jbbDUpXXbCdc3iah4VeUvwYU= github.com/tdewolff/parse/v2 v2.8.5/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo= github.com/tdewolff/test v1.0.11 h1:FdLbwQVHxqG16SlkGveC0JVyrJN62COWTRyUFzfbtBE= github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA= github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0= github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs= moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHcE= ================================================ FILE: _examples/mvc/overview/main.go ================================================ package main import ( "app/controller" "app/database" "app/environment" "app/service" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/mvc" ) func main() { app := iris.New() app.Logger().SetLevel("debug") app.Get("/ping", pong).Describe("healthcheck") mvc.Configure(app.Party("/greet"), setup) // http://localhost:8080/greet?name=kataras addr := ":" + environment.Getenv("PORT", "8080") app.Listen(addr) } func pong(ctx iris.Context) { ctx.WriteString("pong") } /* +-------------------+ | Env (DEV, PROD) | +---------+---------+ | | | | | | | | | DEV | | | PROD -------------------+---------------------+ | +----------------------+------------------- | | | | | | +---+-----+ +----------------v------------------+ +----+----+ | sqlite | | NewDB(Env) DB | | mysql | +---+-----+ +----------------+---+--------------+ +----+----+ | | | | | | | | | | | | +--------------+-----+ +-------------------v---v-----------------+ +----+------+ | greeterWithLogging | | NewGreetService(Env, DB) GreetService | | greeter | +--------------+-----+ +---------------------------+-------------+ +----+------+ | | | | | | | +-----------------------------------------+ | | | GreetController | | | | | | | | | | - Service GreetService <-- | | | | | | | +-------------------+---------------------+ | | | | | | | | | | | +-----------+-----------+ | | | HTTP Request | | | +-----------------------+ | | | /greet?name=kataras | | | +-----------+-----------+ | | | | +------------------+--------+ +------------+------------+ +-------+------------------+ | model.Response (JSON) | | Response (JSON, error) | | Bad Request | +---------------------------+ +-------------------------+ +--------------------------+ | { | | mysql: not implemented | | "msg": "Hello kataras" | +--------------------------+ | } | +---------------------------+ */ func setup(app *mvc.Application) { // Register Dependencies. // Tip: A dependency can depend on other dependencies too. env := environment.ReadEnv("ENVIRONMENT", environment.DEV) app.Register( env, // DEV, PROD database.NewDB, // sqlite, mysql service.NewGreetService, // greeterWithLogging, greeter ) // Register Controllers. app.Handle(new(controller.GreetController)) } ================================================ FILE: _examples/mvc/overview/model/request.go ================================================ package model // Request example incoming request. type Request struct { Name string `json:"name" url:"name"` } ================================================ FILE: _examples/mvc/overview/model/response.go ================================================ package model // Response example server's response. type Response struct { Message string `json:"msg"` } ================================================ FILE: _examples/mvc/overview/service/greet_service.go ================================================ package service import ( "fmt" "app/database" "app/environment" ) // GreetService example service. type GreetService interface { Say(input string) (string, error) } // NewGreetService returns a service backed with a "db" based on "env". func NewGreetService(env environment.Env, db database.DB) GreetService { service := &greeter{db: db, prefix: "Hello"} switch env { case environment.PROD: return service case environment.DEV: return &greeterWithLogging{service} default: panic("unknown environment") } } type greeter struct { prefix string db database.DB } func (s *greeter) Say(input string) (string, error) { if err := s.db.Exec("simulate a query..."); err != nil { return "", err } result := s.prefix + " " + input return result, nil } type greeterWithLogging struct { *greeter } func (s *greeterWithLogging) Say(input string) (string, error) { result, err := s.greeter.Say(input) fmt.Printf("result: %s\nerror: %v\n", result, err) return result, err } ================================================ FILE: _examples/mvc/regexp/main.go ================================================ // Package main shows how to match "/xxx.json" in MVC handler. package main /* There is no MVC naming pattern for such these things,you can imagine the limitations of that. Instead you can use the `BeforeActivation` on your controller to add more advanced routing features (https://github.com/kataras/iris/tree/main/_examples/routing). You can also create your own macro, i.e: /{file:json} or macro function of a specific parameter type i.e: (/{file:string json()}). Read the routing examples and you will gain a deeper view, there are all covered. */ import ( "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/mvc" ) func main() { app := iris.New() mvcApp := mvc.New(app.Party("/module")) mvcApp.Handle(new(myController)) // http://localhost:8080/module/xxx.json (OK) // http://localhost:8080/module/xxx.xml (Not Found) app.Listen(":8080") } type myController struct{} func (m *myController) BeforeActivation(b mvc.BeforeActivation) { // b.Dependencies().Register // b.Router().Use/UseGlobal/Done // and any standard API call you already know // 1-> Method // 2-> Path // 3-> The controller's function name to be parsed as handler // 4-> Any handlers that should run before the HandleJSON // "^[a-zA-Z0-9_.-]+.json$)" to validate file-name pattern and json // or just: ".json$" to validate suffix. b.Handle("GET", "/{file:string regexp(^[a-zA-Z0-9_.-]+.json$))}", "HandleJSON" /*optionalMiddleware*/) } func (m *myController) HandleJSON(file string) string { return "custom serving of json: " + file } ================================================ FILE: _examples/mvc/repository/datamodels/README.md ================================================ # Data Model Layer ================================================ FILE: _examples/mvc/repository/datamodels/movie.go ================================================ // file: datamodels/movie.go package datamodels // Movie is our sample data structure. // Keep note that the tags for public-use (for our web app) // should be kept in other file like "web/viewmodels/movie.go" // which could wrap by embedding the datamodels.Movie or // declare new fields instead butwe will use this datamodel // as the only one Movie model in our application, // for the sake of simplicty. type Movie struct { ID int64 `json:"id"` Name string `json:"name"` Year int `json:"year"` Genre string `json:"genre"` Poster string `json:"poster"` } ================================================ FILE: _examples/mvc/repository/datasource/README.md ================================================ # Data Source / Data Store Layer ================================================ FILE: _examples/mvc/repository/datasource/movies.go ================================================ // file: datasource/movies.go package datasource import "github.com/kataras/iris/v12/_examples/mvc/repository/datamodels" // Movies is our imaginary data source. var Movies = map[int64]datamodels.Movie{ 1: { ID: 1, Name: "Casablanca", Year: 1942, Genre: "Romance", Poster: "https://iris-go.com/static/images/examples/mvc-movies/1.jpg", }, 2: { ID: 2, Name: "Gone with the Wind", Year: 1939, Genre: "Romance", Poster: "https://iris-go.com/static/images/examples/mvc-movies/2.jpg", }, 3: { ID: 3, Name: "Citizen Kane", Year: 1941, Genre: "Mystery", Poster: "https://iris-go.com/static/images/examples/mvc-movies/3.jpg", }, 4: { ID: 4, Name: "The Wizard of Oz", Year: 1939, Genre: "Fantasy", Poster: "https://iris-go.com/static/images/examples/mvc-movies/4.jpg", }, 5: { ID: 5, Name: "North by Northwest", Year: 1959, Genre: "Thriller", Poster: "https://iris-go.com/static/images/examples/mvc-movies/5.jpg", }, } ================================================ FILE: _examples/mvc/repository/main.go ================================================ // file: main.go package main import ( "github.com/kataras/iris/v12/_examples/mvc/repository/datasource" "github.com/kataras/iris/v12/_examples/mvc/repository/repositories" "github.com/kataras/iris/v12/_examples/mvc/repository/services" "github.com/kataras/iris/v12/_examples/mvc/repository/web/controllers" "github.com/kataras/iris/v12/_examples/mvc/repository/web/middleware" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/mvc" ) func main() { app := iris.New() app.Logger().SetLevel("debug") // Load the template files. app.RegisterView(iris.HTML("./web/views", ".html")) // Serve our controllers. mvc.New(app.Party("/hello")).Handle(new(controllers.HelloController)) // You can also split the code you write to configure an mvc.Application // using the `mvc.Configure` method, as shown below. mvc.Configure(app.Party("/movies"), movies) // http://localhost:8080/hello // http://localhost:8080/hello/iris // http://localhost:8080/movies // http://localhost:8080/movies/1 app.Listen(":8080", iris.WithOptimizations) } // note the mvc.Application, it's not iris.Application. func movies(app *mvc.Application) { // Add the basic authentication(admin:password) middleware // for the /movies based requests. app.Router.Use(middleware.BasicAuth) // Create our movie repository with some (memory) data from the datasource. repo := repositories.NewMovieRepository(datasource.Movies) // Create our movie service, we will bind it to the movie app's dependencies. movieService := services.NewMovieService(repo) app.Register(movieService) // serve our movies controller. // Note that you can serve more than one controller // you can also create child mvc apps using the `movies.Party(relativePath)` or `movies.Clone(app.Party(...))` // if you want. app.Handle(new(controllers.MovieController)) } ================================================ FILE: _examples/mvc/repository/models/README.md ================================================ # Domain Models There should be the domain/business-level models. Example: ```go import "github.com/kataras/iris/v12/_examples/mvc/repository/datamodels" type Movie struct { datamodels.Movie } func (m Movie) Validate() (Movie, error) { /* do some checks and return an error if that Movie is not valid */ } ``` However, we will use the "datamodels" as the only one models package because Movie structure we don't need any extra functionality or validation inside it. ================================================ FILE: _examples/mvc/repository/repositories/README.md ================================================ # Repositories The package which has direct access to the "datasource" and can manipulate data directly. ================================================ FILE: _examples/mvc/repository/repositories/movie_repository.go ================================================ // file: repositories/movie_repository.go package repositories import ( "errors" "sync" "github.com/kataras/iris/v12/_examples/mvc/repository/datamodels" ) // Query represents the visitor and action queries. type Query func(datamodels.Movie) bool // MovieRepository handles the basic operations of a movie entity/model. // It's an interface in order to be testable, i.e a memory movie repository or // a connected to an sql database. type MovieRepository interface { Exec(query Query, action Query, limit int, mode int) (ok bool) Select(query Query) (movie datamodels.Movie, found bool) SelectMany(query Query, limit int) (results []datamodels.Movie) InsertOrUpdate(movie datamodels.Movie) (updatedMovie datamodels.Movie, err error) Delete(query Query, limit int) (deleted bool) } // NewMovieRepository returns a new movie memory-based repository, // the one and only repository type in our example. func NewMovieRepository(source map[int64]datamodels.Movie) MovieRepository { return &movieMemoryRepository{source: source} } // movieMemoryRepository is a "MovieRepository" // which manages the movies using the memory data source (map). type movieMemoryRepository struct { source map[int64]datamodels.Movie mu sync.RWMutex } const ( // ReadOnlyMode will RLock(read) the data . ReadOnlyMode = iota // ReadWriteMode will Lock(read/write) the data. ReadWriteMode ) func (r *movieMemoryRepository) Exec(query Query, action Query, actionLimit int, mode int) (ok bool) { loops := 0 if mode == ReadOnlyMode { r.mu.RLock() defer r.mu.RUnlock() } else { r.mu.Lock() defer r.mu.Unlock() } for _, movie := range r.source { ok = query(movie) if ok { if action(movie) { loops++ if actionLimit >= loops { break // break } } } } return } // Select receives a query function // which is fired for every single movie model inside // our imaginary data source. // When that function returns true then it stops the iteration. // // It returns the query's return last known "found" value // and the last known movie model // to help callers to reduce the LOC. // // It's actually a simple but very clever prototype function // I'm using everywhere since I firstly think of it, // hope you'll find it very useful as well. func (r *movieMemoryRepository) Select(query Query) (movie datamodels.Movie, found bool) { found = r.Exec(query, func(m datamodels.Movie) bool { movie = m return true }, 1, ReadOnlyMode) // set an empty datamodels.Movie if not found at all. if !found { movie = datamodels.Movie{} } return } // SelectMany same as Select but returns one or more datamodels.Movie as a slice. // If limit <=0 then it returns everything. func (r *movieMemoryRepository) SelectMany(query Query, limit int) (results []datamodels.Movie) { r.Exec(query, func(m datamodels.Movie) bool { results = append(results, m) return true }, limit, ReadOnlyMode) return } // InsertOrUpdate adds or updates a movie to the (memory) storage. // // Returns the new movie and an error if any. func (r *movieMemoryRepository) InsertOrUpdate(movie datamodels.Movie) (datamodels.Movie, error) { id := movie.ID if id == 0 { // Create new action var lastID int64 // find the biggest ID in order to not have duplications // in productions apps you can use a third-party // library to generate a UUID as string. r.mu.RLock() for _, item := range r.source { if item.ID > lastID { lastID = item.ID } } r.mu.RUnlock() id = lastID + 1 movie.ID = id // map-specific thing r.mu.Lock() r.source[id] = movie r.mu.Unlock() return movie, nil } // Update action based on the movie.ID, // here we will allow updating the poster and genre if not empty. // Alternatively we could do pure replace instead: // r.source[id] = movie // and comment the code below; current, exists := r.Select(func(m datamodels.Movie) bool { return m.ID == id }) if !exists { // ID is not a real one, return an error. return datamodels.Movie{}, errors.New("failed to update a nonexistent movie") } // or comment these and r.source[id] = m for pure replace if movie.Poster != "" { current.Poster = movie.Poster } if movie.Genre != "" { current.Genre = movie.Genre } // map-specific thing r.mu.Lock() r.source[id] = current r.mu.Unlock() return movie, nil } func (r *movieMemoryRepository) Delete(query Query, limit int) bool { return r.Exec(query, func(m datamodels.Movie) bool { delete(r.source, m.ID) return true }, limit, ReadWriteMode) } ================================================ FILE: _examples/mvc/repository/services/README.md ================================================ # Service Layer The package which has access to call functions from the "repositories" and "models" ("datamodels" only in that simple example). It should contain the domain logic. ================================================ FILE: _examples/mvc/repository/services/movie_service.go ================================================ // file: services/movie_service.go package services import ( "github.com/kataras/iris/v12/_examples/mvc/repository/datamodels" "github.com/kataras/iris/v12/_examples/mvc/repository/repositories" ) // MovieService handles some of the CRUID operations of the movie datamodel. // It depends on a movie repository for its actions. // It's here to decouple the data source from the higher level compoments. // As a result a different repository type can be used with the same logic without any aditional changes. // It's an interface and it's used as interface everywhere // because we may need to change or try an experimental different domain logic at the future. type MovieService interface { GetAll() []datamodels.Movie GetByID(id int64) (datamodels.Movie, bool) DeleteByID(id int64) bool UpdatePosterAndGenreByID(id int64, poster string, genre string) (datamodels.Movie, error) } // NewMovieService returns the default movie service. func NewMovieService(repo repositories.MovieRepository) MovieService { return &movieService{ repo: repo, } } type movieService struct { repo repositories.MovieRepository } // GetAll returns all movies. func (s *movieService) GetAll() []datamodels.Movie { return s.repo.SelectMany(func(_ datamodels.Movie) bool { return true }, -1) } // GetByID returns a movie based on its id. func (s *movieService) GetByID(id int64) (datamodels.Movie, bool) { return s.repo.Select(func(m datamodels.Movie) bool { return m.ID == id }) } // UpdatePosterAndGenreByID updates a movie's poster and genre. func (s *movieService) UpdatePosterAndGenreByID(id int64, poster string, genre string) (datamodels.Movie, error) { // update the movie and return it. return s.repo.InsertOrUpdate(datamodels.Movie{ ID: id, Poster: poster, Genre: genre, }) } // DeleteByID deletes a movie by its id. // // Returns true if deleted otherwise false. func (s *movieService) DeleteByID(id int64) bool { return s.repo.Delete(func(m datamodels.Movie) bool { return m.ID == id }, 1) } ================================================ FILE: _examples/mvc/repository/web/controllers/hello_controller.go ================================================ // file: web/controllers/hello_controller.go package controllers import ( "errors" "github.com/kataras/iris/v12/mvc" ) // HelloController is our sample controller // it handles GET: /hello and GET: /hello/{name} type HelloController struct{} var helloView = mvc.View{ Name: "hello/index.html", Data: map[string]any{ "Title": "Hello Page", "MyMessage": "Welcome to my awesome website", }, } // Get will return a predefined view with bind data. // // `mvc.Result` is just an interface with a `Dispatch` function. // `mvc.Response` and `mvc.View` are the builtin result type dispatchers // you can even create custom response dispatchers by // implementing the `github.com/kataras/iris/hero#Result` interface. func (c *HelloController) Get() mvc.Result { return helloView } // you can define a standard error in order to re-use anywhere in your app. var errBadName = errors.New("bad name") // you can just return it as error or even better // wrap this error with an mvc.Response to make it an mvc.Result compatible type. var badName = mvc.Response{Err: errBadName, Code: 400} // GetBy returns a "Hello {name}" response. // Demos: // curl -i http://localhost:8080/hello/iris // curl -i http://localhost:8080/hello/anything func (c *HelloController) GetBy(name string) mvc.Result { if name != "iris" { return badName // or // GetBy(name string) (mvc.Result, error) { // return nil, errBadName // } } // return mvc.Response{Text: "Hello " + name} OR: return mvc.View{ Name: "hello/name.html", Data: name, } } ================================================ FILE: _examples/mvc/repository/web/controllers/movie_controller.go ================================================ // file: web/controllers/movie_controller.go package controllers import ( "errors" "github.com/kataras/iris/v12/_examples/mvc/repository/datamodels" "github.com/kataras/iris/v12/_examples/mvc/repository/services" "github.com/kataras/iris/v12" ) // MovieController is our /movies controller. type MovieController struct { // Our MovieService, it's an interface which // is binded from the main application. Service services.MovieService } // Get returns list of the movies. // Demo: // curl -i http://localhost:8080/movies // // The correct way if you have sensitive data: // // func (c *MovieController) Get() (results []viewmodels.Movie) { // data := c.Service.GetAll() // // for _, movie := range data { // results = append(results, viewmodels.Movie{movie}) // } // return // } // // otherwise just return the datamodels. func (c *MovieController) Get() (results []datamodels.Movie) { return c.Service.GetAll() } // GetBy returns a movie. // Demo: // curl -i http://localhost:8080/movies/1 func (c *MovieController) GetBy(id int64) (movie datamodels.Movie, found bool) { return c.Service.GetByID(id) // it will throw 404 if not found. } // PutBy updates a movie. // Demo: // curl -i -X PUT -F "genre=Thriller" -F "poster=@/Users/kataras/Downloads/out.gif" http://localhost:8080/movies/1 func (c *MovieController) PutBy(ctx iris.Context, id int64) (datamodels.Movie, error) { // get the request data for poster and genre file, info, err := ctx.FormFile("poster") if err != nil { return datamodels.Movie{}, errors.New("failed due form file 'poster' missing") } // we don't need the file so close it now. file.Close() // imagine that is the url of the uploaded file... poster := info.Filename genre := ctx.FormValue("genre") return c.Service.UpdatePosterAndGenreByID(id, poster, genre) } // DeleteBy deletes a movie. // Demo: // curl -i -X DELETE -u admin:password http://localhost:8080/movies/1 func (c *MovieController) DeleteBy(id int64) any { wasDel := c.Service.DeleteByID(id) if wasDel { // return the deleted movie's ID return iris.Map{"deleted": id} } // right here we can see that a method function can return any of those two types(map or int), // we don't have to specify the return type to a specific type. return iris.StatusBadRequest } ================================================ FILE: _examples/mvc/repository/web/middleware/basicauth.go ================================================ // file: web/middleware/basicauth.go package middleware import "github.com/kataras/iris/v12/middleware/basicauth" // BasicAuth middleware sample. var BasicAuth = basicauth.Default(map[string]string{ "admin": "password", }) ================================================ FILE: _examples/mvc/repository/web/viewmodels/README.md ================================================ # View Models There should be the view models, the structure that the client will be able to see. Example: ```go import ( "github.com/kataras/iris/v12/_examples/mvc/repository/datamodels" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/context" ) type Movie struct { datamodels.Movie } func (m Movie) IsValid() bool { /* do some checks and return true if it's valid... */ return m.ID > 0 } ``` Iris is able to convert any custom data Structure into an HTTP Response Dispatcher, so theoretically, something like the following is permitted if it's really necessary; ```go // Dispatch completes the `kataras/iris/mvc#Result` interface. // Sends a `Movie` as a controlled http response. // If its ID is zero or less then it returns a 404 not found error // else it returns its json representation, // (just like the controller's functions do for custom types by default). // // Don't overdo it, the application's logic should not be here. // It's just one more step of validation before the response, // simple checks can be added here. // // It's just a showcase, // imagine the potentials this feature gives when designing a bigger application. // // This is called where the return value from a controller's method functions // is type of `Movie`. // For example the `controllers/movie_controller.go#GetBy`. func (m Movie) Dispatch(ctx iris.Context) { if !m.IsValid() { ctx.NotFound() return } ctx.JSON(m, context.JSON{Indent: " "}) } ``` However, we will use the "datamodels" as the only one models package because Movie structure doesn't contain any sensitive data, clients are able to see all of its fields and we don't need any extra functionality or validation inside it. ================================================ FILE: _examples/mvc/repository/web/views/hello/index.html ================================================ {{.Title}} - My App

{{.MyMessage}}

================================================ FILE: _examples/mvc/repository/web/views/hello/name.html ================================================ {{.}}' Portfolio - My App

Hello {{.}}

================================================ FILE: _examples/mvc/request-default-values/main.go ================================================ package main import ( "fmt" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/mvc" ) // https://github.com/kataras/iris/issues/1706 // When you need that type of logic behind a request input, // e.g. set default values, the right way to do that is // to register a request-scoped dependency for that type. // We have the `Context.ReadQuery(pointer)` // which you can use to bind a struct value, that value can have default values. // Here is how you could do that: func main() { app := iris.New() mvcApp := mvc.New(app) { mvcApp.Register(paramsDependency) mvcApp.Handle(new(controller)) } // http://localhost:8080/records?phone=random&order_by=DESC // http://localhost:8080/records?phone=random app.Listen(":8080") } type params struct { CallID string `url:"phone"` ComDir int `url:"dir"` CaseUserID string `url:"on"` StartYear int `url:"sy"` EndYear int `url:"ey"` OrderBy string `url:"order_by"` Offset int `url:"offset"` } // As we've read in the previous examples, the paramsDependency // describes a request-scoped dependency. // It should accept the iris context (or any previously registered or builtin dependency) // and it should return the value which will be binded to the // controller's methods (or fields) - see `GetRecords`. var paramsDependency = func(ctx iris.Context) params { p := params{ OrderBy: "ASC", // default value. } // Bind the URL values to the "p": ctx.ReadQuery(&p) // Or bind a specific URL value by giving a default value: // p.OrderBy = ctx.URLParamDefault("order_by", "ASC") // // OR make checks for default values after ReadXXX, // e.g. if p.OrderBy == "" {...} /* More options to read a request: // Bind the JSON request body to the "p": ctx.ReadJSON(&p) // Bind the Form to the "p": ctx.ReadForm(&p) // Bind any, based on the client's content-type header: ctx.ReadBody(&p) // Bind the http requests to a struct value: ctx.ReadHeader(&h) */ return p } type controller struct{} func (c *controller) GetRecords(stru params) string { return fmt.Sprintf("%#+v\n", stru) } ================================================ FILE: _examples/mvc/session-controller/main.go ================================================ package main import ( "fmt" "time" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/mvc" "github.com/kataras/iris/v12/sessions" ) // VisitController handles the root route. type VisitController struct { // the current request session, automatically binded. Session *sessions.Session // A time.time which is binded from the MVC application manually. StartTime time.Time } // Get handles index // Method: GET // Path: http://localhost:8080 func (c *VisitController) Get() string { // it increments a "visits" value of integer by one, // if the entry with key 'visits' doesn't exist it will create it for you. visits := c.Session.Increment("visits", 1) // write the current, updated visits. since := time.Now().Sub(c.StartTime).Seconds() return fmt.Sprintf("%d visit(s) from my current session in %0.1f seconds of server's up-time", visits, since) } func newApp() *iris.Application { app := iris.New() // Configure sessions manager as we used to. sess := sessions.New(sessions.Config{Cookie: "mysession_cookie_name"}) app.Use(sess.Handler()) visitApp := mvc.New(app) visitApp.Register(time.Now()) // The `VisitController.Session` is automatically binded to the current `sessions.Session`. visitApp.Handle(new(VisitController)) return app } func main() { app := newApp() // 1. Prepare a client, e.g. your browser // 2. navigate to http://localhost:8080 // 3. refresh the page some times // 4. close the browser // 5. re-open the browser (if it wasn't in private mode) and re-play 2. app.Listen(":8080") } ================================================ FILE: _examples/mvc/session-controller/main_test.go ================================================ package main import ( "testing" "github.com/kataras/iris/v12/httptest" ) func TestMVCSession(t *testing.T) { e := httptest.New(t, newApp(), httptest.URL("http://example.com")) e1 := e.GET("/").Expect().Status(httptest.StatusOK) e1.Cookies().NotEmpty() e1.Body().Contains("1 visit") e.GET("/").Expect().Status(httptest.StatusOK). Body().Contains("2 visit") e.GET("/").Expect().Status(httptest.StatusOK). Body().Contains("3 visit") } ================================================ FILE: _examples/mvc/singleton/main.go ================================================ package main import ( "fmt" "sync/atomic" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/mvc" ) func main() { app := iris.New() mvc.New(app.Party("/")).Handle(&globalVisitorsController{visits: 0}) // http://localhost:8080 app.Listen(":8080") } type globalVisitorsController struct { // When a singleton controller is used then concurent safe access is up to the developers, because // all clients share the same controller instance instead. // Note that any controller's methods // are per-client, but the struct's field can be shared across multiple clients if the structure // does not have any dynamic struct field dependencies that depend on the iris.Context // and ALL field's values are NOT zero, at this case we use uint64 which it's no zero (even if we didn't set it // manually ease-of-understand reasons) because it's a value of &{0}. // All the above declares a Singleton, note that you don't have to write a single line of code to do this, Iris is smart enough. // // see `Get`. visits uint64 } func (c *globalVisitorsController) Get() string { count := atomic.AddUint64(&c.visits, 1) return fmt.Sprintf("Total visitors: %d", count) } ================================================ FILE: _examples/mvc/versioned-controller/main.go ================================================ package main import ( "time" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/mvc" ) // Optional deprecated X-API-XXX headers for version 1. var opts = mvc.DeprecationOptions{ WarnMessage: "deprecated, see ", DeprecationDate: time.Now().UTC(), DeprecationInfo: "a bigger version is available, see for more information", } func main() { app := newApp() // See main_test.go for request examples. app.Listen(":8080") } func newApp() *iris.Application { app := iris.New() dataRouter := app.Party("/data") { m := mvc.New(dataRouter) m.Handle(new(v1Controller), mvc.Version("1.0.0"), mvc.Deprecated(opts)) m.Handle(new(v2Controller), mvc.Version("2.3.0")) m.Handle(new(v3Controller), mvc.Version(">=3.0.0 <4.0.0")) m.Handle(new(noVersionController)) // or if missing it will respond with 501 version not found. } return app } type v1Controller struct{} func (c *v1Controller) Get() string { return "data (v1.x)" } type v2Controller struct{} func (c *v2Controller) Get() string { return "data (v2.x)" } type v3Controller struct{} func (c *v3Controller) Get() string { return "data (v3.x)" } type noVersionController struct{} func (c *noVersionController) Get() string { return "data" } ================================================ FILE: _examples/mvc/versioned-controller/main_test.go ================================================ package main import ( "testing" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/httptest" "github.com/kataras/iris/v12/versioning" ) func TestVersionedController(t *testing.T) { app := newApp() e := httptest.New(t, app) e.GET("/data").WithHeader(versioning.AcceptVersionHeaderKey, "1.0.0").Expect(). Status(iris.StatusOK).Body().IsEqual("data (v1.x)") e.GET("/data").WithHeader(versioning.AcceptVersionHeaderKey, "2.3.0").Expect(). Status(iris.StatusOK).Body().IsEqual("data (v2.x)") e.GET("/data").WithHeader(versioning.AcceptVersionHeaderKey, "3.1.0").Expect(). Status(iris.StatusOK).Body().IsEqual("data (v3.x)") // Test invalid version or no version at all. e.GET("/data").WithHeader(versioning.AcceptVersionHeaderKey, "4.0.0").Expect(). Status(iris.StatusOK).Body().IsEqual("data") e.GET("/data").Expect(). Status(iris.StatusOK).Body().IsEqual("data") // Test Deprecated (v1) ex := e.GET("/data").WithHeader(versioning.AcceptVersionHeaderKey, "1.0.0").Expect() ex.Status(iris.StatusOK).Body().IsEqual("data (v1.x)") ex.Header("X-API-Warn").Equal(opts.WarnMessage) expectedDateStr := opts.DeprecationDate.Format(app.ConfigurationReadOnly().GetTimeFormat()) ex.Header("X-API-Deprecation-Date").Equal(expectedDateStr) } ================================================ FILE: _examples/mvc/vuejs-todo-mvc/README.md ================================================ # A Todo MVC Application using Iris and Vue.js ## Hackernoon Article: https://medium.com/hackernoon/a-todo-mvc-application-using-iris-and-vue-js-5019ff870064 Vue.js is a front-end framework for building web applications using javascript. It has a blazing fast Virtual DOM renderer. Iris is a back-end framework for building web applications using The Go Programming Language (disclaimer: author here). It's one of the fastest and featured web frameworks out there. We wanna use this to serve our "todo service". ## The Tools Programming Languages are just tools for us, but we need a safe, fast and “cross-platform” programming language to power our service. [Go](https://golang.org) is a [rapidly growing](https://www.tiobe.com/tiobe-index/) open source programming language designed for building simple, fast, and reliable software. Take a look [here](https://github.com/golang/go/wiki/GoUsers) which great companies use Go to power their services. ### Install the Go Programming Language Extensive information about downloading & installing Go can be found [here](https://golang.org/dl/). [![](https://i3.ytimg.com/vi/9x-pG3lvLi0/hqdefault.jpg)](https://youtu.be/9x-pG3lvLi0) > Maybe [Windows](https://www.youtube.com/watch?v=WT5mTznJBS0) or [Mac OS X](https://www.youtube.com/watch?v=5qI8z_lB5Lw) user? > The article does not contain an introduction to the language itself, if you’re a newcomer I recommend you to bookmark this article, [learn](https://github.com/golang/go/wiki/Learn) the language’s fundamentals and come back later on. ## The Dependencies Many articles have been written, in the past, that lead developers not to use a web framework because they are useless and "bad". I have to tell you that there is no such thing, it always depends on the (web) framework that you’re going to use. At production environment, we don’t have the time or the experience to code everything that we wanna use in the applications, and if we could are we sure that we can do better and safely than others? In short term: **Good frameworks are helpful tools for any developer, company or startup and "bad" frameworks are waste of time, crystal clear.** You’ll need two dependencies: 1. Vue.js, for our client-side requirements. Download it from [here](https://vuejs.org/), latest v2. 2. The Iris Web Framework, for our server-side requirements. Can be found [here](https://github.com/kataras/iris), latest v12. > If you have Go already installed then just execute `go get github.com/kataras/iris/v12@latest` to install the Iris Web Framework. ## Start If we are all in the same page, it’s time to learn how we can create a live todo application that will be easy to deploy and extend even more! We're going to use a vue.js todo application which uses browser' s local storage and doesn't have any user-specified features like live sync between browser's tabs, you can find the original version inside the vue's [docs](https://vuejs.org/v2/examples/todomvc.html). Assuming that you know how %GOPATH% works, create an empty folder, i.e "vuejs-todo-mvc" in the %GOPATH%/src directory, there you will create those files: - web/public/js/app.js - web/public/index.html - todo/item.go - todo/service.go - web/controllers/todo_controller.go - web/main.go _Read the comments in the source code, they may be very helpful_ ### The client-side (vue.js) ```js /* file: vuejs-todo-mvc/web/public/js/app.js */ // Full spec-compliant TodoMVC with Iris // and hash-based routing in ~200 effective lines of JavaScript. var ws; ((async () => { const events = { todos: { saved: function (ns, msg) { app.todos = msg.unmarshal() // or make a new http fetch // fetchTodos(function (items) { // app.todos = msg.unmarshal() // }); } } }; const conn = await neffos.dial("ws://localhost:8080/todos/sync", events); ws = await conn.connect("todos"); })()).catch(console.error); function fetchTodos(onComplete) { axios.get("/todos").then(response => { if (response.data === null) { return; } onComplete(response.data); }); } var todoStorage = { fetch: function () { var todos = []; fetchTodos(function (items) { for (var i = 0; i < items.length; i++) { todos.push(items[i]); } }); return todos; }, save: function (todos) { axios.post("/todos", JSON.stringify(todos)).then(response => { if (!response.data.success) { window.alert("saving had a failure"); return; } // console.log("send: save"); ws.emit("save") }); } } // visibility filters var filters = { all: function (todos) { return todos }, active: function (todos) { return todos.filter(function (todo) { return !todo.completed }) }, completed: function (todos) { return todos.filter(function (todo) { return todo.completed }) } } // app Vue instance var app = new Vue({ // app initial state data: { todos: todoStorage.fetch(), newTodo: '', editedTodo: null, visibility: 'all' }, // we will not use the "watch" as it works with the fields like "hasChanges" // and callbacks to make it true but let's keep things very simple as it's just a small getting started. // // watch todos change for persistence // watch: { // todos: { // handler: function (todos) { // if (app.hasChanges) { // todoStorage.save(todos); // app.hasChanges = false; // } // }, // deep: true // } // }, // computed properties // http://vuejs.org/guide/computed.html computed: { filteredTodos: function () { return filters[this.visibility](this.todos) }, remaining: function () { return filters.active(this.todos).length }, allDone: { get: function () { return this.remaining === 0 }, set: function (value) { this.todos.forEach(function (todo) { todo.completed = value }) this.notifyChange(); } } }, filters: { pluralize: function (n) { return n === 1 ? 'item' : 'items' } }, // methods that implement data logic. // note there's no DOM manipulation here at all. methods: { notifyChange: function () { todoStorage.save(this.todos) }, addTodo: function () { var value = this.newTodo && this.newTodo.trim() if (!value) { return } this.todos.push({ id: this.todos.length + 1, // just for the client-side. title: value, completed: false }) this.newTodo = '' this.notifyChange(); }, completeTodo: function (todo) { if (todo.completed) { todo.completed = false; } else { todo.completed = true; } this.notifyChange(); }, removeTodo: function (todo) { this.todos.splice(this.todos.indexOf(todo), 1) this.notifyChange(); }, editTodo: function (todo) { this.beforeEditCache = todo.title this.editedTodo = todo }, doneEdit: function (todo) { if (!this.editedTodo) { return } this.editedTodo = null todo.title = todo.title.trim(); if (!todo.title) { this.removeTodo(todo); } this.notifyChange(); }, cancelEdit: function (todo) { this.editedTodo = null todo.title = this.beforeEditCache }, removeCompleted: function () { this.todos = filters.active(this.todos); this.notifyChange(); } }, // a custom directive to wait for the DOM to be updated // before focusing on the input field. // http://vuejs.org/guide/custom-directive.html directives: { 'todo-focus': function (el, binding) { if (binding.value) { el.focus() } } } }) // handle routing function onHashChange() { var visibility = window.location.hash.replace(/#\/?/, '') if (filters[visibility]) { app.visibility = visibility } else { window.location.hash = '' app.visibility = 'all' } } window.addEventListener('hashchange', onHashChange) onHashChange() // mount app.$mount('.todoapp'); ``` Let's add our view, the static html. ```html Iris + Vue.js • TodoMVC

todos

Double-click to edit a todo

``` ### The server-side (iris) Our view model. ```go // file: vuejs-todo-mvc/todo/item.go package todo type Item struct { SessionID string `json:"-"` ID int64 `json:"id,omitempty"` Title string `json:"title"` Completed bool `json:"completed"` } ``` Our service. ```go // file: vuejs-todo-mvc/todo/service.go package todo import ( "sync" ) type Service interface { Get(owner string) []Item Save(owner string, newItems []Item) error } type MemoryService struct { // key = session id, value the list of todo items that this session id has. items map[string][]Item // protected by locker for concurrent access. mu sync.RWMutex } func NewMemoryService() *MemoryService { return &MemoryService{ items: make(map[string][]Item, 0), } } func (s *MemoryService) Get(sessionOwner string) []Item { s.mu.RLock() items := s.items[sessionOwner] s.mu.RUnlock() return items } func (s *MemoryService) Save(sessionOwner string, newItems []Item) error { var prevID int64 for i := range newItems { if newItems[i].ID == 0 { newItems[i].ID = prevID prevID++ } } s.mu.Lock() s.items[sessionOwner] = newItems s.mu.Unlock() return nil } ``` We are going to use some of the MVC functionalities of the iris web framework here but you can do the same with the standard API as well. ```go // file: vuejs-todo-mvc/web/controllers/todo_controller.go package controllers import ( "vuejs-todo-mvc/todo" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/mvc" "github.com/kataras/iris/v12/sessions" "github.com/kataras/iris/v12/websocket" ) // TodoController is our TODO app's web controller. type TodoController struct { Service todo.Service Session *sessions.Session NS *websocket.NSConn } // BeforeActivation called once before the server ran, and before // the routes and dependencies binded. // You can bind custom things to the controller, add new methods, add middleware, // add dependencies to the struct or the method(s) and more. func (c *TodoController) BeforeActivation(b mvc.BeforeActivation) { // this could be binded to a controller's function input argument // if any, or struct field if any: b.Dependencies().Add(func(ctx iris.Context) (items []todo.Item) { ctx.ReadJSON(&items) return }) } // Get handles the GET: /todos route. func (c *TodoController) Get() []todo.Item { return c.Service.Get(c.Session.ID()) } // PostItemResponse the response data that will be returned as json // after a post save action of all todo items. type PostItemResponse struct { Success bool `json:"success"` } var emptyResponse = PostItemResponse{Success: false} // Post handles the POST: /todos route. func (c *TodoController) Post(newItems []todo.Item) PostItemResponse { if err := c.Service.Save(c.Session.ID(), newItems); err != nil { return emptyResponse } return PostItemResponse{Success: true} } func (c *TodoController) Save(msg websocket.Message) error { id := c.Session.ID() c.NS.Conn.Server().Broadcast(nil, websocket.Message{ Namespace: msg.Namespace, Event: "saved", To: id, Body: websocket.Marshal(c.Service.Get(id)), }) return nil } ``` And finally our main application's endpoint. ```go // file: web/main.go package main import ( "strings" "github.com/kataras/iris/v12/_examples/mvc/vuejs-todo-mvc/src/todo" "github.com/kataras/iris/v12/_examples/mvc/vuejs-todo-mvc/src/web/controllers" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/mvc" "github.com/kataras/iris/v12/sessions" "github.com/kataras/iris/v12/websocket" ) func main() { app := iris.New() // serve our app in public, public folder // contains the client-side vue.js application, // no need for any server-side template here, // actually if you're going to just use vue without any // back-end services, you can just stop afer this line and start the server. app.HandleDir("/", iris.Dir("./public")) // configure the http sessions. sess := sessions.New(sessions.Config{ Cookie: "iris_session", }) // create a sub router and register the http controllers. todosRouter := app.Party("/todos") // create our mvc application targeted to /todos relative sub path. todosApp := mvc.New(todosRouter) // any dependencies bindings here... todosApp.Register( todo.NewMemoryService(), ) todosController := new(controllers.TodoController) // controllers registration here... todosApp.Handle(todosController) // Create a sub mvc app for websocket controller. // Inherit the parent's dependencies. todosWebsocketApp := todosApp.Party("/sync") todosWebsocketApp.HandleWebsocket(todosController). SetNamespace("todos"). SetEventMatcher(func(methodName string) (string, bool) { return strings.ToLower(methodName), true }) websocketServer := websocket.New(websocket.DefaultGorillaUpgrader, todosWebsocketApp) idGenerator := func(ctx iris.Context) string { id := sess.Start(ctx).ID() return id } todosWebsocketApp.Router.Get("/", websocket.Handler(websocketServer, idGenerator)) // start the web server at http://localhost:8080 app.Listen(":8080") } ``` Run the Iris web server you've just created by executing `go run main.go` from your current path (%GOPATH%/src/%your_folder%/web/). ```sh $ go run main.go Now listening on: http://localhost:8080 Application Started. Press CTRL+C to shut down. _ ``` Open one or more browser tabs at: http://localhost:8080 and have fun! ![](screen.png) ### Download the Source Code The whole project, all the files you saw in this article are located at: https://github.com/kataras/iris/tree/main/_examples/mvc/vuejs-todo-mvc ## References https://vuejs.org/v2/examples/todomvc.html (using browser's local storage) https://github.com/kataras/iris/tree/main/_examples/mvc (mvc examples and features overview repository) ## Thank you, once again Happy new year and thank you for your pattience, once again:) Don't hesitate to post any questions and provide feedback(I'm very active dev therefore you will be heard here!) Don't forget to check out my medium profile and twitter as well, I'm posting some (useful) stuff there too:) - https://medium.com/@kataras - https://twitter.com/MakisMaropoulos ================================================ FILE: _examples/mvc/vuejs-todo-mvc/src/go.mod ================================================ module github.com/kataras/iris/v12/_examples/mvc/vuejs-todo-mvc/src go 1.25 require github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd require ( github.com/BurntSushi/toml v1.6.0 // indirect github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect github.com/CloudyKit/jet/v6 v6.3.1 // indirect github.com/Joker/jade v1.1.3 // indirect github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 // indirect github.com/andybalholm/brotli v1.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/fatih/structs v1.1.0 // indirect github.com/flosch/pongo2/v4 v4.0.2 // indirect github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/ws v1.4.0 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/iris-contrib/schema v0.0.6 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/kataras/blocks v0.0.8 // indirect github.com/kataras/golog v0.1.12 // indirect github.com/kataras/neffos v0.0.24 // indirect github.com/kataras/pio v0.0.13 // indirect github.com/kataras/sitemap v0.0.6 // indirect github.com/kataras/tunnel v0.0.4 // indirect github.com/klauspost/compress v1.18.2 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mailgun/raymond/v2 v2.0.48 // indirect github.com/mailru/easyjson v0.9.1 // indirect github.com/mediocregopher/radix/v3 v3.8.1 // indirect github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/nats-io/nats.go v1.40.1 // indirect github.com/nats-io/nkeys v0.4.9 // indirect github.com/nats-io/nuid v1.0.1 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/schollz/closestmatch v2.1.0+incompatible // indirect github.com/sirupsen/logrus v1.8.1 // indirect github.com/stretchr/testify v1.11.1 // indirect github.com/tdewolff/minify/v2 v2.24.8 // indirect github.com/tdewolff/parse/v2 v2.8.5 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/yosssi/ace v0.0.5 // indirect golang.org/x/crypto v0.47.0 // indirect golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect golang.org/x/net v0.49.0 // indirect golang.org/x/sys v0.40.0 // indirect golang.org/x/text v0.33.0 // indirect golang.org/x/time v0.14.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: _examples/mvc/vuejs-todo-mvc/src/go.sum ================================================ github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= github.com/CloudyKit/jet/v6 v6.3.1 h1:6IAo5Cx21xrHVaR8zzXN5gJatKV/wO7Nf6bfCnCSbUw= github.com/CloudyKit/jet/v6 v6.3.1/go.mod h1:lf8ksdNsxZt7/yH/3n4vJQWA9RUq4wpaHtArHhGVMOw= github.com/Joker/hpp v1.0.0 h1:65+iuJYdRXv/XyN62C1uEmmOx3432rNG/rKlX6V7Kkc= github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= github.com/Joker/jade v1.1.3 h1:Qbeh12Vq6BxURXT1qZBRHsDxeURB8ztcL6f3EXSGeHk= github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKduM= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 h1:dG7/gLroJGht/jSQtHiLvT48Hxn+crbmvyItZC8cWOs= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05/go.mod h1:NYezi6wtnJtBm5btoprXc5SvAdqH0XTXWnUup0MptAI= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/flosch/pongo2/v4 v4.0.2 h1:gv+5Pe3vaSVmiJvh/BZa82b7/00YUGm0PIyVVLop0Hw= github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+BWPhwZD8= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a h1:l7A0loSszR5zHd/qK53ZIHMO8b3bBSmENnQ6eKnUT0A= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/iris-contrib/httpexpect/v2 v2.15.2 h1:T9THsdP1woyAqKHwjkEsbCnMefsAFvk8iJJKokcJ3Go= github.com/iris-contrib/httpexpect/v2 v2.15.2/go.mod h1:JLDgIqnFy5loDSUv1OA2j0mb6p/rDhiCqigP22Uq9xE= github.com/iris-contrib/schema v0.0.6 h1:CPSBLyx2e91H2yJzPuhGuifVRnZBBJ3pCOMbOvPZaTw= github.com/iris-contrib/schema v0.0.6/go.mod h1:iYszG0IOsuIsfzjymw1kMzTL8YQcCWlm65f3wX8J5iA= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kataras/blocks v0.0.8 h1:MrpVhoFTCR2v1iOOfGng5VJSILKeZZI+7NGfxEh3SUM= github.com/kataras/blocks v0.0.8/go.mod h1:9Jm5zx6BB+06NwA+OhTbHW1xkMOYxahnqTN5DveZ2Yg= github.com/kataras/golog v0.1.12 h1:Bu7I/G4ilJlbfzjmU39O9N+2uO1pBcMK045fzZ4ytNg= github.com/kataras/golog v0.1.12/go.mod h1:wrGSbOiBqbQSQznleVNX4epWM8rl9SJ/rmEacl0yqy4= github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd h1:oMsQgqKACbUdlaIWnN+EZzzAnHkw90hE/y/R0BSij2U= github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd/go.mod h1:yucjtDl9Vn3OctWM1fi0MuM9OckemC7kEreFa9QtPF8= github.com/kataras/neffos v0.0.24 h1:S3lHqJopCfXN285VdlbGeOj+Id83u4xdQKToa+w1vW0= github.com/kataras/neffos v0.0.24/go.mod h1:/3K9zQ0yEC5/xUiSQx46ToWa3xneGfUo/nMit/F5g+U= github.com/kataras/pio v0.0.13 h1:x0rXVX0fviDTXOOLOmr4MUxOabu1InVSTu5itF8CXCM= github.com/kataras/pio v0.0.13/go.mod h1:k3HNuSw+eJ8Pm2lA4lRhg3DiCjVgHlP8hmXApSej3oM= github.com/kataras/sitemap v0.0.6 h1:w71CRMMKYMJh6LR2wTgnk5hSgjVNB9KL60n5e2KHvLY= github.com/kataras/sitemap v0.0.6/go.mod h1:dW4dOCNs896OR1HmG+dMLdT7JjDk7mYBzoIRwuj5jA4= github.com/kataras/tunnel v0.0.4 h1:sCAqWuJV7nPzGrlb0os3j49lk2JhILT0rID38NHNLpA= github.com/kataras/tunnel v0.0.4/go.mod h1:9FkU4LaeifdMWqZu7o20ojmW4B7hdhv2CMLwfnHGpYw= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw= github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18= github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mediocregopher/radix/v3 v3.8.1 h1:rOkHflVuulFKlwsLY01/M2cM2tWCjDoETcMqKbAWu1M= github.com/mediocregopher/radix/v3 v3.8.1/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/nats-io/nats.go v1.40.1 h1:MLjDkdsbGUeCMKFyCFoLnNn/HDTqcgVa3EQm+pMNDPk= github.com/nats-io/nats.go v1.40.1/go.mod h1:wV73x0FSI/orHPSYoyMeJB+KajMDoWyXmFaRrrYaaTo= github.com/nats-io/nkeys v0.4.9 h1:qe9Faq2Gxwi6RZnZMXfmGMZkg3afLLOtrU+gDZJ35b0= github.com/nats-io/nkeys v0.4.9/go.mod h1:jcMqs+FLG+W5YO36OX6wFIFcmpdAns+w1Wm6D3I/evE= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo= github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk= github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tdewolff/minify/v2 v2.24.8 h1:58/VjsbevI4d5FGV0ZSuBrHMSSkH4MCH0sIz/eKIauE= github.com/tdewolff/minify/v2 v2.24.8/go.mod h1:0Ukj0CRpo/sW/nd8uZ4ccXaV1rEVIWA3dj8U7+Shhfw= github.com/tdewolff/parse/v2 v2.8.5 h1:ZmBiA/8Do5Rpk7bDye0jbbDUpXXbCdc3iah4VeUvwYU= github.com/tdewolff/parse/v2 v2.8.5/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo= github.com/tdewolff/test v1.0.11 h1:FdLbwQVHxqG16SlkGveC0JVyrJN62COWTRyUFzfbtBE= github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA= github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0= github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs= moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHcE= ================================================ FILE: _examples/mvc/vuejs-todo-mvc/src/todo/item.go ================================================ package todo type Item struct { SessionID string `json:"-"` ID int64 `json:"id,omitempty"` Title string `json:"title"` Completed bool `json:"completed"` } ================================================ FILE: _examples/mvc/vuejs-todo-mvc/src/todo/service.go ================================================ package todo import ( "sync" ) type Service interface { Get(owner string) []Item Save(owner string, newItems []Item) error } type MemoryService struct { // key = session id, value the list of todo items that this session id has. items map[string][]Item // protected by locker for concurrent access. mu sync.RWMutex } func NewMemoryService() *MemoryService { return &MemoryService{ items: make(map[string][]Item), } } func (s *MemoryService) Get(sessionOwner string) []Item { s.mu.RLock() items := s.items[sessionOwner] s.mu.RUnlock() return items } func (s *MemoryService) Save(sessionOwner string, newItems []Item) error { var prevID int64 for i := range newItems { if newItems[i].ID == 0 { newItems[i].ID = prevID prevID++ } } s.mu.Lock() s.items[sessionOwner] = newItems s.mu.Unlock() return nil } ================================================ FILE: _examples/mvc/vuejs-todo-mvc/src/web/controllers/todo_controller.go ================================================ package controllers import ( "github.com/kataras/iris/v12/_examples/mvc/vuejs-todo-mvc/src/todo" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/mvc" "github.com/kataras/iris/v12/sessions" "github.com/kataras/iris/v12/websocket" ) // TodoController is our TODO app's web controller. type TodoController struct { Service todo.Service Session *sessions.Session NS *websocket.NSConn } // BeforeActivation called once before the server ran, and before // the routes and dependencies binded. // You can bind custom things to the controller, add new methods, add middleware, // add dependencies to the struct or the method(s) and more. func (c *TodoController) BeforeActivation(b mvc.BeforeActivation) { // this could be binded to a controller's function input argument // if any, or struct field if any: b.Dependencies().Register(func(ctx iris.Context) (items []todo.Item) { ctx.ReadJSON(&items) return }) // Note: from Iris v12.2 these type of dependencies are automatically resolved. } // Get handles the GET: /todos route. func (c *TodoController) Get() []todo.Item { return c.Service.Get(c.Session.ID()) } // PostItemResponse the response data that will be returned as json // after a post save action of all todo items. type PostItemResponse struct { Success bool `json:"success"` } var emptyResponse = PostItemResponse{Success: false} // Post handles the POST: /todos route. func (c *TodoController) Post(newItems []todo.Item) PostItemResponse { if err := c.Service.Save(c.Session.ID(), newItems); err != nil { return emptyResponse } return PostItemResponse{Success: true} } func (c *TodoController) Save(msg websocket.Message) error { id := c.Session.ID() c.NS.Conn.Server().Broadcast(nil, websocket.Message{ Namespace: msg.Namespace, Event: "saved", To: id, Body: websocket.Marshal(c.Service.Get(id)), }) return nil } ================================================ FILE: _examples/mvc/vuejs-todo-mvc/src/web/main.go ================================================ package main import ( "strings" "github.com/kataras/iris/v12/_examples/mvc/vuejs-todo-mvc/src/todo" "github.com/kataras/iris/v12/_examples/mvc/vuejs-todo-mvc/src/web/controllers" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/mvc" "github.com/kataras/iris/v12/sessions" "github.com/kataras/iris/v12/websocket" ) func main() { app := iris.New() // serve our app in public, public folder // contains the client-side vue.js application, // no need for any server-side template here, // actually if you're going to just use vue without any // back-end services, you can just stop afer this line and start the server. app.HandleDir("/", iris.Dir("./public")) // configure the http sessions. sess := sessions.New(sessions.Config{ Cookie: "iris_session", }) // create a sub router and register the http controllers. todosRouter := app.Party("/todos") // Register sessions handler. // TodoController.Session will automatically // filled with the current request's session. todosRouter.Use(sess.Handler()) // create our mvc application targeted to /todos relative sub path. todosApp := mvc.New(todosRouter) // any dependencies bindings here... todosApp.Register( todo.NewMemoryService(), ) todosController := new(controllers.TodoController) // controllers registration here... todosApp.Handle(todosController) // Create a sub mvc app for websocket controller. // Inherit the parent's dependencies. todosWebsocketApp := todosApp.Party("/sync") todosWebsocketApp.HandleWebsocket(todosController). SetNamespace("todos"). SetEventMatcher(func(methodName string) (string, bool) { return strings.ToLower(methodName), true }) websocketServer := websocket.New(websocket.DefaultGorillaUpgrader, todosWebsocketApp) idGenerator := func(ctx iris.Context) string { id := sess.Start(ctx).ID() return id } todosWebsocketApp.Router.Get("/", websocket.Handler(websocketServer, idGenerator)) // start the web server at http://localhost:8080 app.Listen(":8080") } ================================================ FILE: _examples/mvc/vuejs-todo-mvc/src/web/public/css/index ================================================ index.css is not here to reduce the disk space for the examples. https://unpkg.com/todomvc-app-css@2.0.4/index.css is used instead. ================================================ FILE: _examples/mvc/vuejs-todo-mvc/src/web/public/index.html ================================================ Iris + Vue.js • TodoMVC

todos

Double-click to edit a todo

================================================ FILE: _examples/mvc/vuejs-todo-mvc/src/web/public/js/app.js ================================================ // Full spec-compliant TodoMVC with Iris // and hash-based routing in ~200 effective lines of JavaScript. var ws; ((async () => { const events = { todos: { saved: function (ns, msg) { app.todos = msg.unmarshal() // or make a new http fetch // fetchTodos(function (items) { // app.todos = msg.unmarshal() // }); } } }; const conn = await neffos.dial("ws://localhost:8080/todos/sync", events); ws = await conn.connect("todos"); })()).catch(console.error); function fetchTodos(onComplete) { axios.get("/todos").then(response => { if (response.data === null) { return; } onComplete(response.data); }); } var todoStorage = { fetch: function () { var todos = []; fetchTodos(function (items) { for (var i = 0; i < items.length; i++) { todos.push(items[i]); } }); return todos; }, save: function (todos) { axios.post("/todos", JSON.stringify(todos)).then(response => { if (!response.data.success) { window.alert("saving had a failure"); return; } // console.log("send: save"); ws.emit("save") }); } } // visibility filters var filters = { all: function (todos) { return todos }, active: function (todos) { return todos.filter(function (todo) { return !todo.completed }) }, completed: function (todos) { return todos.filter(function (todo) { return todo.completed }) } } // app Vue instance var app = new Vue({ // app initial state data: { todos: todoStorage.fetch(), newTodo: '', editedTodo: null, visibility: 'all' }, // we will not use the "watch" as it works with the fields like "hasChanges" // and callbacks to make it true but let's keep things very simple as it's just a small getting started. // // watch todos change for persistence // watch: { // todos: { // handler: function (todos) { // if (app.hasChanges) { // todoStorage.save(todos); // app.hasChanges = false; // } // }, // deep: true // } // }, // computed properties // http://vuejs.org/guide/computed.html computed: { filteredTodos: function () { return filters[this.visibility](this.todos) }, remaining: function () { return filters.active(this.todos).length }, allDone: { get: function () { return this.remaining === 0 }, set: function (value) { this.todos.forEach(function (todo) { todo.completed = value }) this.notifyChange(); } } }, filters: { pluralize: function (n) { return n === 1 ? 'item' : 'items' } }, // methods that implement data logic. // note there's no DOM manipulation here at all. methods: { notifyChange: function () { todoStorage.save(this.todos) }, addTodo: function () { var value = this.newTodo && this.newTodo.trim() if (!value) { return } this.todos.push({ id: this.todos.length + 1, // just for the client-side. title: value, completed: false }) this.newTodo = '' this.notifyChange(); }, completeTodo: function (todo) { if (todo.completed) { todo.completed = false; } else { todo.completed = true; } this.notifyChange(); }, removeTodo: function (todo) { this.todos.splice(this.todos.indexOf(todo), 1) this.notifyChange(); }, editTodo: function (todo) { this.beforeEditCache = todo.title this.editedTodo = todo }, doneEdit: function (todo) { if (!this.editedTodo) { return } this.editedTodo = null todo.title = todo.title.trim(); if (!todo.title) { this.removeTodo(todo); } this.notifyChange(); }, cancelEdit: function (todo) { this.editedTodo = null todo.title = this.beforeEditCache }, removeCompleted: function () { this.todos = filters.active(this.todos); this.notifyChange(); } }, // a custom directive to wait for the DOM to be updated // before focusing on the input field. // http://vuejs.org/guide/custom-directive.html directives: { 'todo-focus': function (el, binding) { if (binding.value) { el.focus() } } } }) // handle routing function onHashChange() { var visibility = window.location.hash.replace(/#\/?/, '') if (filters[visibility]) { app.visibility = visibility } else { window.location.hash = '' app.visibility = 'all' } } window.addEventListener('hashchange', onHashChange) onHashChange() // mount app.$mount('.todoapp'); ================================================ FILE: _examples/mvc/vuejs-todo-mvc/src/web/public/js/lib/.gitkeep ================================================ ================================================ FILE: _examples/mvc/websocket/browser/index.html ================================================ Online visitors MVC example
1 online visitor

    
    
    





================================================
FILE: _examples/mvc/websocket/main.go
================================================
package main

import (
	"fmt"
	"sync/atomic"

	"github.com/kataras/iris/v12"
	"github.com/kataras/iris/v12/mvc"
	"github.com/kataras/iris/v12/websocket"
)

func main() {
	app := iris.New()
	app.Logger().SetLevel("debug")

	// load templates.
	// app.RegisterView(iris.HTML("./views", ".html"))

	// render the ./browser/index.html.
	app.HandleDir("/", iris.Dir("./browser"))

	websocketAPI := app.Party("/websocket")

	m := mvc.New(websocketAPI)
	m.Register(
		&prefixedLogger{prefix: "DEV"},
	)
	m.HandleWebsocket(&websocketController{Namespace: "default", Age: 42, Otherstring: "other string"})

	websocketServer := websocket.New(websocket.DefaultGorillaUpgrader, m)

	websocketAPI.Get("/", websocket.Handler(websocketServer))
	// http://localhost:8080
	app.Listen(":8080")
}

var visits uint64

func increment() uint64 {
	return atomic.AddUint64(&visits, 1)
}

func decrement() uint64 {
	return atomic.AddUint64(&visits, ^uint64(0))
}

type websocketController struct {
	*websocket.NSConn `stateless:"true"`
	Namespace         string
	Age               int
	Otherstring       string

	Logger LoggerService
}

// or
// func (c *websocketController) Namespace() string {
// 	return "default"
// }

func (c *websocketController) OnNamespaceDisconnect(msg websocket.Message) error {
	c.Logger.Log("Disconnected")
	// visits--
	newCount := decrement()
	// This will call the "OnVisit" event on all clients, except the current one,
	// (it can't because it's left but for any case use this type of design)
	c.Conn.Server().Broadcast(nil, websocket.Message{
		Namespace: msg.Namespace,
		Event:     "OnVisit",
		Body:      []byte(fmt.Sprintf("%d", newCount)),
	})

	return nil
}

func (c *websocketController) OnNamespaceConnected(msg websocket.Message) error {
	// println("Broadcast prefix is: " + c.BroadcastPrefix)
	c.Logger.Log("Connected")

	// visits++
	newCount := increment()

	// This will call the "OnVisit" event on all clients, including the current one,
	// with the 'newCount' variable.
	//
	// There are many ways that u can do it and faster, for example u can just send a new visitor
	// and client can increment itself, but here we are just "showcasing" the websocket controller.
	c.Conn.Server().Broadcast(nil, websocket.Message{
		Namespace: msg.Namespace,
		Event:     "OnVisit",
		Body:      []byte(fmt.Sprintf("%d", newCount)),
	})

	return nil
}

func (c *websocketController) OnChat(msg websocket.Message) error {
	ctx := websocket.GetContext(c.Conn)

	ctx.Application().Logger().Infof("[IP: %s] [ID: %s]  broadcast to other clients the message [%s]",
		ctx.RemoteAddr(), c, string(msg.Body))

	c.Conn.Server().Broadcast(c, msg)

	return nil
}

type LoggerService interface {
	Log(string)
}

type prefixedLogger struct {
	prefix string
}

func (s *prefixedLogger) Log(msg string) {
	fmt.Printf("%s: %s\n", s.prefix, msg)
}


================================================
FILE: _examples/mvc/websocket-auth/auth.yml
================================================
Headers: # required.
  - "Authorization"
  - "X-Authorization"
Cookie: # optional.
  Name: "iris_auth_cookie"
  Secure: false
  Hash: "D*G-KaPdSgUkXp2s5v8y/B?E(H+MbQeThWmYq3t6w9z$C&F)J@NcRfUjXn2r4u7x" # length of 64 characters (512-bit).
  Block: "VkYp3s6v9y$B&E)H@McQfTjWmZq4t7w!" # length of 32 characters (256-bit).
Keys:
  - ID: IRIS_AUTH_ACCESS # required.
    Alg: EdDSA
    MaxAge: 2h # 2 hours lifetime for access tokens. 
    Private: |+
      -----BEGIN PRIVATE KEY-----
      MC4CAQAwBQYDK2VwBCIEIFdZWoDdFny5SMnP9Fyfr8bafi/B527EVZh8JJjDTIFO
      -----END PRIVATE KEY-----
    Public: |+
      -----BEGIN PUBLIC KEY-----
      MCowBQYDK2VwAyEAzpgjKSr9E032DX+foiOxq1QDsbzjLxagTN+yVpGWZB4=
      -----END PUBLIC KEY-----
  - ID: IRIS_AUTH_REFRESH # optional. Good practise to have it though.
    Alg: EdDSA
    # 1 month lifetime for refresh tokens,
    # after that period the user has to signin again.
    MaxAge: 720h
    Private: |+
      -----BEGIN PRIVATE KEY-----
      MC4CAQAwBQYDK2VwBCIEIHJ1aoIjA2sRp5eqGjGR3/UMucrHbBdBv9p8uwfzZ1KZ
      -----END PRIVATE KEY-----
    Public: |+
      -----BEGIN PUBLIC KEY-----
      MCowBQYDK2VwAyEAsKKAr+kDtfAqwG7cZdoEAfh9jHt9W8qi9ur5AA1KQAQ=
      -----END PUBLIC KEY-----
    # Example of setting a binary form of the encryption key for refresh tokens,
    # it could be a "string" as well.
    EncryptionKey: !!binary stSNLTu91YyihPxzeEOXKwGVMG00CjcC/68G8nMgmqA=


================================================
FILE: _examples/mvc/websocket-auth/browser/index.html
================================================



    Online visitors MVC example
    



    
1 online visitor

    
    
    





================================================
FILE: _examples/mvc/websocket-auth/main.go
================================================
//go:build go1.18
// +build go1.18

package main

import (
	"fmt"

	"github.com/kataras/iris/v12"
	"github.com/kataras/iris/v12/auth"
	"github.com/kataras/iris/v12/mvc"
	"github.com/kataras/iris/v12/websocket"
)

// $ go run .
func main() {
	app := newApp()

	// http://localhost:8080/signin (creds: kataras2006@hotmail.com 123456)
	// http://localhost:8080/protected
	// http://localhost:8080/signout
	app.Listen(":8080")
}

func newApp() *iris.Application {
	app := iris.New()

	// Auth part.
	app.RegisterView(iris.Blocks("./views", ".html").
		LayoutDir("layouts").
		Layout("main"))

	s := auth.MustLoad[User]("./auth.yml")
	s.AddProvider(NewProvider())

	app.Get("/signin", renderSigninForm)
	app.Post("/signin", s.SigninHandler)
	app.Get("/signout", s.SignoutHandler)
	//

	websocketAPI := app.Party("/protected")
	websocketAPI.Use(s.VerifyHandler())
	websocketAPI.HandleDir("/", iris.Dir("./browser")) // render the ./browser/index.html.

	websocketMVC := mvc.New(websocketAPI)
	websocketMVC.HandleWebsocket(new(websocketController))
	websocketServer := websocket.New(websocket.DefaultGorillaUpgrader, websocketMVC)
	websocketAPI.Get("/ws", s.VerifyHandler() /* optional */, websocket.Handler(websocketServer))

	return app
}

func renderSigninForm(ctx iris.Context) {
	if err := ctx.View("signin", iris.Map{"Title": "Signin Page"}); err != nil {
		ctx.HTML(fmt.Sprintf("

%s

", err.Error())) return } } type websocketController struct { *websocket.NSConn `stateless:"true"` } func (c *websocketController) Namespace() string { return "default" } func (c *websocketController) OnChat(msg websocket.Message) error { ctx := websocket.GetContext(c.Conn) user := auth.GetUser[User](ctx) msg.Body = []byte(fmt.Sprintf("%s: %s", user.Email, string(msg.Body))) c.Conn.Server().Broadcast(c, msg) return nil } ================================================ FILE: _examples/mvc/websocket-auth/user.go ================================================ //go:build go1.18 // +build go1.18 package main type AccessRole uint16 func (r AccessRole) Is(v AccessRole) bool { return r&v != 0 } func (r AccessRole) Allow(v AccessRole) bool { return r&v >= v } const ( InvalidAccessRole AccessRole = 1 << iota Read Write Delete Owner = Read | Write | Delete Member = Read | Write ) type User struct { ID string `json:"id"` Email string `json:"email"` Role AccessRole `json:"role"` } func (u User) GetID() string { return u.ID } ================================================ FILE: _examples/mvc/websocket-auth/user_provider.go ================================================ //go:build go1.18 // +build go1.18 package main import ( "context" "fmt" "sync" "time" "github.com/kataras/iris/v12/auth" ) type Provider struct { dataset []User invalidated map[string]struct{} // key = token. Entry is blocked. invalidatedAll map[string]int64 // key = user id, value = timestamp. Issued before is consider invalid. mu sync.RWMutex } func NewProvider() *Provider { return &Provider{ dataset: []User{ { ID: "id-1", Email: "kataras2006@hotmail.com", Role: Owner, }, { ID: "id-2", Email: "example@example.com", Role: Member, }, }, invalidated: make(map[string]struct{}), invalidatedAll: make(map[string]int64), } } func (p *Provider) Signin(ctx context.Context, username, password string) (User, error) { // fired on SigninHandler. // your database... for _, user := range p.dataset { if user.Email == username { return user, nil } } return User{}, fmt.Errorf("user not found") } func (p *Provider) ValidateToken(ctx context.Context, standardClaims auth.StandardClaims, u User) error { // fired on VerifyHandler. // your database and checks of blocked tokens... // check for specific token ids. p.mu.RLock() _, tokenBlocked := p.invalidated[standardClaims.ID] if !tokenBlocked { // this will disallow refresh tokens with issuer as the blocked access token as well. if standardClaims.Issuer != "" { _, tokenBlocked = p.invalidated[standardClaims.Issuer] } } p.mu.RUnlock() if tokenBlocked { return fmt.Errorf("token was invalidated") } // // check all tokens issuet before the "InvalidateToken" method was fired for this user. p.mu.RLock() ts, oldUserBlocked := p.invalidatedAll[u.ID] p.mu.RUnlock() if oldUserBlocked && standardClaims.IssuedAt <= ts { return fmt.Errorf("token was invalidated") } // return nil // else valid. } func (p *Provider) InvalidateToken(ctx context.Context, standardClaims auth.StandardClaims, u User) error { // fired on SignoutHandler. // invalidate this specific token. p.mu.Lock() p.invalidated[standardClaims.ID] = struct{}{} p.mu.Unlock() return nil } func (p *Provider) InvalidateTokens(ctx context.Context, u User) error { // fired on SignoutAllHandler. // invalidate all previous tokens came from "u". p.mu.Lock() p.invalidatedAll[u.ID] = time.Now().Unix() p.mu.Unlock() return nil } ================================================ FILE: _examples/mvc/websocket-auth/views/layouts/main.html ================================================ {{ if .Title }}{{ .Title }}{{ else }}Default Main Title{{ end }}
{{ template "content" . }}
{{ partial "partials/footer" . }}
================================================ FILE: _examples/mvc/websocket-auth/views/partials/footer.html ================================================ Iris Web Framework © 2023 ================================================ FILE: _examples/mvc/websocket-auth/views/signin.html ================================================ ================================================ FILE: _examples/pprof/main.go ================================================ package main import ( "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/middleware/pprof" ) func main() { app := iris.New() app.Get("/", func(ctx iris.Context) { ctx.HTML("

Please click here") }) p := pprof.New() app.Any("/debug/pprof", p) app.Any("/debug/pprof/{action:path}", p) // ___________ app.Listen(":8080") } ================================================ FILE: _examples/project/README.md ================================================ # Project Structure ```sh $ go run main.go --config=server.yml ``` ================================================ FILE: _examples/project/api/configuration.go ================================================ package api import ( "os" "github.com/kataras/iris/v12" "gopkg.in/yaml.v3" ) // Configuration holds the necessary information // for our server, including the Iris one. type Configuration struct { ServerName string `yaml:"ServerName"` Env string `yaml:"Env"` // The server's host, if empty, defaults to 0.0.0.0 Host string `yaml:"Host"` // The server's port, e.g. 80 Port int `yaml:"Port"` // If not empty runs under tls with this domain using letsencrypt. Domain string `yaml:"Domain"` // Enables write response and read request compression. EnableCompression bool `yaml:"EnableCompression"` // Defines the "Access-Control-Allow-Origin" header of the CORS middleware. // Many can be separated by comma. // Defaults to "*" (allow all). AllowOrigin string `yaml:"AllowOrigin"` // If not empty a request logger is registered, // note that this will cost a lot in performance, use it only for debug. RequestLog string `yaml:"RequestLog"` // The database connection string. ConnString string `yaml:"ConnString"` // Iris specific configuration. Iris iris.Configuration `yaml:"Iris"` } // BindFile binds the yaml file's contents to this Configuration. func (c *Configuration) BindFile(filename string) error { contents, err := os.ReadFile(filename) if err != nil { return err } return yaml.Unmarshal(contents, c) } ================================================ FILE: _examples/project/api/router.go ================================================ package api import ( "time" "github.com/username/project/api/users" "github.com/username/project/pkg/database" "github.com/username/project/user" "github.com/kataras/iris/v12/middleware/modrevision" ) // buildRouter is the most important part of your server. // All root endpoints are registered here. func (srv *Server) buildRouter() { // Add a simple health route. srv.Any("/health", modrevision.New(modrevision.Options{ ServerName: srv.config.ServerName, Env: srv.config.Env, Developer: "kataras", TimeLocation: time.FixedZone("Greece/Athens", 7200), })) api := srv.Party("/api") api.RegisterDependency( database.Open(srv.config.ConnString), user.NewRepository, ) api.PartyConfigure("/user", new(users.API)) } ================================================ FILE: _examples/project/api/server.go ================================================ package api import ( "context" "fmt" "time" "github.com/username/project/pkg/http/handlers" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/middleware/accesslog" "github.com/kataras/golog" ) // Server is a wrapper of the main iris application and our project's custom configuration fields. type Server struct { *iris.Application config Configuration // Here you can keep an instance of the database too. // db *mydatabase_pkg.DB closers []func() // See `AddCloser` method. } // NewServer initializes a new HTTP/2 server. // Use its Run/Listen methods to start it based on network options. func NewServer(c Configuration) *Server { app := iris.New().SetName(c.ServerName) app.Configure(iris.WithConfiguration(c.Iris), iris.WithLowercaseRouting) srv := &Server{ Application: app, config: c, } if err := srv.prepare(); err != nil { srv.Logger().Fatal(err) return nil } return srv } func (srv *Server) prepare() error { // Here you can register the database instance // and prepare any project-relative fields. if srv.Logger().Level == golog.DebugLevel { srv.registerDebugFeatures() } srv.registerMiddlewares() srv.buildRouter() return nil } // registers application-level middlewares. func (srv *Server) registerMiddlewares() { if srv.config.RequestLog != "" { srv.registerAccessLogger() } srv.UseRouter(handlers.CORS(srv.config.AllowOrigin)) if srv.config.EnableCompression { srv.Use(iris.Compression) } } func (srv *Server) registerDebugFeatures() {} func (srv *Server) registerAccessLogger() { // Initialize a new request access log middleware, // note that we use unbuffered data so we can have the results as fast as possible, // this has its cost use it only on debug. // Also, in the future see the iris example to // enable log rotation (date eand filesize-based files). ac := accesslog.FileUnbuffered(srv.config.RequestLog) // The default configuration: ac.Delim = '|' ac.TimeFormat = "2006-01-02 15:04:05" ac.Async = false ac.IP = true ac.BytesReceivedBody = true ac.BytesSentBody = true ac.BytesReceived = false ac.BytesSent = false ac.BodyMinify = false ac.RequestBody = true ac.ResponseBody = false ac.KeepMultiLineError = true ac.PanicLog = accesslog.LogHandler // Default line format if formatter is missing: // Time|Latency|Code|Method|Path|IP|Path Params Query Fields|Bytes Received|Bytes Sent|Request|Response| // // Set Custom Formatter: ac.SetFormatter(&accesslog.JSON{ Indent: " ", HumanTime: true, }) // ac.SetFormatter(&accesslog.CSV{}) // ac.SetFormatter(&accesslog.Template{Text: "{{.Code}}"}) srv.UseRouter(ac.Handler) } // Start runs the server on the TCP network address "0.0.0.0:port" which // handles HTTP/1.1 & 2 requests on incoming connections. func (srv *Server) Start() error { if srv.config.Domain != "" { srv.config.Port = 80 // not required but let's force-modify it. return srv.Application.Run(iris.AutoTLS( ":443", srv.config.Domain, "kataras2006@hotmail.com", )) } srv.ConfigureHost(func(su *iris.Supervisor) { // Set timeouts. More than enough, normally we use 20-30 seconds. su.Server.ReadTimeout = 5 * time.Minute su.Server.WriteTimeout = 5 * time.Minute su.Server.IdleTimeout = 10 * time.Minute su.Server.ReadHeaderTimeout = 2 * time.Minute }) addr := fmt.Sprintf("%s:%d", srv.config.Host, srv.config.Port) return srv.Listen(addr) } // AddCloser adds one or more function that should be called on // manual server shutdown or OS interrupt signals. func (srv *Server) AddCloser(closers ...func()) { for _, closer := range closers { if closer == nil { continue } // Terminate any opened connections on OS interrupt signals. iris.RegisterOnInterrupt(closer) } srv.closers = append(srv.closers, closers...) } // Close gracefully terminates the HTTP server and calls the closers afterwards. func (srv *Server) Close() error { ctx, cancelCtx := context.WithTimeout(context.Background(), 5*time.Second) err := srv.Shutdown(ctx) cancelCtx() for _, closer := range srv.closers { if closer == nil { continue } closer() } return err } ================================================ FILE: _examples/project/api/users/api.go ================================================ package users import ( "github.com/username/project/user" "github.com/kataras/iris/v12" ) type API struct { Users user.Repository // exported field so api/router.go#api.RegisterDependency can bind it. } func (api *API) Configure(r iris.Party) { r.Post("/signup", api.signUp) r.Post("/signin", api.signIn) // Add middlewares such as user verification by bearer token here. // Authenticated routes... r.Get("/", api.getInfo) } func (api *API) getInfo(ctx iris.Context) { ctx.WriteString("...") } func (api *API) signUp(ctx iris.Context) {} func (api *API) signIn(ctx iris.Context) {} ================================================ FILE: _examples/project/cmd/cmd.go ================================================ package cmd import ( "github.com/username/project/api" "github.com/kataras/iris/v12" "github.com/spf13/cobra" ) const defaultConfigFilename = "server.yml" var serverConfig api.Configuration // New returns a new CLI app. // Build with: // $ go build -ldflags="-s -w" func New() *cobra.Command { configFile := defaultConfigFilename rootCmd := &cobra.Command{ Use: "project", Short: "Command line interface for project.", Long: "The root command will start the HTTP server.", Version: "v0.0.1", SilenceErrors: true, SilenceUsage: true, TraverseChildren: true, SuggestionsMinimumDistance: 1, PersistentPreRunE: func(cmd *cobra.Command, args []string) error { // Read configuration from file before any of the commands run functions. return serverConfig.BindFile(configFile) }, RunE: func(cmd *cobra.Command, args []string) error { return startServer() }, } helpTemplate := HelpTemplate{ BuildRevision: iris.BuildRevision, BuildTime: iris.BuildTime, ShowGoRuntimeVersion: true, } rootCmd.SetHelpTemplate(helpTemplate.String()) // Shared flags. flags := rootCmd.PersistentFlags() flags.StringVar(&configFile, "config", configFile, "--config=server.yml a filepath which contains the YAML config format") // Subcommands here. // rootCmd.AddCommand(...) return rootCmd } func startServer() error { srv := api.NewServer(serverConfig) return srv.Start() } ================================================ FILE: _examples/project/cmd/help.go ================================================ package cmd import ( "fmt" "runtime" "strconv" "strings" "time" ) // HelpTemplate is the structure which holds the necessary information for the help command. type HelpTemplate struct { BuildTime string BuildRevision string ShowGoRuntimeVersion bool Template fmt.Stringer } func (h HelpTemplate) String() string { tmpl := `{{with (or .Long .Short)}}{{. | trimTrailingWhitespaces}} {{end}}{{if or .Runnable .HasSubCommands}}{{.UsageString}}{{end}}` if h.BuildRevision != "" { buildTitle := ">>>> build" // if we ever want an emoji, there is one: \U0001f4bb tab := strings.Repeat(" ", len(buildTitle)) n, _ := strconv.ParseInt(h.BuildTime, 10, 64) buildTimeStr := time.Unix(n, 0).Format(time.UnixDate) buildTmpl := fmt.Sprintf("\n%s\n", buildTitle) + fmt.Sprintf("%s revision %s\n", tab, h.BuildRevision) + fmt.Sprintf("%s datetime %s\n", tab, buildTimeStr) if h.ShowGoRuntimeVersion { buildTmpl += fmt.Sprintf("%s runtime %s\n", tab, runtime.Version()) } tmpl += buildTmpl } return tmpl } ================================================ FILE: _examples/project/go.mod ================================================ module github.com/username/project go 1.25 require ( github.com/kataras/golog v0.1.12 github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd github.com/spf13/cobra v1.10.2 gopkg.in/yaml.v3 v3.0.1 ) require ( github.com/BurntSushi/toml v1.6.0 // indirect github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect github.com/CloudyKit/jet/v6 v6.3.1 // indirect github.com/Joker/jade v1.1.3 // indirect github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 // indirect github.com/andybalholm/brotli v1.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/fatih/structs v1.1.0 // indirect github.com/flosch/pongo2/v4 v4.0.2 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/iris-contrib/schema v0.0.6 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/kataras/blocks v0.0.8 // indirect github.com/kataras/pio v0.0.13 // indirect github.com/kataras/sitemap v0.0.6 // indirect github.com/kataras/tunnel v0.0.4 // indirect github.com/klauspost/compress v1.18.2 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mailgun/raymond/v2 v2.0.48 // indirect github.com/mailru/easyjson v0.9.1 // indirect github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/schollz/closestmatch v2.1.0+incompatible // indirect github.com/sirupsen/logrus v1.8.1 // indirect github.com/spf13/pflag v1.0.9 // indirect github.com/stretchr/testify v1.11.1 // indirect github.com/tdewolff/minify/v2 v2.24.8 // indirect github.com/tdewolff/parse/v2 v2.8.5 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/yosssi/ace v0.0.5 // indirect golang.org/x/crypto v0.47.0 // indirect golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect golang.org/x/net v0.49.0 // indirect golang.org/x/sys v0.40.0 // indirect golang.org/x/text v0.33.0 // indirect golang.org/x/time v0.14.0 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/ini.v1 v1.67.0 // indirect ) ================================================ FILE: _examples/project/go.sum ================================================ github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= github.com/CloudyKit/jet/v6 v6.3.1 h1:6IAo5Cx21xrHVaR8zzXN5gJatKV/wO7Nf6bfCnCSbUw= github.com/CloudyKit/jet/v6 v6.3.1/go.mod h1:lf8ksdNsxZt7/yH/3n4vJQWA9RUq4wpaHtArHhGVMOw= github.com/Joker/hpp v1.0.0 h1:65+iuJYdRXv/XyN62C1uEmmOx3432rNG/rKlX6V7Kkc= github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= github.com/Joker/jade v1.1.3 h1:Qbeh12Vq6BxURXT1qZBRHsDxeURB8ztcL6f3EXSGeHk= github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKduM= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 h1:dG7/gLroJGht/jSQtHiLvT48Hxn+crbmvyItZC8cWOs= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05/go.mod h1:NYezi6wtnJtBm5btoprXc5SvAdqH0XTXWnUup0MptAI= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/flosch/pongo2/v4 v4.0.2 h1:gv+5Pe3vaSVmiJvh/BZa82b7/00YUGm0PIyVVLop0Hw= github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+BWPhwZD8= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a h1:l7A0loSszR5zHd/qK53ZIHMO8b3bBSmENnQ6eKnUT0A= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/iris-contrib/httpexpect/v2 v2.15.2 h1:T9THsdP1woyAqKHwjkEsbCnMefsAFvk8iJJKokcJ3Go= github.com/iris-contrib/httpexpect/v2 v2.15.2/go.mod h1:JLDgIqnFy5loDSUv1OA2j0mb6p/rDhiCqigP22Uq9xE= github.com/iris-contrib/schema v0.0.6 h1:CPSBLyx2e91H2yJzPuhGuifVRnZBBJ3pCOMbOvPZaTw= github.com/iris-contrib/schema v0.0.6/go.mod h1:iYszG0IOsuIsfzjymw1kMzTL8YQcCWlm65f3wX8J5iA= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kataras/blocks v0.0.8 h1:MrpVhoFTCR2v1iOOfGng5VJSILKeZZI+7NGfxEh3SUM= github.com/kataras/blocks v0.0.8/go.mod h1:9Jm5zx6BB+06NwA+OhTbHW1xkMOYxahnqTN5DveZ2Yg= github.com/kataras/golog v0.1.12 h1:Bu7I/G4ilJlbfzjmU39O9N+2uO1pBcMK045fzZ4ytNg= github.com/kataras/golog v0.1.12/go.mod h1:wrGSbOiBqbQSQznleVNX4epWM8rl9SJ/rmEacl0yqy4= github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd h1:oMsQgqKACbUdlaIWnN+EZzzAnHkw90hE/y/R0BSij2U= github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd/go.mod h1:yucjtDl9Vn3OctWM1fi0MuM9OckemC7kEreFa9QtPF8= github.com/kataras/pio v0.0.13 h1:x0rXVX0fviDTXOOLOmr4MUxOabu1InVSTu5itF8CXCM= github.com/kataras/pio v0.0.13/go.mod h1:k3HNuSw+eJ8Pm2lA4lRhg3DiCjVgHlP8hmXApSej3oM= github.com/kataras/sitemap v0.0.6 h1:w71CRMMKYMJh6LR2wTgnk5hSgjVNB9KL60n5e2KHvLY= github.com/kataras/sitemap v0.0.6/go.mod h1:dW4dOCNs896OR1HmG+dMLdT7JjDk7mYBzoIRwuj5jA4= github.com/kataras/tunnel v0.0.4 h1:sCAqWuJV7nPzGrlb0os3j49lk2JhILT0rID38NHNLpA= github.com/kataras/tunnel v0.0.4/go.mod h1:9FkU4LaeifdMWqZu7o20ojmW4B7hdhv2CMLwfnHGpYw= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw= github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18= github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo= github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk= github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tdewolff/minify/v2 v2.24.8 h1:58/VjsbevI4d5FGV0ZSuBrHMSSkH4MCH0sIz/eKIauE= github.com/tdewolff/minify/v2 v2.24.8/go.mod h1:0Ukj0CRpo/sW/nd8uZ4ccXaV1rEVIWA3dj8U7+Shhfw= github.com/tdewolff/parse/v2 v2.8.5 h1:ZmBiA/8Do5Rpk7bDye0jbbDUpXXbCdc3iah4VeUvwYU= github.com/tdewolff/parse/v2 v2.8.5/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo= github.com/tdewolff/test v1.0.11 h1:FdLbwQVHxqG16SlkGveC0JVyrJN62COWTRyUFzfbtBE= github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA= github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0= github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs= moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHcE= ================================================ FILE: _examples/project/main.go ================================================ package main import ( "fmt" "os" "github.com/username/project/cmd" ) func main() { app := cmd.New() if err := app.Execute(); err != nil { fmt.Println(err) os.Exit(1) } } ================================================ FILE: _examples/project/pkg/database/database.go ================================================ package database type DB struct { /* ... */ } func Open(connString string) *DB { return &DB{} } ================================================ FILE: _examples/project/pkg/http/handlers/cors.go ================================================ package handlers import "github.com/kataras/iris/v12" // CORS set ups a cors allow-all. // We may need to edit it before deployment. func CORS(allowedOrigin string) iris.Handler { // or "github.com/iris-contrib/middleware/cors" if allowedOrigin == "" { allowedOrigin = "*" } return func(ctx iris.Context) { ctx.Header("Access-Control-Allow-Origin", allowedOrigin) ctx.Header("Access-Control-Allow-Credentials", "true") // July 2021 Mozzila updated the following document: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy ctx.Header("Referrer-Policy", "no-referrer-when-downgrade") ctx.Header("Access-Control-Expose-Headers", "*, Authorization, X-Authorization") if ctx.Method() == iris.MethodOptions { ctx.Header("Access-Control-Allow-Methods", "*") ctx.Header("Access-Control-Allow-Headers", "*") ctx.Header("Access-Control-Max-Age", "86400") ctx.StatusCode(iris.StatusNoContent) return } ctx.Next() } } ================================================ FILE: _examples/project/server.yml ================================================ ServerName: My Project Env: development Host: 0.0.0.0 Port: 80 EnableCompression: true AllowOrigin: "*" Iris: # Defaults to info. LogLevel: info # Defaults to false. EnableOptimizations: true # Defaults to empty. RemoteAddrHeaders: - "X-Real-Ip" - "X-Forwarded-For" - "CF-Connecting-IP" - "True-Client-Ip" - "X-Appengine-Remote-Addr" ================================================ FILE: _examples/project/user/repository.go ================================================ package user import "github.com/username/project/pkg/database" type Repository interface { // Repo methods here... } type repo struct { // Hold database instance here: e.g. db *database.DB } func NewRepository(db *database.DB) Repository { return &repo{db: db} } ================================================ FILE: _examples/project/user/user.go ================================================ package user type User struct { /* ... */ } ================================================ FILE: _examples/recover/main.go ================================================ package main import ( "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/middleware/recover" ) func main() { app := iris.New() app.Use(recover.New()) i := 0 // let's simulate a panic every next request app.Get("/", func(ctx iris.Context) { i++ if i%2 == 0 { panic("a panic here") } ctx.Writef("Hello, refresh one time more to get panic!") }) // http://localhost:8080, refresh it 5-6 times. app.Listen(":8080") } // Note: // app := iris.Default() instead of iris.New() makes use of the recovery middleware automatically. ================================================ FILE: _examples/recover/panic-and-custom-error-handler-with-compression/main.go ================================================ package main import ( "errors" "fmt" "github.com/kataras/iris/v12" // "github.com/kataras/iris/v12/context" ) func main() { app := iris.New() app.UseRouter(iris.Compression) app.UseRouter(myErrorHandler) app.Get("/", handler) app.Listen(":8080") } func myErrorHandler(ctx iris.Context) { recorder := ctx.Recorder() defer func() { var err error if v := recover(); v != nil { // panic if panicErr, ok := v.(error); ok { err = panicErr } else { err = errors.New(fmt.Sprint(v)) } } else { // custom error. err = ctx.GetErr() } if err != nil { // To keep compression after reset: // clear body and any headers created between recorder and handler. recorder.ResetBody() recorder.ResetHeaders() // // To disable compression after reset: // recorder.Reset() // recorder.ResponseWriter.(*context.CompressResponseWriter).Disabled = true // ctx.StopWithJSON(iris.StatusInternalServerError, iris.Map{ "message": err.Error(), }) } }() ctx.Next() } func handler(ctx iris.Context) { ctx.WriteString("Content may fall") ctx.Header("X-Test", "value") // ctx.SetErr(fmt.Errorf("custom error message")) panic("errr!") } ================================================ FILE: _examples/request-body/form-query-headers-params-decoder/main.go ================================================ // package main contains an example on how to register a custom decoder // for a custom type when using the `ReadQuery/ReadParams/ReadHeaders/ReadForm` methods. // // Let's take for example the mongo-driver/primite.ObjectID: // // ObjectID is type ObjectID [12]byte. // You have to register a converter for that custom type. // ReadJSON works because the ObjectID has a MarshalJSON method // which the encoding/json pakcage uses as a converter. // See here: https://godoc.org/go.mongodb.org/mongo-driver/bson/primitive#ObjectID.MarshalJSON. // // To register a converter import the github.com/iris-contrib/schema and call // schema.Query.RegisterConverter(value any, converterFunc Converter). // // The Converter is just a type of func(string) reflect.Value. // // There is another way, but requires introducing a custom type which will wrap the mongo's ObjectID // and implements the encoding.TextUnmarshaler e.g. // func(id *ID) UnmarshalText(text []byte) error){ ...}. package main import ( "reflect" "go.mongodb.org/mongo-driver/bson/primitive" "github.com/iris-contrib/schema" "github.com/kataras/iris/v12" ) type MyType struct { ID primitive.ObjectID `url:"id"` } func main() { // Register on initialization. schema.Query.RegisterConverter(primitive.ObjectID{}, func(value string) reflect.Value { id, err := primitive.ObjectIDFromHex(value) if err != nil { return reflect.Value{} } return reflect.ValueOf(id) }) app := iris.New() app.Get("/", func(ctx iris.Context) { var t MyType err := ctx.ReadQuery(&t) if err != nil && !iris.IsErrPath(err) { ctx.StopWithError(iris.StatusInternalServerError, err) return } ctx.Writef("MyType.ID: %q", t.ID.Hex()) }) // http://localhost:8080?id=507f1f77bcf86cd799439011 app.Listen(":8080") } ================================================ FILE: _examples/request-body/read-body/main.go ================================================ package main import ( "github.com/kataras/iris/v12" ) func main() { app := newApp() // See main_test.go for usage. app.Listen(":8080") } func newApp() *iris.Application { app := iris.New() // To automatically decompress using gzip: // app.Use(iris.GzipReader) app.Use(setAllowedResponses) app.Post("/", readBody) return app } type payload struct { Message string `json:"message" xml:"message" msgpack:"message" yaml:"Message" url:"message" form:"message"` } func readBody(ctx iris.Context) { var p payload // Bind request body to "p" depending on the content-type that client sends the data, // e.g. JSON, XML, YAML, MessagePack, Protobuf, Form and URL Query. err := ctx.ReadBody(&p) if err != nil { ctx.StopWithProblem(iris.StatusBadRequest, iris.NewProblem().Title("Parser issue").Detail(err.Error())) return } // For the sake of the example, log the received payload. ctx.Application().Logger().Infof("Received: %#+v", p) // Send back the payload depending on the accept content type and accept-encoding of the client, // e.g. JSON, XML and so on. ctx.Negotiate(p) } func setAllowedResponses(ctx iris.Context) { // Indicate that the Server can send JSON, XML, YAML and MessagePack for this request. ctx.Negotiation().JSON().XML().YAML().MsgPack() // Add more, allowed by the server format of responses, mime types here... // If client is missing an "Accept: " header then default it to JSON. ctx.Negotiation().Accept.JSON() ctx.Next() } ================================================ FILE: _examples/request-body/read-body/main_test.go ================================================ package main import ( "testing" "github.com/kataras/iris/v12/httptest" ) func TestReadBodyAndNegotiate(t *testing.T) { app := newApp() e := httptest.New(t, app) var ( expectedPayload = payload{Message: "a message"} expectedMsgPackPayload = "\x81\xa7message\xa9a message" expectedXMLPayload = `a message` expectedYAMLPayload = "Message: a message\n" ) // Test send JSON and receive JSON. e.POST("/").WithJSON(expectedPayload).Expect().Status(httptest.StatusOK). JSON().IsEqual(expectedPayload) // Test send Form and receive XML. e.POST("/").WithForm(expectedPayload). WithHeader("Accept", "application/xml"). Expect().Status(httptest.StatusOK). Body().IsEqual(expectedXMLPayload) // Test send URL Query and receive MessagePack. e.POST("/").WithQuery("message", expectedPayload.Message). WithHeader("Accept", "application/msgpack"). Expect().Status(httptest.StatusOK).ContentType("application/msgpack"). Body().IsEqual(expectedMsgPackPayload) // Test send MessagePack and receive MessagePack. e.POST("/").WithBytes([]byte(expectedMsgPackPayload)). WithHeader("Content-Type", "application/msgpack"). WithHeader("Accept", "application/msgpack"). Expect().Status(httptest.StatusOK). ContentType("application/msgpack").Body().IsEqual(expectedMsgPackPayload) // Test send YAML and receive YAML. e.POST("/").WithBytes([]byte(expectedYAMLPayload)). WithHeader("Content-Type", "application/x-yaml"). WithHeader("Accept", "application/x-yaml"). Expect().Status(httptest.StatusOK). ContentType("application/x-yaml").Body().IsEqual(expectedYAMLPayload) } ================================================ FILE: _examples/request-body/read-custom-per-type/main.go ================================================ package main import ( "gopkg.in/yaml.v3" "github.com/kataras/iris/v12" ) func main() { app := newApp() // use Postman or whatever to do a POST request // (however you are always free to use app.Get and GET http method requests to read body of course) // to the http://localhost:8080 with RAW BODY: /* addr: localhost:8080 serverName: Iris */ // // The response should be: // Received: main.config{Addr:"localhost:8080", ServerName:"Iris"} app.Listen(":8080", iris.WithOptimizations) } func newApp() *iris.Application { app := iris.New() app.Post("/", handler) return app } // simple yaml stuff, read more at https://github.com/go-yaml/yaml type config struct { Addr string `yaml:"addr"` ServerName string `yaml:"serverName"` } // Decode implements the `kataras/iris/context#BodyDecoder` optional interface // that any go type can implement in order to be self-decoded when reading the request's body. func (c *config) Decode(body []byte) error { return yaml.Unmarshal(body, c) } func handler(ctx iris.Context) { var c config // // Note: // second parameter is nil because our &c implements the `context#BodyDecoder` // which has a priority over the context#Unmarshaler (which can be a more global option for reading request's body) // see the `request-body/read-custom-via-unmarshaler/main.go` example to learn how to use the context#Unmarshaler too. // // Note 2: // If you need to read the body again for any reason // you should disable the body consumption via `app.Run(..., iris.WithoutBodyConsumptionOnUnmarshal)`. // if err := ctx.UnmarshalBody(&c, nil); err != nil { ctx.StopWithError(iris.StatusBadRequest, err) return } ctx.Writef("Received: %#+v", c) } ================================================ FILE: _examples/request-body/read-custom-per-type/main_test.go ================================================ package main import ( "testing" "github.com/kataras/iris/v12/httptest" ) func TestReadCustomPerType(t *testing.T) { app := newApp() e := httptest.New(t, app) expectedResponse := `Received: main.config{Addr:"localhost:8080", ServerName:"Iris"}` e.POST("/").WithText("addr: localhost:8080\nserverName: Iris").Expect(). Status(httptest.StatusOK).Body().IsEqual(expectedResponse) } ================================================ FILE: _examples/request-body/read-custom-via-unmarshaler/main.go ================================================ package main import ( "gopkg.in/yaml.v3" "github.com/kataras/iris/v12" ) func main() { app := newApp() // use Postman or whatever to do a POST request // (however you are always free to use app.Get and GET http method requests to read body of course) // to the http://localhost:8080 with RAW BODY: /* addr: localhost:8080 serverName: Iris */ // // The response should be: // Received: main.config{Addr:"localhost:8080", ServerName:"Iris"} app.Listen(":8080", iris.WithOptimizations) } func newApp() *iris.Application { app := iris.New() app.Post("/", handler) return app } // simple yaml stuff, read more at https://github.com/go-yaml/yaml type config struct { Addr string `yaml:"addr"` ServerName string `yaml:"serverName"` } /* type myBodyDecoder struct{} var DefaultBodyDecoder = myBodyDecoder{} // Implements the `kataras/iris/context#Unmarshaler` but at our example // we will use the simplest `context#UnmarshalerFunc` to pass just the yaml.Unmarshal. // // Can be used as: ctx.UnmarshalBody(&c, DefaultBodyDecoder) func (r *myBodyDecoder) Unmarshal(data []byte, outPtr any) error { return yaml.Unmarshal(data, outPtr) } */ func handler(ctx iris.Context) { var c config // // Note: // yaml.Unmarshal already implements the `context#Unmarshaler` // so we can use it directly, like the json.Unmarshal(ctx.ReadJSON), xml.Unmarshal(ctx.ReadXML) // and every library which follows the best practises and is aligned with the Go standards. // // Note 2: // If you need to read the body again for any reason // you should disable the body consumption via `app.Run(..., iris.WithoutBodyConsumptionOnUnmarshal)`. // if err := ctx.UnmarshalBody(&c, iris.UnmarshalerFunc(yaml.Unmarshal)); err != nil { ctx.StopWithError(iris.StatusBadRequest, err) return } ctx.Writef("Received: %#+v", c) } ================================================ FILE: _examples/request-body/read-custom-via-unmarshaler/main_test.go ================================================ package main import ( "testing" "github.com/kataras/iris/v12/httptest" ) func TestReadCustomViaUnmarshaler(t *testing.T) { app := newApp() e := httptest.New(t, app) expectedResponse := `Received: main.config{Addr:"localhost:8080", ServerName:"Iris"}` e.POST("/").WithText("addr: localhost:8080\nserverName: Iris").Expect(). Status(httptest.StatusOK).Body().IsEqual(expectedResponse) } ================================================ FILE: _examples/request-body/read-form/checkboxes/main.go ================================================ package main import ( "fmt" "github.com/kataras/iris/v12" ) func main() { app := iris.New() app.RegisterView(iris.HTML("./templates", ".html")) app.Get("/", showForm) app.Post("/", handleForm) app.Listen(":8080") } func showForm(ctx iris.Context) { if err := ctx.View("form.html"); err != nil { ctx.HTML(fmt.Sprintf("

%s

", err.Error())) return } } type formExample struct { Colors []string `form:"colors[]"` // or just colors, it'll work as expected. } func handleForm(ctx iris.Context) { var form formExample err := ctx.ReadForm(&form) if err != nil { ctx.StopWithError(iris.StatusBadRequest, err) return } ctx.JSON(iris.Map{"Colors": form.Colors}) } ================================================ FILE: _examples/request-body/read-form/checkboxes/templates/form.html ================================================ Select a color

Select one or more colors

================================================ FILE: _examples/request-body/read-form/main.go ================================================ // package main contains an example on how to use the ReadForm, but with the same way you can do the ReadJSON & ReadJSON package main import ( "fmt" "github.com/kataras/iris/v12" ) type Visitor struct { Username string Mail string Data []string `form:"mydata"` } func main() { app := iris.New() // set the view html template engine app.RegisterView(iris.HTML("./templates", ".html").Reload(true)) app.Get("/", func(ctx iris.Context) { if err := ctx.View("form.html"); err != nil { ctx.HTML(fmt.Sprintf("

%s

", err.Error())) return } }) app.Post("/form_action", func(ctx iris.Context) { visitor := Visitor{} err := ctx.ReadForm(&visitor) if err != nil { if !iris.IsErrPath(err) /* see: https://github.com/kataras/iris/issues/1157 */ || err == iris.ErrEmptyForm { ctx.StopWithError(iris.StatusInternalServerError, err) return } } ctx.Writef("Visitor: %#v", visitor) }) app.Post("/post_value", func(ctx iris.Context) { username := ctx.PostValueDefault("Username", "iris") ctx.Writef("Username: %s", username) }) app.Listen(":8080", iris.WithEmptyFormError /* returns ErrEmptyForm if the request form body was empty */) } ================================================ FILE: _examples/request-body/read-form/templates/form.html ================================================
Username:
Mail:
Select one or more:

================================================ FILE: _examples/request-body/read-headers/main.go ================================================ // package main contains an example on how to use the ReadHeaders, // same way you can do the ReadQuery, ReadJSON, ReadProtobuf and e.t.c. package main import ( "github.com/kataras/iris/v12" ) type myHeaders struct { RequestID string `header:"X-Request-Id,required"` Authentication string `header:"Authentication,required"` } func main() { app := newApp() // http://localhost:8080 /* myHeaders: main.myHeaders{ RequestID: "373713f0-6b4b-42ea-ab9f-e2e04bc38e73", Authentication: "Bearer my-token", } */ app.Listen(":8080") } func newApp() *iris.Application { app := iris.New() app.Get("/", func(ctx iris.Context) { var hs myHeaders if err := ctx.ReadHeaders(&hs); err != nil { ctx.StopWithError(iris.StatusInternalServerError, err) return } ctx.Writef("myHeaders: %#v", hs) }) return app } ================================================ FILE: _examples/request-body/read-headers/main_test.go ================================================ package main import ( "testing" "github.com/kataras/iris/v12/httptest" ) func TestReadHeaders(t *testing.T) { app := newApp() e := httptest.New(t, app) expectedOKBody := `myHeaders: main.myHeaders{RequestID:"373713f0-6b4b-42ea-ab9f-e2e04bc38e73", Authentication:"Bearer my-token"}` tests := []struct { headers map[string]string code int body string regex bool }{ {headers: map[string]string{ "X-Request-Id": "373713f0-6b4b-42ea-ab9f-e2e04bc38e73", "Authentication": "Bearer my-token", }, code: 200, body: expectedOKBody, regex: false}, {headers: map[string]string{ "x-request-id": "373713f0-6b4b-42ea-ab9f-e2e04bc38e73", "authentication": "Bearer my-token", }, code: 200, body: expectedOKBody, regex: false}, {headers: map[string]string{ "X-Request-Id": "373713f0-6b4b-42ea-ab9f-e2e04bc38e73", "Authentication": "Bearer my-token", }, code: 200, body: expectedOKBody, regex: false}, {headers: map[string]string{ "Authentication": "Bearer my-token", }, code: 500, body: "X-Request-Id is empty", regex: false}, {headers: map[string]string{ "X-Request-Id": "373713f0-6b4b-42ea-ab9f-e2e04bc38e73", }, code: 500, body: "Authentication is empty", regex: false}, {headers: map[string]string{}, code: 500, body: ".*\\(and 1 other error\\)$", regex: true}, } for _, tt := range tests { te := e.GET("/").WithHeaders(tt.headers).Expect().Status(tt.code).Body() if tt.regex { te.Match(tt.body) } else { te.Equal(tt.body) } } } ================================================ FILE: _examples/request-body/read-json/main.go ================================================ package main import ( "github.com/kataras/iris/v12" ) type Company struct { Name string City string Other string } func MyHandler(ctx iris.Context) { var c Company if err := ctx.ReadJSON(&c); err != nil { ctx.StopWithError(iris.StatusBadRequest, err) return } ctx.Writef("Received: %#+v\n", c) } // simple json stuff, read more at https://golang.org/pkg/encoding/json type Person struct { Name string `json:"name"` Age int `json:"age"` } // MyHandler2 reads a collection of Person from JSON post body. func MyHandler2(ctx iris.Context) { var persons []Person err := ctx.ReadJSON(&persons) if err != nil { ctx.StopWithError(iris.StatusBadRequest, err) return } ctx.Writef("Received: %#+v\n", persons) } func main() { app := iris.New() app.Post("/", MyHandler) app.Post("/slice", MyHandler2) // use Postman or whatever to do a POST request // to the http://localhost:8080 with RAW BODY: /* { "Name": "iris-Go", "City": "New York", "Other": "Something here" } */ // and Content-Type to application/json (optionally but good practise) // // The response should be: // Received: main.Company{Name:"iris-Go", City:"New York", Other:"Something here"} app.Listen(":8080", iris.WithOptimizations) } ================================================ FILE: _examples/request-body/read-json-stream/main.go ================================================ package main import "github.com/kataras/iris/v12" func main() { app := iris.New() app.Post("/", postIndex) app.Post("/stream", postIndexStream) /* curl -L -X POST "http://localhost:8080/" \ -H 'Content-Type: application/json' \ --data-raw '{"username":"john"}' curl -L -X POST "http://localhost:8080/stream" \ -H 'Content-Type: application/json' \ --data-raw '{"username":"john"} {"username":"makis"} {"username":"george"} {"username":"michael"} ' If JSONReader.ArrayStream was true then you must provide an array of objects instead, e.g. [{"username":"john"}, {"username":"makis"}, {"username":"george"}, {"username":"michael"}] */ app.Listen(":8080") } type User struct { Username string `json:"username"` } func postIndex(ctx iris.Context) { var u User err := ctx.ReadJSON(&u, iris.JSONReader{ // To throw an error on unknown request payload json fields. DisallowUnknownFields: true, Optimize: true, }) if err != nil { ctx.StopWithError(iris.StatusBadRequest, err) return } ctx.JSON(iris.Map{ "code": iris.StatusOK, "username": u.Username, }) } func postIndexStream(ctx iris.Context) { var users []User job := func(decode iris.DecodeFunc) error { var u User if err := decode(&u); err != nil { return err } users = append(users, u) // When the returned error is not nil the decode operation // is terminated and the error is received by the ReadJSONStream method below, // otherwise it continues to read the next available object. return nil } err := ctx.ReadJSONStream(job, iris.JSONReader{ Optimize: true, DisallowUnknownFields: true, ArrayStream: false, }) if err != nil { ctx.StopWithError(iris.StatusBadRequest, err) return } ctx.JSON(iris.Map{ "code": iris.StatusOK, "users_count": len(users), "users": users, }) } ================================================ FILE: _examples/request-body/read-json-struct-validation/main.go ================================================ // Package main shows the validator(latest, version 10) integration with Iris' Context methods of // `ReadJSON`, `ReadXML`, `ReadMsgPack`, `ReadYAML`, `ReadForm`, `ReadQuery`, `ReadBody`. // // You can find more examples of this 3rd-party library at: // https://github.com/go-playground/validator/blob/master/_examples package main import ( "fmt" "github.com/kataras/iris/v12" // $ go get github.com/go-playground/validator/v10@latest "github.com/go-playground/validator/v10" ) func main() { app := iris.New() app.Validator = validator.New() userRouter := app.Party("/user") { userRouter.Get("/validation-errors", resolveErrorsDocumentation) userRouter.Post("/", postUser) } // Use Postman or any tool to perform a POST request // to the http://localhost:8080/user with RAW BODY of: /* { "fname": "", "lname": "", "age": 45, "email": "mail@example.com", "favColor": "#000", "addresses": [{ "street": "Eavesdown Docks", "planet": "Persphone", "phone": "none", "city": "Unknown" }] } */ /* The response should be: { "title": "Validation error", "detail": "One or more fields failed to be validated", "type": "http://localhost:8080/user/validation-errors", "status": 400, "fields": [ { "tag": "required", "namespace": "User.FirstName", "kind": "string", "type": "string", "value": "", "param": "" }, { "tag": "required", "namespace": "User.LastName", "kind": "string", "type": "string", "value": "", "param": "" } ] } */ app.Listen(":8080") } // User contains user information. type User struct { FirstName string `json:"fname" validate:"required"` LastName string `json:"lname" validate:"required"` Age uint8 `json:"age" validate:"gte=0,lte=130"` Email string `json:"email" validate:"required,email"` FavouriteColor string `json:"favColor" validate:"hexcolor|rgb|rgba"` Addresses []*Address `json:"addresses" validate:"required,dive,required"` // a User can have a home and cottage... } // Address houses a users address information. type Address struct { Street string `json:"street" validate:"required"` City string `json:"city" validate:"required"` Planet string `json:"planet" validate:"required"` Phone string `json:"phone" validate:"required"` } type validationError struct { ActualTag string `json:"tag"` Namespace string `json:"namespace"` Kind string `json:"kind"` Type string `json:"type"` Value string `json:"value"` Param string `json:"param"` } func wrapValidationErrors(errs validator.ValidationErrors) []validationError { validationErrors := make([]validationError, 0, len(errs)) for _, validationErr := range errs { validationErrors = append(validationErrors, validationError{ ActualTag: validationErr.ActualTag(), Namespace: validationErr.Namespace(), Kind: validationErr.Kind().String(), Type: validationErr.Type().String(), Value: fmt.Sprintf("%v", validationErr.Value()), Param: validationErr.Param(), }) } return validationErrors } func postUser(ctx iris.Context) { var user User err := ctx.ReadJSON(&user) if err != nil { // Handle the error, below you will find the right way to do that... if errs, ok := err.(validator.ValidationErrors); ok { // Wrap the errors with JSON format, the underline library returns the errors as interface. validationErrors := wrapValidationErrors(errs) // Fire an application/json+problem response and stop the handlers chain. ctx.StopWithProblem(iris.StatusBadRequest, iris.NewProblem(). Title("Validation error"). Detail("One or more fields failed to be validated"). Type("/user/validation-errors"). Key("errors", validationErrors)) return } // It's probably an internal JSON error, let's dont give more info here. ctx.StopWithStatus(iris.StatusInternalServerError) return } ctx.JSON(iris.Map{"message": "OK"}) } func resolveErrorsDocumentation(ctx iris.Context) { ctx.WriteString("A page that should document to web developers or users of the API on how to resolve the validation errors") } ================================================ FILE: _examples/request-body/read-many/main.go ================================================ package main import ( "github.com/kataras/iris/v12" ) func main() { app := iris.New() app.Post("/", logAllBody, logJSON, logFormValues, func(ctx iris.Context) { // body, err := io.ReadAll(ctx.Request().Body) once or body, err := ctx.GetBody() // as many times as you need. if err != nil { ctx.StopWithError(iris.StatusInternalServerError, err) return } if len(body) == 0 { ctx.WriteString(`The body was empty or iris.WithoutBodyConsumptionOnUnmarshal option is missing from app.Run. Check the terminal window for any queries logs.`) } else { ctx.WriteString("OK body is still:\n") ctx.Write(body) } }) // With ctx.UnmarshalBody, ctx.ReadJSON, ctx.ReadXML, ctx.ReadForm, ctx.FormValues // and ctx.GetBody methods the default golang and net/http behavior // is to consume the readen data - they are not available on any next handlers in the chain - // to change that behavior just pass the `WithoutBodyConsumptionOnUnmarshal` option. app.Listen(":8080", iris.WithoutBodyConsumptionOnUnmarshal) } func logAllBody(ctx iris.Context) { body, err := ctx.GetBody() if err == nil && len(body) > 0 { ctx.Application().Logger().Infof("logAllBody: %s", string(body)) } ctx.Next() } func logJSON(ctx iris.Context) { var p any if err := ctx.ReadJSON(&p); err == nil { ctx.Application().Logger().Infof("logJSON: %#+v", p) } ctx.Next() } func logFormValues(ctx iris.Context) { values := ctx.FormValues() if values != nil { ctx.Application().Logger().Infof("logFormValues: %v", values) } ctx.Next() } ================================================ FILE: _examples/request-body/read-msgpack/main.go ================================================ package main import "github.com/kataras/iris/v12" // User example struct to bind to. type User struct { Firstname string `msgpack:"firstname"` Lastname string `msgpack:"lastname"` City string `msgpack:"city"` Age int `msgpack:"age"` } // readMsgPack reads a `User` from MsgPack post body. func readMsgPack(ctx iris.Context) { var u User err := ctx.ReadMsgPack(&u) if err != nil { ctx.StopWithError(iris.StatusBadRequest, err) return } ctx.Writef("Received: %#+v\n", u) } func main() { app := iris.New() app.Post("/", readMsgPack) // POST: http://localhost:8080 // // To run the example, use a tool like Postman: // 1. Body: Binary // 2. Select File, select the one from "_examples/response-writer/write-rest" example. // The output should be: // Received: main.User{Firstname:"John", Lastname:"Doe", City:"Neither FBI knows!!!", Age:25} app.Listen(":8080") } ================================================ FILE: _examples/request-body/read-params/main.go ================================================ // package main contains an example on how to use the ReadParams, // same way you can do the ReadQuery, ReadJSON, ReadProtobuf and e.t.c. package main import ( "github.com/kataras/iris/v12" ) type myParams struct { Name string `param:"name"` Age int `param:"age"` Tail []string `param:"tail"` } func main() { app := newApp() // http://localhost:8080/kataras/27/iris/web/framework // myParams: main.myParams{Name:"kataras", Age:27, Tail:[]string{"iris", "web", "framework"}} app.Listen(":8080") } func newApp() *iris.Application { app := iris.New() app.Get("/{name}/{age:int}/{tail:path}", func(ctx iris.Context) { var p myParams if err := ctx.ReadParams(&p); err != nil { ctx.StopWithError(iris.StatusInternalServerError, err) return } ctx.Writef("myParams: %#v", p) }) return app } ================================================ FILE: _examples/request-body/read-params/main_test.go ================================================ package main import ( "testing" "github.com/kataras/iris/v12/httptest" ) func TestReadParams(t *testing.T) { app := newApp() e := httptest.New(t, app) expectedBody := `myParams: main.myParams{Name:"kataras", Age:27, Tail:[]string{"iris", "web", "framework"}}` e.GET("/kataras/27/iris/web/framework").Expect().Status(httptest.StatusOK).Body().IsEqual(expectedBody) } ================================================ FILE: _examples/request-body/read-query/main.go ================================================ // package main contains an example on how to use the ReadQuery, // same way you can do the ReadJSON & ReadProtobuf and e.t.c. package main import ( "github.com/kataras/iris/v12" ) type MyType struct { Name string `url:"name,required"` Age int `url:"age"` } func main() { app := iris.New() app.UseRouter(iris.AllowQuerySemicolons) // Optionally: to restore pre go1.17 behavior of url parsing. app.Get("/", func(ctx iris.Context) { var t MyType err := ctx.ReadQuery(&t) // To ignore errors of "required" or when unexpected values are passed to the query, // use the iris.IsErrPath. // It can be ignored, e.g: // if err!=nil && !iris.IsErrPath(err) { ... return } // // To receive an error on EMPTY query when ReadQuery is called // you should enable the `FireEmptyFormError/WithEmptyFormError` ( see below). // To check for the empty error you simple compare the error with the ErrEmptyForm, e.g.: // err == iris.ErrEmptyForm, so, to ignore both path and empty errors, you do: // if err!=nil && err != iris.ErrEmptyForm && !iris.IsErrPath(err) { ctx.StopWithError(...); return } if err != nil { ctx.StopWithError(iris.StatusInternalServerError, err) return } ctx.Writef("MyType: %#v", t) }) app.Get("/simple", func(ctx iris.Context) { names := ctx.URLParamSlice("name") ctx.Writef("names: %v", names) }) // http://localhost:8080?name=iris&age=3 // http://localhost:8080/simple?name=john&name=doe&name=kataras // // Note: this `WithEmptyFormError` will give an error if the query was empty. app.Listen(":8080", iris.WithEmptyFormError, iris.WithoutServerError(iris.ErrServerClosed, iris.ErrURLQuerySemicolon)) } ================================================ FILE: _examples/request-body/read-url/main.go ================================================ // package main contains an example on how to use the ReadURL, // same way you can do the ReadQuery, ReadParams, ReadJSON, ReadProtobuf and e.t.c. package main import ( "github.com/kataras/iris/v12" ) type myURL struct { Name string `url:"name"` // or `param:"name"` Age int `url:"age"` // >> >> Tail []string `url:"tail"` // >> >> } func main() { app := newApp() // http://localhost:8080/iris/web/framework?name=kataras&age=27 // myURL: main.myURL{Name:"kataras", Age:27, Tail:[]string{"iris", "web", "framework"}} app.Listen(":8080") } func newApp() *iris.Application { app := iris.New() app.Get("/{tail:path}", func(ctx iris.Context) { var u myURL // ReadURL is a shortcut of ReadParams + ReadQuery. if err := ctx.ReadURL(&u); err != nil { ctx.StopWithError(iris.StatusInternalServerError, err) return } ctx.Writef("myURL: %#v", u) }) return app } ================================================ FILE: _examples/request-body/read-url/main_test.go ================================================ package main import ( "testing" "github.com/kataras/iris/v12/httptest" ) func TestReadURL(t *testing.T) { app := newApp() e := httptest.New(t, app) expectedBody := `myURL: main.myURL{Name:"kataras", Age:27, Tail:[]string{"iris", "web", "framework"}}` e.GET("/iris/web/framework").WithQuery("name", "kataras").WithQuery("age", 27).Expect().Status(httptest.StatusOK).Body().IsEqual(expectedBody) } ================================================ FILE: _examples/request-body/read-xml/main.go ================================================ package main import ( "encoding/xml" "github.com/kataras/iris/v12" ) func main() { app := newApp() // use Postman or whatever to do a POST request // to the http://localhost:8080 with RAW BODY: /* Description of this person, the body of this inner element. */ // and Content-Type to application/xml (optionally but good practise) // // The response should be: // Received: main.person{XMLName:xml.Name{Space:"", Local:"person"}, Name:"Winston Churchill", Age:90, Description:"Description of this person, the body of this inner element."} app.Listen(":8080", iris.WithOptimizations) } func newApp() *iris.Application { app := iris.New() app.Post("/", handler) return app } // simple xml stuff, read more at https://golang.org/pkg/encoding/xml type person struct { XMLName xml.Name `xml:"person"` // element name Name string `xml:"name,attr"` // ,attr for attribute. Age int `xml:"age,attr"` // ,attr attribute. Description string `xml:"description"` // inner element name, value is its body. } func handler(ctx iris.Context) { var p person if err := ctx.ReadXML(&p); err != nil { ctx.StopWithError(iris.StatusBadRequest, err) return } ctx.Writef("Received: %#+v", p) } ================================================ FILE: _examples/request-body/read-xml/main_test.go ================================================ package main import ( "testing" "github.com/kataras/iris/v12/httptest" ) func TestReadXML(t *testing.T) { app := newApp() e := httptest.New(t, app) expectedResponse := `Received: main.person{XMLName:xml.Name{Space:"", Local:"person"}, Name:"Winston Churchill", Age:90, Description:"Description of this person, the body of this inner element."}` send := `Description of this person, the body of this inner element.` e.POST("/").WithText(send).Expect(). Status(httptest.StatusOK).Body().IsEqual(expectedResponse) } ================================================ FILE: _examples/request-body/read-yaml/main.go ================================================ package main import ( "github.com/kataras/iris/v12" ) func newApp() *iris.Application { app := iris.New() app.Post("/", handler) return app } // simple yaml stuff, read more at https://yaml.org/start.html type product struct { Invoice int `yaml:"invoice"` Tax float32 `yaml:"tax"` Total float32 `yaml:"total"` Comments string `yaml:"comments"` } func handler(ctx iris.Context) { var p product if err := ctx.ReadYAML(&p); err != nil { ctx.StopWithError(iris.StatusBadRequest, err) return } ctx.Writef("Received: %#+v", p) } func main() { app := newApp() app.Listen(":8080") } ================================================ FILE: _examples/request-body/read-yaml/main_test.go ================================================ package main import ( "testing" "github.com/kataras/iris/v12/httptest" ) func TestReadYAML(t *testing.T) { app := newApp() e := httptest.New(t, app) expectedResponse := `Received: main.product{Invoice:34843, Tax:251.42, Total:4443.52, Comments:"Late afternoon is best. Backup contact is Nancy Billsmer @ 338-4338."}` send := `invoice: 34843 tax : 251.42 total: 4443.52 comments: > Late afternoon is best. Backup contact is Nancy Billsmer @ 338-4338.` e.POST("/").WithHeader("Content-Type", "application/x-yaml").WithBytes([]byte(send)).Expect(). Status(httptest.StatusOK).Body().IsEqual(expectedResponse) } ================================================ FILE: _examples/request-ratelimit/rate-middleware/main.go ================================================ package main import ( "time" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/middleware/rate" ) func main() { app := newApp() app.Logger().SetLevel("debug") // * http://localhost:8080/v1 // * http://localhost:8080/v1/other // * http://localhost:8080/v2/list (with X-API-Key request header) // Read more at: https://en.wikipedia.org/wiki/Token_bucket // // Alternatives: // * https://github.com/iris-contrib/middleware/blob/master/throttler/_example/main.go // Read more at: https://en.wikipedia.org/wiki/Generic_cell_rate_algorithm // * https://github.com/iris-contrib/middleware/tree/master/tollboothic/_examples/limit-handler app.Listen(":8080") } func newApp() *iris.Application { app := iris.New() v1 := app.Party("/v1") { // Register the rate limiter middleware at the "/v1" subrouter. // // Fist and second input parameters: // Allow 1 request per second, with a maximum burst size of 5. // // Third optional variadic input parameter: // Can be a cleanup function. // Iris provides a cleanup function that will check for old entries and remove them. // You can customize it, e.g. check every 1 minute // if a client's last visit was 5 minutes ago ("old" entry) // and remove it from the memory. limitV1 := rate.Limit(1, 5, rate.PurgeEvery(time.Minute, 5*time.Minute)) // rate.Every helper: // rate.Limit(rate.Every(time.Minute), 5) v1.Use(limitV1) v1.Get("/", index) v1.Get("/other", other) } v2 := app.Party("/v2") { v2.Use(useAPIKey) // Initialize a new rate limit middleware to limit requests // per API Key(see `useAPIKey` below) instead of client's Remote IP Address. limitV2 := rate.Limit(rate.Every(time.Minute), 300, rate.PurgeEvery(5*time.Minute, 15*time.Minute)) v2.Use(limitV2) v2.Get("/list", list) } return app } func useAPIKey(ctx iris.Context) { apiKey := ctx.GetHeader("X-API-Key") if apiKey == "" { // [validate your API Key here...] ctx.StopWithStatus(iris.StatusForbidden) return } // Change the method that rate limit matches the requests with a specific user // and set our own api key as theirs identifier. rate.SetIdentifier(ctx, apiKey) ctx.Next() } func list(ctx iris.Context) { ctx.JSON(iris.Map{"key": "value"}) } func index(ctx iris.Context) { ctx.HTML("

Index Page

") } func other(ctx iris.Context) { ctx.HTML("

Other Page

") } // Note: Use `ctx.SendFileWithRate` to use a download rate limiter instead. ================================================ FILE: _examples/request-ratelimit/ulule-limiter/main.go ================================================ package main import ( "log" "net/http" "strconv" "time" "github.com/kataras/iris/v12" "github.com/ulule/limiter/v3" "github.com/ulule/limiter/v3/drivers/store/memory" ) func main() { app := iris.New() app.Get("/hello", IPRateLimit(), helloWorldHandler) // 3. Use middleware app.Run(iris.Addr(":8080")) } func helloWorldHandler(ctx iris.Context) { err := ctx.StopWithJSON(iris.StatusOK, iris.Map{ "message": "Hello World!", }) if err != nil { return } } func IPRateLimit() iris.Handler { // 1. Configure rate := limiter.Rate{ Period: 2 * time.Second, Limit: 1, } store := memory.NewStore() ipRateLimiter := limiter.New(store, rate) // 2. Return middleware handler return func(ctx iris.Context) { ip := ctx.RemoteAddr() limiterCtx, err := ipRateLimiter.Get(ctx.Request().Context(), ip) if err != nil { log.Printf("IPRateLimit - ipRateLimiter.Get - err: %v, %s on %s", err, ip, ctx.Request().URL) ctx.StatusCode(http.StatusInternalServerError) ctx.JSON(iris.Map{ "success": false, "message": err, }) return } ctx.Header("X-RateLimit-Limit", strconv.FormatInt(limiterCtx.Limit, 10)) ctx.Header("X-RateLimit-Remaining", strconv.FormatInt(limiterCtx.Remaining, 10)) ctx.Header("X-RateLimit-Reset", strconv.FormatInt(limiterCtx.Reset, 10)) if limiterCtx.Reached { log.Printf("Too Many Requests from %s on %s", ip, ctx.Request().URL) ctx.StatusCode(http.StatusTooManyRequests) ctx.JSON(iris.Map{ "success": false, "message": "Too Many Requests on " + ctx.Request().URL.String(), }) return } ctx.Next() } } ================================================ FILE: _examples/request-referrer/main.go ================================================ package main import ( "github.com/kataras/iris/v12" ) func main() { app := iris.New() app.Get("/", func(ctx iris.Context) { // GetReferrer extracts and returns the information from the "Referer" (or "Referrer") header // and url query parameter as specified in https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy. r := ctx.GetReferrer() switch r.Type { case iris.ReferrerSearch: ctx.Writef("Search %s: %s\n", r.Label, r.Query) ctx.Writef("Google: %s\n", r.GoogleType) case iris.ReferrerSocial: ctx.Writef("Social %s\n", r.Label) case iris.ReferrerIndirect: ctx.Writef("Indirect: %s\n", r.URL) } }) // http://localhost:8080?referrer=https://twitter.com/Xinterio/status/1023566830974251008 // http://localhost:8080?referrer=https://www.google.com/search?q=Top+6+golang+web+frameworks&oq=Top+6+golang+web+frameworks app.Listen(":8080") } ================================================ FILE: _examples/response-writer/cache/client-side/main.go ================================================ // Package main shows how you can use the `WriteWithExpiration` // based on the "modtime", if it's newer than the request header then // it will refresh the contents, otherwise will let the client (99.9% the browser) // to handle the cache mechanism, it's faster than iris.Cache because server-side // has nothing to do and no need to store the responses in the memory. package main import ( "time" "github.com/kataras/iris/v12" ) const refreshEvery = 10 * time.Second func main() { app := iris.New() app.Use(iris.Cache304(refreshEvery)) // same as: // app.Use(func(ctx iris.Context) { // now := time.Now() // if modified, err := ctx.CheckIfModifiedSince(now.Add(-refresh)); !modified && err == nil { // ctx.WriteNotModified() // return // } // ctx.SetLastModified(now) // ctx.Next() // }) app.Get("/", greet) app.Listen(":8080") } func greet(ctx iris.Context) { ctx.Header("X-Custom", "my custom header") ctx.Writef("Hello World! %s", time.Now()) } ================================================ FILE: _examples/response-writer/cache/simple/main.go ================================================ package main import ( "time" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/cache" "github.com/kataras/iris/v12/middleware/basicauth" ) var markdownContents = []byte(`## Hello Markdown This is a sample of Markdown contents Features -------- All features of Sundown are supported, including: * **Compatibility**. The Markdown v1.0.3 test suite passes with the --tidy option. Without --tidy, the differences are mostly in whitespace and entity escaping, where blackfriday is more consistent and cleaner. `) // Cache should not be used on handlers that contain dynamic data. // Cache is a good and a must-feature on static content, i.e "about page" or for a whole blog site. func main() { app := iris.New() app.Logger().SetLevel("debug") app.Get("/", cache.Handler(10*time.Second), writeMarkdown) // To customize the cache handler: // cache.Cache(nil).MaxAge(func(ctx iris.Context) time.Duration { // return time.Duration(ctx.MaxAge()) * time.Second // }).AddRule(...).Store(...) // saves its content on the first request and serves it instead of re-calculating the content. // After 10 seconds it will be cleared and reset. pages := app.Party("/pages") pages.Use(cache.Handler(10 * time.Second)) // Per Party. pages.Get("/", pagesIndex) pages.Post("/", pagesIndexPost) // Note: on authenticated requests // the cache middleare does not run at all (see iris/cache/ruleset). auth := basicauth.Default(map[string]string{ "admin": "admin", }) app.Get("/protected", auth, cache.Handler(5*time.Second), protected) // Set custom cache key/identifier, // for the sake of the example // we will SHARE the keys on both GET and POST routes // so the first one is executed that's the body // for both of the routes. Please don't do that // on production, this is just an example. custom := app.Party("/custom") custom.Use(cache.WithKey("shared")) custom.Use(cache.Handler(10 * time.Second)) custom.Get("/", customIndex) custom.Post("/", customIndexPost) app.Listen(":8080") } func writeMarkdown(ctx iris.Context) { // tap multiple times the browser's refresh button and you will // see this println only once every 10 seconds. println("Handler executed. Content refreshed.") ctx.Markdown(markdownContents) } func pagesIndex(ctx iris.Context) { println("Handler executed. Content refreshed.") ctx.WriteString("GET: hello") } func pagesIndexPost(ctx iris.Context) { println("Handler executed. Content refreshed.") ctx.WriteString("POST: hello") } func protected(ctx iris.Context) { username, _, _ := ctx.Request().BasicAuth() ctx.Writef("Hello, %s!", username) } func customIndex(ctx iris.Context) { ctx.WriteString("Contents from GET custom index") } func customIndexPost(ctx iris.Context) { ctx.WriteString("Contents from POST custom index") } /* Note that `HandleDir` does use the browser's disk caching by-default therefore, register the cache handler AFTER any HandleDir calls, for a faster solution that server doesn't need to keep track of the response navigate to https://github.com/kataras/iris/blob/main/_examples/cache/client-side/main.go. The `HandleDir` has its own cache mechanism, read the 'file-server' examples. */ ================================================ FILE: _examples/response-writer/content-negotiation/main.go ================================================ // Package main contains three different ways to render content based on the client's accepted. package main import "github.com/kataras/iris/v12" type testdata struct { Name string `json:"name" xml:"Name"` Age int `json:"age" xml:"Age"` } func newApp() *iris.Application { app := iris.New() app.Logger().SetLevel("debug") // app.Use(func(ctx iris.Context) { // requestedMime := ctx.URLParamDefault("type", "application/json") // // ctx.Negotiation().Accept.Override().MIME(requestedMime, nil) // ctx.Next() // }) app.Get("/resource", func(ctx iris.Context) { data := testdata{ Name: "test name", Age: 26, } // Server allows response only JSON and XML. These values // are compared with the clients mime needs. Iris comes with default mime types responses // but you can add a custom one by the `Negotiation().Mime(mime, content)` method, // same for the "accept". // You can also pass a custom ContentSelector(mime string) or ContentNegotiator to the // `Context.Negotiate` method if you want to perform more advanced things. // // // By-default the client accept mime is retrieved by the "Accept" header // Indeed you can override or update it by `Negotiation().Accept.XXX` i.e // ctx.Negotiation().Accept.Override().XML() // // All these values can change inside middlewares, the `Negotiation().Override()` and `.Accept.Override()` // can override any previously set values. // Order matters, if the client accepts anything (*/*) // then the first prioritized mime's response data will be rendered. ctx.Negotiation().JSON().XML() // Accept-Charset vs: ctx.Negotiation().Charset("utf-8", "iso-8859-7") // Alternatively you can define the content/data per mime type // anywhere in the handlers chain using the optional "v" variadic // input argument of the Context.Negotiation().JSON,XML,YAML,Binary,Text,HTML(...) and e.t.c // example (order matters): // ctx.Negotiation().JSON(data).XML(data).Any("content for */*") // ctx.Negotiate(nil) // if not nil passed in the `Context.Negotiate` method // then it overrides any contents made by the negotitation builder above. _, err := ctx.Negotiate(data) if err != nil { ctx.Writef("%v", err) } }) app.Get("/resource2", func(ctx iris.Context) { jsonAndXML := testdata{ Name: "test name", Age: 26, } // I prefer that one, as it gives me the freedom to modify // response data per accepted mime content type on middlewares as well. ctx.Negotiation(). JSON(jsonAndXML). XML(jsonAndXML). HTML("

Test Name

Age 26

") ctx.Negotiate(nil) }) app.Get("/resource3", func(ctx iris.Context) { // If that line is missing and the requested // mime type of content is */* or application/xml or application/json // then 406 Not Acceptable http error code will be rendered instead. // // We also add the "gzip" algorithm as an option to encode // resources on send. ctx.Negotiation().JSON().XML().HTML().EncodingGzip() jsonAndXML := testdata{ Name: "test name", Age: 26, } // Prefer that way instead of the '/resource2' above // if "iris.N" is a static one and can be declared // outside of a handler. ctx.Negotiate(iris.N{ // Text: for text/plain, // Markdown: for text/mardown, // Binary: for application/octet-stream, // YAML: for application/x-yaml, // JSONP: for text/javascript // Other: for anything else, JSON: jsonAndXML, // for application/json XML: jsonAndXML, // for application/xml or text/xml HTML: "

Test Name

Age 26

", // for text/html }) }) return app } func main() { app := newApp() app.Listen(":8080") } ================================================ FILE: _examples/response-writer/content-negotiation/main_test.go ================================================ package main import ( "bytes" "compress/gzip" "encoding/xml" "io" "testing" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/httptest" ) func TestContentNegotiation(t *testing.T) { var ( expectedJSONResponse = testdata{ Name: "test name", Age: 26, } expectedXMLResponse, _ = xml.Marshal(expectedJSONResponse) expectedHTMLResponse = "

Test Name

Age 26

" ) app := newApp() app.Configure(iris.WithOptimizations) e := httptest.New(t, app) e.GET("/resource").WithHeader("Accept", "application/json"). Expect().Status(httptest.StatusOK). ContentType("application/json", "utf-8"). JSON().IsEqual(expectedJSONResponse) e.GET("/resource").WithHeader("Accept", "application/xml").WithHeader("Accept-Charset", "iso-8859-7"). Expect().Status(httptest.StatusOK). ContentType("application/xml", "iso-8859-7"). Body().IsEqual(string(expectedXMLResponse)) e.GET("/resource2").WithHeader("Accept", "application/json"). Expect().Status(httptest.StatusOK). ContentType("application/json", "utf-8"). JSON().IsEqual(expectedJSONResponse) e.GET("/resource2").WithHeader("Accept", "application/xml"). Expect().Status(httptest.StatusOK). ContentType("application/xml", "utf-8"). Body().IsEqual(string(expectedXMLResponse)) e.GET("/resource2").WithHeader("Accept", "text/html"). Expect().Status(httptest.StatusOK). ContentType("text/html", "utf-8"). Body().IsEqual(expectedHTMLResponse) e.GET("/resource3").WithHeader("Accept", "application/json"). Expect().Status(httptest.StatusOK). ContentType("application/json", "utf-8"). JSON().IsEqual(expectedJSONResponse) e.GET("/resource3").WithHeader("Accept", "application/xml"). Expect().Status(httptest.StatusOK). ContentType("application/xml", "utf-8"). Body().IsEqual(string(expectedXMLResponse)) // test html with "gzip" encoding algorithm. rawGzipResponse := e.GET("/resource3").WithHeader("Accept", "text/html"). WithHeader("Accept-Encoding", "gzip"). Expect().Status(httptest.StatusOK). ContentType("text/html", "utf-8"). ContentEncoding("gzip"). Body().Raw() zr, err := gzip.NewReader(bytes.NewReader([]byte(rawGzipResponse))) if err != nil { t.Fatal(err) } rawResponse, err := io.ReadAll(zr) if err != nil { t.Fatal(err) } if expected, got := expectedHTMLResponse, string(rawResponse); expected != got { t.Fatalf("expected response to be:\n%s but got:\n%s", expected, got) } } ================================================ FILE: _examples/response-writer/http2push/main.go ================================================ // Server push lets the server preemptively "push" website assets // to the client without the user having explicitly asked for them. // When used with care, we can send what we know the user is going // to need for the page they're requesting. package main import ( "fmt" "net/http" "github.com/kataras/iris/v12" ) func main() { app := iris.New() app.Get("/", pushHandler) app.Get("/main.js", simpleAssetHandler) app.Run(iris.TLS("127.0.0.1:443", "mycert.crt", "mykey.key")) // $ openssl req -new -newkey rsa:4096 -x509 -sha256 \ // -days 365 -nodes -out mycert.crt -keyout mykey.key } func pushHandler(ctx iris.Context) { // The target must either be an absolute path (like "/path") or an absolute // URL that contains a valid host and the same scheme as the parent request. // If the target is a path, it will inherit the scheme and host of the // parent request. target := "/main.js" if pusher, ok := ctx.ResponseWriter().Naive().(http.Pusher); ok { err := pusher.Push(target, nil) if err != nil { if err == iris.ErrPushNotSupported { ctx.StopWithText(iris.StatusHTTPVersionNotSupported, "HTTP/2 push not supported.") } else { ctx.StopWithError(iris.StatusInternalServerError, err) } return } } ctx.HTML(fmt.Sprintf(``, target)) } func simpleAssetHandler(ctx iris.Context) { ctx.ServeFile("./public/main.js") } ================================================ FILE: _examples/response-writer/http2push/mycert.crt ================================================ -----BEGIN CERTIFICATE----- MIIFazCCA1OgAwIBAgIUfwMd9auWixp19UnXOmyxJ9Jkv7IwDQYJKoZIhvcNAQEL BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMDA2MjUwOTUxNDdaFw0yMTA2 MjUwOTUxNDdaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggIiMA0GCSqGSIb3DQEB AQUAA4ICDwAwggIKAoICAQDlVGyGAQ9uyfNbwZyrtYOSjLpxf5NpNToh2OzU7gy2 OexBji5lmWBQ3oYDG+FjAkbHORPzOMNpeMwje+IjGZBw8x6E+8WoGdSzbrEZ6pUV wKJGKEuDlx6g6HEmtv3ZwgGe20gvPjjW+oCO888dwK/mbIHrHTq4nO3o0gAdAJwu amn9BlHU5O4RW7BQ4tLF+j/fBCACWRG1NHXA0AT8eg544GyCdyteAH11oCDsHS8/ DAPsM6t+tZrMCIt9+9dzPdVoOmQNaMMrcz8eJohddRTK6zHe9ixZTt/soayOF7OS QQeekbr3HPYhD450zRVplLMHx7wnph/+O+Po6bqDnUzdnkqAAwwymQapHMuHXZKN rhdfKau3rVo1GeXLIRgeWLUoxFSm4TYshrgt+0AidLRH+dCY7MS9Ngga/sAK3vID gSF75mFgOhY+q7nvY9Ecao6TnoNNRY29hUat4y0VwSyysUy887vHr6lMK5CrAT/l Ch8fuu20HUCoiLwMJvA6+wpivZkuiIvWY7bVGYsEYrrW+bCNN9wCGYTZEyX++os9 v/38wdOqGUT00ewXkjIUFCWbrnxxSr98kF3w3wPf9K4Y40MNxeR90nyX4zjXGF1/ 91msUh+iivsz9mcN9DK83fgTyOsoVLX5cm/L2UBwMacsfjBbN4djOc5IuYMar/VN GQIDAQABo1MwUTAdBgNVHQ4EFgQUtkf+yAvqgZC8f22iJny9hFEDolMwHwYDVR0j BBgwFoAUtkf+yAvqgZC8f22iJny9hFEDolMwDwYDVR0TAQH/BAUwAwEB/zANBgkq hkiG9w0BAQsFAAOCAgEAE2QasBVru618rxupyJgEHw6r4iv7sz1Afz3Q5qJ4oSA9 xVsrVCjr3iHRFSw8Rf670E8Ffk/JjzS65mHw6zeZj/ANBKQWLjRlqzYXeetq5HzG SIgaG7p1RFvvzz3+leFGzjinZ6sKbfB4OB72o2YN+fO8DsDxgGKll0W4KAazizSe HY9Pgu437tWnwF16rFO3IL47n5HzYlRoGIPOpzFoNX5+fyn9GlnKEtONF2QBKTjY rdjvqFRByDiC74d8z/Yx8IiDRn1mTcG90JLR9+c6M7fruha9Y/rJfw+4AhVh5ZDz Bl9rGPjwEs5zwutYvVAJzs7AVcighYP1lHKoJ7DxBDQeyBsYlUNk2l6bmZgLgGUZ +2OyWlqc/jD2GdDsIaZ4i7QqhTI/6aYZIf5zUkblKV1aMSaDulKxRv//OwW28Jax 9EEoV7VaFb3sOkB/tZGhusXeQVtdrhahT3KkZLNwmNXoXWKJ5LjeUlFWJyV6JbDe y/PIWWCwWqyuFCSZS+Cg3RDgAzfSxkI8uVZ+IKKJS3UluDX45lxXtbRrvTQ+oDrA 6ga5c1Vz9C4kn1K5yW4d7QIvg6vPiy7gvl+//sz9oxUM3yswInDBY0HKLgT0Uq9b YzLDh2RSaHsgHMPy2BKqR+q2N+lpg7inAWuJM1Huq6eHFqhiyQkzsfscBd1Dpm8= -----END CERTIFICATE----- ================================================ FILE: _examples/response-writer/http2push/mykey.key ================================================ -----BEGIN PRIVATE KEY----- MIIJRAIBADANBgkqhkiG9w0BAQEFAASCCS4wggkqAgEAAoICAQDlVGyGAQ9uyfNb wZyrtYOSjLpxf5NpNToh2OzU7gy2OexBji5lmWBQ3oYDG+FjAkbHORPzOMNpeMwj e+IjGZBw8x6E+8WoGdSzbrEZ6pUVwKJGKEuDlx6g6HEmtv3ZwgGe20gvPjjW+oCO 888dwK/mbIHrHTq4nO3o0gAdAJwuamn9BlHU5O4RW7BQ4tLF+j/fBCACWRG1NHXA 0AT8eg544GyCdyteAH11oCDsHS8/DAPsM6t+tZrMCIt9+9dzPdVoOmQNaMMrcz8e JohddRTK6zHe9ixZTt/soayOF7OSQQeekbr3HPYhD450zRVplLMHx7wnph/+O+Po 6bqDnUzdnkqAAwwymQapHMuHXZKNrhdfKau3rVo1GeXLIRgeWLUoxFSm4TYshrgt +0AidLRH+dCY7MS9Ngga/sAK3vIDgSF75mFgOhY+q7nvY9Ecao6TnoNNRY29hUat 4y0VwSyysUy887vHr6lMK5CrAT/lCh8fuu20HUCoiLwMJvA6+wpivZkuiIvWY7bV GYsEYrrW+bCNN9wCGYTZEyX++os9v/38wdOqGUT00ewXkjIUFCWbrnxxSr98kF3w 3wPf9K4Y40MNxeR90nyX4zjXGF1/91msUh+iivsz9mcN9DK83fgTyOsoVLX5cm/L 2UBwMacsfjBbN4djOc5IuYMar/VNGQIDAQABAoICAQCtWx1SSxjkcerxsLEDKApW zOTfiUXgoOjZz0ZwS6b2VWDfyWAPU1r4ps39KaU+F+lzDhWjpYQqhbMjG7G9QMTs bQvkEQLAaQ5duU5NPgQG1oCUsj8rMSBpGGz4jBnm834QHMk7VTjYYbKu3WTyo8cU U2/+UDEkfxRlC+IkCmMFv1FxgMZ5PbktC/eDnYMhP2Pq7Q5ZWAVHymk9IMK0LHwm Kdg842K4A3zTXwGkGwetDCMm+YQpG5TxqX/w82BRcCuTR5h8fnYSsWLEIvKwWyIl ppcjaUnrFPG2yhxLqWUIKPpehuEjjhQMt9rDNoh6MHsJZZY5Dp5eq91EIvLoLQ99 hXBmD4P8LDop4r0jniPZJi/ACsaD0jBooA4525+Kouq7RP28Jp/pek7lVOOcBgRv D3zyESbKfqoaOfyfQ2ff4sILnTAr4V2nq3ekphGEYJrWN0ZoADcLdnr1cZ8L+VBI o/4mi5/3HID/UEDliHSa97hxxGBEqTto0ZuXuNwfwx5ho33uVT6zNwRgiJ62Bgu3 Fhk/wVGuZxWvb1KHUNInG9cvsslhO4Vu9wJvYj91BnRq36rsyKKid5DrU+PNgmog lw3IXQpTojyRCYPuG9TKqEZ6b+so7GTKhBOjiwaupMOletVRGSAdbE81VN6HtxNW aj39+FnxzMAlsieib+PBAQKCAQEA+t1fOYSaZBo7pZUmo2S0zulUEJjrYRGKJlWJ 4psWSwFu/7/3UL4q0RBQaSRew9u/YSpaNlBYfcpnFVOjiLwHq5Hx46Eq0BuKsNlJ 1/qxw9qjHqcrOre6K4/7NaWLPuM9fEmV+3MhFVXgv+WC5BHOowRTlOG30vIcC1J2 L5xsBUsxDDY13cD1bLKRmFcyMFM8y7wMZmo7H/WfVmyoPKQaC43pTcmIXH0Jr2Ws Wsfh18mhjtamaOPEFx5K0x4d0PI8tW5ouiUUkVIDaue27XfS969qEChv768/44eX WeqcekaG9jv2noMClt79rYd3Lne9HkgY6IT9FT+JqXfu+KYwuQKCAQEA6gYzUsGB 9GQO8DE8AYn7JwNOtg1X4zKakXiGxH+nuZb7wJjAeGdYqTHySxPBXg0A2nDwoyz5 4sAdLAr3FZoIvTzo7M5KIKFDzfyDmQDavhroH1mBAEiqKGNniP+RND3nWBBqDK1R qcqbhI3Kj5Ycany6a4nP+hZRBIyT9sfJ0S0YruSY8IGXgDwhlJrZ7bsWMZylrgD/ 1qnPL0KqVBY8YR8msRj88h72IlD5o0kwvisOIvyhA0YgwGBb6lg7A+DifiF03ZlS 2yELbIkKDVr+p3jC7MBh4B+OJY68AMl6wVjAaDM1AZnpjKE5YmZg5+Ks5823zILo PrSB9hn0+DIPYQKCAQEAh9x+JuNmzhHa/dkiHNl8hpadHYQD7gUWwZ4P1/bQAv0a xU2MvmDPRXxFYDv/SqlnI1NRmhq3YiDM5SLv7SyQJt4al4IAcsaHvTFgqaSuw3hU YVR9uAYqwE7w6OPn3r4o3Xfoz05Ru4FP//1nfucZ9vVv4rC/4nGWuJcHRM+9PLy1 KnztfVR0VlL7QPrwRnW99kS4nnqn3K4khiTAlF73cAyCLsuXmydoqGIzDtMzv68G XRpo82NvHmoccevcj/2w3T2XYECWvAEjsrEdQ8xiKBwLIAcWYEOUIUCcumiyKBKs IwzkioI/U8AeuO0lobfdZ1n6i2sCuZA4mNxIQseWmQKCAQEA5YkfXdQeuq5JWJ1x 1bCYfjNoSHfd9CH2KSimRqVOxWGpm8Y3QeFbvNgYZjsCNlVauOZ9oA7FKfp0onY+ 0xk56SKM83eCjW6fKrK6AKAt7LhHZDhNpxGek+6r5luE+FCfUGkJG1YD+x2WW/UW 8K6zQF8GGeQZ8Zlh7axUlIBxGpG43BGrUHpLNqPD7BXWGq6dnhufBYRFay8y34/r sH3+yuPa92ki7/geQppZwCZRgLSKMRbIdoWaKhZZEQlpGOzCOiRmk9OGyRcoNVRU X7UYgPqZdc1cMo/AxGWzULJNjMaYMZvIKcHkqOKZfkIcWlSictn7pMPhN1+k+NWM yMORAQKCAQAyXl02h/c2ihx6cjKlnNeDr2ZfzkoiAvFuKaoAR+KVvb9F9X7ZgKSi wudZyelTglIVCYXeRmG09uX3rNGCzFrweRwgn6x/8DnN5pMRJVZOXFdgR+V9uKep K6F7DYbPyggvLOAsezB+09i9lwxM+XdA2whVpL5NFR1rGfFglnE1EQHcEvNONkcv 0h8x9cNSptJyRDLiTIKI9EhonuzwzkGpvjULQE8MLbT8PbjoLFINcE9ZWhwtyw0V XO32KE8iLKt3KzHz9CfTRCI3M7DwD752AC6zRr8ZS/HXzs+5WTkdVVEtRC7Abd3y W2TzuSMYNDu876twbTVQJED3mwOAQ3J7 -----END PRIVATE KEY----- ================================================ FILE: _examples/response-writer/http2push/public/main.js ================================================ window.alert("javascript loaded"); ================================================ FILE: _examples/response-writer/json-third-party/go.mod ================================================ module github.com/kataras/iris/_examples/response-writer/json-third-party go 1.25 require ( github.com/bytedance/sonic v1.14.2 github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd ) require ( github.com/BurntSushi/toml v1.6.0 // indirect github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect github.com/CloudyKit/jet/v6 v6.3.1 // indirect github.com/Joker/jade v1.1.3 // indirect github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 // indirect github.com/andybalholm/brotli v1.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/bytedance/gopkg v0.1.3 // indirect github.com/bytedance/sonic/loader v0.4.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect github.com/fatih/structs v1.1.0 // indirect github.com/flosch/pongo2/v4 v4.0.2 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/iris-contrib/schema v0.0.6 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/kataras/blocks v0.0.8 // indirect github.com/kataras/golog v0.1.12 // indirect github.com/kataras/pio v0.0.13 // indirect github.com/kataras/sitemap v0.0.6 // indirect github.com/kataras/tunnel v0.0.4 // indirect github.com/klauspost/compress v1.18.2 // indirect github.com/klauspost/cpuid/v2 v2.2.9 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mailgun/raymond/v2 v2.0.48 // indirect github.com/mailru/easyjson v0.9.1 // indirect github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/schollz/closestmatch v2.1.0+incompatible // indirect github.com/sirupsen/logrus v1.8.1 // indirect github.com/stretchr/testify v1.11.1 // indirect github.com/tdewolff/minify/v2 v2.24.8 // indirect github.com/tdewolff/parse/v2 v2.8.5 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/yosssi/ace v0.0.5 // indirect golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect golang.org/x/crypto v0.47.0 // indirect golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect golang.org/x/net v0.49.0 // indirect golang.org/x/sys v0.40.0 // indirect golang.org/x/text v0.33.0 // indirect golang.org/x/time v0.14.0 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: _examples/response-writer/json-third-party/go.sum ================================================ github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= github.com/CloudyKit/jet/v6 v6.3.1 h1:6IAo5Cx21xrHVaR8zzXN5gJatKV/wO7Nf6bfCnCSbUw= github.com/CloudyKit/jet/v6 v6.3.1/go.mod h1:lf8ksdNsxZt7/yH/3n4vJQWA9RUq4wpaHtArHhGVMOw= github.com/Joker/hpp v1.0.0 h1:65+iuJYdRXv/XyN62C1uEmmOx3432rNG/rKlX6V7Kkc= github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= github.com/Joker/jade v1.1.3 h1:Qbeh12Vq6BxURXT1qZBRHsDxeURB8ztcL6f3EXSGeHk= github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKduM= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 h1:dG7/gLroJGht/jSQtHiLvT48Hxn+crbmvyItZC8cWOs= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05/go.mod h1:NYezi6wtnJtBm5btoprXc5SvAdqH0XTXWnUup0MptAI= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE= github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980= github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o= github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/flosch/pongo2/v4 v4.0.2 h1:gv+5Pe3vaSVmiJvh/BZa82b7/00YUGm0PIyVVLop0Hw= github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+BWPhwZD8= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a h1:l7A0loSszR5zHd/qK53ZIHMO8b3bBSmENnQ6eKnUT0A= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/iris-contrib/httpexpect/v2 v2.15.2 h1:T9THsdP1woyAqKHwjkEsbCnMefsAFvk8iJJKokcJ3Go= github.com/iris-contrib/httpexpect/v2 v2.15.2/go.mod h1:JLDgIqnFy5loDSUv1OA2j0mb6p/rDhiCqigP22Uq9xE= github.com/iris-contrib/schema v0.0.6 h1:CPSBLyx2e91H2yJzPuhGuifVRnZBBJ3pCOMbOvPZaTw= github.com/iris-contrib/schema v0.0.6/go.mod h1:iYszG0IOsuIsfzjymw1kMzTL8YQcCWlm65f3wX8J5iA= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kataras/blocks v0.0.8 h1:MrpVhoFTCR2v1iOOfGng5VJSILKeZZI+7NGfxEh3SUM= github.com/kataras/blocks v0.0.8/go.mod h1:9Jm5zx6BB+06NwA+OhTbHW1xkMOYxahnqTN5DveZ2Yg= github.com/kataras/golog v0.1.12 h1:Bu7I/G4ilJlbfzjmU39O9N+2uO1pBcMK045fzZ4ytNg= github.com/kataras/golog v0.1.12/go.mod h1:wrGSbOiBqbQSQznleVNX4epWM8rl9SJ/rmEacl0yqy4= github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd h1:oMsQgqKACbUdlaIWnN+EZzzAnHkw90hE/y/R0BSij2U= github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd/go.mod h1:yucjtDl9Vn3OctWM1fi0MuM9OckemC7kEreFa9QtPF8= github.com/kataras/pio v0.0.13 h1:x0rXVX0fviDTXOOLOmr4MUxOabu1InVSTu5itF8CXCM= github.com/kataras/pio v0.0.13/go.mod h1:k3HNuSw+eJ8Pm2lA4lRhg3DiCjVgHlP8hmXApSej3oM= github.com/kataras/sitemap v0.0.6 h1:w71CRMMKYMJh6LR2wTgnk5hSgjVNB9KL60n5e2KHvLY= github.com/kataras/sitemap v0.0.6/go.mod h1:dW4dOCNs896OR1HmG+dMLdT7JjDk7mYBzoIRwuj5jA4= github.com/kataras/tunnel v0.0.4 h1:sCAqWuJV7nPzGrlb0os3j49lk2JhILT0rID38NHNLpA= github.com/kataras/tunnel v0.0.4/go.mod h1:9FkU4LaeifdMWqZu7o20ojmW4B7hdhv2CMLwfnHGpYw= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw= github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18= github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo= github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk= github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tdewolff/minify/v2 v2.24.8 h1:58/VjsbevI4d5FGV0ZSuBrHMSSkH4MCH0sIz/eKIauE= github.com/tdewolff/minify/v2 v2.24.8/go.mod h1:0Ukj0CRpo/sW/nd8uZ4ccXaV1rEVIWA3dj8U7+Shhfw= github.com/tdewolff/parse/v2 v2.8.5 h1:ZmBiA/8Do5Rpk7bDye0jbbDUpXXbCdc3iah4VeUvwYU= github.com/tdewolff/parse/v2 v2.8.5/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo= github.com/tdewolff/test v1.0.11 h1:FdLbwQVHxqG16SlkGveC0JVyrJN62COWTRyUFzfbtBE= github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA= github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0= github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs= moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHcE= ================================================ FILE: _examples/response-writer/json-third-party/main.go ================================================ package main import ( "github.com/kataras/iris/v12" // This is a 3rd-party library, which you can use to override the default behavior of ctx.JSON method. "github.com/bytedance/sonic" ) func init() { applyIrisGlobalPatches() // <- IMPORTANT. } func applyIrisGlobalPatches() { var json = sonic.ConfigFastest // Apply global modifications to the context REST writers // without modifications to your web server's handlers code. iris.Patches().Context().Writers().JSON(func(ctx iris.Context, v any, options *iris.JSON) error { enc := json.NewEncoder(ctx.ResponseWriter()) enc.SetEscapeHTML(!options.UnescapeHTML) enc.SetIndent("", options.Indent) return enc.Encode(v) }) } // User example struct for json. type User struct { Firstname string `json:"firstname"` Lastname string `json:"lastname"` City string `json:"city"` Age int `json:"age"` } func main() { app := iris.New() app.Get("/", func(ctx iris.Context) { user := User{ Firstname: "Gerasimos", Lastname: "Maropoulos", City: "Athens", Age: 29, } // Use ctx.JSON as you used to. ctx.JSON(user) }) app.Listen(":8080") } ================================================ FILE: _examples/response-writer/protobuf/README.md ================================================ # Protocol Buffers The `Context.Protobuf(proto.Message)` is the method which sends protos to the client. It accepts a [proto.Message](https://godoc.org/google.golang.org/protobuf/proto#Message) value. > Note: Iris is using the newest version of the Go protocol buffers implementation. Read more about it at [The Go Blog: A new Go API for Protocol Buffers](https://blog.golang.org/protobuf-apiv2). 1. Install the protoc-gen-go tool. ```sh $ go get -u google.golang.org/protobuf/cmd/protoc-gen-go@latest ``` 2. Generate proto ```sh $ protoc -I protos/ protos/hello.proto --go_out=. ``` ================================================ FILE: _examples/response-writer/protobuf/go.mod ================================================ module app go 1.25 require ( github.com/golang/protobuf v1.5.0 github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd google.golang.org/protobuf v1.36.11 ) require ( github.com/BurntSushi/toml v1.6.0 // indirect github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect github.com/CloudyKit/jet/v6 v6.3.1 // indirect github.com/Joker/jade v1.1.3 // indirect github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 // indirect github.com/andybalholm/brotli v1.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/fatih/structs v1.1.0 // indirect github.com/flosch/pongo2/v4 v4.0.2 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/iris-contrib/schema v0.0.6 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/kataras/blocks v0.0.8 // indirect github.com/kataras/golog v0.1.12 // indirect github.com/kataras/pio v0.0.13 // indirect github.com/kataras/sitemap v0.0.6 // indirect github.com/kataras/tunnel v0.0.4 // indirect github.com/klauspost/compress v1.18.2 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mailgun/raymond/v2 v2.0.48 // indirect github.com/mailru/easyjson v0.9.1 // indirect github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/schollz/closestmatch v2.1.0+incompatible // indirect github.com/sirupsen/logrus v1.8.1 // indirect github.com/stretchr/testify v1.11.1 // indirect github.com/tdewolff/minify/v2 v2.24.8 // indirect github.com/tdewolff/parse/v2 v2.8.5 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/yosssi/ace v0.0.5 // indirect golang.org/x/crypto v0.47.0 // indirect golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect golang.org/x/net v0.49.0 // indirect golang.org/x/sys v0.40.0 // indirect golang.org/x/text v0.33.0 // indirect golang.org/x/time v0.14.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: _examples/response-writer/protobuf/go.sum ================================================ github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= github.com/CloudyKit/jet/v6 v6.3.1 h1:6IAo5Cx21xrHVaR8zzXN5gJatKV/wO7Nf6bfCnCSbUw= github.com/CloudyKit/jet/v6 v6.3.1/go.mod h1:lf8ksdNsxZt7/yH/3n4vJQWA9RUq4wpaHtArHhGVMOw= github.com/Joker/hpp v1.0.0 h1:65+iuJYdRXv/XyN62C1uEmmOx3432rNG/rKlX6V7Kkc= github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= github.com/Joker/jade v1.1.3 h1:Qbeh12Vq6BxURXT1qZBRHsDxeURB8ztcL6f3EXSGeHk= github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKduM= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 h1:dG7/gLroJGht/jSQtHiLvT48Hxn+crbmvyItZC8cWOs= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05/go.mod h1:NYezi6wtnJtBm5btoprXc5SvAdqH0XTXWnUup0MptAI= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/flosch/pongo2/v4 v4.0.2 h1:gv+5Pe3vaSVmiJvh/BZa82b7/00YUGm0PIyVVLop0Hw= github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+BWPhwZD8= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a h1:l7A0loSszR5zHd/qK53ZIHMO8b3bBSmENnQ6eKnUT0A= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/iris-contrib/httpexpect/v2 v2.15.2 h1:T9THsdP1woyAqKHwjkEsbCnMefsAFvk8iJJKokcJ3Go= github.com/iris-contrib/httpexpect/v2 v2.15.2/go.mod h1:JLDgIqnFy5loDSUv1OA2j0mb6p/rDhiCqigP22Uq9xE= github.com/iris-contrib/schema v0.0.6 h1:CPSBLyx2e91H2yJzPuhGuifVRnZBBJ3pCOMbOvPZaTw= github.com/iris-contrib/schema v0.0.6/go.mod h1:iYszG0IOsuIsfzjymw1kMzTL8YQcCWlm65f3wX8J5iA= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kataras/blocks v0.0.8 h1:MrpVhoFTCR2v1iOOfGng5VJSILKeZZI+7NGfxEh3SUM= github.com/kataras/blocks v0.0.8/go.mod h1:9Jm5zx6BB+06NwA+OhTbHW1xkMOYxahnqTN5DveZ2Yg= github.com/kataras/golog v0.1.12 h1:Bu7I/G4ilJlbfzjmU39O9N+2uO1pBcMK045fzZ4ytNg= github.com/kataras/golog v0.1.12/go.mod h1:wrGSbOiBqbQSQznleVNX4epWM8rl9SJ/rmEacl0yqy4= github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd h1:oMsQgqKACbUdlaIWnN+EZzzAnHkw90hE/y/R0BSij2U= github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd/go.mod h1:yucjtDl9Vn3OctWM1fi0MuM9OckemC7kEreFa9QtPF8= github.com/kataras/pio v0.0.13 h1:x0rXVX0fviDTXOOLOmr4MUxOabu1InVSTu5itF8CXCM= github.com/kataras/pio v0.0.13/go.mod h1:k3HNuSw+eJ8Pm2lA4lRhg3DiCjVgHlP8hmXApSej3oM= github.com/kataras/sitemap v0.0.6 h1:w71CRMMKYMJh6LR2wTgnk5hSgjVNB9KL60n5e2KHvLY= github.com/kataras/sitemap v0.0.6/go.mod h1:dW4dOCNs896OR1HmG+dMLdT7JjDk7mYBzoIRwuj5jA4= github.com/kataras/tunnel v0.0.4 h1:sCAqWuJV7nPzGrlb0os3j49lk2JhILT0rID38NHNLpA= github.com/kataras/tunnel v0.0.4/go.mod h1:9FkU4LaeifdMWqZu7o20ojmW4B7hdhv2CMLwfnHGpYw= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw= github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18= github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo= github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk= github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tdewolff/minify/v2 v2.24.8 h1:58/VjsbevI4d5FGV0ZSuBrHMSSkH4MCH0sIz/eKIauE= github.com/tdewolff/minify/v2 v2.24.8/go.mod h1:0Ukj0CRpo/sW/nd8uZ4ccXaV1rEVIWA3dj8U7+Shhfw= github.com/tdewolff/parse/v2 v2.8.5 h1:ZmBiA/8Do5Rpk7bDye0jbbDUpXXbCdc3iah4VeUvwYU= github.com/tdewolff/parse/v2 v2.8.5/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo= github.com/tdewolff/test v1.0.11 h1:FdLbwQVHxqG16SlkGveC0JVyrJN62COWTRyUFzfbtBE= github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA= github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0= github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs= moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHcE= ================================================ FILE: _examples/response-writer/protobuf/main.go ================================================ package main import ( "app/protos" "github.com/kataras/iris/v12" ) func main() { app := iris.New() app.Get("/", send) app.Get("/json", sendAsJSON) app.Post("/read", read) app.Post("/read_json", readFromJSON) app.Listen(":8080") } func send(ctx iris.Context) { response := &protos.HelloReply{Message: "Hello, World!"} ctx.Protobuf(response) } func sendAsJSON(ctx iris.Context) { response := &protos.HelloReply{Message: "Hello, World!"} options := iris.JSON{ Proto: iris.ProtoMarshalOptions{ AllowPartial: true, Multiline: true, Indent: " ", }, } ctx.JSON(response, options) } func read(ctx iris.Context) { var request protos.HelloRequest err := ctx.ReadProtobuf(&request) if err != nil { ctx.StopWithError(iris.StatusBadRequest, err) return } ctx.Writef("HelloRequest.Name = %s", request.Name) } func readFromJSON(ctx iris.Context) { var request protos.HelloRequest err := ctx.ReadJSONProtobuf(&request) if err != nil { ctx.StopWithError(iris.StatusBadRequest, err) return } ctx.Writef("HelloRequest.Name = %s", request.Name) } ================================================ FILE: _examples/response-writer/protobuf/protos/hello.pb.go ================================================ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.25.0 // protoc v3.11.1 // source: hello.proto package protos import ( proto "github.com/golang/protobuf/proto" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" ) const ( // Verify that this generated code is sufficiently up-to-date. _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) // Verify that runtime/protoimpl is sufficiently up-to-date. _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) // This is a compile-time assertion that a sufficiently up-to-date version // of the legacy proto package is being used. const _ = proto.ProtoPackageIsVersion4 type HelloRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` } func (x *HelloRequest) Reset() { *x = HelloRequest{} if protoimpl.UnsafeEnabled { mi := &file_hello_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *HelloRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*HelloRequest) ProtoMessage() {} func (x *HelloRequest) ProtoReflect() protoreflect.Message { mi := &file_hello_proto_msgTypes[0] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use HelloRequest.ProtoReflect.Descriptor instead. func (*HelloRequest) Descriptor() ([]byte, []int) { return file_hello_proto_rawDescGZIP(), []int{0} } func (x *HelloRequest) GetName() string { if x != nil { return x.Name } return "" } type HelloReply struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` } func (x *HelloReply) Reset() { *x = HelloReply{} if protoimpl.UnsafeEnabled { mi := &file_hello_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *HelloReply) String() string { return protoimpl.X.MessageStringOf(x) } func (*HelloReply) ProtoMessage() {} func (x *HelloReply) ProtoReflect() protoreflect.Message { mi := &file_hello_proto_msgTypes[1] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use HelloReply.ProtoReflect.Descriptor instead. func (*HelloReply) Descriptor() ([]byte, []int) { return file_hello_proto_rawDescGZIP(), []int{1} } func (x *HelloReply) GetMessage() string { if x != nil { return x.Message } return "" } var File_hello_proto protoreflect.FileDescriptor var file_hello_proto_rawDesc = []byte{ 0x0a, 0x0b, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x22, 0x22, 0x0a, 0x0c, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x26, 0x0a, 0x0a, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x42, 0x0a, 0x5a, 0x08, 0x2e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( file_hello_proto_rawDescOnce sync.Once file_hello_proto_rawDescData = file_hello_proto_rawDesc ) func file_hello_proto_rawDescGZIP() []byte { file_hello_proto_rawDescOnce.Do(func() { file_hello_proto_rawDescData = protoimpl.X.CompressGZIP(file_hello_proto_rawDescData) }) return file_hello_proto_rawDescData } var file_hello_proto_msgTypes = make([]protoimpl.MessageInfo, 2) var file_hello_proto_goTypes = []any{ (*HelloRequest)(nil), // 0: protos.HelloRequest (*HelloReply)(nil), // 1: protos.HelloReply } var file_hello_proto_depIdxs = []int32{ 0, // [0:0] is the sub-list for method output_type 0, // [0:0] is the sub-list for method input_type 0, // [0:0] is the sub-list for extension type_name 0, // [0:0] is the sub-list for extension extendee 0, // [0:0] is the sub-list for field type_name } func init() { file_hello_proto_init() } func file_hello_proto_init() { if File_hello_proto != nil { return } if !protoimpl.UnsafeEnabled { file_hello_proto_msgTypes[0].Exporter = func(v any, i int) any { switch v := v.(*HelloRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_hello_proto_msgTypes[1].Exporter = func(v any, i int) any { switch v := v.(*HelloReply); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_hello_proto_rawDesc, NumEnums: 0, NumMessages: 2, NumExtensions: 0, NumServices: 0, }, GoTypes: file_hello_proto_goTypes, DependencyIndexes: file_hello_proto_depIdxs, MessageInfos: file_hello_proto_msgTypes, }.Build() File_hello_proto = out.File file_hello_proto_rawDesc = nil file_hello_proto_goTypes = nil file_hello_proto_depIdxs = nil } ================================================ FILE: _examples/response-writer/protobuf/protos/hello.proto ================================================ syntax = "proto3"; package protos; option go_package = "./protos"; message HelloRequest { string name = 1; } message HelloReply { string message = 1; } ================================================ FILE: _examples/response-writer/sse/main.go ================================================ // Package main shows how to send continuous event messages to the clients through SSE via a broker. // Read details at: https://www.w3schools.com/htmL/html5_serversentevents.asp and // https://robots.thoughtbot.com/writing-a-server-sent-events-server-in-go package main import ( "encoding/json" "fmt" "time" "github.com/kataras/golog" "github.com/kataras/iris/v12" ) // A Broker holds open client connections, // listens for incoming events on its Notifier channel // and broadcast event data to all registered connections. type Broker struct { // Events are pushed to this channel by the main events-gathering routine. Notifier chan []byte // New client connections. newClients chan chan []byte // Closed client connections. closingClients chan chan []byte // Client connections registry. clients map[chan []byte]bool } // NewBroker returns a new broker factory. func NewBroker() *Broker { b := &Broker{ Notifier: make(chan []byte, 1), newClients: make(chan chan []byte), closingClients: make(chan chan []byte), clients: make(map[chan []byte]bool), } // Set it running - listening and broadcasting events. go b.listen() return b } // Listen on different channels and act accordingly. func (b *Broker) listen() { for { select { case s := <-b.newClients: // A new client has connected. // Register their message channel. b.clients[s] = true golog.Infof("Client added. %d registered clients", len(b.clients)) case s := <-b.closingClients: // A client has dettached and we want to // stop sending them messages. delete(b.clients, s) golog.Infof("Removed client. %d registered clients", len(b.clients)) case event := <-b.Notifier: // We got a new event from the outside! // Send event to all connected clients. for clientMessageChan := range b.clients { clientMessageChan <- event } } } } func (b *Broker) ServeHTTP(ctx iris.Context) { // Make sure that the writer supports flushing. flusher, ok := ctx.ResponseWriter().Flusher() if !ok { ctx.StopWithText(iris.StatusHTTPVersionNotSupported, "Streaming unsupported!") return } // Set the headers related to event streaming, you can omit the "application/json" if you send plain text. // If you develop a go client, you must have: "Accept" : "application/json, text/event-stream" header as well. ctx.ContentType("application/json, text/event-stream") ctx.Header("Cache-Control", "no-cache") ctx.Header("Connection", "keep-alive") // We also add a Cross-origin Resource Sharing header so browsers on different domains can still connect. ctx.Header("Access-Control-Allow-Origin", "*") // Each connection registers its own message channel with the Broker's connections registry. messageChan := make(chan []byte) // Signal the broker that we have a new connection. b.newClients <- messageChan // Listen to connection close and when the entire request handler chain exits(this handler here) and un-register messageChan. ctx.OnClose(func(iris.Context) { // Remove this client from the map of connected clients // when this handler exits. b.closingClients <- messageChan }) // Block waiting for messages broadcast on this connection's messageChan. for { // Write to the ResponseWriter. // Server Sent Events compatible. ctx.Writef("data: %s\n\n", <-messageChan) // or json: data:{obj}. // Flush the data immediately instead of buffering it for later. flusher.Flush() } } type event struct { Timestamp int64 `json:"timestamp"` Message string `json:"message"` } const script = `` func main() { broker := NewBroker() go func() { for { time.Sleep(2 * time.Second) now := time.Now() evt := event{ Timestamp: now.Unix(), Message: fmt.Sprintf("Hello at %s", now.Format(time.RFC1123)), } evtBytes, err := json.Marshal(evt) if err != nil { golog.Error(err) continue } broker.Notifier <- evtBytes } }() app := iris.New() app.Logger().SetLevel("debug") app.Get("/", func(ctx iris.Context) { ctx.HTML( `SSE` + script + `

Waiting for messages...

Timestamp (server) Message
`) }) app.Get("/events", broker.ServeHTTP) // http://localhost:8080 // http://localhost:8080/events app.Listen(":8080") } ================================================ FILE: _examples/response-writer/sse/optional.sse.mini.js.html ================================================ SSE (javascript side)

Open the browser's console(F12) and watch for incoming event messages

================================================ FILE: _examples/response-writer/sse-third-party/main.go ================================================ package main import ( "time" "github.com/kataras/iris/v12" "github.com/r3labs/sse/v2" ) // First of all install the sse third-party package (you can use other if you don't like this approach or go ahead to the "sse" example) // $ go get github.com/r3labs/sse/v2@v2.7.4 func main() { app := iris.New() s := sse.New() /* This creates a new stream inside of the scheduler. Seeing as there are no consumers, publishing a message to this channel will do nothing. Clients can connect to this stream once the iris handler is started by specifying stream as a url parameter, like so: http://localhost:8080/events?stream=messages */ s.CreateStream("messages") app.Any("/events", iris.FromStd(s)) go func() { // You design when to send messages to the client, // here we just wait 5 seconds to send the first message // in order to give u time to open a browser window... time.Sleep(5 * time.Second) // Publish a payload to the stream. s.Publish("messages", &sse.Event{ Data: []byte("ping"), }) time.Sleep(3 * time.Second) s.Publish("messages", &sse.Event{ Data: []byte("second message"), }) time.Sleep(2 * time.Second) s.Publish("messages", &sse.Event{ Data: []byte("third message"), }) }() // ... app.Listen(":8080") } /* For a golang SSE client you can look at: https://github.com/r3labs/sse#example-client */ ================================================ FILE: _examples/response-writer/sse-third-party-2/index.html ================================================ SSE Examples Messages
================================================ FILE: _examples/response-writer/sse-third-party-2/main.go ================================================ package main import ( "log" "net/http" "os" "strconv" "time" "github.com/alexandrevicenzi/go-sse" "github.com/kataras/iris/v12" ) // Install the sse third-party package. // $ go get -u github.com/alexandrevicenzi/go-sse // // Documentation: https://pkg.go.dev/github.com/alexandrevicenzi/go-sse func main() { s := sse.NewServer(&sse.Options{ // Increase default retry interval to 10s. RetryInterval: 10 * 1000, // CORS headers Headers: map[string]string{ "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, OPTIONS", "Access-Control-Allow-Headers": "Keep-Alive,X-Requested-With,Cache-Control,Content-Type,Last-Event-ID", }, // Custom channel name generator ChannelNameFunc: func(request *http.Request) string { return request.URL.Path }, // Print debug info Logger: log.New(os.Stdout, "go-sse: ", log.Ldate|log.Ltime|log.Lshortfile), }) defer s.Shutdown() app := iris.New() app.Get("/", func(ctx iris.Context) { ctx.ServeFile("./index.html") }) app.Get("/events/{channel}", iris.FromStd(s)) go func() { for { s.SendMessage("/events/channel-1", sse.SimpleMessage(time.Now().Format("2006/02/01/ 15:04:05"))) time.Sleep(5 * time.Second) } }() go func() { i := 0 for { i++ s.SendMessage("/events/channel-2", sse.SimpleMessage(strconv.Itoa(i))) time.Sleep(5 * time.Second) } }() app.Listen(":3000") } ================================================ FILE: _examples/response-writer/stream-writer/main.go ================================================ package main import ( "errors" "io" "time" // showcase the delay "github.com/kataras/iris/v12" ) var errDone = errors.New("done") func main() { app := iris.New() app.Get("/", func(ctx iris.Context) { ctx.ContentType("text/html") ctx.Header("Transfer-Encoding", "chunked") i := 0 ints := []int{1, 2, 3, 5, 7, 9, 11, 13, 15, 17, 23, 29} // Send the response in chunks and wait for half a second between each chunk, // until connection close. err := ctx.StreamWriter(func(w io.Writer) error { ctx.Writef("Message number %d
", ints[i]) time.Sleep(500 * time.Millisecond) // simulate delay. if i == len(ints)-1 { return errDone // ends the loop. } i++ return nil // continue write }) if err != errDone { // Test it by canceling the request before the stream ends: // [ERRO] $DATETIME stream: context canceled. ctx.Application().Logger().Errorf("stream: %v", err) } }) type messageNumber struct { Number int `json:"number"` } app.Get("/json", func(ctx iris.Context) { ctx.Header("Transfer-Encoding", "chunked") i := 0 ints := []int{1, 2, 3, 5, 7, 9, 11, 13, 15, 17, 23, 29} // Send the response in chunks and wait for half a second between each chunk, // until connection close. notifyClose := ctx.Request().Context().Done() for { select { case <-notifyClose: // err := ctx.Request().Context().Err() ctx.Application().Logger().Infof("Connection closed, loop end.") return default: ctx.JSON(messageNumber{Number: ints[i]}) ctx.WriteString("\n") time.Sleep(500 * time.Millisecond) // simulate delay. if i == len(ints)-1 { ctx.Application().Logger().Infof("Loop end.") return } i++ ctx.ResponseWriter().Flush() } } }) app.Listen(":8080") } /* Look the following methods too: - Context.OnClose(callback) - Context.OnConnectionClose(callback) and - Context.Request().Context().Done()/.Err() too */ ================================================ FILE: _examples/response-writer/write-rest/main.go ================================================ package main import ( "encoding/xml" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/x/errors" ) // User example struct for json and msgpack. type User struct { Firstname string `json:"firstname" msgpack:"firstname"` Lastname string `json:"lastname" msgpack:"lastname"` City string `json:"city" msgpack:"city"` Age int `json:"age" msgpack:"age"` } // ExampleXML just a test struct to view represents xml content-type type ExampleXML struct { XMLName xml.Name `xml:"example"` One string `xml:"one,attr"` Two string `xml:"two,attr"` } // ExampleYAML just a test struct to write yaml to the client. type ExampleYAML struct { Name string `yaml:"name"` ServerAddr string `yaml:"ServerAddr"` } func main() { app := iris.New() // Optionally, set a custom handler for JSON, JSONP, Protobuf, MsgPack, YAML, Markdown... // write errors. app.SetContextErrorHandler(new(errorHandler)) // Read app.Post("/decode", func(ctx iris.Context) { // Read https://github.com/kataras/iris/blob/main/_examples/request-body/read-json/main.go as well. var user User err := ctx.ReadJSON(&user) if err != nil { errors.InvalidArgument.Details(ctx, "unable to parse body", err.Error()) return } ctx.Writef("%s %s is %d years old and comes from %s!", user.Firstname, user.Lastname, user.Age, user.City) }) // Write app.Get("/encode", func(ctx iris.Context) { u := User{ Firstname: "John", Lastname: "Doe", City: "Neither FBI knows!!!", Age: 25, } // Manually setting a content type: ctx.ContentType("text/javascript") ctx.JSON(u) }) // Use Secure field to prevent json hijacking. // It prepends `"while(1),"` to the body when the data is array. app.Get("/json_secure", func(ctx iris.Context) { response := []string{"val1", "val2", "val3"} options := iris.JSON{Indent: "", Secure: true} ctx.JSON(response, options) // Will output: while(1);["val1","val2","val3"] }) // Use ASCII field to generate ASCII-only JSON // with escaped non-ASCII characters. app.Get("/json_ascii", func(ctx iris.Context) { response := iris.Map{"lang": "GO-虹膜", "tag": "
"} options := iris.JSON{Indent: " ", ASCII: true} ctx.JSON(response, options) /* Will output: { "lang": "GO-\u8679\u819c", "tag": "\u003cbr\u003e" } */ }) // Do not replace special HTML characters with their unicode entities // using the UnescapeHTML field. app.Get("/json_raw", func(ctx iris.Context) { options := iris.JSON{UnescapeHTML: true} ctx.JSON(iris.Map{ "html": "Hello, world!", }, options) // Will output: {"html":"Hello, world!"} }) // Other content types, app.Get("/binary", func(ctx iris.Context) { // useful when you want force-download of contents of raw bytes form. ctx.Binary([]byte("Some binary data here.")) }) app.Get("/text", func(ctx iris.Context) { ctx.Text("Plain text here") }) app.Get("/json", func(ctx iris.Context) { ctx.JSON(map[string]string{"hello": "json"}) // or myjsonStruct{hello:"json} }) app.Get("/jsonp", func(ctx iris.Context) { ctx.JSONP(map[string]string{"hello": "jsonp"}, iris.JSONP{Callback: "callbackName"}) }) app.Get("/xml", func(ctx iris.Context) { ctx.XML(ExampleXML{One: "hello", Two: "xml"}) // OR: // ctx.XML(iris.XMLMap("keys", iris.Map{"key": "value"})) }) app.Get("/markdown", func(ctx iris.Context) { ctx.Markdown([]byte("# Hello Dynamic Markdown -- iris")) }) app.Get("/yaml", func(ctx iris.Context) { ctx.YAML(ExampleYAML{Name: "Iris", ServerAddr: "localhost:8080"}) // OR: // ctx.YAML(iris.Map{"name": "Iris", "serverAddr": "localhost:8080"}) }) // app.Get("/protobuf", func(ctx iris.Context) { // ctx.Protobuf(proto.Message) // }) app.Get("/msgpack", func(ctx iris.Context) { u := User{ Firstname: "John", Lastname: "Doe", City: "Neither FBI knows!!!", Age: 25, } ctx.MsgPack(u) }) // http://localhost:8080/decode // http://localhost:8080/encode // http://localhost:8080/json_secure // http://localhost:8080/json_ascii // // http://localhost:8080/binary // http://localhost:8080/text // http://localhost:8080/json // http://localhost:8080/jsonp // http://localhost:8080/xml // http://localhost:8080/markdown // http://localhost:8080/msgpack // // `iris.WithOptimizations` is an optional configurator, // if passed to the `Run` then it will ensure that the application // response to the client as fast as possible. // // // `iris.WithoutServerError` is an optional configurator, // if passed to the `Run` then it will not print its passed error as an actual server error. app.Listen(":8080", iris.WithOptimizations) } type errorHandler struct{} func (h *errorHandler) HandleContextError(ctx iris.Context, err error) { errors.Internal.Err(ctx, err) } ================================================ FILE: _examples/routing/basic/.dockerignore ================================================ .git node_modules bin ================================================ FILE: _examples/routing/basic/Dockerfile ================================================ # docker build -t myapp . # docker run --rm -it -p 8080:8080 myapp:latest FROM golang:latest AS builder RUN apt-get update ENV GO111MODULE=on \ CGO_ENABLED=0 \ GOOS=linux \ GOARCH=amd64 WORKDIR /go/src/app COPY go.mod . RUN go mod download COPY . . RUN go install FROM scratch COPY --from=builder /go/bin/app . ENTRYPOINT ["./app"] ================================================ FILE: _examples/routing/basic/README.md ================================================ # Basic Example The only requirement for this example is [Docker](https://docs.docker.com/install/). ## Docker Compose The Docker Compose is pre-installed with Docker for Windows. For linux please follow the steps described at: https://docs.docker.com/compose/install/. Build and run the application for linux arch and expose it on http://localhost:8080. ```sh $ docker-compose up ``` See [docker-compose file](docker-compose.yml). ## Without Docker Compose 1. Build the image as "myapp" (docker build) 2. Run the image and map exposed ports (-p 8080:8080) 3. Attach the interactive mode so CTRL/CMD+C signals are respected to shutdown the Iris Server (-it) 4. Cleanup the image on finish (--rm) ```sh $ docker build -t myapp . $ docker run --rm -it -p 8080:8080 myapp:latest ``` ================================================ FILE: _examples/routing/basic/docker-compose.yml ================================================ # docker-compose up [--build] version: '3' services: app: build: . ports: - 8080:8080 ================================================ FILE: _examples/routing/basic/main.go ================================================ package main import ( "github.com/kataras/iris/v12" ) func newApp() *iris.Application { app := iris.New() app.Logger().SetLevel("debug") // registers a custom handler for 404 not found http (error) status code, // fires when route not found or manually by ctx.StatusCode(iris.StatusNotFound). app.OnErrorCode(iris.StatusNotFound, notFoundHandler) // GET -> HTTP Method // / -> Path // func(ctx iris.Context) -> The route's handler. // // Third receiver should contains the route's handler(s), they are executed by order. app.Handle("GET", "/", func(ctx iris.Context) { ctx.HTML("Hello from " + ctx.Path()) // Hello from / }) app.Get("/home", func(ctx iris.Context) { ctx.Writef(`Same as app.Handle("GET", "/", [...])`) }) // Different path parameters types in the same path. // Note that: fallback should registered first e.g. {path} {string}, // because the handler on this case is executing from last to top. app.Get("/u/{p:path}", func(ctx iris.Context) { ctx.Writef(":string, :int, :uint, :alphabetical and :path in the same path pattern.") }) app.Get("/u/{username:string}", func(ctx iris.Context) { ctx.Writef("before username (string), current route name: %s\n", ctx.RouteName()) ctx.Next() }, func(ctx iris.Context) { ctx.Writef("username (string): %s", ctx.Params().Get("username")) }) app.Get("/u/{firstname:alphabetical}", func(ctx iris.Context) { ctx.Writef("before firstname (alphabetical), current route name: %s\n", ctx.RouteName()) ctx.Next() }, func(ctx iris.Context) { ctx.Writef("firstname (alphabetical): %s", ctx.Params().Get("firstname")) }) app.Get("/u/{id:int}", func(ctx iris.Context) { ctx.Writef("before id (int), current route name: %s\n", ctx.RouteName()) ctx.Next() }, func(ctx iris.Context) { ctx.Writef("id (int): %d", ctx.Params().GetIntDefault("id", 0)) }) app.Get("/u/{uid:uint}", func(ctx iris.Context) { ctx.Writef("before uid (uint), current route name: %s\n", ctx.RouteName()) ctx.Next() }, func(ctx iris.Context) { ctx.Writef("uid (uint): %d", ctx.Params().GetUintDefault("uid", 0)) }) /* /u/some/path/here maps to :path /u/abcd maps to :alphabetical (if :alphabetical registered otherwise :string) /u/42 maps to :uint (if :uint registered otherwise :int) /u/-1 maps to :int (if :int registered otherwise :string) /u/abcd123 maps to :string */ // Pssst, don't forget dynamic-path example for more "magic"! app.Get("/api/users/{userid:uint64 min(1)}", func(ctx iris.Context) { userID, err := ctx.Params().GetUint64("userid") if err != nil { ctx.Writef("error while trying to parse userid parameter," + "this will never happen if :uint64 is being used because if it's not a valid uint64 it will fire Not Found automatically.") ctx.StatusCode(iris.StatusBadRequest) return } ctx.JSON(map[string]any{ // you can pass any custom structured go value of course. "user_id": userID, }) }) // app.Post("/", func(ctx iris.Context){}) -> for POST http method. // app.Put("/", func(ctx iris.Context){})-> for "PUT" http method. // app.Delete("/", func(ctx iris.Context){})-> for "DELETE" http method. // app.Options("/", func(ctx iris.Context){})-> for "OPTIONS" http method. // app.Trace("/", func(ctx iris.Context){})-> for "TRACE" http method. // app.Head("/", func(ctx iris.Context){})-> for "HEAD" http method. // app.Connect("/", func(ctx iris.Context){})-> for "CONNECT" http method. // app.Patch("/", func(ctx iris.Context){})-> for "PATCH" http method. // app.Any("/", func(ctx iris.Context){}) for all http methods. // More than one route can contain the same path with a different http mapped method. // You can catch any route creation errors with: // route, err := app.Get(...) // set a name to a route: route.Name = "myroute" // You can also group routes by path prefix, sharing middleware(s) and done handlers. adminRoutes := app.Party("/admin", adminMiddleware) adminRoutes.Done(func(ctx iris.Context) { // executes always last if ctx.Next() ctx.Application().Logger().Infof("response sent to " + ctx.Path()) }) // adminRoutes.Layout("/views/layouts/admin.html") // set a view layout for these routes, see more at view examples. // GET: http://localhost:8080/admin adminRoutes.Get("/", func(ctx iris.Context) { // [...] ctx.StatusCode(iris.StatusOK) // default is 200 == iris.StatusOK ctx.HTML("

Hello from admin/

") ctx.Next() // in order to execute the party's "Done" Handler(s) }) // GET: http://localhost:8080/admin/login adminRoutes.Get("/login", func(ctx iris.Context) { // [...] }) // POST: http://localhost:8080/admin/login adminRoutes.Post("/login", func(ctx iris.Context) { // [...] }) // subdomains, easier than ever, should add localhost or 127.0.0.1 into your hosts file, // etc/hosts on unix or C:/windows/system32/drivers/etc/hosts on windows. v1 := app.Party("v1.") { // braces are optional, it's just type of style, to group the routes visually. // http://v1.localhost:8080 // Note: for versioning-specific features checkout the _examples/routing/versioning instead. v1.Get("/", func(ctx iris.Context) { ctx.HTML(`Version 1 API. go to /api/users`) }) usersAPI := v1.Party("/api/users") { // http://v1.localhost:8080/api/users usersAPI.Get("/", func(ctx iris.Context) { ctx.Writef("All users") }) // http://v1.localhost:8080/api/users/42 usersAPI.Get("/{userid:int}", func(ctx iris.Context) { ctx.Writef("user with id: %d", ctx.Params().GetIntDefault("userid", 0)) }) } } // wildcard subdomains. wildcardSubdomain := app.WildcardSubdomain() { wildcardSubdomain.Get("/", func(ctx iris.Context) { ctx.Writef("Subdomain can be anything, now you're here from: %s", ctx.Subdomain()) }) } return app } func main() { app := newApp() // http://localhost:8080 // http://localhost:8080/home // http://localhost:8080/api/users/42 // http://localhost:8080/admin // http://localhost:8080/admin/login // // http://localhost:8080/api/users/0 // http://localhost:8080/api/users/blabla // http://localhost:8080/wontfound // // http://localhost:8080/u/abcd // http://localhost:8080/u/42 // http://localhost:8080/u/-1 // http://localhost:8080/u/abcd123 // http://localhost:8080/u/some/path/here // // if hosts edited: // http://v1.localhost:8080 // http://v1.localhost:8080/api/users // http://v1.localhost:8080/api/users/42 // http://anything.localhost:8080 app.Listen(":8080") } func adminMiddleware(ctx iris.Context) { // [...] ctx.Next() // to move to the next handler, or don't that if you have any auth logic. } func notFoundHandler(ctx iris.Context) { ctx.HTML("Custom route for 404 not found http code, here you can render a view, html, json any valid response.") } // Notes: // A path parameter name should contain only alphabetical letters, symbols, containing '_' and numbers are NOT allowed. // If route failed to be registered, the app will panic without any warnings // if you didn't catch the second return value(error) on .Handle/.Get.... // See "file-server/single-page-application" to see how another feature, "WrapRouter", works. ================================================ FILE: _examples/routing/basic/main_test.go ================================================ package main import ( "fmt" "testing" "github.com/kataras/iris/v12/httptest" ) // Shows a very basic usage of the httptest. // The tests are written in a way to be easy to understand, // for a more comprehensive testing examples check out the: // _examples/routing/main_test.go, // _examples/routing/subdomains/www/main_test.go // _examples/file-server and e.t.c. // Almost every example which covers // a new feature from you to learn // contains a test file as well. func TestRoutingBasic(t *testing.T) { expectedUResponse := func(paramName, paramType, paramValue string) string { s := fmt.Sprintf("before %s (%s), current route name: GET/u/{%s:%s}\n", paramName, paramType, paramName, paramType) s += fmt.Sprintf("%s (%s): %s", paramName, paramType, paramValue) return s } var ( expectedNotFoundResponse = "Custom route for 404 not found http code, here you can render a view, html, json any valid response." expectedIndexResponse = "Hello from /" expectedHomeResponse = `Same as app.Handle("GET", "/", [...])` expectedUpathResponse = ":string, :int, :uint, :alphabetical and :path in the same path pattern." expectedUStringResponse = expectedUResponse("username", "string", "abcd123") expectedUIntResponse = expectedUResponse("id", "int", "-1") expectedUUintResponse = expectedUResponse("uid", "uint", "42") expectedUAlphabeticalResponse = expectedUResponse("firstname", "alphabetical", "abcd") expectedAPIUsersIndexResponse = map[string]any{"user_id": 42} expectedAdminIndexResponse = "

Hello from admin/

" expectedSubdomainV1IndexResponse = `Version 1 API. go to /api/users` expectedSubdomainV1APIUsersIndexResponse = "All users" expectedSubdomainV1APIUsersIndexWithParamResponse = "user with id: 42" expectedSubdomainWildcardIndexResponse = "Subdomain can be anything, now you're here from: any-subdomain-here" ) app := newApp() e := httptest.New(t, app) e.GET("/anotfound").Expect().Status(httptest.StatusNotFound). Body().IsEqual(expectedNotFoundResponse) e.GET("/").Expect().Status(httptest.StatusOK). Body().IsEqual(expectedIndexResponse) e.GET("/home").Expect().Status(httptest.StatusOK). Body().IsEqual(expectedHomeResponse) e.GET("/u/some/path/here").Expect().Status(httptest.StatusOK). Body().IsEqual(expectedUpathResponse) e.GET("/u/abcd123").Expect().Status(httptest.StatusOK). Body().IsEqual(expectedUStringResponse) e.GET("/u/-1").Expect().Status(httptest.StatusOK). Body().IsEqual(expectedUIntResponse) e.GET("/u/42").Expect().Status(httptest.StatusOK). Body().IsEqual(expectedUUintResponse) e.GET("/u/abcd").Expect().Status(httptest.StatusOK). Body().IsEqual(expectedUAlphabeticalResponse) e.GET("/api/users/42").Expect().Status(httptest.StatusOK). JSON().IsEqual(expectedAPIUsersIndexResponse) e.GET("/admin").Expect().Status(httptest.StatusOK). Body().IsEqual(expectedAdminIndexResponse) e.Request("GET", "/").WithURL("http://v1.example.com").Expect().Status(httptest.StatusOK). Body().IsEqual(expectedSubdomainV1IndexResponse) e.Request("GET", "/api/users").WithURL("http://v1.example.com").Expect().Status(httptest.StatusOK). Body().IsEqual(expectedSubdomainV1APIUsersIndexResponse) e.Request("GET", "/api/users/42").WithURL("http://v1.example.com").Expect().Status(httptest.StatusOK). Body().IsEqual(expectedSubdomainV1APIUsersIndexWithParamResponse) e.Request("GET", "/").WithURL("http://any-subdomain-here.example.com").Expect().Status(httptest.StatusOK). Body().IsEqual(expectedSubdomainWildcardIndexResponse) } ================================================ FILE: _examples/routing/conditional-chain/main.go ================================================ package main import ( "github.com/kataras/iris/v12" ) func newApp() *iris.Application { app := iris.New() v1 := app.Party("/api/v1") myFilter := func(ctx iris.Context) bool { // don't do that on production, use session or/and database calls and etc. ok, _ := ctx.URLParamBool("admin") return ok } onlyWhenFilter1 := func(ctx iris.Context) { ctx.Application().Logger().Infof("admin: %#+v", ctx.URLParams()) ctx.Writef("Admin\n") ctx.Next() } onlyWhenFilter2 := func(ctx iris.Context) { // You can always use the per-request storage // to perform actions like this ofc. // // this handler: ctx.Values().Set("is_admin", true) // next handler: isAdmin := ctx.Values().GetBoolDefault("is_admin", false) // // but, let's simplify it: ctx.HTML("

Hello Admin


") ctx.Next() } // HERE: // It can be registered anywhere, as a middleware. // It will fire the `onlyWhenFilter1` and `onlyWhenFilter2` as middlewares (with ctx.Next()) // if myFilter pass otherwise it will just continue the handler chain with ctx.Next() by ignoring // the `onlyWhenFilter1` and `onlyWhenFilter2`. myMiddleware := iris.NewConditionalHandler(myFilter, onlyWhenFilter1, onlyWhenFilter2) v1UsersRouter := v1.Party("/users", myMiddleware) v1UsersRouter.Get("/", func(ctx iris.Context) { ctx.HTML("requested: /api/v1/users") }) return app } func main() { app := newApp() // http://localhost:8080/api/v1/users // http://localhost:8080/api/v1/users?admin=true app.Listen(":8080") } ================================================ FILE: _examples/routing/conditional-chain/main_test.go ================================================ package main import ( "testing" "github.com/kataras/iris/v12/httptest" ) func TestNewConditionalHandler(t *testing.T) { app := newApp() e := httptest.New(t, app) e.GET("/api/v1/users").Expect().Status(httptest.StatusOK). Body().IsEqual("requested: /api/v1/users") e.GET("/api/v1/users").WithQuery("admin", "true").Expect().Status(httptest.StatusOK). Body().IsEqual("Admin\n

Hello Admin


requested: /api/v1/users") } ================================================ FILE: _examples/routing/custom-context/main.go ================================================ package main import ( "fmt" "sync" "github.com/kataras/iris/v12" ) func main() { // 1. Create the iris app instance. app := iris.New() // 2. Create the Context Wrapper which will be used to wrap the handlers // that expect a *myCustomContext instead of iris.Context. w := iris.NewContextWrapper(&myCustomContextPool{}) // OR: // w := iris.NewContextWrapper(iris.NewContextPool[myCustomContext, *myCustomContext]()) // The example custom context pool operates exactly the same as the result of iris.NewContextPool. // 3. Register the handler(s) which expects a *myCustomContext instead of iris.Context. // The `w.Handler` will wrap the handler and will call the `Acquire` and `Release` // methods of the `myCustomContextPool` to get and release the *myCustomContext. app.Get("/", w.Handler(index)) // 4. Start the server. app.Listen(":8080") } func index(ctx *myCustomContext) { ctx.HTML("

Hello, World!

") } /* Custom Context Pool */ // Create the context sync pool for our custom context, // the pool must implement Acquire() T and Release(T) methods to satisfy the iris.ContextPool interface. type myCustomContextPool struct { pool sync.Pool } // Acquire returns a new custom context from the pool. func (p *myCustomContextPool) Acquire(ctx iris.Context) *myCustomContext { v := p.pool.Get() if v == nil { v = &myCustomContext{ Context: ctx, // custom fields here... } } return v.(*myCustomContext) } // Release puts a custom context back to the pool. func (p *myCustomContextPool) Release(t *myCustomContext) { // You can take advantage of this method to clear the context // and re-use it on the Acquire method, use the sync.Pool. p.pool.Put(t) } /* Custom Context */ // Create a custom context. type myCustomContext struct { // It's just an embedded field which is set on AcquireFunc, // so you can use myCustomContext with the same methods as iris.Context, // override existing iris.Context's methods or add custom methods. // You can use the `Context` field to access the original context. iris.Context } // SetContext sets the original iris.Context, // should be implemented by custom context type(s) when // the ContextWrapper uses a context Pool through the iris.NewContextPool function. // Comment line 15, uncomment line 17 and the method below. func (c *myCustomContext) SetContext(ctx iris.Context) { c.Context = ctx } func (c *myCustomContext) HTML(format string, args ...any) (int, error) { c.Application().Logger().Info("HTML was called from custom Context") return c.Context.HTML(fmt.Sprintf(format, args...)) } ================================================ FILE: _examples/routing/custom-router/main.go ================================================ package main import ( "strings" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/core/router" ) /* A Router should contain all three of the following methods: - HandleRequest should handle the request based on the Context. HandleRequest(ctx iris.Context) - Build should builds the handler, it's being called on router's BuildRouter. Build(provider router.RoutesProvider) error - RouteExists reports whether a particular route exists. RouteExists(ctx iris.Context, method, path string) bool - FireErrorCode(ctx iris.Context) should handle the given ctx.GetStatusCode(). For a more detailed, complete and useful example you can take a look at the iris' router itself which is located at: https://github.com/kataras/iris/tree/main/core/router/handler.go which completes this exact interface, the `router#RequestHandler`. */ type customRouter struct { // a copy of routes (safer because you will not be able to alter a route on serve-time without a `app.RefreshRouter` call): // []router.Route // or just expect the whole routes provider: provider router.RoutesProvider } // HandleRequest a silly example which finds routes based only on the first part of the requested path // which must be a static one as well, the rest goes to fill the parameters. func (r *customRouter) HandleRequest(ctx iris.Context) { path := ctx.Path() ctx.Application().Logger().Infof("Requested resource path: %s", path) parts := strings.Split(path, "/")[1:] staticPath := "/" + parts[0] for _, route := range r.provider.GetRoutes() { if strings.HasPrefix(route.Path, staticPath) && route.Method == ctx.Method() { paramParts := parts[1:] for _, paramValue := range paramParts { for _, p := range route.Tmpl().Params { ctx.Params().Set(p.Name, paramValue) } } ctx.SetCurrentRoute(route.ReadOnly) ctx.Do(route.Handlers) return } } // if nothing found... ctx.StatusCode(iris.StatusNotFound) } func (r *customRouter) Build(provider router.RoutesProvider) error { for _, route := range provider.GetRoutes() { // do any necessary validation or conversations based on your custom logic here // but always run the "BuildHandlers" for each registered route. route.BuildHandlers() // [...] r.routes = append(r.routes, *route) } r.provider = provider return nil } func (r *customRouter) RouteExists(ctx iris.Context, method, path string) bool { // [...] return false } func (r *customRouter) FireErrorCode(ctx iris.Context) { // responseStatusCode := ctx.GetStatusCode() // set by prior ctx.StatusCode calls // [...] } func main() { app := iris.New() // In case you are wondering, the parameter types and macros like "{param:string $func()}" still work inside // your custom router if you fetch by the Route's Handler // because they are middlewares under the hood, so you don't have to implement the logic of handling them manually, // though you have to match what requested path is what route and fill the ctx.Params(), this is the work of your custom router. app.Get("/hello/{name}", func(ctx iris.Context) { name := ctx.Params().Get("name") ctx.Writef("Hello %s\n", name) }) app.Get("/cs/{num:uint64 min(10) else 400}", func(ctx iris.Context) { num := ctx.Params().GetUint64Default("num", 0) ctx.Writef("num is: %d\n", num) }) // To replace the existing router with a customized one by using the iris/context.Context // you have to use the `app.BuildRouter` method before `app.Run` and after the routes registered. // You should pass your custom router's instance as the second input arg, which must completes the `router#RequestHandler` // interface as shown above. // // To see how you can build something even more low-level without direct iris' context support (you can do that manually as well) // navigate to the "custom-wrapper" example instead. myCustomRouter := new(customRouter) app.BuildRouter(app.ContextPool, myCustomRouter, app.APIBuilder, true) app.Listen(":8080") } ================================================ FILE: _examples/routing/custom-wrapper/main.go ================================================ package main import ( "net/http" "strings" "github.com/kataras/iris/v12" ) // In this example you'll just see one use case of .WrapRouter. // You can use the .WrapRouter to add custom logic when or when not the router should // be executed in order to execute the registered routes' handlers. func newApp() *iris.Application { app := iris.New() app.OnErrorCode(iris.StatusNotFound, func(ctx iris.Context) { ctx.HTML("Resource Not found") }) app.Get("/profile/{username}", func(ctx iris.Context) { ctx.Writef("Hello %s", ctx.Params().Get("username")) }) app.HandleDir("/", iris.Dir("./public")) myOtherHandler := func(ctx iris.Context) { ctx.Writef("inside a handler which is fired manually by our custom router wrapper") } // wrap the router with a native net/http handler. // if url does not contain any "." (i.e: .css, .js...) // (depends on the app , you may need to add more file-server exceptions), // then the handler will execute the router that is responsible for the // registered routes (look "/" and "/profile/{username}") // if not then it will serve the files based on the root "/" path. app.WrapRouter(func(w http.ResponseWriter, r *http.Request, router http.HandlerFunc) { path := r.URL.Path if strings.HasPrefix(path, "/other") { // acquire and release a context in order to use it to execute // our custom handler // remember: we use net/http.Handler because here we are in the "low-level", before the router itself. ctx := app.ContextPool.Acquire(w, r) myOtherHandler(ctx) app.ContextPool.Release(ctx) return } router.ServeHTTP(w, r) // else continue serving routes as usual. }) return app } func main() { app := newApp() // http://localhost:8080 // http://localhost:8080/index.html // http://localhost:8080/app.js // http://localhost:8080/css/main.css // http://localhost:8080/profile/anyusername // http://localhost:8080/other/random app.Listen(":8080") // Note: In this example we just saw one use case, // you may want to .WrapRouter or .Downgrade in order to bypass the iris' default router, i.e: // you can use that method to setup custom proxies too. } ================================================ FILE: _examples/routing/custom-wrapper/main_test.go ================================================ package main import ( "os" "path/filepath" "strings" "testing" "github.com/kataras/iris/v12/httptest" ) type resource string func (r resource) String() string { return string(r) } func (r resource) strip(strip string) string { s := r.String() return strings.TrimPrefix(s, strip) } func (r resource) loadFromBase(dir string) string { filename := r.String() if filename == "/" { filename = "/index.html" } fullpath := filepath.Join(dir, filename) b, err := os.ReadFile(fullpath) if err != nil { panic(fullpath + " failed with error: " + err.Error()) } return string(b) } var urls = []resource{ "/", "/index.html", "/app.js", "/css/main.css", } func TestCustomWrapper(t *testing.T) { app := newApp() e := httptest.New(t, app) for _, u := range urls { url := u.String() contents := u.loadFromBase("./public") e.GET(url).Expect(). Status(httptest.StatusOK). Body().IsEqual(contents) } e.GET("/other/something").Expect().Status(httptest.StatusOK) } ================================================ FILE: _examples/routing/custom-wrapper/public/app.js ================================================ window.alert("app.js loaded from \"/"); ================================================ FILE: _examples/routing/custom-wrapper/public/css/main.css ================================================ body { background-color: black; } ================================================ FILE: _examples/routing/custom-wrapper/public/index.html ================================================ Index Page

Hello from index.html

================================================ FILE: _examples/routing/dynamic-path/at-username/main.go ================================================ package main import "github.com/kataras/iris/v12" func main() { app := iris.New() app.Get("/", func(ctx iris.Context) { ctx.Writef("Hello %s", "world") }) // This is an Iris-only feature across all web frameworks // in every programming language for years. // Dynamic Route Path Parameters Functions. // Set min length characters to 2. // Prefix of the username is '@' // Otherwise 404. // // You can also use the regexp(...) function for more advanced expressions. app.Get("/{username:string min(2) prefix(@)}", func(ctx iris.Context) { username := ctx.Params().Get("username")[1:] ctx.Writef("Username is %s", username) }) // http://localhost:8080 -> FOUND (Hello world) // http://localhost:8080/other -> NOT FOUND // http://localhost:8080/@ -> NOT FOUND // http://localhost:8080/@kataras -> FOUND (username is kataras) app.Listen(":8080") } ================================================ FILE: _examples/routing/dynamic-path/main.go ================================================ package main import ( "regexp" "github.com/kataras/iris/v12" ) func main() { app := iris.New() // At the previous example "routing/basic", // we've seen static routes, group of routes, subdomains, wildcard subdomains, a small example of parameterized path // with a single known paramete and custom http errors, now it's time to see wildcard parameters and macros. // Iris, like net/http std package registers route's handlers // by a Handler, the iris' type of handler is just a func(ctx iris.Context) // where context comes from github.com/kataras/iris/context. // // Iris has the easiest and the most powerful routing process you have ever meet. // // At the same time, // Iris has its own interpeter(yes like a programming language) // for route's path syntax and their dynamic path parameters parsing and evaluation, // We call them "macros" for shortcut. // How? It calculates its needs and if not any special regexp needed then it just // registers the route with the low-level underline path syntax, // otherwise it pre-compiles the regexp and adds the necessary middleware(s). // // Standard macro types for parameters: // +------------------------+ // | {param:string} | // +------------------------+ // string type // anything (single path segmnent) // // +-------------------------------+ // | {param:int} | // +-------------------------------+ // int type // -9223372036854775808 to 9223372036854775807 (x64) or -2147483648 to 2147483647 (x32), depends on the host arch // // +------------------------+ // | {param:int8} | // +------------------------+ // int8 type // -128 to 127 // // +------------------------+ // | {param:int16} | // +------------------------+ // int16 type // -32768 to 32767 // // +------------------------+ // | {param:int32} | // +------------------------+ // int32 type // -2147483648 to 2147483647 // // +------------------------+ // | {param:int64} | // +------------------------+ // int64 type // -9223372036854775808 to 9223372036854775807 // // +------------------------+ // | {param:uint} | // +------------------------+ // uint type // 0 to 18446744073709551615 (x64) or 0 to 4294967295 (x32) // // +------------------------+ // | {param:uint8} | // +------------------------+ // uint8 type // 0 to 255 // // +------------------------+ // | {param:uint16} | // +------------------------+ // uint16 type // 0 to 65535 // // +------------------------+ // | {param:uint32} | // +------------------------+ // uint32 type // 0 to 4294967295 // // +------------------------+ // | {param:uint64} | // +------------------------+ // uint64 type // 0 to 18446744073709551615 // // +---------------------------------+ // | {param:bool} or {param:boolean} | // +---------------------------------+ // bool type // only "1" or "t" or "T" or "TRUE" or "true" or "True" // or "0" or "f" or "F" or "FALSE" or "false" or "False" // // +------------------------+ // | {param:alphabetical} | // +------------------------+ // alphabetical/letter type // letters only (upper or lowercase) // // +------------------------+ // | {param:file} | // +------------------------+ // file type // letters (upper or lowercase) // numbers (0-9) // underscore (_) // dash (-) // point (.) // no spaces ! or other character // // +------------------------+ // | {param:path} | // +------------------------+ // path type // anything, should be the last part, can be more than one path segment, // i.e: "/test/{param:path}" and request: "/test/path1/path2/path3" , ctx.Params().Get("param") == "path1/path2/path3" // // +------------------------+ // | {param:uuid} | // +------------------------+ // UUIDv4 (and v1) path parameter validation. // // +------------------------+ // | {param:mail} | // +------------------------+ // Email without domain validation. // // +------------------------+ // | {param:email} | // +------------------------+ // Email with domain validation. // // // +------------------------+ // | {param:date} | // +------------------------+ // yyyy/mm/dd format e.g. /blog/{param:date} matches /blog/2022/04/21. // // +------------------------+ // | {param:weekday} | // +------------------------+ // positive integer 0 to 6 or // string of time.Weekday longname format ("sunday" to "monday" or "Sunday" to "Monday") // format e.g. /schedule/{param:weekday} matches /schedule/monday. // // If type is missing then parameter's type is defaulted to string, so // {param} is identical to {param:string}. // // If a function not found on that type then the `string` macro type's functions are being used. // // // Besides the fact that iris provides the basic types and some default "macro funcs" // you are able to register your own too!. // // Register a named path parameter function: // app.Macros().Number.RegisterFunc("min", func(argument int) func(paramValue string) bool { // [...] // return true/false -> true means valid. // }) // // at the func(argument ...) you can have any standard type, it will be validated before the server starts // so don't care about performance here, the only thing it runs at serve time is the returning func(paramValue string) bool. // // {param:string equal(iris)} , "iris" will be the argument here: // app.Macros().String.RegisterFunc("equal", func(argument string) func(paramValue string) bool { // return func(paramValue string) bool { return argument == paramValue } // }) // Optionally, set custom handler on path parameter type error: app.Macros().Get("uuid").HandleError(func(ctx iris.Context, paramIndex int, err error) { ctx.StatusCode(iris.StatusBadRequest) param := ctx.Params().GetEntryAt(paramIndex) ctx.JSON(iris.Map{ "error": err.Error(), "message": "invalid path parameter", "parameter": param.Key, "value": param.ValueRaw, }) }) // http://localhost:8080/user/bb4f33e4-dc08-40d8-9f2b-e8b2bb615c0e -> OK // http://localhost:8080/user/dsadsa-invalid-uuid -> NOT FOUND app.Get("/user/{id:uuid}", func(ctx iris.Context) { id := ctx.Params().Get("id") ctx.WriteString(id) }) // +------------------------+ // | {param:email} | // +------------------------+ // Email + mx look uppath parameter validation. // Note that, you can also use the simpler ":mail" to accept any domain email. // http://localhost:8080/user/email/kataras2006@hotmail.com -> OK // http://localhost:8080/user/email/b-c@ -> NOT FOUND app.Get("/user/email/{user_email:email}", func(ctx iris.Context) { email := ctx.Params().Get("user_email") ctx.WriteString(email) }) // http://localhost:8080/blog/2022/04/21 app.Get("/blog/{date:date}", func(ctx iris.Context) { // rawTimeValue := ctx.Params().GetEntry("d").ValueRaw.(time.Time) // OR rawTimeValue, _ := ctx.Params().GetTime("date") // yearMonthDay := rawTimeValue.Format("2006/01/02") // OR yearMonthDay := ctx.Params().SimpleDate("date") ctx.Writef("Raw time.Time.String value: %v\nyyyy/mm/dd: %s\n", rawTimeValue, yearMonthDay) }) // 0 to 7 or "Sunday" to "Monday" or "sunday" to "monday". Leading zeros don't matter. // http://localhost:8080/schedule/monday or http://localhost:8080/schedule/Monday or // http://localhost:8080/schedule/1 or http://localhost:8080/schedule/0001. app.Get("/schedule/{day:weekday}", func(ctx iris.Context) { day, _ := ctx.Params().GetWeekday("day") ctx.Writef("Weekday requested was: %v\n", day) }) // you can use the "string" type which is valid for a single path parameter that can be anything. app.Get("/username/{name}", func(ctx iris.Context) { ctx.Writef("Hello %s", ctx.Params().Get("name")) }) // type is missing = {name:string} // Let's register our first macro attached to uint64 macro type. // "min" = the function // "minValue" = the argument of the function // func(uint64) bool = our func's evaluator, this executes in serve time when // a user requests a path which contains the :uint64 macro parameter type with the min(...) macro parameter function. app.Macros().Get("uint64").RegisterFunc("min", func(minValue uint64) func(uint64) bool { // type of "paramValue" should match the type of the internal macro's evaluator function, which in this case is "uint64". return func(paramValue uint64) bool { return paramValue >= minValue } }) // http://localhost:8080/profile/id>=20 // this will throw 404 even if it's found as route on : /profile/0, /profile/blabla, /profile/-1 // macro parameter functions are optional of course. app.Get("/profile/{id:uint64 min(20)}", func(ctx iris.Context) { // second parameter is the error but it will always nil because we use macros, // the validaton already happened. id := ctx.Params().GetUint64Default("id", 0) ctx.Writef("Hello id: %d", id) }) // to change the error code per route's macro evaluator: app.Get("/profile/{id:uint64 min(1)}/friends/{friendid:uint64 min(1) else 504}", func(ctx iris.Context) { id := ctx.Params().GetUint64Default("id", 0) friendid := ctx.Params().GetUint64Default("friendid", 0) ctx.Writef("Hello id: %d looking for friend id: %d", id, friendid) }) // this will throw e 504 error code instead of 404 if all route's macros not passed. // :uint8 0 to 255. app.Get("/ages/{age:uint8 else 400}", func(ctx iris.Context) { age, _ := ctx.Params().GetUint8("age") ctx.Writef("age selected: %d", age) }) // Another example using a custom regexp or any custom logic. // Register your custom argument-less macro function to the :string param type. latLonExpr := "^-?[0-9]{1,3}(?:\\.[0-9]{1,10})?$" latLonRegex, err := regexp.Compile(latLonExpr) if err != nil { panic(err) } // MatchString is a type of func(string) bool, so we use it as it is. app.Macros().Get("string").RegisterFunc("coordinate", latLonRegex.MatchString) app.Get("/coordinates/{lat:string coordinate() else 502}/{lon:string coordinate() else 502}", func(ctx iris.Context) { ctx.Writef("Lat: %s | Lon: %s", ctx.Params().Get("lat"), ctx.Params().Get("lon")) }) // // Another one is by using a custom body. app.Macros().Get("string").RegisterFunc("range", func(minLength, maxLength int) func(string) bool { return func(paramValue string) bool { return len(paramValue) >= minLength && len(paramValue) <= maxLength } }) app.Get("/limitchar/{name:string range(1,200)}", func(ctx iris.Context) { name := ctx.Params().Get("name") ctx.Writef(`Hello %s | the name should be between 1 and 200 characters length otherwise this handler will not be executed`, name) }) // // Register your custom macro function which accepts a slice of strings `[...,...]`. app.Macros().Get("string").RegisterFunc("has", func(validNames []string) func(string) bool { return func(paramValue string) bool { for _, validName := range validNames { if validName == paramValue { return true } } return false } }) app.Get("/static_validation/{name:string has([kataras,gerasimos,maropoulos])}", func(ctx iris.Context) { name := ctx.Params().Get("name") ctx.Writef(`Hello %s | the name should be "kataras" or "gerasimos" or "maropoulos" otherwise this handler will not be executed`, name) }) // // http://localhost:8080/game/a-zA-Z/level/42 // remember, alphabetical is lowercase or uppercase letters only. app.Get("/game/{name:alphabetical}/level/{level:int}", func(ctx iris.Context) { ctx.Writef("name: %s | level: %s", ctx.Params().Get("name"), ctx.Params().Get("level")) }) app.Get("/lowercase/static", func(ctx iris.Context) { ctx.Writef("static and dynamic paths are not conflicted anymore!") }) // let's use a trivial custom regexp that validates a single path parameter // which its value is only lowercase letters. // http://localhost:8080/lowercase/anylowercase app.Get("/lowercase/{name:string regexp(^[a-z]+)}", func(ctx iris.Context) { ctx.Writef("name should be only lowercase, otherwise this handler will never executed: %s", ctx.Params().Get("name")) }) // http://localhost:8080/single_file/app.js app.Get("/single_file/{myfile:file}", func(ctx iris.Context) { ctx.Writef("file type validates if the parameter value has a form of a file name, got: %s", ctx.Params().Get("myfile")) }) // http://localhost:8080/myfiles/any/directory/here/ // this is the only macro type that accepts any number of path segments. app.Get("/myfiles/{directory:path}", func(ctx iris.Context) { ctx.Writef("path type accepts any number of path segments, path after /myfiles/ is: %s", ctx.Params().Get("directory")) }) // for wildcard path (any number of path segments) without validation you can use: // /myfiles/* // http://localhost:8080/trimmed/42.html app.Get("/trimmed/{uid:string regexp(^[0-9]{1,20}.html$)}", iris.TrimParamFilePart, func(ctx iris.Context) { // // The above line is useless now that we've registered the TrimParamFilePart middleware: // uid := ctx.Params().GetTrimFileUint64("uid") // TrimParamFilePart can be registered to a Party (group of routes) too. uid := ctx.Params().GetUint64Default("uid", 0) ctx.Writef("Param value: %d\n", uid) }) // "{param}"'s performance is exactly the same of ":param"'s. // alternatives -> ":param" for single path parameter and "*" for wildcard path parameter. // Note these: // if "/mypath/*" then the parameter name is "*". // if "/mypath/{myparam:path}" then the parameter has two names, one is the "*" and the other is the user-defined "myparam". // WARNING: // A path parameter name should contain only alphabetical letters or digits. Symbols like '_' are NOT allowed. // Last, do not confuse `ctx.Params()` with `ctx.Values()`. // Path parameter's values can be retrieved from `ctx.Params()`, // context's local storage that can be used to communicate between handlers and middleware(s) can be stored to `ctx.Values()`. // // When registering different parameter types in the same exact path pattern, the path parameter's name // should differ e.g. // /path/{name:string} // /path/{id:uint} // // Note: // If * path part is declared at the end of the route path, then // it's considered a wildcard (same as {p:path}). In order to declare // literal * and over pass this limitation use the string's path parameter 'eq' function // as shown below: // app.Get("/*/*/{p:string eq(*)}", handler) <- This will match only: /*/*/* and not /*/*/anything. app.Listen(":8080") } ================================================ FILE: _examples/routing/dynamic-path/root-wildcard/main.go ================================================ package main import ( "github.com/kataras/iris/v12" ) func main() { app := iris.New() // this works as expected now, // will handle all GET requests // except: // / -> because of app.Get("/", ...) // /other/anything/here -> because of app.Get("/other/{paramother:path}", ...) // /other2/anything/here -> because of app.Get("/other2/{paramothersecond:path}", ...) // /other2/static2 -> because of app.Get("/other2/static", ...) // // It isn't conflicts with the rest of the routes, without routing performance cost! // // i.e /something/here/that/cannot/be/found/by/other/registered/routes/order/not/matters app.Get("/{p:path}", h) // app.Get("/static/{p:path}", staticWildcardH) // this will handle only GET / app.Get("/", staticPath) // this will handle all GET requests starting with "/other/" // // i.e /other/more/than/one/path/parts app.Get("/other/{paramother:path}", other) // this will handle all GET requests starting with "/other2/" // except /other2/static (because of the next static route) // // i.e /other2/more/than/one/path/parts app.Get("/other2/{paramothersecond:path}", other2) // this will handle only GET "/other2/static" app.Get("/other2/static2", staticPathOther2) app.Listen(":8080") } func h(ctx iris.Context) { param := ctx.Params().Get("p") ctx.WriteString(param) } func staticWildcardH(ctx iris.Context) { param := ctx.Params().Get("p") ctx.WriteString("from staticWildcardH: param=" + param) } func other(ctx iris.Context) { param := ctx.Params().Get("paramother") ctx.Writef("from other: %s", param) } func other2(ctx iris.Context) { param := ctx.Params().Get("paramothersecond") ctx.Writef("from other2: %s", param) } func staticPath(ctx iris.Context) { ctx.Writef("from the static path(/): %s", ctx.Path()) } func staticPathOther2(ctx iris.Context) { ctx.Writef("from the static path(/other2/static2): %s", ctx.Path()) } ================================================ FILE: _examples/routing/dynamic-path/same-pattern-different-func/main.go ================================================ package main import "github.com/kataras/iris/v12" func main() { app := newApp() app.Logger().SetLevel("debug") app.Listen(":8080") } func newApp() *iris.Application { app := iris.New() app.HandleMany(iris.MethodGet, "/ /api/{page:string suffix(.html)}", handler1) app.Get("/api/{name:string suffix(.zip)}", handler2) return app } func handler1(ctx iris.Context) { reply(ctx) } func handler2(ctx iris.Context) { reply(ctx) } func reply(ctx iris.Context) { ctx.JSON(iris.Map{ "handler": ctx.HandlerName(), "params": ctx.Params().Store, }) } ================================================ FILE: _examples/routing/dynamic-path/same-pattern-different-func/main_test.go ================================================ package main import ( "testing" "github.com/kataras/iris/v12/core/memstore" "github.com/kataras/iris/v12/httptest" ) func TestSameParameterTypeDifferentMacroFunctions(t *testing.T) { app := newApp() e := httptest.New(t, app) type resp struct { Handler string `json:"handler"` Params memstore.Store `json:"params"` } var ( expectedIndex = resp{ Handler: "iris/_examples/routing/dynamic-path/same-pattern-different-func.handler1", Params: nil, } expectedHTMLPage = resp{ Handler: "iris/_examples/routing/dynamic-path/same-pattern-different-func.handler1", Params: memstore.Store{ {Key: "page", ValueRaw: "random.html"}, }, } expectedZipName = resp{ Handler: "iris/_examples/routing/dynamic-path/same-pattern-different-func.handler2", Params: memstore.Store{ {Key: "name", ValueRaw: "random.zip"}, }, } ) e.GET("/").Expect().Status(httptest.StatusOK).JSON().IsEqual(expectedIndex) e.GET("/api/random.html").Expect().Status(httptest.StatusOK).JSON().IsEqual(expectedHTMLPage) e.GET("/api/random.zip").Expect().Status(httptest.StatusOK).JSON().IsEqual(expectedZipName) } ================================================ FILE: _examples/routing/dynamic-path/same-pattern-different-func/use-global/main.go ================================================ package main // #1552 import ( "github.com/kataras/iris/v12" ) func main() { app := newApp() app.Listen(":8080") } func newApp() *iris.Application { app := iris.New() app.UseGlobal(middleware("first")) app.UseGlobal(middleware("second")) app.DoneGlobal(onDone) app.Get("/{name prefix(one)}", handler("first route")) app.Get("/{name prefix(two)}", handler("second route")) app.Get("/{name prefix(three)}", handler("third route")) return app } func middleware(str string) iris.Handler { return func(ctx iris.Context) { ctx.Writef("Called %s middleware\n", str) ctx.Next() } } func handler(str string) iris.Handler { return func(ctx iris.Context) { ctx.Writef("%s\n", str) ctx.Next() // or ignroe that and use app.SetRegisterRules. } } func onDone(ctx iris.Context) { ctx.Writef("Called done: %s", ctx.Params().Get("name")) } ================================================ FILE: _examples/routing/dynamic-path/same-pattern-different-func/use-global/main_test.go ================================================ package main import ( "fmt" "testing" "github.com/kataras/iris/v12/httptest" ) func TestSamePatternDifferentFuncUseGlobal(t *testing.T) { app := newApp() e := httptest.New(t, app) expectedResultFmt := "Called first middleware\nCalled second middleware\n%s\nCalled done: %s" tests := map[string]string{ "/one-num": "first route", "/two-num": "second route", "/three-num": "third route", } for path, mainBody := range tests { result := fmt.Sprintf(expectedResultFmt, mainBody, path[1:]) e.GET(path).Expect().Status(httptest.StatusOK).Body().IsEqual(result) } } ================================================ FILE: _examples/routing/hello-world/main.go ================================================ package main import ( "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/middleware/logger" "github.com/kataras/iris/v12/middleware/recover" ) func main() { app := iris.New() app.Logger().SetLevel("debug") // Optionally, add two builtin handlers // that can recover from any http-relative panics // and log the requests to the terminal. app.Use(recover.New()) app.Use(logger.New()) // Method: GET // Resource: http://localhost:8080 app.Handle("GET", "/", func(ctx iris.Context) { ctx.HTML("

Welcome

") }) // same as app.Handle("GET", "/ping", [...]) // Method: GET // Resource: http://localhost:8080/ping app.Get("/ping", func(ctx iris.Context) { ctx.WriteString("pong") }) // Method: GET // Resource: http://localhost:8080/hello app.Get("/hello", func(ctx iris.Context) { ctx.JSON(iris.Map{"message": "Hello Iris!"}) }) // http://localhost:8080 // http://localhost:8080/ping // http://localhost:8080/hello app.Listen(":8080") } ================================================ FILE: _examples/routing/http-errors/main.go ================================================ package main import ( "github.com/kataras/iris/v12" ) // See _examples/routing/http-wire-errors as well. func main() { app := iris.New() // Catch a specific error code. app.OnErrorCode(iris.StatusInternalServerError, func(ctx iris.Context) { ctx.HTML("Message: " + ctx.Values().GetString("message") + "") }) // Catch all error codes [app.OnAnyErrorCode...] app.Get("/", func(ctx iris.Context) { ctx.HTML(`Click here to pretend an HTTP error`) }) app.Get("/my500", func(ctx iris.Context) { ctx.Values().Set("message", "this is the error message") ctx.StatusCode(500) }) app.Get("/u/{firstname:alphabetical}", func(ctx iris.Context) { ctx.Writef("Hello %s", ctx.Params().Get("firstname")) }) // Read more at: https://github.com/kataras/iris/issues/1335 app.Get("/product-problem", problemExample) app.Get("/product-error", func(ctx iris.Context) { ctx.Writef("explain the error") }) // http://localhost:8080 // http://localhost:8080/my500 // http://localhost:8080/u/gerasimos // http://localhost:8080/product-problem app.Listen(":8080") } func newProductProblem(productName, detail string) iris.Problem { return iris.NewProblem(). // The type URI, if relative it automatically convert to absolute. Type("/product-error"). // The title, if empty then it gets it from the status code. Title("Product validation problem"). // Any optional details. Detail(detail). // The status error code, required. Status(iris.StatusBadRequest). // Any custom key-value pair. Key("productName", productName) // Optional cause of the problem, chain of Problems. // Cause(iris.NewProblem().Type("/error").Title("cause of the problem").Status(400)) } func problemExample(ctx iris.Context) { /* p := iris.NewProblem(). Type("/validation-error"). Title("Your request parameters didn't validate"). Detail("Optional details about the error."). Status(iris.StatusBadRequest). Key("customField1", customValue1) Key("customField2", customValue2) ctx.Problem(p) // OR ctx.Problem(iris.Problem{ "type": "/validation-error", "title": "Your request parameters didn't validate", "detail": "Optional details about the error.", "status": iris.StatusBadRequest, "customField1": customValue1, "customField2": customValue2, }) // OR */ // Response like JSON but with indent of " " and // content type of "application/problem+json" ctx.Problem(newProductProblem("product name", "problem error details"), iris.ProblemOptions{ // Optional JSON renderer settings. JSON: iris.JSON{ Indent: " ", }, // OR // Render as XML: // // RenderXML: true, // XML: iris.XML{Indent: " "}, // and ctx.StatusCode(200) to see the result on browser as a user. // // The below `RetryAfter` field sets the "Retry-After" response header. // // Can accept: // time.Time for HTTP-Date, // time.Duration, int64, float64, int for seconds // or string for date or duration. // Examples: // time.Now().Add(5 * time.Minute), // 300 * time.Second, // "5m", // RetryAfter: 300, // A function that, if specified, can dynamically set // retry-after based on the request. Useful for ProblemOptions reusability. // Overrides the RetryAfter field. // // RetryAfterFunc: func(iris.Context) any { [...] } }) } ================================================ FILE: _examples/routing/http-errors/reset-body/main.go ================================================ package main import ( "fmt" "github.com/kataras/iris/v12" ) func main() { app := newApp() app.Listen(":8080") } func newApp() *iris.Application { app := iris.New() app.Use(iris.Compression) app.OnAnyErrorCode(onErrorCode) app.Get("/", handler) app.Configure(iris.WithResetOnFireErrorCode) return app } // This is the default error handler Iris uses for any error codes. func onErrorCode(ctx iris.Context) { if err := ctx.GetErr(); err != nil { ctx.WriteString(err.Error()) } else { ctx.WriteString(iris.StatusText(ctx.GetStatusCode())) } } func handler(ctx iris.Context) { ctx.Record() ctx.WriteString("This should NOT be written") // [....something bad happened after we "write"] err := fmt.Errorf("custom error") ctx.StopWithError(iris.StatusBadRequest, err) } ================================================ FILE: _examples/routing/http-errors/reset-body/main_test.go ================================================ package main import ( "testing" "github.com/kataras/iris/v12/httptest" ) func TestResetCompressionAndFireError(t *testing.T) { // #1569 app := newApp() e := httptest.New(t, app) e.GET("/").Expect().Status(httptest.StatusBadRequest).Body().IsEqual("custom error") } ================================================ FILE: _examples/routing/http-wire-errors/main.go ================================================ package main import ( "fmt" "github.com/kataras/iris/v12" // IMPORTANT, import this sub-package. // Note tht it does NOT break compatibility with the // standard "errors" package as the New, // Is, As, Unwrap functions are aliases to the standard package. "github.com/kataras/iris/v12/x/errors" ) // Optionally, register custom error codes. // // The default list of error code names: // errors.Cancelled // errors.Unknown // errors.InvalidArgument // errors.DeadlineExceeded // errors.NotFound // errors.AlreadyExists // errors.PermissionDenied // errors.Unauthenticated // errors.ResourceExhausted // errors.FailedPrecondition // errors.Aborted // errors.OutOfRange // errors.Unimplemented // errors.Internal // errors.Unavailable // errors.DataLoss var ( Custom = errors.Register("CUSTOM_CANONICAL_ERROR_NAME", iris.StatusBadRequest) ) func main() { app := iris.New() // Custom error code name. app.Get("/custom", fireCustomErrorCodeName) // Send a simple 400 request with message and an error // or with more details and data. app.Post("/invalid_argument", fireInvalidArgument) // Compatibility with the iris.Problem type (and any other custom type). app.Get("/problem", fireErrorWithProblem) app.Listen(":8080") } func fireCustomErrorCodeName(ctx iris.Context) { Custom.Details(ctx, "message", fmt.Sprintf("details with arguments: %s", "an argument")) } func fireInvalidArgument(ctx iris.Context) { var req = struct { Username string `json:"username"` }{} if err := ctx.ReadJSON(&req); err != nil { errors.InvalidArgument.Err(ctx, err) return } ctx.WriteString(req.Username) // Other examples: errors.InvalidArgument/NotFound/Internal and e.t.c. // .Message(ctx, "message %s", "optional argument") // .Details(ctx, "message", "details %s", "optional details argument") // .Data(ctx, "message", anyTypeOfValue) // .DataWithDetails(ctx, "unable to read the body", "malformed json", iris.Map{"custom": "data of any type"}) // .Log(ctx, "message %s", "optional argument") // .LogErr(ctx, err) } func fireErrorWithProblem(ctx iris.Context) { myCondition := true if myCondition { problem := iris.NewProblem(). // The type URI, if relative it automatically convert to absolute. Type("/product-error"). // The title, if empty then it gets it from the status code. Title("Product validation problem"). // Any optional details. Detail("details about the product error"). // The status error code of the problem, can be optional here. // Status(iris.StatusBadRequest). // Any custom key-value pair. Key("product_name", "the product name") errors.InvalidArgument.Data(ctx, "unable to process the request", problem) return /* Prints to the client: { "http_error_code": { "canonical_name": "INVALID_ARGUMENT", "status": 400 }, "message": "unable to process the request", "data": { "detail": "details about the product error", "product_name": "the product name", "title": "Product validation problem", "type": "/product-error" } } */ } } ================================================ FILE: _examples/routing/http-wire-errors/service/main.go ================================================ package main import ( "context" "fmt" "strings" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/x/errors" "github.com/kataras/iris/v12/x/errors/validation" "github.com/kataras/iris/v12/x/pagination" ) func main() { app := iris.New() // Create a new service and pass it to the handlers. service := new(myService) app.Post("/", errors.Intercept(afterServiceCallButBeforeDataSent), createHandler(service)) // OR: errors.CreateHandler(service.Create) app.Get("/", listAllHandler(service)) // OR errors.Handler(service.ListAll, errors.Value(ListRequest{})) app.Post("/page", listHandler(service)) // OR: errors.ListHandler(service.ListPaginated) app.Delete("/{id:string}", deleteHandler(service)) // OR: errors.NoContentOrNotModifiedHandler(service.DeleteWithFeedback, errors.PathParam[string]("id")) app.Listen(":8080") } func createHandler(service *myService) iris.Handler { return func(ctx iris.Context) { // What it does? // 1. Reads the request body and binds it to the CreateRequest struct. // 2. Calls the service.Create function with the given request body. // 3. If the service.Create returns an error, it sends an appropriate error response to the client. // 4. If the service.Create returns a response, it sets the status code to 201 (Created) and sends the response as a JSON payload to the client. // // Useful for create operations. errors.Create(ctx, service.Create) } } func listAllHandler(service *myService) iris.Handler { return func(ctx iris.Context) { // What it does? // 1. If the 3rd variadic (optional) parameter is empty (not our case here), it reads the request body and binds it to the ListRequest struct, // otherwise (our case) it calls the service.ListAll function directly with the given input parameter (empty ListRequest struct value in our case). // 2. Calls the service.ListAll function with the ListRequest value. // 3. If the service.ListAll returns an error, it sends an appropriate error response to the client. // 4. If the service.ListAll returns a response, it sets the status code to 200 (OK) and sends the response as a JSON payload to the client. // // Useful for get single, fetch multiple and search operations. errors.OK(ctx, service.ListAll, ListRequest{}) } } func listHandler(service *myService) iris.Handler { return func(ctx iris.Context) { errors.List(ctx, service.ListPaginated) } } func deleteHandler(service *myService) iris.Handler { return func(ctx iris.Context) { id := ctx.Params().Get("id") // What it does? // 1. Calls the service.DeleteWithFeedback function with the given input parameter. // 2. If the service.DeleteWithFeedback returns an error, it sends an appropriate error response to the client. // 3.If the service.DeleteWithFeedback doesn't return an error then it sets the status code to 204 (No Content) and // sends the response as a JSON payload to the client. // errors.NoContent(ctx, service.Delete, id) // OR: // 1. Calls the service.DeleteWithFeedback function with the given input parameter. // 2. If the service.DeleteWithFeedback returns an error, it sends an appropriate error response to the client. // 3. If the service.DeleteWithFeedback returns true, it sets the status code to 204 (No Content). // 4. If the service.DeleteWithFeedback returns false, it sets the status code to 304 (Not Modified). // // Useful for update and delete operations. errors.NoContentOrNotModified(ctx, service.DeleteWithFeedback, id) } } type ( myService struct{} CreateRequest struct { Fullname string `json:"fullname"` Age int `json:"age"` Hobbies []string `json:"hobbies"` } CreateResponse struct { ID string `json:"id"` Firstname string `json:"firstname"` Lastname string `json:"lastname"` Age int `json:"age"` Hobbies []string `json:"hobbies"` } ) // HandleRequest implements the errors.RequestHandler interface. // It validates the request body and returns an error if the request body is invalid. // You can also alter the "r" CreateRequest before calling the service method, // e.g. give a default value to a field if it's empty or set an ID based on a path parameter. // OR // Custom function per route: // // r.Post("/", errors.Validation(validateCreateRequest), createHandler(service)) // [more code here...] // // func validateCreateRequest(ctx iris.Context, r *CreateRequest) error { // return validation.Join( // validation.String("fullname", r.Fullname).NotEmpty().Fullname().Length(3, 50), // validation.Number("age", r.Age).InRange(18, 130), // validation.Slice("hobbies", r.Hobbies).Length(1, 10), // ) // } func (r *CreateRequest) HandleRequest(ctx iris.Context) error { // To pass custom validation functions: // return validation.Join( // validation.String("fullname", r.Fullname).Func(customStringFuncHere), // OR // validation.Field("any_field", r.AnyFieldValue).Func(customAnyFuncHere)) return validation.Join( validation.String("fullname", r.Fullname).Fullname().Length(3, 50), validation.Number("age", r.Age).InRange(18, 130), validation.Slice("hobbies", r.Hobbies).Length(1, 10), ) /* Example Output: { "http_error_code": { "canonical_name": "INVALID_ARGUMENT", "status": 400 }, "message": "validation failure", "details": "fields were invalid", "validation": [ { "field": "fullname", "value": "", "reason": "must not be empty, must contain first and last name, must be between 3 and 50 characters" }, { "field": "age", "value": 0, "reason": "must be in range of [18, 130]" }, { "field": "hobbies", "value": null, "reason": "must be between 1 and 10 elements" } ] } */ } /* // HandleResponse implements the errors.ResponseHandler interface. func (r *CreateRequest) HandleResponse(ctx iris.Context, resp *CreateResponse) error { fmt.Printf("request got: %+v\nresponse sent: %#+v\n", r, resp) return nil // fmt.Errorf("let's fire an internal server error just for the shake of the example") // return nil to continue. } */ func afterServiceCallButBeforeDataSent(ctx iris.Context, req CreateRequest, resp *CreateResponse) error { fmt.Printf("intercept: request got: %+v\nresponse sent: %#+v\n", req, resp) return nil } func (s *myService) Create(ctx context.Context, in CreateRequest) (CreateResponse, error) { arr := strings.Split(in.Fullname, " ") firstname, lastname := arr[0], arr[1] id := "test_id" resp := CreateResponse{ ID: id, Firstname: firstname, Lastname: lastname, Age: in.Age, Hobbies: in.Hobbies, } return resp, nil // , errors.New("create: test error") } type ListRequest struct { } func (s *myService) ListAll(ctx context.Context, in ListRequest) ([]CreateResponse, error) { resp := []CreateResponse{ { ID: "test-id-1", Firstname: "test first name 1", Lastname: "test last name 1", }, { ID: "test-id-2", Firstname: "test first name 2", Lastname: "test last name 2", }, { ID: "test-id-3", Firstname: "test first name 3", Lastname: "test last name 3", }, } return resp, nil //, errors.New("list all: test error") } type ListFilter struct { Firstname string `json:"firstname"` } func (s *myService) ListPaginated(ctx context.Context, opts pagination.ListOptions, filter ListFilter) ([]CreateResponse, int /* any number type */, error) { all, err := s.ListAll(ctx, ListRequest{}) if err != nil { return nil, 0, err } filteredResp := make([]CreateResponse, 0) for _, v := range all { if strings.Contains(v.Firstname, filter.Firstname) { filteredResp = append(filteredResp, v) } if len(filteredResp) == opts.GetLimit() { break } } return filteredResp, len(all), nil // errors.New("list paginated: test error") } func (s *myService) GetByID(ctx context.Context, id string) (CreateResponse, error) { return CreateResponse{Firstname: "Gerasimos"}, nil // errors.New("get by id: test error") } func (s *myService) Delete(ctx context.Context, id string) error { return nil // errors.New("delete: test error") } func (s *myService) Update(ctx context.Context, req CreateRequest) (bool, error) { return true, nil // false, errors.New("update: test error") } func (s *myService) DeleteWithFeedback(ctx context.Context, id string) (bool, error) { return true, nil // false, errors.New("delete: test error") } ================================================ FILE: _examples/routing/intelligence/main.go ================================================ package main import "github.com/kataras/iris/v12" func main() { app := iris.New() app.Get("/home", handler) app.Get("/contact", handler) app.Get("/contract", handler) // http://localhost:8080/home // http://localhost:8080/hom // // http://localhost:8080/contact // http://localhost:8080/cont // // http://localhost:8080/contract // http://localhost:8080/contr app.Listen(":8080", iris.WithPathIntelligence) } func handler(ctx iris.Context) { ctx.Writef("Path: %s", ctx.Path()) } ================================================ FILE: _examples/routing/intelligence/manual/main.go ================================================ package main import ( "fmt" "github.com/kataras/iris/v12" ) func main() { app := iris.New() app.OnErrorCode(iris.StatusNotFound, notFound) // [register some routes...] app.Get("/home", handler) app.Get("/news", handler) app.Get("/news/politics", handler) app.Get("/user/profile", handler) app.Get("/user", handler) app.Get("/newspaper", handler) app.Get("/user/{id}", handler) app.Listen(":8080") } func notFound(ctx iris.Context) { suggestPaths := ctx.FindClosest(3) if len(suggestPaths) == 0 { ctx.WriteString("404 not found") return } ctx.HTML("Did you mean?
    ") for _, s := range suggestPaths { ctx.HTML(fmt.Sprintf(`
  • %s
  • `, s, s)) } ctx.HTML("
") } func handler(ctx iris.Context) { ctx.Writef("Path: %s", ctx.Path()) } ================================================ FILE: _examples/routing/macros/main.go ================================================ // Package main shows how you can register a custom parameter type and macro functions that belongs to it. // See _examples/routing/dynamic-path/main.go first. package main import ( "fmt" "reflect" "sort" "strings" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/hero" ) func main() { app := iris.New() app.Logger().SetLevel("debug") app.Macros().Register("slice", "", []string{}, false, true, func(paramValue string) (any, bool) { return strings.Split(paramValue, "/"), true }).RegisterFunc("contains", func(expectedItems []string) func(paramValue []string) bool { sort.Strings(expectedItems) return func(paramValue []string) bool { if len(paramValue) != len(expectedItems) { return false } sort.Strings(paramValue) for i := 0; i < len(paramValue); i++ { if paramValue[i] != expectedItems[i] { return false } } return true } }) // In order to use your new param type inside MVC controller's function input argument or a hero function input argument // you have to tell the Iris what type it is, the `ValueRaw` of the parameter is the same type // as you defined it above with the func(paramValue string) (any, bool). // The new value and its type(from string to your new custom type) it is stored only once now, // you don't have to do any conversions for simple cases like this. context.ParamResolvers[reflect.TypeOf([]string{})] = func(paramIndex int) any { return func(ctx iris.Context) []string { // When you want to retrieve a parameter with a value type that it is not supported by-default, such as ctx.Params().GetInt // then you can use the `GetEntry` or `GetEntryAt` and cast its underline `ValueRaw` to the desired type. // The type should be the same as the macro's evaluator function (last argument on the Macros#Register) return value. return ctx.Params().GetEntryAt(paramIndex).ValueRaw.([]string) } } /* http://localhost:8080/test_slice_hero/myvaluei1/myavlue2 -> myparam's value (a trailing path parameter type) is: []string{"myvalue1", "myavlue2"} */ app.Get("/test_slice_hero/{myparam:slice}", hero.Handler(func(myparam []string) string { return fmt.Sprintf("myparam's value (a trailing path parameter type) is: %#v\n", myparam) })) /* http://localhost:8080/test_slice_contains/notcontains1/value2 -> (404) Not Found http://localhost:8080/test_slice_contains/value1/value2 -> myparam's value (a trailing path parameter type) is: []string{"value1", "value2"} */ app.Get("/test_slice_contains/{myparam:slice contains([value1,value2])}", func(ctx iris.Context) { // When it is not a builtin function available to retrieve your value with the type you want, such as ctx.Params().GetInt // then you can use the `GetEntry.ValueRaw` to get the real value, which is set-ed by your macro above. myparam := ctx.Params().GetEntry("myparam").ValueRaw.([]string) ctx.Writef("myparam's value (a trailing path parameter type) is: %#v\n", myparam) }) app.Listen(":8080") } ================================================ FILE: _examples/routing/main.go ================================================ package main import ( "github.com/kataras/iris/v12" ) /* Read: "overview" "basic" "dynamic-path" and "reverse" examples if you want to release iris' real power. */ const maxBodySize = 1 << 20 const notFoundHTML = "

custom http error page

" func registerErrors(app *iris.Application) { // set a custom 404 handler app.OnErrorCode(iris.StatusNotFound, func(ctx iris.Context) { ctx.HTML(notFoundHTML) }) } func registerGamesRoutes(app *iris.Application) { gamesMiddleware := func(ctx iris.Context) { ctx.Next() } // party is just a group of routes with the same prefix // and middleware, i.e: "/games" and gamesMiddleware. games := app.Party("/games", gamesMiddleware) { // braces are optional of course, it's just a style of code // "GET" method games.Get("/{gameID:uint64}/clans", h) games.Get("/{gameID:uint64}/clans/clan/{clanPublicID:uint64}", h) games.Get("/{gameID:uint64}/clans/search", h) // "PUT" method games.Put("/{gameID:uint64}/players/{clanPublicID:uint64}", h) games.Put("/{gameID:uint64}/clans/clan/{clanPublicID:uint64}", h) // remember: "clanPublicID" should not be changed to other routes with the same prefix. // "POST" method games.Post("/{gameID:uint64}/clans", h) games.Post("/{gameID:uint64}/players", h) games.Post("/{gameID:uint64}/clans/{clanPublicID:uint64}/leave", h) games.Post("/{gameID:uint64}/clans/{clanPublicID:uint64}/memberships/application", h) games.Post("/{gameID:uint64}/clans/{clanPublicID:uint64}/memberships/application/{action}", h) // {action} == {action:string} games.Post("/{gameID:uint64}/clans/{clanPublicID:uint64}/memberships/invitation", h) games.Post("/{gameID:uint64}/clans/{clanPublicID:uint64}/memberships/invitation/{action}", h) games.Post("/{gameID:uint64}/clans/{clanPublicID:uint64}/memberships/delete", h) games.Post("/{gameID:uint64}/clans/{clanPublicID:uint64}/memberships/promote", h) games.Post("/{gameID:uint64}/clans/{clanPublicID:uint64}/memberships/demote", h) gamesCh := games.Party("/challenge") { // games/challenge gamesCh.Get("/", h) gamesChBeginner := gamesCh.Party("/beginner") { // games/challenge/beginner/start gamesChBeginner.Get("/start", h) levelBeginner := gamesChBeginner.Party("/level") { // games/challenge/beginner/level/first levelBeginner.Get("/first", h) } } gamesChIntermediate := gamesCh.Party("/intermediate") { // games/challenge/intermediate gamesChIntermediate.Get("/", h) // games/challenge/intermediate/start gamesChIntermediate.Get("/start", h) } } } } func registerSubdomains(app *iris.Application) { mysubdomain := app.Subdomain("mysubdomain") // http://mysubdomain.myhost.com mysubdomain.Get("/", h) willdcardSubdomain := app.WildcardSubdomain() willdcardSubdomain.Get("/", h) willdcardSubdomain.Party("/party").Get("/", h) } func newApp() *iris.Application { app := iris.New() registerErrors(app) registerGamesRoutes(app) registerSubdomains(app) app.Handle("GET", "/healthcheck", h) // "POST" method // this handler reads raw body from the client/request // and sends back the same body // remember, we have limit to that body in order // to protect ourselves from "over heating". app.Post("/", iris.LimitRequestBodySize(maxBodySize), func(ctx iris.Context) { // get request body b, err := ctx.GetBody() // if is larger then send a bad request status if err != nil { ctx.StatusCode(iris.StatusBadRequest) ctx.WriteString(err.Error()) return } // send back the post body ctx.Write(b) }) app.HandleMany("POST PUT", "/postvalue", func(ctx iris.Context) { name := ctx.PostValueDefault("name", "iris") headervale := ctx.GetHeader("headername") ctx.Writef("Hello %s | %s", name, headervale) }) return app } func h(ctx iris.Context) { method := ctx.Method() // the http method requested a server's resource. subdomain := ctx.Subdomain() // the subdomain, if any. // the request path (without scheme and host). path := ctx.Path() // how to get all parameters, if we don't know // the names: paramsLen := ctx.Params().Len() ctx.Params().Visit(func(name string, value string) { ctx.Writef("%s = %s\n", name, value) }) ctx.Writef("Info\n\n") ctx.Writef("Method: %s\nSubdomain: %s\nPath: %s\nParameters length: %d", method, subdomain, path, paramsLen) } func main() { app := newApp() app.Logger().SetLevel("debug") /* // GET http://localhost:8080/healthcheck http://localhost:8080/games/42/clans http://localhost:8080/games/42/clans/clan/93 http://localhost:8080/games/42/clans/search http://mysubdomain.localhost:8080/ // PUT http://localhost:8080/postvalue http://localhost:8080/games/42/players/93 http://localhost:8080/games/42/clans/clan/93 // POST http://localhost:8080/ http://localhost:8080/postvalue http://localhost:8080/games/42/clans http://localhost:8080/games/42/players http://localhost:8080/games/42/clans/93/leave http://localhost:8080/games/42/clans/93/memberships/application http://localhost:8080/games/42/clans/93/memberships/application/anystring http://localhost:8080/games/42/clans/93/memberships/invitation http://localhost:8080/games/42/clans/93/memberships/invitation/anystring http://localhost:8080/games/42/clans/93/memberships/delete http://localhost:8080/games/42/clans/93/memberships/promote http://localhost:8080/games/42/clans/93/memberships/demote // FIRE NOT FOUND http://localhost:8080/coudlntfound */ app.Listen(":8080") } ================================================ FILE: _examples/routing/main_test.go ================================================ package main import ( "strconv" "strings" "testing" "github.com/kataras/iris/v12/httptest" ) func calculatePathAndResponse(method, subdomain, path string, paramKeyValue ...string) (string, string) { paramsLen := 0 if l := len(paramKeyValue); l >= 2 { paramsLen = len(paramKeyValue) / 2 } paramsInfo := "" if paramsLen > 0 { for i := 0; i < len(paramKeyValue); i++ { paramKey := paramKeyValue[i] i++ if i >= len(paramKeyValue) { panic("paramKeyValue should be align with path parameters {} and must be placed in order") } paramValue := paramKeyValue[i] paramsInfo += paramKey + " = " + paramValue + "\n" beginParam := strings.IndexByte(path, '{') endParam := strings.IndexByte(path, '}') if beginParam == -1 || endParam == -1 { panic("something wrong with parameters, please define them in order") } path = path[:beginParam] + paramValue + path[endParam+1:] } } return path, paramsInfo + `Info Method: ` + method + ` Subdomain: ` + subdomain + ` Path: ` + path + ` Parameters length: ` + strconv.Itoa(paramsLen) } type troute struct { method, subdomain, path string status int expectedBody string contentType string } func newTroute(method, subdomain, path string, status int, paramKeyValue ...string) troute { finalPath, expectedBody := calculatePathAndResponse(method, subdomain, path, paramKeyValue...) contentType := "text/plain; charset=UTF-8" if status == httptest.StatusNotFound { expectedBody = notFoundHTML contentType = "text/html; charset=UTF-8" } return troute{ contentType: contentType, method: method, subdomain: subdomain, path: finalPath, status: status, expectedBody: expectedBody, } } func TestRouting(t *testing.T) { app := newApp() e := httptest.New(t, app) tests := []troute{ // GET newTroute("GET", "", "/healthcheck", httptest.StatusOK), newTroute("GET", "", "/games/{gameID}/clans", httptest.StatusOK, "gameID", "42"), newTroute("GET", "", "/games/{gameID}/clans/clan/{clanPublicID}", httptest.StatusOK, "gameID", "42", "clanPublicID", "93"), newTroute("GET", "", "/games/{gameID}/clans/search", httptest.StatusOK, "gameID", "42"), newTroute("GET", "", "/games/challenge", httptest.StatusOK), newTroute("GET", "", "/games/challenge/beginner/start", httptest.StatusOK), newTroute("GET", "", "/games/challenge/beginner/level/first", httptest.StatusOK), newTroute("GET", "", "/games/challenge/intermediate", httptest.StatusOK), newTroute("GET", "", "/games/challenge/intermediate/start", httptest.StatusOK), newTroute("GET", "mysubdomain", "/", httptest.StatusOK), newTroute("GET", "mywildcardsubdomain", "/", httptest.StatusOK), newTroute("GET", "mywildcardsubdomain", "/party", httptest.StatusOK), // PUT newTroute("PUT", "", "/games/{gameID}/players/{clanPublicID}", httptest.StatusOK, "gameID", "42", "clanPublicID", "93"), newTroute("PUT", "", "/games/{gameID}/clans/clan/{clanPublicID}", httptest.StatusOK, "gameID", "42", "clanPublicID", "93"), // POST newTroute("POST", "", "/games/{gameID}/clans", httptest.StatusOK, "gameID", "42"), newTroute("POST", "", "/games/{gameID}/players", httptest.StatusOK, "gameID", "42"), newTroute("POST", "", "/games/{gameID}/clans/{clanPublicID}/leave", httptest.StatusOK, "gameID", "42", "clanPublicID", "93"), newTroute("POST", "", "/games/{gameID}/clans/{clanPublicID}/memberships/application", httptest.StatusOK, "gameID", "42", "clanPublicID", "93"), newTroute("POST", "", "/games/{gameID}/clans/{clanPublicID}/memberships/application/{action}", httptest.StatusOK, "gameID", "42", "clanPublicID", "93", "action", "somethinghere"), newTroute("POST", "", "/games/{gameID}/clans/{clanPublicID}/memberships/invitation", httptest.StatusOK, "gameID", "42", "clanPublicID", "93"), newTroute("POST", "", "/games/{gameID}/clans/{clanPublicID}/memberships/invitation/{action}", httptest.StatusOK, "gameID", "42", "clanPublicID", "93", "action", "somethinghere"), newTroute("POST", "", "/games/{gameID}/clans/{clanPublicID}/memberships/delete", httptest.StatusOK, "gameID", "42", "clanPublicID", "93"), newTroute("POST", "", "/games/{gameID}/clans/{clanPublicID}/memberships/promote", httptest.StatusOK, "gameID", "42", "clanPublicID", "93"), newTroute("POST", "", "/games/{gameID}/clans/{clanPublicID}/memberships/demote", httptest.StatusOK, "gameID", "42", "clanPublicID", "93"), // POST: / will be tested alone // custom not found newTroute("GET", "", "/notfound", httptest.StatusNotFound), newTroute("POST", "", "/notfound2", httptest.StatusNotFound), newTroute("PUT", "", "/notfound3", httptest.StatusNotFound), newTroute("GET", "mysubdomain", "/notfound42", httptest.StatusNotFound), } for _, tt := range tests { et := e.Request(tt.method, tt.path) if tt.subdomain != "" { et.WithURL("http://" + tt.subdomain + ".localhost:8080") } et.Expect().Status(tt.status).Body().IsEqual(tt.expectedBody) } // test POST "/" limit data and post data return // test with small body e.POST("/").WithBytes([]byte("ok")).Expect().Status(httptest.StatusOK).Body().IsEqual("ok") // test with equal to max body size limit bsent := make([]byte, maxBodySize, maxBodySize) e.POST("/").WithBytes(bsent).Expect().Status(httptest.StatusOK).Body().Length().Equal(len(bsent)) // test with larger body sent and wait for the custom response largerBSent := make([]byte, maxBodySize+1, maxBodySize+1) e.POST("/").WithBytes(largerBSent).Expect().Status(httptest.StatusBadRequest).Body().IsEqual("http: request body too large") // test the post value (both post and put) and headers. e.PUT("/postvalue").WithFormField("name", "test_put"). WithHeader("headername", "headervalue_put").Expect(). Status(httptest.StatusOK).Body().IsEqual("Hello test_put | headervalue_put") e.POST("/postvalue").WithFormField("name", "test_post"). WithHeader("headername", "headervalue_post").Expect(). Status(httptest.StatusOK).Body().IsEqual("Hello test_post | headervalue_post") } ================================================ FILE: _examples/routing/overview/main.go ================================================ package main import ( "os" "time" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/middleware/logger" ) func main() { app := iris.New() // Set Logger level to "debug", // see your terminal and the created file. app.Logger().SetLevel("debug") // Write logs to a file too. f := newLogFile() defer f.Close() app.Logger().AddOutput(f) // Register a request logger middleware to the application. app.Use(logger.New()) // GET: http://localhost:8080 app.Get("/", info) // GET: http://localhost:8080/profile/anyusername // // Want to use a custom regex expression instead? // Easy: app.Get("/profile/{username:string regexp(^[a-zA-Z ]+$)}") app.Get("/profile/{username:string}", info) // If parameter type is missing then it's string which accepts anything, // i.e: /{paramname} it's exactly the same as /{paramname:string}. // The below is exactly the same as // {username:string} // // GET: http://localhost:8080/profile/anyusername/backups/any/number/of/paths/here app.Get("/profile/{username}/backups/{filepath:path}", info) // Favicon // GET: http://localhost:8080/favicon.ico app.Favicon("./public/images/favicon.ico") // Static assets // GET: http://localhost:8080/assets/css/main.css // maps to ./public/assets/css/main.css file at system location. app.HandleDir("/assets", iris.Dir("./public/assets")) /* OR // GET: http://localhost:8080/css/main.css // maps to ./public/assets/css/main.css file at system location. app.HandleDir("/css", iris.Dir("./public/assets/css")) // GET: http://localhost:8080/css/bootstrap.min.css // maps to ./public/assets/css/bootstrap.min.css file at system location. app.HandleDir("/css", iris.Dir("./public/assets/css")) */ // Grouping usersRoutes := app.Party("/users") // GET: http://localhost:8080/users/help usersRoutes.Get("/help", func(ctx iris.Context) { ctx.Writef("GET / -- fetch all users\n") ctx.Writef("GET /$ID -- fetch a user by id\n") ctx.Writef("POST / -- create new user\n") ctx.Writef("PUT /$ID -- update an existing user\n") ctx.Writef("DELETE /$ID -- delete an existing user\n") }) // GET: http://localhost:8080/users usersRoutes.Get("/", func(ctx iris.Context) { ctx.Writef("get all users") }) // GET: http://localhost:8080/users/42 // **/users/42 and /users/help works after iris version 7.0.5** usersRoutes.Get("/{id:uint64}", func(ctx iris.Context) { id, _ := ctx.Params().GetUint64("id") ctx.Writef("get user by id: %d", id) }) // POST: http://localhost:8080/users usersRoutes.Post("/", func(ctx iris.Context) { username, password := ctx.PostValue("username"), ctx.PostValue("password") ctx.Writef("create user for username= %s and password= %s", username, password) }) // PUT: http://localhost:8080/users usersRoutes.Put("/{id:uint64}", func(ctx iris.Context) { id, _ := ctx.Params().GetUint64("id") // or .Get to get its string represatantion. username := ctx.PostValue("username") ctx.Writef("update user for id= %d and new username= %s", id, username) }) // DELETE: http://localhost:8080/users/42 usersRoutes.Delete("/{id:uint64}", func(ctx iris.Context) { id, _ := ctx.Params().GetUint64("id") ctx.Writef("delete user by id: %d", id) }).Describe("deletes a user") // Subdomains, depends on the host, you have to edit the hosts or nginx/caddy's configuration if you use them. // // See more subdomains examples at _examples/routing/subdomains folder. adminRoutes := app.Party("admin.") // GET: http://admin.localhost:8080 adminRoutes.Get("/", info) // GET: http://admin.localhost:8080/settings adminRoutes.Get("/settings", info) // Wildcard/dynamic subdomain dynamicSubdomainRoutes := app.WildcardSubdomain() // GET: http://any_thing_here.localhost:8080 dynamicSubdomainRoutes.Get("/", info) app.Delete("/something", func(ctx iris.Context) { name := ctx.URLParam("name") ctx.WriteString(name) }) app.None("/secret", privateHandler) app.Get("/public", execPrivateHandler) // GET: http://localhost:8080/ // GET: http://localhost:8080/profile/anyusername // GET: http://localhost:8080/profile/anyusername/backups/any/number/of/paths/here // GET: http://localhost:8080/users/help // GET: http://localhost:8080/users // GET: http://localhost:8080/users/42 // POST: http://localhost:8080/users // PUT: http://localhost:8080/users // DELETE: http://localhost:8080/users/42 // DELETE: http://localhost:8080/something?name=iris // GET: http://admin.localhost:8080 // GET: http://admin.localhost:8080/settings // GET: http://any_thing_here.localhost:8080 app.Listen(":8080") } func privateHandler(ctx iris.Context) { ctx.WriteString(`This can only be executed programmatically through server's another route: ctx.Exec(iris.MethodNone, "/secret")`) } func execPrivateHandler(ctx iris.Context) { ctx.Exec(iris.MethodNone, "/secret") } func info(ctx iris.Context) { method := ctx.Method() // the http method requested a server's resource. subdomain := ctx.Subdomain() // the subdomain, if any. // the request path (without scheme and host). path := ctx.Path() // how to get all parameters, if we don't know // the names: paramsLen := ctx.Params().Len() ctx.Params().Visit(func(name string, value string) { ctx.Writef("%s = %s\n", name, value) }) ctx.Writef("\nInfo\n\n") ctx.Writef("Method: %s\nSubdomain: %s\nPath: %s\nParameters length: %d", method, subdomain, path, paramsLen) } // get a filename based on the date, file logs works that way the most times // but these are just a sugar. func todayFilename() string { today := time.Now().Format("Jan 02 2006") return today + ".txt" } func newLogFile() *os.File { filename := todayFilename() // open an output file, this will append to the today's file if server restarted. f, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) if err != nil { panic(err) } return f } ================================================ FILE: _examples/routing/overview/public/assets/css/main.css ================================================ body { background-color: black; } ================================================ FILE: _examples/routing/overview-2/main.go ================================================ package main import ( "fmt" "github.com/kataras/iris/v12" ) // User is just a bindable object structure. type User struct { Username string `json:"username"` Firstname string `json:"firstname"` Lastname string `json:"lastname"` City string `json:"city"` Age int `json:"age"` } func main() { app := iris.New() app.Logger().SetLevel("debug") // app.Logger().SetLevel("disable") to disable the logger. // Define templates using the std html/template engine. // Parse and load all files inside "./views" folder with ".html" file extension. // Reload the templates on each request (development mode). app.RegisterView(iris.HTML("./views", ".html").Reload(true)) // Register custom handler for specific http errors. app.OnErrorCode(iris.StatusInternalServerError, func(ctx iris.Context) { // .Values are used to communicate between handlers, middleware. errMessage := ctx.Values().GetString("error") if errMessage != "" { ctx.Writef("Internal server error: %s", errMessage) return } ctx.Writef("(Unexpected) internal server error") }) app.Use(func(ctx iris.Context) { ctx.Application().Logger().Infof("Begin request for path: %s", ctx.Path()) ctx.Next() }) // app.Done(func(ctx iris.Context) {]}) // POST: scheme://mysubdomain.$domain.com/decode app.Subdomain("mysubdomain.").Post("/decode", func(ctx iris.Context) {}) // Method POST: http://localhost:8080/decode app.Post("/decode", func(ctx iris.Context) { var user User ctx.ReadJSON(&user) ctx.Writef("%s %s is %d years old and comes from %s", user.Firstname, user.Lastname, user.Age, user.City) }) // Method GET: http://localhost:8080/encode app.Get("/encode", func(ctx iris.Context) { doe := User{ Username: "Johndoe", Firstname: "John", Lastname: "Doe", City: "Neither FBI knows!!!", Age: 25, } ctx.JSON(doe) }) // Method GET: http://localhost:8080/profile/anytypeofstring app.Get("/profile/{username:string}", profileByUsername) usersRoutes := app.Party("/users", logThisMiddleware) { // Method GET: http://localhost:8080/users/42 usersRoutes.Get("/{id:int min(1)}", getUserByID) // Method POST: http://localhost:8080/users/create usersRoutes.Post("/create", createUser) } app.Get("/", func(ctx iris.Context) { ctx.HTML("
    ") for _, link := range []string{"/encode", "/profile/username", "/users/42"} { ctx.HTML(fmt.Sprintf(`
  • %s
  • `, link, link)) } ctx.HTML("
") }) // Listen for incoming HTTP/1.x & HTTP/2 clients on localhost port 8080. app.Listen(":8080", iris.WithCharset("utf-8")) } func logThisMiddleware(ctx iris.Context) { ctx.Application().Logger().Infof("Path: %s | IP: %s", ctx.Path(), ctx.RemoteAddr()) // .Next is required to move forward to the chain of handlers, // if missing then it stops the execution at this handler. ctx.Next() } func profileByUsername(ctx iris.Context) { // .Params are used to get dynamic path parameters. username := ctx.Params().Get("username") ctx.ViewData("Username", username) // renders "./views/user/profile.html" // with {{ .Username }} equals to the username dynamic path parameter. if err := ctx.View("user/profile.html"); err != nil { ctx.HTML(fmt.Sprintf("

%s

", err.Error())) return } } func getUserByID(ctx iris.Context) { userID := ctx.Params().Get("id") // Or convert directly using: .Values().GetInt/GetInt64 etc... // your own db fetch here instead of user :=... user := User{Username: "username" + userID} ctx.XML(user) } func createUser(ctx iris.Context) { var user User err := ctx.ReadForm(&user) if err != nil { ctx.Values().Set("error", "creating user, read and parse form failed. "+err.Error()) ctx.StatusCode(iris.StatusInternalServerError) return } // renders "./views/user/create_verification.html" // with {{ . }} equals to the User object, i.e {{ .Username }} , {{ .Firstname}} etc... ctx.ViewData("", user) if err := ctx.View("user/create_verification.html"); err != nil { ctx.HTML(fmt.Sprintf("

%s

", err.Error())) return } } ================================================ FILE: _examples/routing/overview-2/views/user/create_verification.html ================================================ Create verification

Create Verification

Username Firstname Lastname City Age
{{ .Username }} {{ .Firstname }} {{ .Lastname }} {{ .City }} {{ .Age }}
================================================ FILE: _examples/routing/overview-2/views/user/profile.html ================================================ Profile page

Profile

{{ .Username }} ================================================ FILE: _examples/routing/party-controller/go.mod ================================================ module github.com/kataras/iris/_examples/routing/party-controller go 1.25 require github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd require ( github.com/BurntSushi/toml v1.6.0 // indirect github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect github.com/CloudyKit/jet/v6 v6.3.1 // indirect github.com/Joker/jade v1.1.3 // indirect github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 // indirect github.com/andybalholm/brotli v1.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/fatih/structs v1.1.0 // indirect github.com/flosch/pongo2/v4 v4.0.2 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/iris-contrib/schema v0.0.6 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/kataras/blocks v0.0.8 // indirect github.com/kataras/golog v0.1.12 // indirect github.com/kataras/pio v0.0.13 // indirect github.com/kataras/sitemap v0.0.6 // indirect github.com/kataras/tunnel v0.0.4 // indirect github.com/klauspost/compress v1.18.2 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mailgun/raymond/v2 v2.0.48 // indirect github.com/mailru/easyjson v0.9.1 // indirect github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/schollz/closestmatch v2.1.0+incompatible // indirect github.com/sirupsen/logrus v1.8.1 // indirect github.com/stretchr/testify v1.11.1 // indirect github.com/tdewolff/minify/v2 v2.24.8 // indirect github.com/tdewolff/parse/v2 v2.8.5 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/yosssi/ace v0.0.5 // indirect golang.org/x/crypto v0.47.0 // indirect golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect golang.org/x/net v0.49.0 // indirect golang.org/x/sys v0.40.0 // indirect golang.org/x/text v0.33.0 // indirect golang.org/x/time v0.14.0 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: _examples/routing/party-controller/go.sum ================================================ github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= github.com/CloudyKit/jet/v6 v6.3.1 h1:6IAo5Cx21xrHVaR8zzXN5gJatKV/wO7Nf6bfCnCSbUw= github.com/CloudyKit/jet/v6 v6.3.1/go.mod h1:lf8ksdNsxZt7/yH/3n4vJQWA9RUq4wpaHtArHhGVMOw= github.com/Joker/hpp v1.0.0 h1:65+iuJYdRXv/XyN62C1uEmmOx3432rNG/rKlX6V7Kkc= github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= github.com/Joker/jade v1.1.3 h1:Qbeh12Vq6BxURXT1qZBRHsDxeURB8ztcL6f3EXSGeHk= github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKduM= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 h1:dG7/gLroJGht/jSQtHiLvT48Hxn+crbmvyItZC8cWOs= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05/go.mod h1:NYezi6wtnJtBm5btoprXc5SvAdqH0XTXWnUup0MptAI= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/flosch/pongo2/v4 v4.0.2 h1:gv+5Pe3vaSVmiJvh/BZa82b7/00YUGm0PIyVVLop0Hw= github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+BWPhwZD8= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a h1:l7A0loSszR5zHd/qK53ZIHMO8b3bBSmENnQ6eKnUT0A= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/iris-contrib/httpexpect/v2 v2.15.2 h1:T9THsdP1woyAqKHwjkEsbCnMefsAFvk8iJJKokcJ3Go= github.com/iris-contrib/httpexpect/v2 v2.15.2/go.mod h1:JLDgIqnFy5loDSUv1OA2j0mb6p/rDhiCqigP22Uq9xE= github.com/iris-contrib/schema v0.0.6 h1:CPSBLyx2e91H2yJzPuhGuifVRnZBBJ3pCOMbOvPZaTw= github.com/iris-contrib/schema v0.0.6/go.mod h1:iYszG0IOsuIsfzjymw1kMzTL8YQcCWlm65f3wX8J5iA= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kataras/blocks v0.0.8 h1:MrpVhoFTCR2v1iOOfGng5VJSILKeZZI+7NGfxEh3SUM= github.com/kataras/blocks v0.0.8/go.mod h1:9Jm5zx6BB+06NwA+OhTbHW1xkMOYxahnqTN5DveZ2Yg= github.com/kataras/golog v0.1.12 h1:Bu7I/G4ilJlbfzjmU39O9N+2uO1pBcMK045fzZ4ytNg= github.com/kataras/golog v0.1.12/go.mod h1:wrGSbOiBqbQSQznleVNX4epWM8rl9SJ/rmEacl0yqy4= github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd h1:oMsQgqKACbUdlaIWnN+EZzzAnHkw90hE/y/R0BSij2U= github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd/go.mod h1:yucjtDl9Vn3OctWM1fi0MuM9OckemC7kEreFa9QtPF8= github.com/kataras/pio v0.0.13 h1:x0rXVX0fviDTXOOLOmr4MUxOabu1InVSTu5itF8CXCM= github.com/kataras/pio v0.0.13/go.mod h1:k3HNuSw+eJ8Pm2lA4lRhg3DiCjVgHlP8hmXApSej3oM= github.com/kataras/sitemap v0.0.6 h1:w71CRMMKYMJh6LR2wTgnk5hSgjVNB9KL60n5e2KHvLY= github.com/kataras/sitemap v0.0.6/go.mod h1:dW4dOCNs896OR1HmG+dMLdT7JjDk7mYBzoIRwuj5jA4= github.com/kataras/tunnel v0.0.4 h1:sCAqWuJV7nPzGrlb0os3j49lk2JhILT0rID38NHNLpA= github.com/kataras/tunnel v0.0.4/go.mod h1:9FkU4LaeifdMWqZu7o20ojmW4B7hdhv2CMLwfnHGpYw= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw= github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18= github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo= github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk= github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tdewolff/minify/v2 v2.24.8 h1:58/VjsbevI4d5FGV0ZSuBrHMSSkH4MCH0sIz/eKIauE= github.com/tdewolff/minify/v2 v2.24.8/go.mod h1:0Ukj0CRpo/sW/nd8uZ4ccXaV1rEVIWA3dj8U7+Shhfw= github.com/tdewolff/parse/v2 v2.8.5 h1:ZmBiA/8Do5Rpk7bDye0jbbDUpXXbCdc3iah4VeUvwYU= github.com/tdewolff/parse/v2 v2.8.5/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo= github.com/tdewolff/test v1.0.11 h1:FdLbwQVHxqG16SlkGveC0JVyrJN62COWTRyUFzfbtBE= github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA= github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0= github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs= moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHcE= ================================================ FILE: _examples/routing/party-controller/main.go ================================================ package main import ( "github.com/kataras/iris/v12" "github.com/kataras/iris/_examples/routing/party-controller/pkg/weatherapi" ) // Example of usage of Party Controllers. // The method of zero-performance cost at serve-time, APIs run as fast as common Iris handlers. func main() { app := iris.New() // Define a group under /api request path. api := app.Party("/api") // Register one or more dependencies. api.RegisterDependency(weatherapi.NewClient(weatherapi.Options{ APIKey: "{YOUR_API_KEY}", })) // Register a party controller under the "/weather" sub request path. api.PartyConfigure("/weather", new(WeatherController)) // Start the local server at 8080 port. app.Listen(":8080") } // Just like the MVC controllers, route group(aka Party) controller's // fields are injected by the parent or current party's RegisterDependency method. // // This controller structure could be live to another sub-package of our application as well. type WeatherController struct { Client *weatherapi.Client // This is automatically injected by .RegisterDependency. } func (api *WeatherController) Configure(r iris.Party) { // Register routes under /api/weather. r.Get("/current", api.getCurrentData) } // Normal Iris Handler. func (api *WeatherController) getCurrentData(ctx iris.Context) { city := ctx.URLParamDefault("city", "Athens") // Call the controller's "Client"'s GetCurrentByCity method // to retrieve data from external provider and push them to our clients. data, err := api.Client.GetCurrentByCity(ctx, city) if err != nil { ctx.StatusCode(iris.StatusBadRequest) ctx.JSON(iris.Map{ "error": err.Error(), }) return } ctx.JSON(data) } ================================================ FILE: _examples/routing/party-controller/pkg/weatherapi/client.go ================================================ package weatherapi import ( "context" "net/url" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/x/client" ) // The BaseURL of our API client. const BaseURL = "https://api.weatherapi.com/v1" type ( Options struct { APIKey string `json:"api_key" yaml:"APIKey" toml:"APIKey"` } Client struct { *client.Client } ) func NewClient(opts Options) *Client { apiKeyParameterSetter := client.RequestParam("key", opts.APIKey) c := client.New(client.BaseURL(BaseURL), client.PersistentRequestOptions(apiKeyParameterSetter)) return &Client{c} } func (c *Client) GetCurrentByCity(ctx context.Context, city string) (resp Response, err error) { urlpath := "/current.json" // ?q=Athens&aqi=no params := client.RequestQuery(url.Values{ "q": []string{city}, "aqi": []string{"no"}, }) err = c.Client.ReadJSON(ctx, &resp, iris.MethodGet, urlpath, nil, params) return } ================================================ FILE: _examples/routing/party-controller/pkg/weatherapi/response.go ================================================ package weatherapi type Response struct { Location struct { Name string `json:"name"` Region string `json:"region"` Country string `json:"country"` Lat float64 `json:"lat"` Lon float64 `json:"lon"` TzID string `json:"tz_id"` LocaltimeEpoch int `json:"localtime_epoch"` Localtime string `json:"localtime"` } `json:"location"` Current struct { LastUpdatedEpoch int `json:"last_updated_epoch"` LastUpdated string `json:"last_updated"` TempC float64 `json:"temp_c"` TempF float64 `json:"temp_f"` IsDay int `json:"is_day"` Condition struct { Text string `json:"text"` Icon string `json:"icon"` Code int `json:"code"` } `json:"condition"` WindMph float64 `json:"wind_mph"` WindKph float64 `json:"wind_kph"` WindDegree int `json:"wind_degree"` WindDir string `json:"wind_dir"` PressureMb float64 `json:"pressure_mb"` PressureIn float64 `json:"pressure_in"` PrecipMm float64 `json:"precip_mm"` PrecipIn float64 `json:"precip_in"` Humidity int `json:"humidity"` Cloud int `json:"cloud"` FeelslikeC float64 `json:"feelslike_c"` FeelslikeF float64 `json:"feelslike_f"` VisKm float64 `json:"vis_km"` VisMiles float64 `json:"vis_miles"` Uv float64 `json:"uv"` GustMph float64 `json:"gust_mph"` GustKph float64 `json:"gust_kph"` } `json:"current"` } ================================================ FILE: _examples/routing/remove-handler/main.go ================================================ package main import "github.com/kataras/iris/v12" func main() { app := newApp() app.Listen(":8080") } func newApp() *iris.Application { app := iris.New() api := app.Party("/api") api.Use(myMiddleware) users := api.Party("/users") users.Get("/", usersIndex).RemoveHandler(myMiddleware) // OR for all routes under a Party (or Application): // users.RemoveHandler(...) return app } func myMiddleware(ctx iris.Context) { ctx.WriteString("Middleware\n") } func usersIndex(ctx iris.Context) { ctx.WriteString("OK") } ================================================ FILE: _examples/routing/remove-handler/main_test.go ================================================ package main import ( "testing" "github.com/kataras/iris/v12/httptest" ) func TestSimpleRouteRemoveHandler(t *testing.T) { app := newApp() e := httptest.New(t, app) e.GET("/api/users").Expect().Status(httptest.StatusOK).Body().IsEqual("OK") } ================================================ FILE: _examples/routing/remove-route/main.go ================================================ package main import ( "github.com/kataras/iris/v12" ) func main() { app := iris.New() app.Get("/", index) app.Get("/about", about).SetName("about_page") app.RemoveRoute("about_page") // http://localhost:8080 // http://localhost:8080/about (Not Found) app.Listen(":8080") } func index(ctx iris.Context) { ctx.WriteString("Hello, Gophers!") } func about(ctx iris.Context) { ctx.HTML("

About Page

") } ================================================ FILE: _examples/routing/reverse/main.go ================================================ package main import ( "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/core/router" ) func main() { app := iris.New() // need for manually reverse routing when needed outside of view engine. // you normally don't need it because of the {{ urlpath "routename" "path" "values" "here"}} rv := router.NewRoutePathReverser(app) myroute := app.Get("/anything/{anythingparameter:path}", func(ctx iris.Context) { paramValue := ctx.Params().Get("anythingparameter") ctx.Writef("The path after /anything is: %s", paramValue) }) myroute.Name = "myroute" // useful for links, although iris' view engine has the {{ urlpath "routename" "path values"}} already. app.Get("/reverse_myroute", func(ctx iris.Context) { myrouteRequestPath := rv.Path(myroute.Name, "any/path") ctx.HTML("Should be /anything/any/path: " + myrouteRequestPath) }) // execute a route, similar to redirect but without redirect :) app.Get("/execute_myroute", func(ctx iris.Context) { ctx.Exec("GET", "/anything/any/path") // like it was called by the client. }) // http://localhost:8080/reverse_myroute // http://localhost:8080/execute_myroute // http://localhost:8080/anything/any/path/here // // See view/template_html_4 example for more reverse routing examples // using the reverse router component and the {{url}} and {{urlpath}} template functions. app.Listen(":8080") } ================================================ FILE: _examples/routing/rewrite/hosts ================================================ 127.0.0.1 mydomain.com 127.0.0.1 www.mydomain.com 127.0.0.1 test.mydomain.com 127.0.0.1 newtest.mydomain.com ================================================ FILE: _examples/routing/rewrite/main.go ================================================ package main import ( "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/middleware/rewrite" ) func main() { app := iris.New() app.Get("/", index) app.Get("/about", about) app.Get("/docs", docs) app.Get("/users", listUsers) app.Subdomain("test").Get("/", testIndex) newtest := app.Subdomain("newtest") newtest.Get("/", newTestIndex) newtest.Get("/about", newTestAbout) redirects := rewrite.Load("redirects.yml") app.WrapRouter(redirects) // http://mydomain.com:8080/seo/about -> http://www.mydomain.com:8080/about // http://test.mydomain.com:8080 -> http://newtest.mydomain.com:8080 // http://test.mydomain.com:8080/seo/about -> http://newtest.mydomain.com:8080/about // http://mydomain.com:8080/seo -> http://www.mydomain.com:8080 // http://mydomain.com:8080/about // http://mydomain.com:8080/docs/v12/hello -> http://www.mydomain.com:8080/docs // http://mydomain.com:8080/docs/v12some -> http://www.mydomain.com:8080/docs // http://mydomain.com:8080/oldsome -> http://www.mydomain.com:8080 // http://mydomain.com:8080/oldindex/random -> http://www.mydomain.com:8080 // http://mydomain.com:8080/users.json -> http://www.mydomain.com:8080/users?format=json app.Listen(":8080") } func index(ctx iris.Context) { ctx.WriteString("Index") } func about(ctx iris.Context) { ctx.WriteString("About") } func docs(ctx iris.Context) { ctx.WriteString("Docs") } func listUsers(ctx iris.Context) { format := ctx.URLParamDefault("format", "text") /* switch format{ case "json": ctx.JSON(response) case "xml": ctx.XML(response) // [...] } */ ctx.Writef("Format: %s", format) } func testIndex(ctx iris.Context) { ctx.WriteString(`Test Subdomain Index (This should never be executed, redirects to newtest subdomain)`) } func newTestIndex(ctx iris.Context) { ctx.WriteString("New Test Subdomain Index") } func newTestAbout(ctx iris.Context) { ctx.WriteString("New Test Subdomain About") } /* More... rewriteOptions := rewrite.Options{ RedirectMatch: []string{ "301 /seo/(.*) /$1", "301 /docs/v12(.*) /docs", "301 /old(.*) /", "301 ^(http|https)://test.(.*) $1://newtest.$2", "0 /(.*).(json|xml) /$1?format=$2", }, PrimarySubdomain: "www", } rewriteEngine, err := rewrite.New(rewriteOptions) // To use it per-party use its `Handler` method. Even if not route match: app.UseRouter(rewriteEngine.Handler) // To use it per-party when route match: app.Use(rewriteEngine.Handler) // // To use it on a single route just pass it to the Get/Post method. // // To make the entire application respect the redirect rules // you have to wrap the Iris Router and pass the `Rewrite` method instead // as we did at this example: // app.WrapRouter(rewriteEngine.Rewrite) */ ================================================ FILE: _examples/routing/rewrite/redirects.yml ================================================ RedirectMatch: # REDIRECT_CODE_DIGITS | PATTERN_REGEX | TARGET_REPL # Redirects /seo/* to /* - 301 /seo/(.*) /$1 # Redirects /docs/v12* to /docs - 301 /docs/v12(.*) /docs # Redirects /old(.*) to / - 301 /old(.*) / # Redirects http or https://test.* to http or https://newtest.* - 301 ^(http|https)://test.(.*) $1://newtest.$2 # Handles /*.json or .xml as *?format=json or xml, # WITHOUT redirect. See /users route. # When Code is 0 then it does not redirect the request, # instead it changes the request URL # and leaves a route handle the request. - 0 /(.*).(json|xml) /$1?format=$2 # Redirects root domain to www. # Creation of a www subdomain inside the Application is unnecessary, # all requests are handled by the root Application itself. PrimarySubdomain: www ================================================ FILE: _examples/routing/route-handlers-execution-rules/main.go ================================================ /* Package main is a simple example of the behavior change of the execution flow of the handlers, normally we need the `ctx.Next()` to call the next handler in a route's handler chain, but with the `ExecutionRules` we can change this default behavior. Please read below before continue. The `Party#SetExecutionRules` alters the execution flow of the route handlers. For example, if for some reason the desired result is the (done or all) handlers to be executed no matter what, even if no `ctx.Next()` is called in the previous handlers: app.SetExecutionRules(iris.ExecutionRules { Begin: iris.ExecutionOptions{Force: true}, # begin handlers(.Use) Main: iris.ExecutionOptions{Force: true}, # main handler (.Handle/Get...) Done: iris.ExecutionOptions{Force: true}, # done handlers (.Done) }) Note that if `true` then the only remained way to "break" the handler chain is by calling the `ctx.StopExecution()` (now that `ctx.Next()` doesn't even matter). These rules are per-party, so if a `Party` creates a child one then the same rules will be applied to that as well. Reset of these rules to their defaults (before `Party#Handle`) can be done with `Party#SetExecutionRules(iris.ExecutionRules{})`. */ package main import "github.com/kataras/iris/v12" func main() { app := iris.New() app.SetExecutionRules(iris.ExecutionRules{ // * From `Use[all]` to `Handle[last]` future route handlers, // execute all (even if `ctx.Next()` is missing): // Begin: true, // // * All `Handle` future route handlers, execute all: // Main: true, // // * From `Handle[last]` to `Done[last]` future route handlers, execute all: Done: iris.ExecutionOptions{Force: true}, }) app.Done(doneHandler) app.Get("/", mainHandler) // http://localhost:8080 app.Listen(":8080") } func mainHandler(ctx iris.Context) { ctx.WriteString("From Main Handler\n") // ctx.Next() is not required now that we have declared // Done: iris.ExecutionOptions{Force: true}. } func doneHandler(ctx iris.Context) { ctx.WriteString("From Done Handler\n") } ================================================ FILE: _examples/routing/route-register-rule/main.go ================================================ package main import "github.com/kataras/iris/v12" func main() { app := newApp() // Navigate through https://github.com/kataras/iris/issues/1448 for details. // // GET: http://localhost:8080 // POST, PUT, DELETE, CONNECT, HEAD, PATCH, OPTIONS, TRACE : http://localhost:8080 app.Listen(":8080") } func newApp() *iris.Application { app := iris.New() // Skip and do NOT override existing regitered route, continue normally. // Applies to a Party and its children, in this case the whole application's routes. app.SetRegisterRule(iris.RouteSkip) /* Read also: // The default behavior, will override the getHandler to anyHandler on `app.Any` call. app.SetRegistRule(iris.RouteOverride) // Stops the execution and fires an error before server boot. app.SetRegisterRule(iris.RouteError) // If ctx.StopExecution or StopWithXXX then the next route will be executed // (see mvc/authenticated-controller example too). app.SetRegisterRule(iris.RouteOverlap) */ app.Get("/", getHandler) // app.Any does NOT override the previous GET route because of `iris.RouteSkip` rule. app.Any("/", anyHandler) return app } func getHandler(ctx iris.Context) { ctx.Writef("From GET: %s", ctx.GetCurrentRoute().MainHandlerName()) } func anyHandler(ctx iris.Context) { ctx.Writef("From %s: %s", ctx.Method(), ctx.GetCurrentRoute().MainHandlerName()) } ================================================ FILE: _examples/routing/route-register-rule/main_test.go ================================================ package main import ( "testing" "github.com/kataras/iris/v12/core/router" "github.com/kataras/iris/v12/httptest" ) func TestRouteRegisterRuleExample(t *testing.T) { app := newApp() e := httptest.New(t, app) for _, method := range router.AllMethods { tt := e.Request(method, "/").Expect().Status(httptest.StatusOK).Body() if method == "GET" { tt.Equal("From GET: iris/_examples/routing/route-register-rule.getHandler") } else { tt.Equal("From " + method + ": iris/_examples/routing/route-register-rule.anyHandler") } } } ================================================ FILE: _examples/routing/route-state/main.go ================================================ package main import ( "github.com/kataras/iris/v12" ) func main() { app := iris.New() none := app.None("/invisible/{username}", func(ctx iris.Context) { ctx.Writef("Hello %s with method: %s", ctx.Params().Get("username"), ctx.Method()) if from := ctx.Values().GetString("from"); from != "" { ctx.Writef("\nI see that you're coming from %s", from) } }) app.Get("/change", func(ctx iris.Context) { if none.IsOnline() { none.Method = iris.MethodNone } else { none.Method = iris.MethodGet } // refresh re-builds the router at serve-time in order to be notified for its new routes. app.RefreshRouter() }) app.Get("/execute", func(ctx iris.Context) { if !none.IsOnline() { ctx.Values().Set("from", "/execute with offline access") ctx.Exec("NONE", "/invisible/iris") return } // same as navigating to "http://localhost:8080/invisible/iris" when /change has being invoked and route state changed // from "offline" to "online" ctx.Values().Set("from", "/execute") // values and session can be shared when calling Exec from a "foreign" context. // ctx.Exec("NONE", "/invisible/iris") // or after "/change": ctx.Exec("GET", "/invisible/iris") }) app.Listen(":8080", iris.WithDynamicHandler) } ================================================ FILE: _examples/routing/sitemap/main.go ================================================ package main import ( "time" "github.com/kataras/iris/v12" ) const startURL = "http://localhost:8080" func main() { app := newApp() // http://localhost:8080/sitemap.xml // Lists only online GET static routes. // // Reference: https://www.sitemaps.org/protocol.html app.Listen(":8080", iris.WithSitemap(startURL)) } func newApp() *iris.Application { app := iris.New() app.Logger().SetLevel("debug") lastModified, _ := time.Parse("2006-01-02T15:04:05-07:00", "2019-12-13T21:50:33+02:00") app.Get("/home", handler).SetLastMod(lastModified).SetChangeFreq("hourly").SetPriority(1) app.Get("/articles", handler).SetChangeFreq("daily") app.Get("/path1", handler) app.Get("/path2", handler) app.Post("/this-should-not-be-listed", handler) app.Get("/this/{myparam}/should/not/be/listed", handler) app.Get("/this-should-not-be-listed-offline", handler).SetStatusOffline() // These should be excluded as well app.Get("/about", handler).ExcludeSitemap() app.Get("/offline", handler).SetStatusOffline() return app } func handler(ctx iris.Context) { ctx.WriteString(ctx.Path()) } ================================================ FILE: _examples/routing/sitemap/main_test.go ================================================ package main import ( "testing" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/httptest" ) func TestSitemap(t *testing.T) { const expectedFullSitemapXML = `http://localhost:8080/home2019-12-13T21:50:33+02:00hourly1http://localhost:8080/articlesdailyhttp://localhost:8080/path1http://localhost:8080/path2` app := newApp() app.Configure(iris.WithSitemap(startURL)) e := httptest.New(t, app) e.GET("/sitemap.xml").Expect().Status(httptest.StatusOK).Body().IsEqual(expectedFullSitemapXML) } ================================================ FILE: _examples/routing/subdomains/http-errors-view/main.go ================================================ package main import ( "fmt" "github.com/kataras/iris/v12" ) func main() { newApp().Listen("mydomain.com:80", iris.WithLogLevel("debug")) } func newApp() *iris.Application { app := iris.New() // Create the "test.mydomain.com" subdomain. test := app.Subdomain("test") // Register views for the test subdomain. test.RegisterView(iris.HTML("./views", ".html"). Layout("layouts/test.layout.html")) // Optionally, to minify the HTML5 error response. // Note that minification might be slower, caching is advised. // test.UseError(iris.Minify) // or pass it to OnErrorCode: // Register error code 404 handler. test.OnErrorCode(iris.StatusNotFound, iris.Minify, handleNotFoundTestSubdomain) test.Get("/", testIndex) return app } func handleNotFoundTestSubdomain(ctx iris.Context) { if err := ctx.View("error.html", iris.Map{ "ErrorCode": ctx.GetStatusCode(), }); err != nil { ctx.HTML(fmt.Sprintf("

%s

", err.Error())) return } } func testIndex(ctx iris.Context) { ctx.Writef("%s index page\n", ctx.Subdomain()) } ================================================ FILE: _examples/routing/subdomains/http-errors-view/main_test.go ================================================ package main import ( "testing" "github.com/kataras/iris/v12/httptest" ) func TestSubdomainsHTTPErrorsView(t *testing.T) { app := newApp() // hard coded. expectedHTMLResponse := ` Test Subdomain

Oups, you've got an error!

Not Found

` e := httptest.New(t, app) got := e.GET("/not_found").WithURL("http://test.mydomain.com").Expect().Status(httptest.StatusNotFound). ContentType("text/html", "utf-8").Body().Raw() if expected, _ := app.Minifier().String("text/html", expectedHTMLResponse); expected != got { t.Fatalf("expected:\n'%s'\nbut got:\n'%s'", expected, got) } } ================================================ FILE: _examples/routing/subdomains/http-errors-view/views/error.html ================================================

Oups, you've got an error!

{{ if .ErrorCode }} {{ $tmplName := print "partials/" .ErrorCode ".html"}} {{ render $tmplName . }} {{ else }} {{ render "partials/500.html" . }} {{ end }}
================================================ FILE: _examples/routing/subdomains/http-errors-view/views/index.html ================================================
Index Page
================================================ FILE: _examples/routing/subdomains/http-errors-view/views/layouts/layout.html ================================================ Website Layout

This is the global layout


{{ yield . }} ================================================ FILE: _examples/routing/subdomains/http-errors-view/views/layouts/test.layout.html ================================================ Test Subdomain {{ yield . }} ================================================ FILE: _examples/routing/subdomains/http-errors-view/views/partials/404.html ================================================

Not Found

================================================ FILE: _examples/routing/subdomains/http-errors-view/views/partials/500.html ================================================

Internal Server Error

================================================ FILE: _examples/routing/subdomains/multi/hosts ================================================ # Copyright (c) 1993-2009 Microsoft Corp. # # This is a sample HOSTS file used by Microsoft TCP/IP for Windows. # # This file contains the mappings of IP addresses to host names. Each # entry should be kept on an individual line. The IP address should # be placed in the first column followed by the corresponding host name. # The IP address and the host name should be separated by at least one # space. # # Additionally, comments (such as these) may be inserted on individual # lines or following the machine name denoted by a '#' symbol. # # For example: # # 102.54.94.97 rhino.acme.com # source server # 38.25.63.10 x.acme.com # x client host # localhost name resolution is handled within DNS itself. 127.0.0.1 localhost ::1 localhost #-iris-For development machine, you have to configure your dns also for online, search google how to do it if you don't know 127.0.0.1 domain.local 127.0.0.1 system.domain.local 127.0.0.1 dashboard.domain.local #-END iris- ================================================ FILE: _examples/routing/subdomains/multi/main.go ================================================ package main import ( "github.com/kataras/iris/v12" ) func main() { app := iris.New() /* * Setup static files */ app.HandleDir("/assets", iris.Dir("./public/assets")) app.HandleDir("/upload_resources", iris.Dir("./public/upload_resources")) dashboard := app.Party("dashboard.") { dashboard.Get("/", func(ctx iris.Context) { ctx.Writef("HEY FROM dashboard") }) } system := app.Party("system.") { system.Get("/", func(ctx iris.Context) { ctx.Writef("HEY FROM system") }) } app.Get("/", func(ctx iris.Context) { ctx.Writef("HEY FROM frontend /") }) // http://domain.local:80 // http://dashboard.local // http://system.local // Make sure you prepend the "http" in your browser // because .local is a virtual domain we think to show case you // that you can declare any syntactical correct name as a subdomain in iris. app.Listen("domain.local:80") // for beginners: look ../hosts file } ================================================ FILE: _examples/routing/subdomains/redirect/hosts ================================================ 127.0.0.1 mydomain.com 127.0.0.1 www.mydomain.com 127.0.0.1 otherdomain.com # Windows: Drive:/Windows/system32/drivers/etc/hosts, on Linux: /etc/hosts ================================================ FILE: _examples/routing/subdomains/redirect/main.go ================================================ package main import ( "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/middleware/rewrite" ) func main() { app := newApp() // http://mydomain.com -> http://www.mydomain.com // http://mydomain.com/user -> http://www.mydomain.com/user // http://mydomain.com/user/login -> http://www.mydomain.com/user/login app.Listen(":80") } func newApp() *iris.Application { app := iris.New() app.Logger().SetLevel("debug") static := app.Subdomain("static") static.Get("/", staticIndex) app.Get("/", index) userRouter := app.Party("/user") userRouter.Get("/", userGet) userRouter.Get("/login", userGetLogin) // redirects := rewrite.Load("redirects.yml") // ^ see _examples/routing/rewrite example for that. // // Now let's do that by code. rewriteEngine, _ := rewrite.New(rewrite.Options{ PrimarySubdomain: "www", }) // Enable this line for debugging: // rewriteEngine.SetLogger(app.Logger()) app.WrapRouter(rewriteEngine.Rewrite) return app } func staticIndex(ctx iris.Context) { ctx.Writef("This is the static.mydomain.com index.") } func index(ctx iris.Context) { ctx.Writef("This is the www.mydomain.com index.") } func userGet(ctx iris.Context) { // Also, ctx.Subdomain(), ctx.SubdomainFull(), ctx.Host() and ctx.Path() // can be helpful when working with subdomains. ctx.Writef("This is the www.mydomain.com/user endpoint.") } func userGetLogin(ctx iris.Context) { ctx.Writef("This is the www.mydomain.com/user/login endpoint.") } ================================================ FILE: _examples/routing/subdomains/redirect/multi-instances/go.mod ================================================ module github.com/kataras/iris/_examples/routing/subdomains/redirect/multi-instances go 1.25 require github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd require ( github.com/BurntSushi/toml v1.6.0 // indirect github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect github.com/CloudyKit/jet/v6 v6.3.1 // indirect github.com/Joker/jade v1.1.3 // indirect github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 // indirect github.com/andybalholm/brotli v1.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/fatih/structs v1.1.0 // indirect github.com/flosch/pongo2/v4 v4.0.2 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/iris-contrib/schema v0.0.6 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/kataras/blocks v0.0.8 // indirect github.com/kataras/golog v0.1.12 // indirect github.com/kataras/pio v0.0.13 // indirect github.com/kataras/sitemap v0.0.6 // indirect github.com/kataras/tunnel v0.0.4 // indirect github.com/klauspost/compress v1.18.2 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mailgun/raymond/v2 v2.0.48 // indirect github.com/mailru/easyjson v0.9.1 // indirect github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/schollz/closestmatch v2.1.0+incompatible // indirect github.com/sirupsen/logrus v1.8.1 // indirect github.com/stretchr/testify v1.11.1 // indirect github.com/tdewolff/minify/v2 v2.24.8 // indirect github.com/tdewolff/parse/v2 v2.8.5 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/yosssi/ace v0.0.5 // indirect golang.org/x/crypto v0.47.0 // indirect golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect golang.org/x/net v0.49.0 // indirect golang.org/x/sys v0.40.0 // indirect golang.org/x/text v0.33.0 // indirect golang.org/x/time v0.14.0 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: _examples/routing/subdomains/redirect/multi-instances/go.sum ================================================ github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= github.com/CloudyKit/jet/v6 v6.3.1 h1:6IAo5Cx21xrHVaR8zzXN5gJatKV/wO7Nf6bfCnCSbUw= github.com/CloudyKit/jet/v6 v6.3.1/go.mod h1:lf8ksdNsxZt7/yH/3n4vJQWA9RUq4wpaHtArHhGVMOw= github.com/Joker/hpp v1.0.0 h1:65+iuJYdRXv/XyN62C1uEmmOx3432rNG/rKlX6V7Kkc= github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= github.com/Joker/jade v1.1.3 h1:Qbeh12Vq6BxURXT1qZBRHsDxeURB8ztcL6f3EXSGeHk= github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKduM= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 h1:dG7/gLroJGht/jSQtHiLvT48Hxn+crbmvyItZC8cWOs= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05/go.mod h1:NYezi6wtnJtBm5btoprXc5SvAdqH0XTXWnUup0MptAI= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/flosch/pongo2/v4 v4.0.2 h1:gv+5Pe3vaSVmiJvh/BZa82b7/00YUGm0PIyVVLop0Hw= github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+BWPhwZD8= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a h1:l7A0loSszR5zHd/qK53ZIHMO8b3bBSmENnQ6eKnUT0A= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/iris-contrib/httpexpect/v2 v2.15.2 h1:T9THsdP1woyAqKHwjkEsbCnMefsAFvk8iJJKokcJ3Go= github.com/iris-contrib/httpexpect/v2 v2.15.2/go.mod h1:JLDgIqnFy5loDSUv1OA2j0mb6p/rDhiCqigP22Uq9xE= github.com/iris-contrib/schema v0.0.6 h1:CPSBLyx2e91H2yJzPuhGuifVRnZBBJ3pCOMbOvPZaTw= github.com/iris-contrib/schema v0.0.6/go.mod h1:iYszG0IOsuIsfzjymw1kMzTL8YQcCWlm65f3wX8J5iA= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kataras/blocks v0.0.8 h1:MrpVhoFTCR2v1iOOfGng5VJSILKeZZI+7NGfxEh3SUM= github.com/kataras/blocks v0.0.8/go.mod h1:9Jm5zx6BB+06NwA+OhTbHW1xkMOYxahnqTN5DveZ2Yg= github.com/kataras/golog v0.1.12 h1:Bu7I/G4ilJlbfzjmU39O9N+2uO1pBcMK045fzZ4ytNg= github.com/kataras/golog v0.1.12/go.mod h1:wrGSbOiBqbQSQznleVNX4epWM8rl9SJ/rmEacl0yqy4= github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd h1:oMsQgqKACbUdlaIWnN+EZzzAnHkw90hE/y/R0BSij2U= github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd/go.mod h1:yucjtDl9Vn3OctWM1fi0MuM9OckemC7kEreFa9QtPF8= github.com/kataras/pio v0.0.13 h1:x0rXVX0fviDTXOOLOmr4MUxOabu1InVSTu5itF8CXCM= github.com/kataras/pio v0.0.13/go.mod h1:k3HNuSw+eJ8Pm2lA4lRhg3DiCjVgHlP8hmXApSej3oM= github.com/kataras/sitemap v0.0.6 h1:w71CRMMKYMJh6LR2wTgnk5hSgjVNB9KL60n5e2KHvLY= github.com/kataras/sitemap v0.0.6/go.mod h1:dW4dOCNs896OR1HmG+dMLdT7JjDk7mYBzoIRwuj5jA4= github.com/kataras/tunnel v0.0.4 h1:sCAqWuJV7nPzGrlb0os3j49lk2JhILT0rID38NHNLpA= github.com/kataras/tunnel v0.0.4/go.mod h1:9FkU4LaeifdMWqZu7o20ojmW4B7hdhv2CMLwfnHGpYw= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw= github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18= github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo= github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk= github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tdewolff/minify/v2 v2.24.8 h1:58/VjsbevI4d5FGV0ZSuBrHMSSkH4MCH0sIz/eKIauE= github.com/tdewolff/minify/v2 v2.24.8/go.mod h1:0Ukj0CRpo/sW/nd8uZ4ccXaV1rEVIWA3dj8U7+Shhfw= github.com/tdewolff/parse/v2 v2.8.5 h1:ZmBiA/8Do5Rpk7bDye0jbbDUpXXbCdc3iah4VeUvwYU= github.com/tdewolff/parse/v2 v2.8.5/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo= github.com/tdewolff/test v1.0.11 h1:FdLbwQVHxqG16SlkGveC0JVyrJN62COWTRyUFzfbtBE= github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA= github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0= github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs= moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHcE= ================================================ FILE: _examples/routing/subdomains/redirect/multi-instances/main.go ================================================ package main import ( _ "github.com/kataras/iris/_examples/routing/subdomains/redirect/multi-instances/other" _ "github.com/kataras/iris/_examples/routing/subdomains/redirect/multi-instances/root" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/apps" ) // In this example, you wanna use three different applications exposed as one. // The first one is the "other" package, the second is the "root", // the third is the switcher application which will expose the above. // Unlike the previous example, on this one we will NOT redirect, // the Hosts switcher will just pass the request to the matched Application to handle. // This is NOT an alternative of your favorite load balancer. // Read the comments carefully, if you need more information // you can head over to the "apps" package's godocs and tests. func main() { // The `apps.Hosts` switch provider: // The pattern. A regexp for matching the host part of incoming requests. // The target. An iris.Application instance (created by iris.New()) // OR // You can use the Application's name (app.SetName("myapp")). // Example: // package rootdomain // func init() { // app := iris.New().SetName("root app") // ... // } // On the main package add an empty import statement: ` _ import "rootdomain"` // And set the "root app" as the key to reference that application (of the same program). // Thats the target we wanna use now ^ (see ../hosts file). // OR // An external host or a local running in the same machine but different port or host behind proxy. switcher := apps.Switch(apps.Hosts{ {"^(www.)?mydomain.com$", "root app"}, {"^otherdomain.com$", "other app"}, }) // The registration order matters, so we can register a fallback server (when host no match) // using "*". However, you have alternatives by using the Switch Iris Application value // (let's call it "switcher"): // 1. Handle the not founds, e.g. switcher.OnErrorCode(404, ...) // 2. Use the switcher.WrapRouter, e.g. to log the flow of a request of all hosts exposed. // 3. Just register routes to match, e.g. switcher.Get("/", ...) switcher.Get("/", fallback) // OR // Change the response code to 502 // instead of 404 and write a message: // switcher.OnErrorCode(iris.StatusNotFound, fallback) // The switcher is a common Iris Application, so you have access to the Iris features. // And it should be listening to a host:port in order to match and serve its apps. // // http://mydomain.com (OK) // http://www.mydomain.com (OK) // http://mydomain.com/dsa (404) // http://no.mydomain.com (502 Bad Host) // // http://otherdomain.com (OK) // http://www.otherdomain.com (502 Bad Host) // http://otherdomain.com/dsa (404 JSON) // ... switcher.Listen(":80") } func fallback(ctx iris.Context) { ctx.StatusCode(iris.StatusBadGateway) ctx.Writef("Bad Host %s", ctx.Host()) } ================================================ FILE: _examples/routing/subdomains/redirect/multi-instances/other/server.go ================================================ package other import ( "time" "github.com/kataras/iris/v12" ) func init() { app := iris.New() app.SetName("other app") app.OnAnyErrorCode(handleErrors) app.Get("/", index) } func index(ctx iris.Context) { ctx.HTML("Other Index (App Name: %s | Host: %s)", ctx.Application().String(), ctx.Host()) } func handleErrors(ctx iris.Context) { errCode := ctx.GetStatusCode() ctx.JSON(iris.Map{ "Server": ctx.Application().String(), "Code": errCode, "Message": iris.StatusText(errCode), "Timestamp": time.Now().Unix(), }) } ================================================ FILE: _examples/routing/subdomains/redirect/multi-instances/root/server.go ================================================ package root import "github.com/kataras/iris/v12" func init() { app := iris.New() app.SetName("root app") app.Get("/", index) } func index(ctx iris.Context) { ctx.HTML("Main Root Index (App Name: %s | Host: %s)", ctx.Application().String(), ctx.Host()) } ================================================ FILE: _examples/routing/subdomains/single/hosts ================================================ # Copyright (c) 1993-2009 Microsoft Corp. # # This is a sample HOSTS file used by Microsoft TCP/IP for Windows. # # This file contains the mappings of IP addresses to host names. Each # entry should be kept on an individual line. The IP address should # be placed in the first column followed by the corresponding host name. # The IP address and the host name should be separated by at least one # space. # # Additionally, comments (such as these) may be inserted on individual # lines or following the machine name denoted by a '#' symbol. # # For example: # # 102.54.94.97 rhino.acme.com # source server # 38.25.63.10 x.acme.com # x client host # localhost name resolution is handled within DNS itself. 127.0.0.1 localhost ::1 localhost #-iris-For development machine, you have to configure your dns also for online, search google how to do it if you don't know 127.0.0.1 mydomain.com 127.0.0.1 admin.mydomain.com #-END iris- ================================================ FILE: _examples/routing/subdomains/single/main.go ================================================ // Package main register static subdomains, simple as parties, check ./hosts if you use windows package main import ( "github.com/kataras/iris/v12" ) func main() { app := iris.New() // Subdomain method is just another Party. admin := app.Subdomain("admin") { // admin.mydomain.com admin.Get("/", func(c iris.Context) { c.Writef("INDEX FROM admin.mydomain.com") }) // admin.mydomain.com/hey admin.Get("/hey", func(c iris.Context) { c.Writef("HEY FROM admin.mydomain.com/hey") }) // admin.mydomain.com/hey2 admin.Get("/hey2", func(c iris.Context) { c.Writef("HEY SECOND FROM admin.mydomain.com/hey") }) } // mydomain.com app.Get("/", func(c iris.Context) { c.Writef("INDEX FROM no-subdomain hey") }) // mydomain.com/hey app.Get("/hey", func(c iris.Context) { c.Writef("HEY FROM no-subdomain hey") }) // http://admin.mydomain.com // http://admin.mydomain.com/hey // http://admin.mydomain.com/hey2 // http://mydomain.com // http://mydomain.com/hey app.Listen("mydomain.com:80") // for beginners: look ../hosts file } ================================================ FILE: _examples/routing/subdomains/wildcard/hosts ================================================ # Copyright (c) 1993-2009 Microsoft Corp. # # This is a sample HOSTS file used by Microsoft TCP/IP for Windows. # # This file contains the mappings of IP addresses to host names. Each # entry should be kept on an individual line. The IP address should # be placed in the first column followed by the corresponding host name. # The IP address and the host name should be separated by at least one # space. # # Additionally, comments (such as these) may be inserted on individual # lines or following the machine name denoted by a '#' symbol. # # For example: # # 102.54.94.97 rhino.acme.com # source server # 38.25.63.10 x.acme.com # x client host # localhost name resolution is handled within DNS itself. 127.0.0.1 localhost ::1 localhost #-iris-For development machine, you have to configure your dns also for online, search google how to do it if you don't know 127.0.0.1 mydomain.com 127.0.0.1 username1.mydomain.com 127.0.0.1 username2.mydomain.com 127.0.0.1 username3.mydomain.com 127.0.0.1 username4.mydomain.com 127.0.0.1 username5.mydomain.com 127.0.0.1 en-us.test.mydomain.com #-END iris- ================================================ FILE: _examples/routing/subdomains/wildcard/main.go ================================================ // Package main an example on how to catch dynamic subdomains - wildcard. // On the first example we learnt how to create routes for static subdomains, subdomains you know that you will have. // Here we will see an example how to catch unknown subdomains, dynamic subdomains, like username.mydomain.com:8080. package main import ( "github.com/kataras/iris/v12" ) // register a dynamic-wildcard subdomain to your server machine(dns/...) first, check ./hosts if you use windows. // run this file and try to redirect: http://username1.mydomain.com:8080/ , http://username2.mydomain.com:8080/ , http://username1.mydomain.com/something, http://username1.mydomain.com/something/sadsadsa func main() { app := iris.New() /* Keep note that you can use both type of subdomains (named and wildcard(*.) ) admin.mydomain.com, and for other the Party(*.) but this is not this example's purpose admin := app.Party("admin.") { // admin.mydomain.com admin.Get("/", func(ctx iris.Context) { ctx.Writef("INDEX FROM admin.mydomain.com") }) // admin.mydomain.com/hey admin.Get("/hey", func(ctx iris.Context) { ctx.Writef("HEY FROM admin.mydomain.com/hey") }) // admin.mydomain.com/hey2 admin.Get("/hey2", func(ctx iris.Context) { ctx.Writef("HEY SECOND FROM admin.mydomain.com/hey") }) }*/ // no order, you can register subdomains at the end also. dynamicSubdomains := app.WildcardSubdomain() { dynamicSubdomains.Get("/", dynamicSubdomainHandler) dynamicSubdomains.Get("/something", dynamicSubdomainHandler) dynamicSubdomains.Get("/something/{paramfirst}", dynamicSubdomainHandlerWithParam) } app.Get("/", func(ctx iris.Context) { ctx.Writef("Hello from mydomain.com path: %s", ctx.Path()) }) app.Get("/hello", func(ctx iris.Context) { ctx.Writef("Hello from mydomain.com path: %s", ctx.Path()) }) // http://mydomain.com:8080 // http://username1.mydomain.com:8080 // http://username2.mydomain.com:8080/something // http://username3.mydomain.com:8080/something/yourname // http://en-us.test.mydomain.com:8080/something/42 app.Listen("mydomain.com:8080") // for beginners: look ../hosts file } func dynamicSubdomainHandler(ctx iris.Context) { username := ctx.Subdomain() ctx.Writef("Hello from dynamic subdomain path: %s, here you can handle the route for dynamic subdomains, handle the user: %s", ctx.Path(), username) // if http://username4.mydomain.com:8080/ prints: // Hello from dynamic subdomain path: /, here you can handle the route for dynamic subdomains, handle the user: username4 } func dynamicSubdomainHandlerWithParam(ctx iris.Context) { username := ctx.Subdomain() ctx.Writef("Hello from dynamic (full) subdomain: %s and path: %s, here you can handle the route for dynamic subdomains, handle the user: %s", ctx.SubdomainFull(), ctx.Path(), username) ctx.Writef("The paramfirst is: %s", ctx.Params().Get("paramfirst")) } ================================================ FILE: _examples/routing/subdomains/www/hosts ================================================ # Copyright (c) 1993-2009 Microsoft Corp. # # This is a sample HOSTS file used by Microsoft TCP/IP for Windows. # # This file contains the mappings of IP addresses to host names. Each # entry should be kept on an individual line. The IP address should # be placed in the first column followed by the corresponding host name. # The IP address and the host name should be separated by at least one # space. # # Additionally, comments (such as these) may be inserted on individual # lines or following the machine name denoted by a '#' symbol. # # For example: # # 102.54.94.97 rhino.acme.com # source server # 38.25.63.10 x.acme.com # x client host # localhost name resolution is handled within DNS itself. 127.0.0.1 localhost ::1 localhost #-iris-For development machine, you have to configure your dns also for online, search google how to do it if you don't know 127.0.0.1 mydomain.com 127.0.0.1 www.mydomain.com #-END iris- ================================================ FILE: _examples/routing/subdomains/www/main.go ================================================ package main import "github.com/kataras/iris/v12" func newApp() *iris.Application { app := iris.New() app.Get("/", info) app.Get("/about", info) app.Get("/contact", info) app.PartyFunc("/api/users", func(r iris.Party) { r.Get("/", info) r.Get("/{id:uint64}", info) r.Post("/", info) r.Put("/{id:uint64}", info) }) /* <- same as: usersAPI := app.Party("/api/users") { // those brackets are just syntactic-sugar things. // This method is rarely used but you can make use of it when you want // scoped variables to that code block only. usersAPI.Get/Post... } usersAPI.Get/Post... */ www := app.Party("www.") { // Just to show how you can get all routes and copy them to another // party or subdomain: // Get all routes that are registered so far, including all "Parties" and subdomains: currentRoutes := app.GetRoutes() // Register them to the www subdomain/VHost as well: for _, r := range currentRoutes { www.Handle(r.Method, r.Tmpl().Src, r.Handlers...) } // http://www.mydomain.com/hi www.Get("/hi", func(ctx iris.Context) { ctx.Writef("hi from www.mydomain.com") }) } // See "subdomains/redirect" to register redirect router wrappers between subdomains, // i.e mydomain.com to www.mydomain.com (like facebook does for SEO reasons(;)). // And ./www-method example. return app } func main() { app := newApp() // http://mydomain.com // http://mydomain.com/about // http://imydomain.com/contact // http://mydomain.com/api/users // http://mydomain.com/api/users/42 // http://www.mydomain.com // http://www.mydomain.com/hi // http://www.mydomain.com/about // http://www.mydomain.com/contact // http://www.mydomain.com/api/users // http://www.mydomain.com/api/users/42 app.Listen("mydomain.com:80") } func info(ctx iris.Context) { method := ctx.Method() subdomain := ctx.Subdomain() path := ctx.Path() ctx.Writef("\nInfo\n\n") ctx.Writef("Method: %s\nSubdomain: %s\nPath: %s", method, subdomain, path) } ================================================ FILE: _examples/routing/subdomains/www/main_test.go ================================================ package main import ( "fmt" "testing" "github.com/kataras/iris/v12/httptest" ) type testRoute struct { path string method string subdomain string } func (r testRoute) response() string { msg := fmt.Sprintf("\nInfo\n\nMethod: %s\nSubdomain: %s\nPath: %s", r.method, r.subdomain, r.path) return msg } func TestSubdomainWWW(t *testing.T) { app := newApp() tests := []testRoute{ // host {"/", "GET", ""}, {"/about", "GET", ""}, {"/contact", "GET", ""}, {"/api/users", "GET", ""}, {"/api/users/42", "GET", ""}, {"/api/users", "POST", ""}, {"/api/users/42", "PUT", ""}, // www sub domain {"/", "GET", "www"}, {"/about", "GET", "www"}, {"/contact", "GET", "www"}, {"/api/users", "GET", "www"}, {"/api/users/42", "GET", "www"}, {"/api/users", "POST", "www"}, {"/api/users/42", "PUT", "www"}, } host := "localhost:1111" e := httptest.New(t, app, httptest.Debug(false)) for _, test := range tests { req := e.Request(test.method, test.path) if subdomain := test.subdomain; subdomain != "" { req.WithURL("http://" + subdomain + "." + host) } req.Expect(). Status(httptest.StatusOK). Body().IsEqual(test.response()) } } ================================================ FILE: _examples/routing/subdomains/www/www-method/main.go ================================================ package main import "github.com/kataras/iris/v12" func newApp() *iris.Application { app := iris.New() // This will create a new "www" subdomain // and redirect root-level domain requests // to that one: www := app.WWW() www.Get("/", info) www.Get("/about", info) www.Get("/contact", info) www.PartyFunc("/api/users", func(r iris.Party) { r.Get("/", info) r.Get("/{id:uint64}", info) r.Post("/", info) r.Put("/{id:uint64}", info) }) return app } func main() { app := newApp() // http://mydomain.com // http://mydomain.com/about // http://imydomain.com/contact // http://mydomain.com/api/users // http://mydomain.com/api/users/42 // http://www.mydomain.com // http://www.mydomain.com/hi // http://www.mydomain.com/about // http://www.mydomain.com/contact // http://www.mydomain.com/api/users // http://www.mydomain.com/api/users/42 app.Listen("mydomain.com:80") } func info(ctx iris.Context) { method := ctx.Method() subdomain := ctx.Subdomain() path := ctx.Path() ctx.Writef("\nInfo\n\n") ctx.Writef("Method: %s\nSubdomain: %s\nPath: %s", method, subdomain, path) } ================================================ FILE: _examples/routing/versioning/main.go ================================================ package main import ( "fmt" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/versioning" ) func main() { app := iris.New() app.OnErrorCode(iris.StatusNotFound, func(ctx iris.Context) { ctx.WriteString(`Root not found handler. This will be applied everywhere except the /api/* requests.`) }) api := app.Party("/api") // Optional, set version aliases (literal strings). // We use `UseRouter` instead of `Use` // to handle HTTP errors per version, but it's up to you. api.UseRouter(versioning.Aliases(versioning.AliasMap{ // If no version provided by the client, default it to the "1.0.0". versioning.Empty: "1.0.0", // If a "latest" version is provided by the client, // set the version to be compared to "3.0.0". "latest": "3.0.0", })) /* A version is extracted through the versioning.GetVersion function, request headers: - Accept-Version: 1.0.0 - Accept: application/json; version=1.0.0 You can customize it by setting a version based on the request context: api.Use(func(ctx *context.Context) { if version := ctx.URLParam("version"); version != "" { versioning.SetVersion(ctx, version) } ctx.Next() }) OR: api.Use(versioning.FromQuery("version", "")) */ // |----------------| // | The fun begins | // |----------------| // Create a new Group, which is a compatible Party, // based on version constraints. v1 := versioning.NewGroup(api, ">=1.0.0 <2.0.0") // To mark an API version as deprecated use the Deprecated method. // v1.Deprecated(versioning.DefaultDeprecationOptions) // Optionally, set custom view engine and path // for templates based on the version. v1.RegisterView(iris.HTML("./v1", ".html")) // Optionally, set custom error handler(s) based on the version. // Keep in mind that if you do this, you will // have to register error handlers // for the rest of the parties as well. v1.OnErrorCode(iris.StatusNotFound, testError("v1")) // Register resources based on the version. v1.Get("/", testHandler("v1")) v1.Get("/render", testView) // Do the same for version 2 and version 3, // for the sake of the example. v2 := versioning.NewGroup(api, ">=2.0.0 <3.0.0") v2.RegisterView(iris.HTML("./v2", ".html")) v2.OnErrorCode(iris.StatusNotFound, testError("v2")) v2.Get("/", testHandler("v2")) v2.Get("/render", testView) v3 := versioning.NewGroup(api, ">=3.0.0 <4.0.0") v3.RegisterView(iris.HTML("./v3", ".html")) v3.OnErrorCode(iris.StatusNotFound, testError("v3")) v3.Get("/", testHandler("v3")) v3.Get("/render", testView) app.Listen(":8080") } func testHandler(v string) iris.Handler { return func(ctx iris.Context) { ctx.JSON(iris.Map{ "version": v, "message": "Hello, world!", }) } } func testError(v string) iris.Handler { return func(ctx iris.Context) { ctx.Writef("not found: %s", v) } } func testView(ctx iris.Context) { if err := ctx.View("index.html"); err != nil { ctx.HTML(fmt.Sprintf("

%s

", err.Error())) return } } ================================================ FILE: _examples/routing/versioning/v1/index.html ================================================

This is the directory for version 1 templates

================================================ FILE: _examples/routing/versioning/v2/index.html ================================================

This is the directory for version 2 templates

================================================ FILE: _examples/routing/versioning/v3/index.html ================================================

This is the directory for version 3 templates

================================================ FILE: _examples/routing/writing-a-middleware/globally/main.go ================================================ package main import "github.com/kataras/iris/v12" func main() { app := iris.New() // register the "before" handler as the first handler which will be executed // on all domain's routes. // Or use the `UseGlobal` to register a middleware which will fire across subdomains. // app.Use(before) // register the "after" handler as the last handler which will be executed // after all domain's routes' handler(s). // // Or use the `DoneGlobal` to append handlers that will be fired globally. // app.Done(after) // register our routes. app.Get("/", indexHandler) app.Get("/contact", contactHandler) // Order of those calls doesn't matter, `UseGlobal` and `DoneGlobal` // are applied to existing routes and future routes. // // Remember: the `Use` and `Done` are applied to the current party's and its children, // so if we used the `app.Use/Done` before the routes registration // it would work like UseGlobal/DoneGlobal in this case, because the `app` is the root party. // // See `app.Party/PartyFunc` for more. app.UseGlobal(before) app.DoneGlobal(after) app.Listen(":8080") } func before(ctx iris.Context) { shareInformation := "this is a sharable information between handlers" requestPath := ctx.Path() println("Before the indexHandler or contactHandler: " + requestPath) ctx.Values().Set("info", shareInformation) ctx.Next() } func after(ctx iris.Context) { println("After the indexHandler or contactHandler") } func indexHandler(ctx iris.Context) { println("Inside indexHandler") // take the info from the "before" handler. info := ctx.Values().GetString("info") // write something to the client as a response. ctx.HTML("

Response

") ctx.HTML("
Info: " + info) ctx.Next() // execute the "after" handler registered via `DoneGlobal`. } func contactHandler(ctx iris.Context) { println("Inside contactHandler") // write something to the client as a response. ctx.HTML("

Contact

") ctx.Next() // execute the "after" handler registered via `DoneGlobal`. } ================================================ FILE: _examples/routing/writing-a-middleware/per-route/main.go ================================================ package main import "github.com/kataras/iris/v12" func main() { app := iris.New() // or app.Use(before) and app.Done(after). app.Get("/", before, mainHandler, after) // Use registers a middleware(prepend handlers) to all party's, and its children that will be registered // after. // // (`app` is the root children so those use and done handlers will be registered everywhere) app.Use(func(ctx iris.Context) { println(`before the party's routes and its children, but this is not applied to the '/' route because it's registered before the middleware, order matters.`) ctx.Next() }) app.Done(func(ctx iris.Context) { println("this is executed always last, if the previous handler calls the `ctx.Next()`, it's the reverse of `.Use`") message := ctx.Values().GetString("message") println("message: " + message) }) app.Get("/home", func(ctx iris.Context) { ctx.HTML("

Home

") ctx.Values().Set("message", "this is the home message, ip: "+ctx.RemoteAddr()) ctx.Next() // call the done handlers. }) child := app.Party("/child") child.Get("/", func(ctx iris.Context) { ctx.Writef(`this is the localhost:8080/child route. All Use and Done handlers that are registered to the parent party, are applied here as well.`) ctx.Next() // call the done handlers. }) app.Listen(":8080") } func before(ctx iris.Context) { shareInformation := "this is a sharable information between handlers" requestPath := ctx.Path() println("Before the mainHandler: " + requestPath) ctx.Values().Set("info", shareInformation) ctx.Next() // execute the next handler, in this case the main one. } func after(ctx iris.Context) { println("After the mainHandler") } func mainHandler(ctx iris.Context) { println("Inside mainHandler") // take the info from the "before" handler. info := ctx.Values().GetString("info") // write something to the client as a response. ctx.HTML("

Response

") ctx.HTML("
Info: " + info) ctx.Next() // execute the "after". } ================================================ FILE: _examples/routing/writing-a-middleware/share-funcs/main.go ================================================ // Package main shows how you can share a // function between handlers of the same chain. // Note that, this case is very rarely used and it exists, // mostly, for 3rd-party middleware creators. // // The middleware creator registers a dynamic function by Context.SetFunc and // the route handler just needs to call Context.CallFunc(funcName, arguments), // without knowning what is the specific middleware's implementation or who was the creator // of that function, it may be a basicauth middleware's logout or session's logout. // // See Context.SetLogoutFunc and Context.Logout methods too (these are not covered here). package main import ( "fmt" "github.com/kataras/iris/v12" ) func main() { app := newApp() // GET: http://localhost:8080 app.Listen(":8080") } func newApp() *iris.Application { app := iris.New() app.Use(middleware) // OR app.UseRouter(middleware) // to register it everywhere, // including the HTTP errors. app.Get("/", handler) app.Get("/2", middleware2, handler2) app.Get("/3", middleware3, handler3) return app } // Assume: this is a middleware which does not export // the 'hello' function for several reasons // but we offer a 'greeting' optional feature to the route handler. func middleware(ctx iris.Context) { ctx.SetFunc("greet", hello) ctx.Next() } // Assume: this is a handler which needs to "greet" the client but // the function for that job is not predictable, // it may change - dynamically (SetFunc) - depending on // the middlewares registered before this route handler. // E.g. it may be a "Hello $name" or "Greetings $Name". func handler(ctx iris.Context) { outputs, err := ctx.CallFunc("greet", "Gophers") if err != nil { ctx.StopWithError(iris.StatusInternalServerError, err) return } response := outputs[0].Interface().(string) ctx.WriteString(response) } func middleware2(ctx iris.Context) { ctx.SetFunc("greet", sayHello) ctx.Next() } func handler2(ctx iris.Context) { _, err := ctx.CallFunc("greet", "Gophers [2]") if err != nil { ctx.StopWithError(iris.StatusInternalServerError, err) return } } func middleware3(ctx iris.Context) { ctx.SetFunc("job", function3) ctx.Next() } func handler3(ctx iris.Context) { _, err := ctx.CallFunc("job") if err != nil { ctx.StopWithError(iris.StatusInternalServerError, err) return } ctx.WriteString("OK, job was executed.\nSee the command prompt.") } /* | ------------------------ | | function implementations | | ------------------------ | */ func hello(name string) string { return fmt.Sprintf("Hello, %s!", name) } func sayHello(ctx iris.Context, name string) { ctx.WriteString(hello(name)) } func function3() { fmt.Printf("function3 called\n") } ================================================ FILE: _examples/routing/writing-a-middleware/share-funcs/main_test.go ================================================ package main import ( "testing" "github.com/kataras/iris/v12/httptest" ) func TestShareFuncs(t *testing.T) { app := newApp() e := httptest.New(t, app) e.GET("/").Expect().Status(httptest.StatusOK).Body().IsEqual("Hello, Gophers!") e.GET("/2").Expect().Status(httptest.StatusOK).Body().IsEqual("Hello, Gophers [2]!") e.GET("/3").Expect().Status(httptest.StatusOK).Body().IsEqual("OK, job was executed.\nSee the command prompt.") } ================================================ FILE: _examples/routing/writing-a-middleware/share-services/main.go ================================================ package main import ( "fmt" "github.com/kataras/iris/v12" ) func main() { app := newApp() // GET: http://localhost:8080 app.Listen(":8080") } func newApp() *iris.Application { app := iris.New() app.Use(middleware) // OR app.UseRouter(middleware) // to register it everywhere, // including the HTTP errors. app.Get("/", handler) return app } func middleware(ctx iris.Context) { service := &helloService{ Greeting: "Hello", } setService(ctx, service) ctx.Next() } func handler(ctx iris.Context) { service, ok := getService(ctx) if !ok { ctx.StopWithStatus(iris.StatusInternalServerError) return } response := service.Greet("Gophers") ctx.WriteString(response) } /* | ---------------------- | | service implementation | | ---------------------- | */ const serviceContextKey = "app.service" func setService(ctx iris.Context, service GreetService) { ctx.Values().Set(serviceContextKey, service) } func getService(ctx iris.Context) (GreetService, bool) { v := ctx.Values().Get(serviceContextKey) if v == nil { return nil, false } service, ok := v.(GreetService) if !ok { return nil, false } return service, true } // A GreetService example. type GreetService interface { Greet(name string) string } type helloService struct { Greeting string } func (m *helloService) Greet(name string) string { return fmt.Sprintf("%s, %s!", m.Greeting, name) } ================================================ FILE: _examples/routing/writing-a-middleware/share-services/main_test.go ================================================ package main import ( "testing" "github.com/kataras/iris/v12/httptest" ) func TestShareServices(t *testing.T) { app := newApp() e := httptest.New(t, app) e.GET("/").Expect().Status(httptest.StatusOK).Body().IsEqual("Hello, Gophers!") } ================================================ FILE: _examples/sessions/basic/main.go ================================================ package main import ( "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/sessions" ) const cookieNameForSessionID = "session_id_cookie" func secret(ctx iris.Context) { // Check if user is authenticated if auth, _ := sessions.Get(ctx).GetBoolean("authenticated"); !auth { ctx.StatusCode(iris.StatusForbidden) return } // Print secret message ctx.WriteString("The cake is a lie!") } func login(ctx iris.Context) { session := sessions.Get(ctx) // Authentication goes here // ... // Set user as authenticated session.Set("authenticated", true) } func logout(ctx iris.Context) { session := sessions.Get(ctx) // Revoke users authentication session.Set("authenticated", false) } func main() { app := iris.New() sess := sessions.New(sessions.Config{ Cookie: cookieNameForSessionID, // CookieSecureTLS: true, AllowReclaim: true, }) app.Use(sess.Handler()) // ^ or comment this line and use sess.Start(ctx) inside your handlers // instead of sessions.Get(ctx). app.Get("/secret", secret) app.Get("/login", login) app.Get("/logout", logout) app.Listen(":8080") } ================================================ FILE: _examples/sessions/database/badger/main.go ================================================ package main import ( "time" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/sessions" "github.com/kataras/iris/v12/sessions/sessiondb/badger" "github.com/kataras/iris/v12/_examples/sessions/overview/example" ) func main() { db, err := badger.New("./data") if err != nil { panic(err) } // close and unlock the database when control+C/cmd+C pressed iris.RegisterOnInterrupt(func() { db.Close() }) defer db.Close() // close and unlock the database if application errored. // The default transcoder is the JSON one, // based on the https://golang.org/pkg/encoding/json/#Unmarshal // you can only retrieve numbers as float64 types: // * bool, for booleans // * float64, for numbers // * string, for strings // * []any, for arrays // * map[string]any, for objects. // If you want to save the data per go-specific types // you should change the DefaultTranscoder to the GobTranscoder one: // sessions.DefaultTranscoder = sessions.GobTranscoder{} sess := sessions.New(sessions.Config{ Cookie: "sessionscookieid", Expires: 1 * time.Minute, // <=0 means unlimited life. Defaults to 0. AllowReclaim: true, }) sess.OnDestroy(func(sid string) { println(sid + " expired and destroyed from memory and its values from database") }) // // IMPORTANT: // sess.UseDatabase(db) app := example.NewApp(sess) app.Listen(":8080") } ================================================ FILE: _examples/sessions/database/boltdb/main.go ================================================ package main import ( "os" "time" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/sessions" "github.com/kataras/iris/v12/sessions/sessiondb/boltdb" "github.com/kataras/iris/v12/_examples/sessions/overview/example" ) func main() { db, err := boltdb.New("./sessions.db", os.FileMode(0750)) if err != nil { panic(err) } // close and unlobkc the database when control+C/cmd+C pressed iris.RegisterOnInterrupt(func() { db.Close() }) defer db.Close() // close and unlock the database if application errored. sess := sessions.New(sessions.Config{ Cookie: "sessionscookieid", Expires: 45 * time.Minute, // <=0 means unlimited life. Defaults to 0. AllowReclaim: true, }) // // IMPORTANT: // sess.UseDatabase(db) // The default database's values encoder and decoder // calls the value's `Marshal/Unmarshal` methods (if any) // otherwise JSON is selected, // the JSON format can be stored to any database and // it supports both builtin language types(e.g. string, int) and custom struct values. // Also, and the most important, the values can be // retrieved/logged/monitored by a third-party program // written in any other language as well. // // You can change this behavior by registering a custom `Transcoder`. // Iris provides a `GobTranscoder` which is mostly suitable // if your session values are going to be custom Go structs. // Select this if you always retrieving values through Go. // Don't forget to initialize a call of gob.Register when necessary. // Read https://golang.org/pkg/encoding/gob/ for more. // // You can also implement your own `sessions.Transcoder` and use it, // i.e: a transcoder which will allow(on Marshal: return its byte representation and nil error) // or dissalow(on Marshal: return non nil error) certain types. // // gob.Register(example.BusinessModel{}) // sessions.DefaultTranscoder = sessions.GobTranscoder{} app := example.NewApp(sess) app.Listen(":8080") } ================================================ FILE: _examples/sessions/database/redis/Dockerfile ================================================ FROM golang:latest AS builder RUN apt-get update WORKDIR /go/src/app ENV GO111MODULE=on \ CGO_ENABLED=0 \ GOOS=linux \ GOARCH=amd64 # Caching go modules and build the binary. COPY go.mod . RUN go mod download COPY . . RUN go install FROM scratch COPY --from=builder /go/bin/app . ENTRYPOINT ["./app"] ================================================ FILE: _examples/sessions/database/redis/docker-compose.yml ================================================ # docker-compose up [--build] version: '3' services: redis-server: image: redis app1: build: . depends_on: - redis-server ports: - 8080:8080 environment: - PORT=8080 - REDIS_ADDR=redis-server:6379 app2: build: . depends_on: - redis-server ports: - 9090:9090 environment: - PORT=9090 - REDIS_ADDR=redis-server:6379 ================================================ FILE: _examples/sessions/database/redis/go.mod ================================================ module app go 1.25 require github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd require ( github.com/BurntSushi/toml v1.6.0 // indirect github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect github.com/CloudyKit/jet/v6 v6.3.1 // indirect github.com/Joker/jade v1.1.3 // indirect github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 // indirect github.com/andybalholm/brotli v1.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/fatih/structs v1.1.0 // indirect github.com/flosch/pongo2/v4 v4.0.2 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/iris-contrib/schema v0.0.6 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/kataras/blocks v0.0.8 // indirect github.com/kataras/golog v0.1.12 // indirect github.com/kataras/pio v0.0.13 // indirect github.com/kataras/sitemap v0.0.6 // indirect github.com/kataras/tunnel v0.0.4 // indirect github.com/klauspost/compress v1.18.2 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mailgun/raymond/v2 v2.0.48 // indirect github.com/mailru/easyjson v0.9.1 // indirect github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/redis/go-redis/v9 v9.17.2 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/schollz/closestmatch v2.1.0+incompatible // indirect github.com/sirupsen/logrus v1.8.1 // indirect github.com/stretchr/testify v1.11.1 // indirect github.com/tdewolff/minify/v2 v2.24.8 // indirect github.com/tdewolff/parse/v2 v2.8.5 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/yosssi/ace v0.0.5 // indirect golang.org/x/crypto v0.47.0 // indirect golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect golang.org/x/net v0.49.0 // indirect golang.org/x/sys v0.40.0 // indirect golang.org/x/text v0.33.0 // indirect golang.org/x/time v0.14.0 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: _examples/sessions/database/redis/go.sum ================================================ github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= github.com/CloudyKit/jet/v6 v6.3.1 h1:6IAo5Cx21xrHVaR8zzXN5gJatKV/wO7Nf6bfCnCSbUw= github.com/CloudyKit/jet/v6 v6.3.1/go.mod h1:lf8ksdNsxZt7/yH/3n4vJQWA9RUq4wpaHtArHhGVMOw= github.com/Joker/hpp v1.0.0 h1:65+iuJYdRXv/XyN62C1uEmmOx3432rNG/rKlX6V7Kkc= github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= github.com/Joker/jade v1.1.3 h1:Qbeh12Vq6BxURXT1qZBRHsDxeURB8ztcL6f3EXSGeHk= github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKduM= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 h1:dG7/gLroJGht/jSQtHiLvT48Hxn+crbmvyItZC8cWOs= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05/go.mod h1:NYezi6wtnJtBm5btoprXc5SvAdqH0XTXWnUup0MptAI= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/flosch/pongo2/v4 v4.0.2 h1:gv+5Pe3vaSVmiJvh/BZa82b7/00YUGm0PIyVVLop0Hw= github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+BWPhwZD8= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a h1:l7A0loSszR5zHd/qK53ZIHMO8b3bBSmENnQ6eKnUT0A= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/iris-contrib/httpexpect/v2 v2.15.2 h1:T9THsdP1woyAqKHwjkEsbCnMefsAFvk8iJJKokcJ3Go= github.com/iris-contrib/httpexpect/v2 v2.15.2/go.mod h1:JLDgIqnFy5loDSUv1OA2j0mb6p/rDhiCqigP22Uq9xE= github.com/iris-contrib/schema v0.0.6 h1:CPSBLyx2e91H2yJzPuhGuifVRnZBBJ3pCOMbOvPZaTw= github.com/iris-contrib/schema v0.0.6/go.mod h1:iYszG0IOsuIsfzjymw1kMzTL8YQcCWlm65f3wX8J5iA= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kataras/blocks v0.0.8 h1:MrpVhoFTCR2v1iOOfGng5VJSILKeZZI+7NGfxEh3SUM= github.com/kataras/blocks v0.0.8/go.mod h1:9Jm5zx6BB+06NwA+OhTbHW1xkMOYxahnqTN5DveZ2Yg= github.com/kataras/golog v0.1.12 h1:Bu7I/G4ilJlbfzjmU39O9N+2uO1pBcMK045fzZ4ytNg= github.com/kataras/golog v0.1.12/go.mod h1:wrGSbOiBqbQSQznleVNX4epWM8rl9SJ/rmEacl0yqy4= github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd h1:oMsQgqKACbUdlaIWnN+EZzzAnHkw90hE/y/R0BSij2U= github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd/go.mod h1:yucjtDl9Vn3OctWM1fi0MuM9OckemC7kEreFa9QtPF8= github.com/kataras/pio v0.0.13 h1:x0rXVX0fviDTXOOLOmr4MUxOabu1InVSTu5itF8CXCM= github.com/kataras/pio v0.0.13/go.mod h1:k3HNuSw+eJ8Pm2lA4lRhg3DiCjVgHlP8hmXApSej3oM= github.com/kataras/sitemap v0.0.6 h1:w71CRMMKYMJh6LR2wTgnk5hSgjVNB9KL60n5e2KHvLY= github.com/kataras/sitemap v0.0.6/go.mod h1:dW4dOCNs896OR1HmG+dMLdT7JjDk7mYBzoIRwuj5jA4= github.com/kataras/tunnel v0.0.4 h1:sCAqWuJV7nPzGrlb0os3j49lk2JhILT0rID38NHNLpA= github.com/kataras/tunnel v0.0.4/go.mod h1:9FkU4LaeifdMWqZu7o20ojmW4B7hdhv2CMLwfnHGpYw= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw= github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18= github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI= github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo= github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk= github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tdewolff/minify/v2 v2.24.8 h1:58/VjsbevI4d5FGV0ZSuBrHMSSkH4MCH0sIz/eKIauE= github.com/tdewolff/minify/v2 v2.24.8/go.mod h1:0Ukj0CRpo/sW/nd8uZ4ccXaV1rEVIWA3dj8U7+Shhfw= github.com/tdewolff/parse/v2 v2.8.5 h1:ZmBiA/8Do5Rpk7bDye0jbbDUpXXbCdc3iah4VeUvwYU= github.com/tdewolff/parse/v2 v2.8.5/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo= github.com/tdewolff/test v1.0.11 h1:FdLbwQVHxqG16SlkGveC0JVyrJN62COWTRyUFzfbtBE= github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA= github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0= github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs= moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHcE= ================================================ FILE: _examples/sessions/database/redis/main.go ================================================ package main import ( "fmt" "os" "strings" "time" "github.com/kataras/iris/v12/sessions" "github.com/kataras/iris/v12/sessions/sessiondb/redis" "github.com/kataras/iris/v12/_examples/sessions/overview/example" ) // 1. Install Redis: // 1.1 Windows: https://github.com/ServiceStack/redis-windows // 1.2 Other: https://redis.io/download // 2. Run command: go run -mod=mod main.go // // Tested with redis version 3.0.503. func main() { // These are the default values, // you can replace them based on your running redis' server settings: db := redis.New(redis.Config{ Network: "tcp", Addr: getenv("REDIS_ADDR", "127.0.0.1:6379"), Timeout: time.Duration(30) * time.Second, MaxActive: 10, Username: "", Password: "", Database: "", Prefix: "myapp-", Driver: redis.GoRedis(), // defaults to this driver. // To set a custom, existing go-redis client, use the "SetClient" method: // Driver: redis.GoRedis().SetClient(customGoRedisClient) }) // Optionally configure the underline driver: // driver := redis.GoRedis() // // To set a custom client: // driver.SetClient(customGoRedisClient) // OR: // driver.ClientOptions = redis.Options{...} // driver.ClusterOptions = redis.ClusterOptions{...} // redis.New(redis.Config{Driver: driver, ...}) defer db.Close() // close the database connection if application errored. sess := sessions.New(sessions.Config{ Cookie: "_session_id", Expires: 0, // defaults to 0: unlimited life. Another good value is: 45 * time.Minute, AllowReclaim: true, CookieSecureTLS: true, }) // // IMPORTANT: // sess.UseDatabase(db) app := example.NewApp(sess) // TIP scaling-out Iris sessions using redis: // $ docker-compose up // http://localhost:8080/set/$key/$value // The value will be available on all Iris servers as well. // E.g. http://localhost:9090/get/$key and vice versa. addr := fmt.Sprintf(":%s", getenv("PORT", "8080")) app.Listen(addr) } func getenv(key string, def string) string { if v := os.Getenv(strings.ToUpper(key)); v != "" { return v } return def } ================================================ FILE: _examples/sessions/flash-messages/main.go ================================================ package main import ( "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/sessions" ) func main() { app := iris.New() sess := sessions.New(sessions.Config{Cookie: "_session_id", AllowReclaim: true}) app.Use(sess.Handler()) app.Get("/set", func(ctx iris.Context) { s := sessions.Get(ctx) s.SetFlash("name", "iris") ctx.Writef("Message set, is available for the next request") }) app.Get("/get", func(ctx iris.Context) { s := sessions.Get(ctx) name := s.GetFlashString("name") if name == "" { ctx.Writef("Empty name!!") return } ctx.Writef("Hello %s", name) }) app.Get("/test", func(ctx iris.Context) { s := sessions.Get(ctx) name := s.GetFlashString("name") if name == "" { ctx.Writef("Empty name!!") return } ctx.Writef("Ok you are coming from /set ,the value of the name is %s", name) ctx.Writef(", and again from the same context: %s", name) }) app.Listen(":8080") } ================================================ FILE: _examples/sessions/overview/example/example.go ================================================ package example import ( "errors" "fmt" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/sessions" ) // BusinessModel is just a Go struct value that we will use in our session example, // never save sensitive information, like passwords, here. type BusinessModel struct { Name string } // NewApp returns a new application for showcasing the sessions feature. func NewApp(sess *sessions.Sessions) *iris.Application { app := iris.New() app.Use(sess.Handler()) // register the session manager on a group of routes or the root app. app.Get("/", func(ctx iris.Context) { session := sessions.Get(ctx) // same as sess.Start(ctx, cookieOptions...) if session.Len() == 0 { ctx.HTML(`no session values stored yet. Navigate to: set page`) return } ctx.HTML("
    ") session.Visit(func(key string, value any) { ctx.HTML(fmt.Sprintf("
  • %s = %v
  • ", key, value)) }) ctx.HTML("
") }) // set session values. app.Get("/set", func(ctx iris.Context) { session := sessions.Get(ctx) isNew := session.IsNew() session.Set("username", "iris") ctx.Writef("All ok session set to: %s [isNew=%t]", session.GetString("username"), isNew) }) app.Get("/get", func(ctx iris.Context) { session := sessions.Get(ctx) // get a specific value, as string, // if not found then it returns just an empty string. name := session.GetString("username") ctx.Writef("The username on the /set was: %s", name) }) app.Get("/set-struct", func(ctx iris.Context) { session := sessions.Get(ctx) session.Set("struct", BusinessModel{Name: "John Doe"}) ctx.WriteString("All ok session value of the 'struct' was set.") }) app.Get("/get-struct", func(ctx iris.Context) { session := sessions.Get(ctx) var v BusinessModel if err := session.Decode("struct", &v); err != nil { ctx.StopWithError(iris.StatusInternalServerError, err) return } ctx.Writef("Session value of the 'struct' is: %#+v", v) }) app.Get("/set/{key}/{value}", func(ctx iris.Context) { session := sessions.Get(ctx) key := ctx.Params().Get("key") value := ctx.Params().Get("value") isNew := session.IsNew() session.Set(key, value) ctx.Writef("All ok session value of the '%s' is: %s [isNew=%t]", key, session.GetString(key), isNew) }) app.Get("/get/{key}", func(ctx iris.Context) { session := sessions.Get(ctx) // get a specific key, as string, if no found returns just an empty string key := ctx.Params().Get("key") value := session.Get(key) ctx.Writef("The [%s:%T] on the /set was: %v", key, value, value) }) app.Get("/set/{type}/{key}/{value}", func(ctx iris.Context) { session := sessions.Get(ctx) key := ctx.Params().Get("key") var value any switch ctx.Params().Get("type") { case "int": value = ctx.Params().GetIntDefault("value", 0) case "float64": value = ctx.Params().GetFloat64Default("value", 0.0) default: value = ctx.Params().Get("value") } session.Set(key, value) value = session.Get(key) ctx.Writef("Key: %s, Type: %T, Value: %v", key, value, value) }) app.Get("/delete", func(ctx iris.Context) { session := sessions.Get(ctx) // delete a specific key session.Delete("username") }) app.Get("/clear", func(ctx iris.Context) { session := sessions.Get(ctx) // removes all entries. session.Clear() }) app.Get("/update", func(ctx iris.Context) { session := sessions.Get(ctx) // shifts the expiration based on the session's `Lifetime`. if err := session.Man.ShiftExpiration(ctx); err != nil { if errors.Is(err, sessions.ErrNotFound) { ctx.StatusCode(iris.StatusNotFound) } else if errors.Is(err, sessions.ErrNotImplemented) { ctx.StatusCode(iris.StatusNotImplemented) } else { ctx.StatusCode(iris.StatusNotModified) } ctx.Writef("%v", err) ctx.Application().Logger().Error(err) } }) app.Get("/destroy", func(ctx iris.Context) { session := sessions.Get(ctx) // Man(anager)'s Destroy, removes the entire session data and cookie session.Man.Destroy(ctx) }) // Note about Destroy: // // You can destroy a session outside of a handler too, using the: // sess.DestroyByID // sess.DestroyAll // remember: slices and maps are muttable by-design // The `SetImmutable` makes sure that they will be stored and received // as immutable, so you can't change them directly by mistake. // // Use `SetImmutable` consistently, it's slower than `Set`. // Read more about muttable and immutable go types: https://stackoverflow.com/a/8021081 app.Get("/set-immutable", func(ctx iris.Context) { session := sessions.Get(ctx) business := []BusinessModel{{Name: "Edward"}, {Name: "value 2"}} session.SetImmutable("businessEdit", business) var businessGet []BusinessModel err := session.Decode("businessEdit", &businessGet) if err != nil { ctx.StopWithError(iris.StatusInternalServerError, err) return } // try to change it, if we used `Set` instead of `SetImmutable` this // change will affect the underline array of the session's value "businessEdit", but now it will not. businessGet[0].Name = "Gabriel" }) app.Get("/get-immutable", func(ctx iris.Context) { var models []BusinessModel err := sessions.Get(ctx).Decode("businessEdit", &models) if err != nil { ctx.StopWithError(iris.StatusInternalServerError, err) return } if models == nil { ctx.HTML("please navigate to the /set-immutable first") return } firstModel := models[0] // businessGet[0].Name is equal to Edward initially if firstModel.Name != "Edward" { panic("Report this as a bug, immutable data cannot be changed from the caller without re-SetImmutable") } ctx.Writef("[]businessModel[0].Name remains: %s", firstModel.Name) // the name should remains "Edward" }) return app } ================================================ FILE: _examples/sessions/overview/main.go ================================================ package main import ( "time" "github.com/kataras/iris/v12/_examples/sessions/overview/example" "github.com/kataras/iris/v12/sessions" ) func main() { sess := sessions.New(sessions.Config{ // Cookie string, the session's client cookie name, for example: "_session_id" // // Defaults to "irissessionid" Cookie: "_session_id", // it's time.Duration, from the time cookie is created, how long it can be alive? // 0 means no expire, unlimited life. // -1 means expire when browser closes // or set a value, like 2 hours: Expires: time.Hour * 2, // if you want to invalid cookies on different subdomains // of the same host, then enable it. // Defaults to false. DisableSubdomainPersistence: false, // Allow getting the session value stored by the request from the same request. AllowReclaim: true, /* SessionIDGenerator: func(ctx iris.Context) string { id:= ctx.GetHeader("X-Session-Id") if id == "" { id = // [generate ID here and set the header] ctx.Header("X-Session-Id", id) } return id }, */ }) app := example.NewApp(sess) app.Listen(":8080") } ================================================ FILE: _examples/sessions/securecookie/main.go ================================================ package main // developers can use any library to add a custom cookie encoder/decoder. // At this example we use the gorilla's securecookie package: // $ go get github.com/gorilla/securecookie // $ go run main.go import ( "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/sessions" "github.com/kataras/iris/v12/_examples/sessions/overview/example" "github.com/gorilla/securecookie" ) func newApp() *iris.Application { cookieName := "_session_id" // AES only supports key sizes of 16, 24 or 32 bytes. // You either need to provide exactly that amount or you derive the key from what you type in. hashKey := securecookie.GenerateRandomKey(64) blockKey := securecookie.GenerateRandomKey(32) s := securecookie.New(hashKey, blockKey) mySessions := sessions.New(sessions.Config{ Cookie: cookieName, Encoding: s, AllowReclaim: true, }) // mySessions.UseDatabase(...see sessions/database example folder) return example.NewApp(mySessions) } func main() { app := newApp() app.Listen(":8080") } ================================================ FILE: _examples/sessions/securecookie/main_test.go ================================================ package main import ( "testing" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/httptest" ) func TestSessionsEncodeDecode(t *testing.T) { app := newApp() e := httptest.New(t, app, httptest.URL("http://example.com")) es := e.GET("/set").Expect() es.Status(iris.StatusOK) es.Cookies().NotEmpty() es.Body().IsEqual("All ok session set to: iris [isNew=true]") e.GET("/get").Expect().Status(iris.StatusOK).Body().IsEqual("The username on the /set was: iris") // delete and re-get e.GET("/delete").Expect().Status(iris.StatusOK) e.GET("/get").Expect().Status(iris.StatusOK).Body().IsEqual("The username on the /set was: ") // set, clear and re-get e.GET("/set").Expect().Body().IsEqual("All ok session set to: iris [isNew=false]") e.GET("/clear").Expect().Status(iris.StatusOK) e.GET("/get").Expect().Status(iris.StatusOK).Body().IsEqual("The username on the /set was: ") } ================================================ FILE: _examples/sessions/viewdata/main.go ================================================ package main import ( "fmt" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/sessions" ) func main() { app := iris.New() app.RegisterView(iris.HTML("./views", ".html")) sess := sessions.New(sessions.Config{Cookie: "session_cookie", AllowReclaim: true}) app.Use(sess.Handler()) // ^ use app.UseRouter instead to access sessions on HTTP errors too. // Register our custom middleware, after the sessions middleware. app.Use(setSessionViewData) app.Get("/", index) app.Listen(":8080") } func setSessionViewData(ctx iris.Context) { session := sessions.Get(ctx) ctx.ViewData("session", session) ctx.Next() } func index(ctx iris.Context) { session := sessions.Get(ctx) session.Set("username", "kataras") if err := ctx.View("index"); err != nil { ctx.HTML(fmt.Sprintf("

%s

", err.Error())) return } /* OR without middleware: if err := ctx.View("index", iris.Map{ "session": session, // {{.session.Get "username"}} // OR to pass only the 'username': // "username": session.Get("username"), // {{.username}} }) */ } ================================================ FILE: _examples/sessions/viewdata/views/index.html ================================================ Sessions View Data Hello {{.session.Get "username"}} ================================================ FILE: _examples/testing/ginkgotest/ginkgotest_suite_test.go ================================================ package main_test import ( "github.com/kataras/iris/v12" "testing" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) func TestGinkgotest(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "Ginkgotest Suite") } func newApp(authentication iris.Handler) *iris.Application { app := iris.New() app.Get("/", func(ctx iris.Context) { ctx.Redirect("/admin") }) // to party needAuth := app.Party("/admin", authentication) { //http://localhost:8080/admin needAuth.Get("/", h) // http://localhost:8080/admin/profile needAuth.Get("/profile", h) // http://localhost:8080/admin/settings needAuth.Get("/settings", h) } return app } func h(ctx iris.Context) { username, password, _ := ctx.Request().BasicAuth() // third parameter it will be always true because the middleware // makes sure for that, otherwise this handler will not be executed. // OR: // // user := ctx.User().(*myUserType) // ctx.Writef("%s %s:%s", ctx.Path(), user.Username, user.Password) // OR if you don't have registered custom User structs: // // ctx.User().GetUsername() // ctx.User().GetPassword() ctx.Writef("%s %s:%s", ctx.Path(), username, password) } ================================================ FILE: _examples/testing/ginkgotest/go.mod ================================================ module ginkgotest go 1.25 require ( github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd github.com/onsi/ginkgo/v2 v2.11.0 github.com/onsi/gomega v1.27.10 ) require ( github.com/BurntSushi/toml v1.6.0 // indirect github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect github.com/CloudyKit/jet/v6 v6.3.1 // indirect github.com/Joker/jade v1.1.3 // indirect github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 // indirect github.com/ajg/form v1.5.1 // indirect github.com/andybalholm/brotli v1.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fatih/color v1.15.0 // indirect github.com/fatih/structs v1.1.0 // indirect github.com/flosch/pongo2/v4 v4.0.2 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/imkira/go-interpol v1.1.0 // indirect github.com/iris-contrib/httpexpect/v2 v2.15.2 // indirect github.com/iris-contrib/schema v0.0.6 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/kataras/blocks v0.0.8 // indirect github.com/kataras/golog v0.1.12 // indirect github.com/kataras/pio v0.0.13 // indirect github.com/kataras/sitemap v0.0.6 // indirect github.com/kataras/tunnel v0.0.4 // indirect github.com/klauspost/compress v1.18.2 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mailgun/raymond/v2 v2.0.48 // indirect github.com/mailru/easyjson v0.9.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sanity-io/litter v1.5.5 // indirect github.com/schollz/closestmatch v2.1.0+incompatible // indirect github.com/sergi/go-diff v1.0.0 // indirect github.com/sirupsen/logrus v1.8.1 // indirect github.com/stretchr/testify v1.11.1 // indirect github.com/tdewolff/minify/v2 v2.24.8 // indirect github.com/tdewolff/parse/v2 v2.8.5 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 // indirect github.com/yosssi/ace v0.0.5 // indirect github.com/yudai/gojsondiff v1.0.0 // indirect github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect golang.org/x/crypto v0.47.0 // indirect golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect golang.org/x/net v0.49.0 // indirect golang.org/x/sys v0.40.0 // indirect golang.org/x/text v0.33.0 // indirect golang.org/x/time v0.14.0 // indirect golang.org/x/tools v0.40.0 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect moul.io/http2curl/v2 v2.3.0 // indirect ) ================================================ FILE: _examples/testing/ginkgotest/go.sum ================================================ github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= github.com/CloudyKit/jet/v6 v6.3.1 h1:6IAo5Cx21xrHVaR8zzXN5gJatKV/wO7Nf6bfCnCSbUw= github.com/CloudyKit/jet/v6 v6.3.1/go.mod h1:lf8ksdNsxZt7/yH/3n4vJQWA9RUq4wpaHtArHhGVMOw= github.com/Joker/hpp v1.0.0 h1:65+iuJYdRXv/XyN62C1uEmmOx3432rNG/rKlX6V7Kkc= github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= github.com/Joker/jade v1.1.3 h1:Qbeh12Vq6BxURXT1qZBRHsDxeURB8ztcL6f3EXSGeHk= github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKduM= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 h1:dG7/gLroJGht/jSQtHiLvT48Hxn+crbmvyItZC8cWOs= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05/go.mod h1:NYezi6wtnJtBm5btoprXc5SvAdqH0XTXWnUup0MptAI= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/flosch/pongo2/v4 v4.0.2 h1:gv+5Pe3vaSVmiJvh/BZa82b7/00YUGm0PIyVVLop0Hw= github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+BWPhwZD8= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a h1:l7A0loSszR5zHd/qK53ZIHMO8b3bBSmENnQ6eKnUT0A= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/iris-contrib/httpexpect/v2 v2.15.2 h1:T9THsdP1woyAqKHwjkEsbCnMefsAFvk8iJJKokcJ3Go= github.com/iris-contrib/httpexpect/v2 v2.15.2/go.mod h1:JLDgIqnFy5loDSUv1OA2j0mb6p/rDhiCqigP22Uq9xE= github.com/iris-contrib/schema v0.0.6 h1:CPSBLyx2e91H2yJzPuhGuifVRnZBBJ3pCOMbOvPZaTw= github.com/iris-contrib/schema v0.0.6/go.mod h1:iYszG0IOsuIsfzjymw1kMzTL8YQcCWlm65f3wX8J5iA= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kataras/blocks v0.0.8 h1:MrpVhoFTCR2v1iOOfGng5VJSILKeZZI+7NGfxEh3SUM= github.com/kataras/blocks v0.0.8/go.mod h1:9Jm5zx6BB+06NwA+OhTbHW1xkMOYxahnqTN5DveZ2Yg= github.com/kataras/golog v0.1.12 h1:Bu7I/G4ilJlbfzjmU39O9N+2uO1pBcMK045fzZ4ytNg= github.com/kataras/golog v0.1.12/go.mod h1:wrGSbOiBqbQSQznleVNX4epWM8rl9SJ/rmEacl0yqy4= github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd h1:oMsQgqKACbUdlaIWnN+EZzzAnHkw90hE/y/R0BSij2U= github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd/go.mod h1:yucjtDl9Vn3OctWM1fi0MuM9OckemC7kEreFa9QtPF8= github.com/kataras/pio v0.0.13 h1:x0rXVX0fviDTXOOLOmr4MUxOabu1InVSTu5itF8CXCM= github.com/kataras/pio v0.0.13/go.mod h1:k3HNuSw+eJ8Pm2lA4lRhg3DiCjVgHlP8hmXApSej3oM= github.com/kataras/sitemap v0.0.6 h1:w71CRMMKYMJh6LR2wTgnk5hSgjVNB9KL60n5e2KHvLY= github.com/kataras/sitemap v0.0.6/go.mod h1:dW4dOCNs896OR1HmG+dMLdT7JjDk7mYBzoIRwuj5jA4= github.com/kataras/tunnel v0.0.4 h1:sCAqWuJV7nPzGrlb0os3j49lk2JhILT0rID38NHNLpA= github.com/kataras/tunnel v0.0.4/go.mod h1:9FkU4LaeifdMWqZu7o20ojmW4B7hdhv2CMLwfnHGpYw= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw= github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18= github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU= github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM= github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7/go.mod h1:zO8QMzTeZd5cpnIkz/Gn6iK0jDfGicM1nynOkkPIl28= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo= github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk= github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8= github.com/tdewolff/minify/v2 v2.24.8 h1:58/VjsbevI4d5FGV0ZSuBrHMSSkH4MCH0sIz/eKIauE= github.com/tdewolff/minify/v2 v2.24.8/go.mod h1:0Ukj0CRpo/sW/nd8uZ4ccXaV1rEVIWA3dj8U7+Shhfw= github.com/tdewolff/parse/v2 v2.8.5 h1:ZmBiA/8Do5Rpk7bDye0jbbDUpXXbCdc3iah4VeUvwYU= github.com/tdewolff/parse/v2 v2.8.5/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo= github.com/tdewolff/test v1.0.11 h1:FdLbwQVHxqG16SlkGveC0JVyrJN62COWTRyUFzfbtBE= github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA= github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0= github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= github.com/yudai/pp v2.0.1+incompatible h1:Q4//iY4pNF6yPLZIigmvcl7k/bPgrcTPIFIcmawg5bI= github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs= moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHcE= ================================================ FILE: _examples/testing/ginkgotest/main.go ================================================ package main import ( "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/middleware/basicauth" ) func newApp() *iris.Application { app := iris.New() opts := basicauth.Options{ Allow: basicauth.AllowUsers(map[string]string{"myusername": "mypassword"}), } authentication := basicauth.New(opts) // or just: basicauth.Default(map...) app.Get("/", func(ctx iris.Context) { ctx.Redirect("/admin") }) // to party needAuth := app.Party("/admin", authentication) { //http://localhost:8080/admin needAuth.Get("/", h) // http://localhost:8080/admin/profile needAuth.Get("/profile", h) // http://localhost:8080/admin/settings needAuth.Get("/settings", h) } return app } func h(ctx iris.Context) { username, password, _ := ctx.Request().BasicAuth() // third parameter it will be always true because the middleware // makes sure for that, otherwise this handler will not be executed. // OR: // // user := ctx.User().(*myUserType) // ctx.Writef("%s %s:%s", ctx.Path(), user.Username, user.Password) // OR if you don't have registered custom User structs: // // ctx.User().GetUsername() // ctx.User().GetPassword() ctx.Writef("%s %s:%s", ctx.Path(), username, password) } func main() { app := newApp() app.Listen(":8080") } ================================================ FILE: _examples/testing/ginkgotest/main_test.go ================================================ package main_test import ( "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/httptest" "github.com/kataras/iris/v12/middleware/basicauth" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) var _ = Describe("Ginkgotest", func() { var ( e *httptest.Expect app *iris.Application opts basicauth.Options authentication iris.Handler // or just: basicauth.Default(map...) ) BeforeEach(func() { opts = basicauth.Options{ Allow: basicauth.AllowUsers(map[string]string{"myusername": "mypassword"}), } authentication = basicauth.New(opts) app = newApp(authentication) e = httptest.New(GinkgoT(), app, httptest.Strict(true)) }) When("no basic auth", Ordered, func() { It("redirects to /admin without basic auth", func() { response := e.GET("/").Expect().Raw() Expect(httptest.StatusUnauthorized).To(Equal(response.StatusCode)) }) It("without basic auth", func() { // without basic auth response := e.GET("/").Expect().Raw() Expect(httptest.StatusUnauthorized).To(Equal(response.StatusCode)) }) }) When("valid basic auth", func() { It("with basic auth /admin", func() { expect := e.GET("/admin").WithBasicAuth("myusername", "mypassword").Expect() Expect(httptest.StatusOK).To(Equal(expect.Raw().StatusCode)) Expect("/admin myusername:mypassword").To(Equal(expect.Body().Raw())) }) It("with basic auth /admin/profile", func() { expect := e.GET("/admin/profile").WithBasicAuth("myusername", "mypassword").Expect() Expect(httptest.StatusOK).To(Equal(expect.Raw().StatusCode)) Expect("/admin/profile myusername:mypassword").To(Equal(expect.Body().Raw())) }) It("with basic auth /admin/profile", func() { expect := e.GET("/admin/settings").WithBasicAuth("myusername", "mypassword").Expect() Expect(httptest.StatusOK).To(Equal(expect.Raw().StatusCode)) Expect("/admin/settings myusername:mypassword").To(Equal(expect.Body().Raw())) }) }) When("invalid basic auth", func() { It("invalid basic auth /admin/settings", func() { expect := e.GET("/admin/settings").WithBasicAuth("invalidusername", "invalidpassword").Expect() Expect(httptest.StatusUnauthorized).To(Equal(expect.Raw().StatusCode)) }) }) }) ================================================ FILE: _examples/testing/httptest/main.go ================================================ package main import ( "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/middleware/basicauth" ) func newApp() *iris.Application { app := iris.New() opts := basicauth.Options{ Allow: basicauth.AllowUsers(map[string]string{"myusername": "mypassword"}), } authentication := basicauth.New(opts) // or just: basicauth.Default(map...) app.Get("/", func(ctx iris.Context) { ctx.Redirect("/admin") }) // to party needAuth := app.Party("/admin", authentication) { //http://localhost:8080/admin needAuth.Get("/", h) // http://localhost:8080/admin/profile needAuth.Get("/profile", h) // http://localhost:8080/admin/settings needAuth.Get("/settings", h) } return app } func h(ctx iris.Context) { username, password, _ := ctx.Request().BasicAuth() // third parameter it will be always true because the middleware // makes sure for that, otherwise this handler will not be executed. // OR: // // user := ctx.User().(*myUserType) // ctx.Writef("%s %s:%s", ctx.Path(), user.Username, user.Password) // OR if you don't have registered custom User structs: // // ctx.User().GetUsername() // ctx.User().GetPassword() ctx.Writef("%s %s:%s", ctx.Path(), username, password) } func main() { app := newApp() app.Listen(":8080") } ================================================ FILE: _examples/testing/httptest/main_test.go ================================================ package main import ( "testing" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/httptest" ) // $ go test -v func TestNewApp(t *testing.T) { app := newApp() e := httptest.New(t, app, httptest.Strict(true)) // redirects to /admin without basic auth e.GET("/").Expect().Status(httptest.StatusUnauthorized) // without basic auth e.GET("/admin").Expect().Status(httptest.StatusUnauthorized) // with valid basic auth e.GET("/admin").WithBasicAuth("myusername", "mypassword").Expect(). Status(httptest.StatusOK).Body().IsEqual("/admin myusername:mypassword") e.GET("/admin/profile").WithBasicAuth("myusername", "mypassword").Expect(). Status(httptest.StatusOK).Body().IsEqual("/admin/profile myusername:mypassword") e.GET("/admin/settings").WithBasicAuth("myusername", "mypassword").Expect(). Status(httptest.StatusOK).Body().IsEqual("/admin/settings myusername:mypassword") // with invalid basic auth e.GET("/admin/settings").WithBasicAuth("invalidusername", "invalidpassword"). Expect().Status(httptest.StatusUnauthorized) } func TestHandlerUsingNetHTTP(t *testing.T) { handler := func(ctx iris.Context) { ctx.WriteString("Hello, World!") } // A shortcut for net/http/httptest.NewRecorder/NewRequest. w := httptest.NewRecorder() r := httptest.NewRequest("GET", "/", nil) httptest.Do(w, r, handler) if expected, got := "Hello, World!", w.Body.String(); expected != got { t.Fatalf("expected body: %s but got: %s", expected, got) } } ================================================ FILE: _examples/url-shortener/README.md ================================================ ## A URL Shortener Service using Go, Iris and Bolt Hackernoon Article: https://medium.com/hackernoon/a-url-shortener-service-using-go-iris-and-bolt-4182f0b00ae7 ================================================ FILE: _examples/url-shortener/factory.go ================================================ package main import ( "net/url" "github.com/google/uuid" ) // Generator the type to generate keys(short urls) type Generator func() string // DefaultGenerator is the defautl url generator var DefaultGenerator = func() string { id, _ := uuid.NewRandom() return id.String() } // Factory is responsible to generate keys(short urls) type Factory struct { store Store generator Generator } // NewFactory receives a generator and a store and returns a new url Factory. func NewFactory(generator Generator, store Store) *Factory { return &Factory{ store: store, generator: generator, } } // Gen generates the key. func (f *Factory) Gen(uri string) (key string, err error) { // we don't return the parsed url because #hash are converted to uri-compatible // and we don't want to encode/decode all the time, there is no need for that, // we save the url as the user expects if the uri validation passed. _, err = url.ParseRequestURI(uri) if err != nil { return "", err } key = f.generator() // Make sure that the key is unique for { if v := f.store.Get(key); v == "" { break } key = f.generator() } return key, nil } ================================================ FILE: _examples/url-shortener/main.go ================================================ // Package main shows how you can create a simple URL Shortener. // // Article: https://medium.com/@kataras/a-url-shortener-service-using-go-iris-and-bolt-4182f0b00ae7 // // $ go get go.etcd.io/bbolt/... // $ go get github.com/google/uuid // $ cd $GOPATH/src/github.com/kataras/iris/_examples/url-shortener // $ go build -mod=mod // $ ./url-shortener package main import ( "fmt" "html/template" "github.com/kataras/iris/v12" ) func main() { // assign a variable to the DB so we can use its features later. db := NewDB("shortener.db") // Pass that db to our app, in order to be able to test the whole app with a different database later on. app := newApp(db) // release the "db" connection when server goes off. iris.RegisterOnInterrupt(db.Close) app.Listen(":8080") } func newApp(db *DB) *iris.Application { app := iris.Default() // or app := iris.New() // create our factory, which is the manager for the object creation. // between our web app and the db. factory := NewFactory(DefaultGenerator, db) // serve the "./templates" directory's "*.html" files with the HTML std view engine. tmpl := iris.HTML("./templates", ".html").Reload(true) // register any template func(s) here. // // Look ./templates/index.html#L16 tmpl.AddFunc("IsPositive", func(n int) bool { return n > 0 }) app.RegisterView(tmpl) // Serve static files (css) app.HandleDir("/static", iris.Dir("./resources")) indexHandler := func(ctx iris.Context) { ctx.ViewData("URL_COUNT", db.Len()) if err := ctx.View("index.html"); err != nil { ctx.HTML(fmt.Sprintf("

%s

", err.Error())) return } } app.Get("/", indexHandler) // find and execute a short url by its key // used on http://localhost:8080/u/dsaoj41u321dsa execShortURL := func(ctx iris.Context, key string) { if key == "" { ctx.StatusCode(iris.StatusBadRequest) return } value := db.Get(key) if value == "" { ctx.StatusCode(iris.StatusNotFound) ctx.Writef("Short URL for key: '%s' not found", key) return } ctx.Redirect(value, iris.StatusBadGateway) } app.Get("/u/{shortkey}", func(ctx iris.Context) { execShortURL(ctx, ctx.Params().Get("shortkey")) }) app.Post("/shorten", func(ctx iris.Context) { formValue := ctx.FormValue("url") if formValue == "" { ctx.ViewData("FORM_RESULT", "You need to a enter a URL") ctx.StatusCode(iris.StatusLengthRequired) } else { key, err := factory.Gen(formValue) if err != nil { ctx.ViewData("FORM_RESULT", "Invalid URL") ctx.StatusCode(iris.StatusBadRequest) } else { if err = db.Set(key, formValue); err != nil { ctx.ViewData("FORM_RESULT", "Internal error while saving the URL") app.Logger().Infof("while saving URL: " + err.Error()) ctx.StatusCode(iris.StatusInternalServerError) } else { ctx.StatusCode(iris.StatusOK) shortenURL := "http://" + app.ConfigurationReadOnly().GetVHost() + "/u/" + key ctx.ViewData("FORM_RESULT", template.HTML("
"+shortenURL+" 
")) } } } indexHandler(ctx) // no redirect, we need the FORM_RESULT. }) app.Post("/clear_cache", func(ctx iris.Context) { db.Clear() ctx.Redirect("/") }) return app } ================================================ FILE: _examples/url-shortener/main_test.go ================================================ package main import ( "os" "testing" "time" "github.com/kataras/iris/v12/httptest" ) // TestURLShortener tests the simple tasks of our url shortener application. // Note that it's a pure test. // The rest possible checks is up to you, take it as as an exercise! func TestURLShortener(t *testing.T) { // temp db file f, err := os.CreateTemp("", "shortener") if err != nil { t.Fatalf("creating temp file for database failed: %v", err) } db := NewDB(f.Name()) app := newApp(db) e := httptest.New(t, app) originalURL := "https://google.com" // save e.POST("/shorten"). WithFormField("url", originalURL).Expect(). Status(httptest.StatusOK).Body().Contains("



    
    Golang URL Shortener
    



    

Golang URL Shortener

{{ .FORM_RESULT}}

{{ if IsPositive .URL_COUNT }}

{{ .URL_COUNT }} URLs shortened

{{ end }}
================================================ FILE: _examples/view/context-view-data/main.go ================================================ package main import ( "time" "github.com/kataras/iris/v12" ) const ( DefaultTitle = "My Awesome Site" DefaultLayout = "layouts/layout.html" ) func main() { app := iris.New() // output startup banner and error logs on os.Stdout // set the view engine target to ./templates folder app.RegisterView(iris.HTML("./templates", ".html").Reload(true)) app.Use(func(ctx iris.Context) { // set the title, current time and a layout in order to be used if and when the next handler(s) calls the .Render function ctx.ViewData("Title", DefaultTitle) now := time.Now().Format(ctx.Application().ConfigurationReadOnly().GetTimeFormat()) ctx.ViewData("CurrentTime", now) ctx.ViewLayout(DefaultLayout) ctx.Next() }) app.Get("/", func(ctx iris.Context) { ctx.ViewData("BodyMessage", "a sample text here... set by the route handler") if err := ctx.View("index.html"); err != nil { ctx.Application().Logger().Infof(err.Error()) } }) app.Get("/about", func(ctx iris.Context) { ctx.ViewData("Title", "My About Page") ctx.ViewData("BodyMessage", "about text here... set by the route handler") // same file, just to keep things simple. if err := ctx.View("index.html"); err != nil { ctx.Application().Logger().Infof(err.Error()) } }) // http://localhost:8080 // http://localhost:8080/about app.Listen(":8080") } // Notes: ViewData("", myCustomStruct{}) will set this myCustomStruct value as a root binding data, // so any View("other", "otherValue") will probably fail. // To clear the binding data: ctx.Set(ctx.Application().ConfigurationReadOnly().GetViewDataContextKey(), nil) ================================================ FILE: _examples/view/context-view-data/templates/index.html ================================================

Title: {{.Title}}

{{.BodyMessage}}


Current time: {{.CurrentTime}} ================================================ FILE: _examples/view/context-view-data/templates/layouts/layout.html ================================================ My WebsiteLayout {{ yield . }} ================================================ FILE: _examples/view/context-view-engine/main.go ================================================ package main import ( "fmt" "github.com/kataras/iris/v12" ) func main() { app := iris.New() // Register a root view engine, as usual, // will be used to render files through Context.View method // when no Party or Handler-specific view engine is available. app.RegisterView(iris.Blocks("./views/public", ".html")) // http://localhost:8080 app.Get("/", index) // Register a view engine per group of routes. adminGroup := app.Party("/admin") adminGroup.RegisterView(iris.Blocks("./views/admin", ".html")) // http://localhost:8080/admin adminGroup.Get("/", admin) // Register a view engine on-fly for the current chain of handlers. views := iris.Blocks("./views/on-fly", ".html") if err := views.Load(); err != nil { app.Logger().Fatal(err) } // http://localhost:8080/on-fly app.Get("/on-fly", setViews(views), onFly) app.Listen(":8080") } func index(ctx iris.Context) { data := iris.Map{ "Title": "Public Index Title", } ctx.ViewLayout("main") if err := ctx.View("index", data); err != nil { ctx.HTML(fmt.Sprintf("

%s

", err.Error())) return } } func admin(ctx iris.Context) { data := iris.Map{ "Title": "Admin Panel", } ctx.ViewLayout("main") if err := ctx.View("index", data); err != nil { ctx.HTML(fmt.Sprintf("

%s

", err.Error())) return } } func setViews(views iris.ViewEngine) iris.Handler { return func(ctx iris.Context) { ctx.ViewEngine(views) ctx.Next() } } func onFly(ctx iris.Context) { data := iris.Map{ "Message": "View engine changed through 'setViews' custom middleware.", } if err := ctx.View("index", data); err != nil { ctx.HTML(fmt.Sprintf("

%s

", err.Error())) return } } ================================================ FILE: _examples/view/context-view-engine/views/admin/index.html ================================================ {{ define "content" }}

Hello, Admin!

{{ end }} ================================================ FILE: _examples/view/context-view-engine/views/admin/layouts/main.html ================================================ {{ .Title }} {{ template "content" .}}

Copyright © 2022 Admin

================================================ FILE: _examples/view/context-view-engine/views/on-fly/index.html ================================================

On-fly

{{.Message}}

================================================ FILE: _examples/view/context-view-engine/views/public/500.html ================================================ {{ define "content" }}

Internal Server Error

{{ end }} {{ define "message" }}

{{.Message}}

{{ end }} ================================================ FILE: _examples/view/context-view-engine/views/public/index.html ================================================

Index Body

================================================ FILE: _examples/view/context-view-engine/views/public/layouts/error.html ================================================ {{.Code}} {{ template "content" .}} {{block "message" .}}{{end}} ================================================ FILE: _examples/view/context-view-engine/views/public/layouts/main.html ================================================ {{ if .Title }}{{ .Title }}{{ else }}Default Main Title{{ end }} {{ template "content" . }}
{{ partial "partials/footer" . }}
================================================ FILE: _examples/view/context-view-engine/views/public/partials/footer.html ================================================

Footer Partial

================================================ FILE: _examples/view/embedding-templates-into-app/embedded/templates/layouts/layout.html ================================================ Layout

This is the global layout


{{ yield . }} ================================================ FILE: _examples/view/embedding-templates-into-app/embedded/templates/layouts/mylayout.html ================================================ my Layout

This is the layout for the /my/ and /my/other routes only


{{ yield . }} ================================================ FILE: _examples/view/embedding-templates-into-app/embedded/templates/page1.html ================================================

Page 1 {{ greet "iris developer"}}

{{ render "partials/page1_partial1.html" . }}
================================================ FILE: _examples/view/embedding-templates-into-app/embedded/templates/partials/page1_partial1.html ================================================

Page 1's Partial 1

================================================ FILE: _examples/view/embedding-templates-into-app/main.go ================================================ package main import ( "embed" "fmt" "github.com/kataras/iris/v12" ) //go:embed embedded/* var embeddedFS embed.FS func main() { app := iris.New() tmpl := iris.HTML(embeddedFS, ".html").RootDir("embedded/templates") tmpl.Layout("layouts/layout.html") tmpl.AddFunc("greet", func(s string) string { return "Greetings " + s + "!" }) app.RegisterView(tmpl) app.Get("/", func(ctx iris.Context) { if err := ctx.View("page1.html"); err != nil { ctx.HTML(fmt.Sprintf("

%s

", err.Error())) return } }) // remove the layout for a specific route app.Get("/nolayout", func(ctx iris.Context) { ctx.ViewLayout(iris.NoLayout) if err := ctx.View("page1.html"); err != nil { ctx.HTML(fmt.Sprintf("

%s

", err.Error())) return } }) // set a layout for a party, .Layout should be BEFORE any Get or other Handle party's method my := app.Party("/my").Layout("layouts/mylayout.html") { // both of these will use the layouts/mylayout.html as their layout. my.Get("/", func(ctx iris.Context) { if err := ctx.View("page1.html"); err != nil { ctx.HTML(fmt.Sprintf("

%s

", err.Error())) return } }) my.Get("/other", func(ctx iris.Context) { if err := ctx.View("page1.html"); err != nil { ctx.HTML(fmt.Sprintf("

%s

", err.Error())) return } }) } // http://localhost:8080 // http://localhost:8080/nolayout // http://localhost:8080/my // http://localhost:8080/my/other app.Listen(":8080") } ================================================ FILE: _examples/view/embedding-templates-into-app-bindata/bindata.go ================================================ // Code generated by go-bindata. (@generated) DO NOT EDIT. // Package main generated by go-bindata.// sources: // templates/layouts/layout.html // templates/layouts/mylayout.html // templates/page1.html // templates/partials/page1_partial1.html package main import ( "bytes" "compress/gzip" "fmt" "io" "net/http" "os" "path/filepath" "strings" "time" ) func bindataRead(data []byte, name string) ([]byte, error) { gz, err := gzip.NewReader(bytes.NewBuffer(data)) if err != nil { return nil, fmt.Errorf("read %q: %v", name, err) } var buf bytes.Buffer _, err = io.Copy(&buf, gz) clErr := gz.Close() if err != nil { return nil, fmt.Errorf("read %q: %v", name, err) } if clErr != nil { return nil, err } return buf.Bytes(), nil } type asset struct { bytes []byte info os.FileInfo } type bindataFileInfo struct { name string size int64 mode os.FileMode modTime time.Time } // Name return file name func (fi bindataFileInfo) Name() string { return fi.name } // Size return file size func (fi bindataFileInfo) Size() int64 { return fi.size } // Mode return file mode func (fi bindataFileInfo) Mode() os.FileMode { return fi.mode } // ModTime return file modify time func (fi bindataFileInfo) ModTime() time.Time { return fi.modTime } // IsDir return file whether a directory func (fi bindataFileInfo) IsDir() bool { return fi.mode&os.ModeDir != 0 } // Sys return file is sys mode func (fi bindataFileInfo) Sys() any { return nil } type assetFile struct { *bytes.Reader name string childInfos []os.FileInfo childInfoOffset int } type assetOperator struct{} // Open implement http.FileSystem interface func (f *assetOperator) Open(name string) (http.File, error) { var err error if len(name) > 0 && name[0] == '/' { name = name[1:] } content, err := Asset(name) if err == nil { return &assetFile{name: name, Reader: bytes.NewReader(content)}, nil } children, err := AssetDir(name) if err == nil { childInfos := make([]os.FileInfo, 0, len(children)) for _, child := range children { childPath := filepath.Join(name, child) info, errInfo := AssetInfo(filepath.Join(name, child)) if errInfo == nil { childInfos = append(childInfos, info) } else { childInfos = append(childInfos, newDirFileInfo(childPath)) } } return &assetFile{name: name, childInfos: childInfos}, nil } else { // If the error is not found, return an error that will // result in a 404 error. Otherwise the server returns // a 500 error for files not found. if strings.Contains(err.Error(), "not found") { return nil, os.ErrNotExist } return nil, err } } // Close no need do anything func (f *assetFile) Close() error { return nil } // Readdir read dir's children file info func (f *assetFile) Readdir(count int) ([]os.FileInfo, error) { if len(f.childInfos) == 0 { return nil, os.ErrNotExist } if count <= 0 { return f.childInfos, nil } if f.childInfoOffset+count > len(f.childInfos) { count = len(f.childInfos) - f.childInfoOffset } offset := f.childInfoOffset f.childInfoOffset += count return f.childInfos[offset : offset+count], nil } // Stat read file info from asset item func (f *assetFile) Stat() (os.FileInfo, error) { if len(f.childInfos) != 0 { return newDirFileInfo(f.name), nil } return AssetInfo(f.name) } // newDirFileInfo return default dir file info func newDirFileInfo(name string) os.FileInfo { return &bindataFileInfo{ name: name, size: 0, mode: os.FileMode(2147484068), // equal os.FileMode(0644)|os.ModeDir modTime: time.Time{}} } // AssetFile return a http.FileSystem instance that data backend by asset func AssetFile() http.FileSystem { return &assetOperator{} } var _layoutsLayoutHtml = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x34\xce\xc1\xa9\xc3\x30\x0c\x06\xe0\xf3\x33\x78\x07\xbd\x01\x8c\xc9\x5d\x78\x82\x9e\x4a\x17\x70\x6a\x51\x19\x94\xa4\x38\xca\xc1\x84\xec\x5e\xec\xba\x27\x49\xf0\x89\xff\x47\xd6\x45\x82\x35\xc8\x14\x53\x9b\x9a\x55\x28\xdc\x62\xdd\x0e\x45\xff\xbd\xac\xb1\x06\xfd\x4f\xcc\x5b\xaa\xc1\x9a\x3f\xe4\x29\x3c\x38\xef\x90\x77\x50\x26\x78\xc9\x36\x47\x01\x19\xaf\x3c\x75\x34\x17\xf0\x7d\xf9\x77\x0e\xee\xb4\x26\x2a\x5d\x3f\x8f\x52\x68\x55\x50\x5a\xde\x12\x95\x80\xa9\x10\x38\xd7\xec\x79\x42\xcd\x24\x09\xae\xab\x05\x8f\x40\xf4\xa3\xeb\x27\x00\x00\xff\xff\x68\xca\x16\xc2\xb4\x00\x00\x00") func layoutsLayoutHtmlBytes() ([]byte, error) { return bindataRead( _layoutsLayoutHtml, "layouts/layout.html", ) } func layoutsLayoutHtml() (*asset, error) { bytes, err := layoutsLayoutHtmlBytes() if err != nil { return nil, err } info := bindataFileInfo{name: "layouts/layout.html", size: 180, mode: os.FileMode(438), modTime: time.Unix(1599156854, 0)} a := &asset{bytes: bytes, info: info} return a, nil } var _layoutsMylayoutHtml = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x34\x8f\x4d\x6a\xc5\x30\x0c\x84\xd7\x35\xf8\x0e\xd3\x03\x18\x93\xbd\xf1\x09\xba\x2a\xbd\x80\x53\xab\xc8\xe0\x9f\xe2\x28\x0b\x13\x72\xf7\x47\x9c\xbc\x95\x46\x62\x46\x7c\xe3\x58\x4a\xf6\x5a\x39\xa6\x10\xaf\x29\x49\x32\xf9\x32\xf0\x15\x46\xdb\xc5\xd9\xfb\xa0\x95\x56\xce\xbe\x4d\x6b\x8b\xc3\x6b\xf5\xe1\x78\xf1\x3f\x9c\x36\xa4\x0d\xc2\x84\x3c\x33\xf8\x6b\x7d\xae\xb6\x0c\x8b\x50\xe3\x14\x4d\x98\x3a\x7a\xdb\x85\x36\xb4\x9a\x87\xb3\xbc\xcc\x27\x6b\x87\x9d\xe2\xd3\x18\x7c\x53\x8d\x74\xc7\x7f\xf7\xde\xa9\x0a\x84\xca\x7f\x0e\x42\x60\xea\x04\x63\x2e\xef\x71\x60\x24\xca\x11\xe7\x79\x81\x3d\x40\xce\x3e\x75\x5e\x01\x00\x00\xff\xff\x64\xea\xc5\x1d\xd7\x00\x00\x00") func layoutsMylayoutHtmlBytes() ([]byte, error) { return bindataRead( _layoutsMylayoutHtml, "layouts/mylayout.html", ) } func layoutsMylayoutHtml() (*asset, error) { bytes, err := layoutsMylayoutHtmlBytes() if err != nil { return nil, err } info := bindataFileInfo{name: "layouts/mylayout.html", size: 215, mode: os.FileMode(438), modTime: time.Unix(1599156854, 0)} a := &asset{bytes: bytes, info: info} return a, nil } var _page1Html = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x3c\xca\x41\xaa\xc2\x30\x10\x00\xd0\xf5\x2f\xf4\x0e\xc3\xec\xbf\x25\x5b\x8d\x3d\x83\x37\x90\x69\x33\xa4\xa1\x63\x53\x26\x69\x40\x42\xee\x2e\xa2\xb8\x7c\xf0\xac\x0b\x05\x52\x7e\x0a\x5f\x71\xa2\x79\xf5\x1a\x8f\xcd\xfd\xcf\x51\xa2\x9e\x61\x12\x9a\xd7\x0b\xfc\x74\x30\x8e\x7d\xd7\x77\x7f\x76\x31\xe3\x8d\x3c\x83\x81\x5a\xc1\x2b\x73\x06\x0c\x1a\x12\x38\x2e\x2c\x71\x67\xc5\xd6\xec\xb0\x98\xcf\xaf\x15\x94\x37\xc7\x0a\xb8\x93\xe6\x40\x92\x86\x9d\x3c\x9b\xfb\x97\xe6\xb4\xe4\x87\x60\x6b\xef\x6e\x07\x17\xca\xd8\x77\xaf\x00\x00\x00\xff\xff\x47\x41\x4a\x5c\x9d\x00\x00\x00") func page1HtmlBytes() ([]byte, error) { return bindataRead( _page1Html, "page1.html", ) } func page1Html() (*asset, error) { bytes, err := page1HtmlBytes() if err != nil { return nil, err } info := bindataFileInfo{name: "page1.html", size: 157, mode: os.FileMode(438), modTime: time.Unix(1599156854, 0)} a := &asset{bytes: bytes, info: info} return a, nil } var _partialsPage1_partial1Html = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xb2\x49\xc9\x2c\x53\x28\x2e\xa9\xcc\x49\xb5\x55\x4a\x4a\x4c\xce\x4e\x2f\xca\x2f\xcd\x4b\xd1\x4d\xce\xcf\xc9\x2f\xb2\x52\x28\xcf\xc8\x2c\x49\xb5\x56\x80\xf2\x8a\x52\x53\x94\xec\x78\xb9\x38\x6d\x32\x0c\xed\x02\x12\xd3\x53\x15\x0c\xd5\x8b\x15\x02\x12\x8b\x4a\x32\x13\x73\x14\x0c\x6d\xf4\x33\x0c\xed\x78\xb9\x6c\xf4\x53\x32\xcb\xec\x78\xb9\x00\x01\x00\x00\xff\xff\xa2\xa6\x60\xb6\x59\x00\x00\x00") func partialsPage1_partial1HtmlBytes() ([]byte, error) { return bindataRead( _partialsPage1_partial1Html, "partials/page1_partial1.html", ) } func partialsPage1_partial1Html() (*asset, error) { bytes, err := partialsPage1_partial1HtmlBytes() if err != nil { return nil, err } info := bindataFileInfo{name: "partials/page1_partial1.html", size: 89, mode: os.FileMode(438), modTime: time.Unix(1599156854, 0)} a := &asset{bytes: bytes, info: info} return a, nil } // Asset loads and returns the asset for the given name. // It returns an error if the asset could not be found or // could not be loaded. func Asset(name string) ([]byte, error) { cannonicalName := strings.Replace(name, "\\", "/", -1) if f, ok := _bindata[cannonicalName]; ok { a, err := f() if err != nil { return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err) } return a.bytes, nil } return nil, fmt.Errorf("Asset %s not found", name) } // MustAsset is like Asset but panics when Asset would return an error. // It simplifies safe initialization of global variables. func MustAsset(name string) []byte { a, err := Asset(name) if err != nil { panic("asset: Asset(" + name + "): " + err.Error()) } return a } // AssetInfo loads and returns the asset info for the given name. // It returns an error if the asset could not be found or // could not be loaded. func AssetInfo(name string) (os.FileInfo, error) { cannonicalName := strings.Replace(name, "\\", "/", -1) if f, ok := _bindata[cannonicalName]; ok { a, err := f() if err != nil { return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err) } return a.info, nil } return nil, fmt.Errorf("AssetInfo %s not found", name) } // AssetNames returns the names of the assets. func AssetNames() []string { names := make([]string, 0, len(_bindata)) for name := range _bindata { names = append(names, name) } return names } // _bindata is a table, holding each asset generator, mapped to its name. var _bindata = map[string]func() (*asset, error){ "layouts/layout.html": layoutsLayoutHtml, "layouts/mylayout.html": layoutsMylayoutHtml, "page1.html": page1Html, "partials/page1_partial1.html": partialsPage1_partial1Html, } // AssetDir returns the file names below a certain // directory embedded in the file by go-bindata. // For example if you run go-bindata on data/... and data contains the // following hierarchy: // // data/ // foo.txt // img/ // a.png // b.png // // then AssetDir("data") would return []string{"foo.txt", "img"} // AssetDir("data/img") would return []string{"a.png", "b.png"} // AssetDir("foo.txt") and AssetDir("notexist") would return an error // AssetDir("") will return []string{"data"}. func AssetDir(name string) ([]string, error) { node := _bintree if len(name) != 0 { cannonicalName := strings.Replace(name, "\\", "/", -1) pathList := strings.Split(cannonicalName, "/") for _, p := range pathList { node = node.Children[p] if node == nil { return nil, fmt.Errorf("Asset %s not found", name) } } } if node.Func != nil { return nil, fmt.Errorf("Asset %s not found", name) } rv := make([]string, 0, len(node.Children)) for childName := range node.Children { rv = append(rv, childName) } return rv, nil } type bintree struct { Func func() (*asset, error) Children map[string]*bintree } var _bintree = &bintree{nil, map[string]*bintree{ "layouts": {nil, map[string]*bintree{ "layout.html": {layoutsLayoutHtml, map[string]*bintree{}}, "mylayout.html": {layoutsMylayoutHtml, map[string]*bintree{}}, }}, "page1.html": {page1Html, map[string]*bintree{}}, "partials": {nil, map[string]*bintree{ "page1_partial1.html": {partialsPage1_partial1Html, map[string]*bintree{}}, }}, }} // RestoreAsset restores an asset under the given directory func RestoreAsset(dir, name string) error { data, err := Asset(name) if err != nil { return err } info, err := AssetInfo(name) if err != nil { return err } err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755)) if err != nil { return err } err = os.WriteFile(_filePath(dir, name), data, info.Mode()) if err != nil { return err } err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime()) if err != nil { return err } return nil } // RestoreAssets restores an asset under the given directory recursively func RestoreAssets(dir, name string) error { children, err := AssetDir(name) // File if err != nil { return RestoreAsset(dir, name) } // Dir for _, child := range children { err = RestoreAssets(dir, filepath.Join(name, child)) if err != nil { return err } } return nil } func _filePath(dir, name string) string { cannonicalName := strings.Replace(name, "\\", "/", -1) return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...) } ================================================ FILE: _examples/view/embedding-templates-into-app-bindata/main.go ================================================ package main import ( "fmt" "github.com/kataras/iris/v12" ) func main() { app := iris.New() // $ go install github.com/go-bindata/go-bindata/v3/go-bindata@latest // $ go-bindata -fs -prefix "templates" ./templates/... // $ go run . // html files are not used, you can delete the folder and run the example. tmpl := iris.HTML(AssetFile(), ".html") tmpl.Layout("layouts/layout.html") tmpl.AddFunc("greet", func(s string) string { return "Greetings " + s + "!" }) app.RegisterView(tmpl) app.Get("/", func(ctx iris.Context) { if err := ctx.View("page1.html"); err != nil { ctx.StatusCode(iris.StatusInternalServerError) ctx.WriteString(err.Error()) } }) // remove the layout for a specific route app.Get("/nolayout", func(ctx iris.Context) { ctx.ViewLayout(iris.NoLayout) if err := ctx.View("page1.html"); err != nil { ctx.StatusCode(iris.StatusInternalServerError) ctx.WriteString(err.Error()) } }) // set a layout for a party, .Layout should be BEFORE any Get or other Handle party's method my := app.Party("/my").Layout("layouts/mylayout.html") { // both of these will use the layouts/mylayout.html as their layout. my.Get("/", func(ctx iris.Context) { if err := ctx.View("page1.html"); err != nil { ctx.HTML(fmt.Sprintf("

%s

", err.Error())) return } }) my.Get("/other", func(ctx iris.Context) { if err := ctx.View("page1.html"); err != nil { ctx.HTML(fmt.Sprintf("

%s

", err.Error())) return } }) } // http://localhost:8080 // http://localhost:8080/nolayout // http://localhost:8080/my // http://localhost:8080/my/other app.Listen(":8080") } ================================================ FILE: _examples/view/embedding-templates-into-app-bindata/templates/layouts/layout.html ================================================ Layout

This is the global layout


{{ yield . }} ================================================ FILE: _examples/view/embedding-templates-into-app-bindata/templates/layouts/mylayout.html ================================================ my Layout

This is the layout for the /my/ and /my/other routes only


{{ yield . }} ================================================ FILE: _examples/view/embedding-templates-into-app-bindata/templates/page1.html ================================================

Page 1 {{ greet "iris developer"}}

{{ render "partials/page1_partial1.html" . }}
================================================ FILE: _examples/view/embedding-templates-into-app-bindata/templates/partials/page1_partial1.html ================================================

Page 1's Partial 1

================================================ FILE: _examples/view/fallback/main.go ================================================ package main import ( "fmt" "github.com/kataras/iris/v12" ) func main() { app := iris.New() app.RegisterView(iris.HTML("./view", ".html")) // Use the FallbackView helper Register a fallback view // filename per-party when the provided was not found. app.FallbackView(iris.FallbackView("fallback.html")) // Use the FallbackViewLayout helper to register a fallback view layout. app.FallbackView(iris.FallbackViewLayout("layout.html")) // Register a custom fallback function per-party to handle everything. // You can register more than one. If fails (returns a not nil error of ErrViewNotExists) // then it proceeds to the next registered fallback. app.FallbackView(iris.FallbackViewFunc(func(ctx iris.Context, err iris.ErrViewNotExist) error { // err.Name is the previous template name. // err.IsLayout reports whether the failure came from the layout template. // err.Data is the template data provided to the previous View call. // [...custom logic e.g. ctx.View("fallback.html", err.Data)] return err })) app.Get("/", index) app.Listen(":8080") } // Register fallback view(s) in a middleware. // func fallbackInsideAMiddleware(ctx iris.Context) { // ctx.FallbackView(...) // To remove all previous registered fallbacks, pass nil. // ctx.FallbackView(nil) // ctx.Next() // } func index(ctx iris.Context) { if err := ctx.View("blabla.html"); err != nil { ctx.HTML(fmt.Sprintf("

%s

", err.Error())) return } } ================================================ FILE: _examples/view/fallback/view/fallback.html ================================================

Fallback view

================================================ FILE: _examples/view/herotemplate/README.md ================================================ # Hero Template Example This folder contains the iris version of the original hero's example: https://github.com/shiyanhui/hero/tree/master/examples/app. Iris is 100% compatible with `net/http` so you don't have to change anything else except the handler input from the original example. The only inline handler's changes were: From: ```go if _, err := w.Write(buffer.Bytes()); err != nil { // and template.UserListToWriter(userList, w) ``` To: ```go if _, err := ctx.Write(buffer.Bytes()); err != nil { // and template.UserListToWriter(userList, ctx) ``` So easy. Read more at: https://github.com/shiyanhui/hero ================================================ FILE: _examples/view/herotemplate/app.go ================================================ package main import ( "bytes" "github.com/kataras/iris/v12/_examples/view/herotemplate/template" "github.com/kataras/iris/v12" ) // $ go get -u github.com/shiyanhui/hero/hero // $ go run app.go // // Read more at https://github.com/shiyanhui/hero/hero func main() { app := iris.New() app.Get("/users", func(ctx iris.Context) { ctx.CompressWriter(true) ctx.ContentType("text/html") userList := []string{ "Alice", "Bob", "Tom", } // Had better use buffer sync.Pool. // Hero(github.com/shiyanhui/hero/hero) exports GetBuffer and PutBuffer for this. // // buffer := hero.GetBuffer() // defer hero.PutBuffer(buffer) // buffer := new(bytes.Buffer) // template.UserList(userList, buffer) // ctx.Write(buffer.Bytes()) // iris context implements the io.Writer: // _, err := template.UserListToWriter(userList, ctx) // OR: buffer := new(bytes.Buffer) template.UserList(userList, buffer) _, err := ctx.Write(buffer.Bytes()) if err != nil { ctx.StopWithError(iris.StatusInternalServerError, err) return } }) app.Listen(":8080") } ================================================ FILE: _examples/view/herotemplate/template/index.html ================================================ <%@ body { %> <% } %> ================================================ FILE: _examples/view/herotemplate/template/index.html.go ================================================ // Code generated by hero. // DO NOT EDIT! package template ================================================ FILE: _examples/view/herotemplate/template/user.html ================================================
  • <%= user %>
  • ================================================ FILE: _examples/view/herotemplate/template/user.html.go ================================================ // Code generated by hero. // DO NOT EDIT! package template ================================================ FILE: _examples/view/herotemplate/template/userlist.html ================================================ <%: func UserList(userList []string, buffer *bytes.Buffer) %> <%~ "index.html" %> <%@ body { %> <% for _, user := range userList { %>
      <%+ "user.html" %>
    <% } %> <% } %> ================================================ FILE: _examples/view/herotemplate/template/userlist.html.go ================================================ // Code generated by hero. // DO NOT EDIT! package template import ( "bytes" "github.com/shiyanhui/hero" ) func UserList(userList []string, buffer *bytes.Buffer) { buffer.WriteString(` `) for _, user := range userList { buffer.WriteString(`
      `) buffer.WriteString(`
    • `) hero.EscapeHTML(user, buffer) buffer.WriteString(`
    • `) buffer.WriteString(`
    `) } buffer.WriteString(` `) } ================================================ FILE: _examples/view/herotemplate/template/userlistwriter.html ================================================ <%: func UserListToWriter(userList []string, w io.Writer) (int, error)%> <%~ "index.html" %> <%@ body { %> <% for _, user := range userList { %>
      <%+ "user.html" %>
    <% } %> <% } %> ================================================ FILE: _examples/view/herotemplate/template/userlistwriter.html.go ================================================ // Code generated by hero. // DO NOT EDIT! package template import ( "io" "github.com/shiyanhui/hero" ) func UserListToWriter(userList []string, w io.Writer) (int, error) { _buffer := hero.GetBuffer() defer hero.PutBuffer(_buffer) _buffer.WriteString(` `) for _, user := range userList { _buffer.WriteString(`
      `) _buffer.WriteString(`
    • `) hero.EscapeHTML(user, _buffer) _buffer.WriteString(`
    • `) _buffer.WriteString(`
    `) } _buffer.WriteString(` `) return w.Write(_buffer.Bytes()) } ================================================ FILE: _examples/view/layout/ace/main.go ================================================ package main import ( "fmt" "github.com/kataras/iris/v12" ) func main() { app := iris.New() // By default Ace minifies the template before render, // using the SetIndent method, we make it to match // the rest of the template results. app.RegisterView(iris.Ace("./views", ".ace").SetIndent(" ")) app.Get("/", index) app.Listen(":8080") } func index(ctx iris.Context) { data := iris.Map{ "Title": "Page Title", "FooterText": "Footer contents", "Message": "Main contents", } ctx.ViewLayout("layouts/main") if err := ctx.View("index", data); err != nil { ctx.HTML(fmt.Sprintf("

    %s

    ", err.Error())) return } } ================================================ FILE: _examples/view/layout/ace/views/index.ace ================================================ h1 Index Body h3 Message: {{.Message}} ================================================ FILE: _examples/view/layout/ace/views/layouts/main.ace ================================================ = doctype html html head title {{.Title}} body {{ yield . }} footer = include partials/footer.ace . ================================================ FILE: _examples/view/layout/ace/views/partials/footer.ace ================================================ h3 Footer Partial h4 {{.FooterText}} ================================================ FILE: _examples/view/layout/blocks/main.go ================================================ package main import ( "fmt" "github.com/kataras/iris/v12" ) func main() { app := iris.New() app.RegisterView(iris.Blocks("./views", ".html")) // Note, in Blocks engine, layouts // are used by their base names, the // blocks.LayoutDir(layoutDir) defaults to "./layouts". // .Blocks(...).Layout("main") for default layout for all views, it can be modified through ctx.ViewLayout though. app.Get("/", index) app.Listen(":8080") } func index(ctx iris.Context) { data := iris.Map{ "Title": "Page Title", "FooterText": "Footer contents", "Message": "Main contents", } ctx.ViewLayout("main") if err := ctx.View("index", data); err != nil { ctx.HTML(fmt.Sprintf("

    %s

    ", err.Error())) return } } ================================================ FILE: _examples/view/layout/blocks/views/index.html ================================================

    Index Body

    Message: {{.Message}}

    ================================================ FILE: _examples/view/layout/blocks/views/layouts/main.html ================================================ {{.Title}} {{ template "content" . }}
    {{ partial "partials/footer" . }}
    ================================================ FILE: _examples/view/layout/blocks/views/partials/footer.html ================================================

    Footer Partial

    {{.FooterText}}

    ================================================ FILE: _examples/view/layout/django/main.go ================================================ package main import ( "fmt" "github.com/kataras/iris/v12" ) func main() { app := iris.New() app.RegisterView(iris.Django("./views", ".html")) app.Get("/", index) app.Listen(":8080") } func index(ctx iris.Context) { data := iris.Map{ "Title": "Page Title", "FooterText": "Footer contents", "Message": "Main contents", } // On Django this is ignored: ctx.ViewLayout("layouts/main") // Layouts are only rendered from inside the index page itself // using the "extends" keyword. if err := ctx.View("index", data); err != nil { ctx.HTML(fmt.Sprintf("

    %s

    ", err.Error())) return } } ================================================ FILE: _examples/view/layout/django/views/index.html ================================================ {% extends "layouts/main.html" %} {% block content %}

    Index Body

    Message: {{Message}}

    {% endblock %} ================================================ FILE: _examples/view/layout/django/views/layouts/main.html ================================================ {{Title}} {% block content %} {% endblock %}
    {% include "../partials/footer.html" %}
    ================================================ FILE: _examples/view/layout/django/views/partials/footer.html ================================================

    Footer Partial

    {{FooterText}}

    ================================================ FILE: _examples/view/layout/handlebars/main.go ================================================ package main import ( "fmt" "github.com/kataras/iris/v12" ) func main() { app := iris.New() app.RegisterView(iris.Handlebars("./views", ".html")) app.Get("/", index) app.Listen(":8080") } func index(ctx iris.Context) { data := iris.Map{ "Title": "Page Title", "FooterText": "Footer contents", "Message": "Main contents", } ctx.ViewLayout("layouts/main") if err := ctx.View("index", data); err != nil { ctx.HTML(fmt.Sprintf("

    %s

    ", err.Error())) return } } ================================================ FILE: _examples/view/layout/handlebars/views/index.html ================================================

    Index Body

    Message: {{Message}}

    ================================================ FILE: _examples/view/layout/handlebars/views/layouts/main.html ================================================ {{Title}} {{ yield . }}
    {{ render "partials/footer.html" .}}
    ================================================ FILE: _examples/view/layout/handlebars/views/partials/footer.html ================================================

    Footer Partial

    {{FooterText}}

    ================================================ FILE: _examples/view/layout/html/main.go ================================================ package main import ( "fmt" "github.com/kataras/iris/v12" ) func main() { app := iris.New() app.RegisterView(iris.HTML("./views", ".html")) app.Get("/", index) app.Listen(":8080") } func index(ctx iris.Context) { data := iris.Map{ "Title": "Page Title", "FooterText": "Footer contents", "Message": "Main contents", } ctx.ViewLayout("layouts/main") if err := ctx.View("index", data); err != nil { ctx.HTML(fmt.Sprintf("

    %s

    ", err.Error())) return } } ================================================ FILE: _examples/view/layout/html/views/index.html ================================================

    Index Body

    Message: {{.Message}}

    ================================================ FILE: _examples/view/layout/html/views/layouts/main.html ================================================ {{.Title}} {{ yield . }}
    {{ render "partials/footer.html" . }}
    ================================================ FILE: _examples/view/layout/html/views/partials/footer.html ================================================

    Footer Partial

    {{.FooterText}}

    ================================================ FILE: _examples/view/layout/jet/main.go ================================================ package main import ( "fmt" "github.com/kataras/iris/v12" ) func main() { app := iris.New() app.RegisterView(iris.Jet("./views", ".jet")) app.Get("/", index) app.Listen(":8080") } func index(ctx iris.Context) { data := iris.Map{ "Title": "Page Title", "FooterText": "Footer contents", "Message": "Main contents", } // On Jet this is ignored: ctx.ViewLayout("layouts/main") // Layouts are only rendered from inside the index page itself // using the "extends" keyword. if err := ctx.View("index", data); err != nil { ctx.HTML(fmt.Sprintf("

    %s

    ", err.Error())) return } } ================================================ FILE: _examples/view/layout/jet/views/index.jet ================================================ {{ extends "../layouts/main.jet" }} {{ block documentBody() }}

    Index Body

    Message: {{.Message}}

    {{ end }} ================================================ FILE: _examples/view/layout/jet/views/layouts/main.jet ================================================ {{.Title}} {{ yield . documentBody() }}
    {{ include "../partials/footer.jet" . }}
    ================================================ FILE: _examples/view/layout/jet/views/partials/footer.jet ================================================

    Footer Partial

    {{.FooterText}}

    ================================================ FILE: _examples/view/layout/pug/main.go ================================================ package main import ( "fmt" "github.com/kataras/iris/v12" ) func main() { app := iris.New() app.RegisterView(iris.Pug("./views", ".pug")) app.Get("/", index) app.Listen(":8080") } func index(ctx iris.Context) { data := iris.Map{ "Title": "Page Title", "FooterText": "Footer contents", "Message": "Main contents", } // On Pug this is ignored: ctx.ViewLayout("layouts/main") // Layouts are only rendered from inside the index page itself // using the "extends" keyword. if err := ctx.View("index", data); err != nil { ctx.HTML(fmt.Sprintf("

    %s

    ", err.Error())) return } } ================================================ FILE: _examples/view/layout/pug/views/index.pug ================================================ extends layouts/main.pug block content h1 Index Body h3 Message: {{.Message}} ================================================ FILE: _examples/view/layout/pug/views/layouts/main.pug ================================================ doctype html html head title {{.Title}} body block content footer include ../partials/footer.pug ================================================ FILE: _examples/view/layout/pug/views/partials/footer.pug ================================================ h3 Footer Partial h4 {{.FooterText}} ================================================ FILE: _examples/view/overview/main.go ================================================ package main import ( "fmt" "html/template" "time" "github.com/kataras/iris/v12" ) // ViewFunctions presents some builtin functions // for html view engines. See `View.Funcs` or `view/html.Funcs` and etc. var Functions = template.FuncMap{ "Now": time.Now, } func main() { app := iris.New() // with default template funcs: // // - {{ urlpath "mynamedroute" "pathParameter_ifneeded" }} // - {{ render "header.html" . }} // - {{ render_r "header.html" . }} // partial relative path to current page // - {{ yield . }} // - {{ current . }} app.RegisterView(iris.HTML("./templates", ".html"). Funcs(Functions). // Optionally register some more builtin functions. Reload(false)) // Set Reload to true on development. app.Get("/", func(ctx iris.Context) { // enable compression based on Accept-Encoding (e.g. "gzip"), // alternatively: app.Use(iris.Compression). ctx.CompressWriter(true) // the .Name inside the ./templates/hi.html. ctx.ViewData("Name", "iris") // render the template with the file name relative to the './templates'. // file extension is OPTIONAL. if err := ctx.View("hi.html"); err != nil { ctx.HTML(fmt.Sprintf("

    %s

    ", err.Error())) return } }) app.Get("/example_map", func(ctx iris.Context) { examplePage := iris.Map{ "Name": "Example Name", "Age": 42, "Items": []string{"Example slice entry 1", "entry 2", "entry 3"}, "Map": iris.Map{"map key": "map value", "other key": "other value"}, "Nested": iris.Map{"Title": "Iris E-Book", "Pages": 620}, } if err := ctx.View("example.html", examplePage); err != nil { ctx.HTML(fmt.Sprintf("

    %s

    ", err.Error())) return } }) app.Get("/example_struct", func(ctx iris.Context) { type book struct { Title string Pages int } var examplePage = struct { Name string Age int Items []string Map map[string]any Nested book }{ "Example Name", 42, []string{"Example slice entry 1", "entry 2", "entry 3"}, iris.Map{"map key": "map value", "other key": "other value"}, book{ "Iris E-Book", 620, }, } if err := ctx.View("example.html", examplePage); err != nil { ctx.HTML(fmt.Sprintf("

    %s

    ", err.Error())) return } }) app.Get("/functions", func(ctx iris.Context) { var functionsPage = struct { // A function. Now func() time.Time // A struct field which contains methods. Ctx iris.Context }{ Now: time.Now, Ctx: ctx, } if err := ctx.View("functions.html", functionsPage); err != nil { ctx.HTML(fmt.Sprintf("

    %s

    ", err.Error())) return } }) // http://localhost:8080/ app.Listen(":8080") } ================================================ FILE: _examples/view/overview/templates/example.html ================================================ HTML Template Example

    Name: {{.Name}}

    Age: {{.Age}}

    Slice

    {{range $idx, $item := .Items}} {{ $idx }} - {{ $item }}
    {{end}}

    Map

    {{range $key, $value := .Map}} {{ $key }} - {{ $value }}
    {{end}}

    Nested

    Title:{{ .Nested.Title}}
    Read more at:
    https://pkg.go.dev/html/template ================================================ FILE: _examples/view/overview/templates/functions.html ================================================

    Function: {{ Now }}

    Field: {{ .Ctx.Request.URL.Path }}

    Field Struct's Function (Method): {{ .Ctx.FullRequestURI }}

    ================================================ FILE: _examples/view/overview/templates/hi.html ================================================ Hi iris

    Hi {{.Name}}

    ================================================ FILE: _examples/view/parse-template/django/main.go ================================================ package main import ( "fmt" "github.com/kataras/iris/v12" ) func main() { e := iris.Django(nil, ".html") // You can still use a file system though. e.AddFunc("greet", func(name string) string { return "Hello, " + name + "!" }) err := e.ParseTemplate("program.html", []byte(`

    {{greet(Name)}}

    `)) if err != nil { panic(err) } e.Reload(true) app := iris.New() app.RegisterView(e) app.Get("/", index) app.Listen(":8080") } func index(ctx iris.Context) { if err := ctx.View("program.html", iris.Map{ "Name": "Gerasimos", }); err != nil { ctx.HTML(fmt.Sprintf("

    %s

    ", err.Error())) return } } ================================================ FILE: _examples/view/parse-template/handlebars/main.go ================================================ package main import ( "fmt" "github.com/kataras/iris/v12" ) func main() { e := iris.Handlebars(nil, ".html") // You can still use a file system though. e.ParseTemplate("program.html", `

    {{greet Name}}

    `, iris.Map{ "greet": func(name string) string { return "Hello, " + name + "!" }, }) e.Reload(true) app := iris.New() app.RegisterView(e) app.Get("/", index) app.Listen(":8080") } func index(ctx iris.Context) { if err := ctx.View("program.html", iris.Map{ "Name": "Gerasimos", }); err != nil { ctx.HTML(fmt.Sprintf("

    %s

    ", err.Error())) return } } ================================================ FILE: _examples/view/parse-template/jet/main.go ================================================ package main import ( "fmt" "reflect" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/view" ) func main() { e := iris.Jet(nil, ".jet") // You can still use a file system though. e.AddFunc("greet", func(args view.JetArguments) reflect.Value { msg := "Hello, " + args.Get(0).String() + "!" return reflect.ValueOf(msg) }) err := e.ParseTemplate("program.jet", `

    {{greet(.Name)}}

    `) if err != nil { panic(err) } e.Reload(true) app := iris.New() app.RegisterView(e) app.Get("/", index) app.Listen(":8080") } func index(ctx iris.Context) { if err := ctx.View("program.jet", iris.Map{ "Name": "Gerasimos", }); err != nil { ctx.HTML(fmt.Sprintf("

    %s

    ", err.Error())) return } } ================================================ FILE: _examples/view/parse-template/main.go ================================================ // Package main shows how to parse a template through custom byte slice content. // The following works with HTML, Pug and Ace template parsers. // To learn how you can manually parse a template from a text for the rest // template parsers navigate through the example's subdirectories. package main import ( "fmt" "github.com/kataras/iris/v12" ) func main() { // To not load any templates from files or embedded data, // pass nil or empty string on the first argument: // e := iris.HTML(nil, ".html") e := iris.HTML("./views", ".html") // e := iris.Pug("./views",".pug") // e := iris.Ace("./views",".ace") e.ParseTemplate("program.html", []byte(`

    {{greet .Name}}

    `), iris.Map{ "greet": func(name string) string { return "Hello, " + name + "!" }, }) e.Reload(true) app := iris.New() app.RegisterView(e) app.Get("/", index) app.Get("/layout", layout) app.Listen(":8080") } func index(ctx iris.Context) { if err := ctx.View("program.html", iris.Map{ "Name": "Gerasimos", }); err != nil { ctx.HTML(fmt.Sprintf("

    %s

    ", err.Error())) return } } func layout(ctx iris.Context) { ctx.ViewLayout("layouts/main.html") index(ctx) } ================================================ FILE: _examples/view/parse-template/views/layouts/main.html ================================================ My Layout

    [layout] Body content is below...

    {{ yield . }} ================================================ FILE: _examples/view/quicktemplate/README.md ================================================ First of all, install [quicktemplate](https://github.com/valyala/quicktemplate) package and [quicktemplate compiler](https://github.com/valyala/quicktemplate/tree/master/qtc) ```sh go get -u github.com/valyala/quicktemplate go get -u github.com/valyala/quicktemplate/qtc ``` The example has the Go code compiled already for you, therefore: ```sh go run main.go # http://localhost:8080 ``` However there is an instruction below, full documentation can be found at https://github.com/valyala/quicktemplate. Save your template files into `templates` folder under the extension *.qtpl, open your terminal and run `qtc` inside this folder. If all went ok, `*.qtpl.go` files must appear in the `templates` folder. These files contain the Go code for all `*.qtpl` files. > Remember, each time you change a a `/templates/*.qtpl` file you have to run the `qtc` command and re-build your application. ================================================ FILE: _examples/view/quicktemplate/controllers/execute_template.go ================================================ package controllers import ( "github.com/kataras/iris/v12/_examples/view/quicktemplate/templates" "github.com/kataras/iris/v12" ) // ExecuteTemplate renders a "tmpl" partial template to the `Context.ResponseWriter`. func ExecuteTemplate(ctx iris.Context, tmpl templates.Partial) { ctx.CompressWriter(true) ctx.ContentType("text/html") templates.WriteTemplate(ctx, tmpl) } ================================================ FILE: _examples/view/quicktemplate/controllers/hello.go ================================================ package controllers import ( "github.com/kataras/iris/v12/_examples/view/quicktemplate/templates" "github.com/kataras/iris/v12" ) // Hello renders our ../templates/hello.qtpl file using the compiled ../templates/hello.qtpl.go file. func Hello(ctx iris.Context) { // vars := make(map[string]any) // vars["message"] = "Hello World!" // vars["name"] = ctx.Params().Get("name") // [...] // &templates.Hello{ Vars: vars } // [...] // However, as an alternative, we recommend that you should the `ctx.ViewData(key, value)` // in order to be able modify the `templates.Hello#Vars` from a middleware(other handlers) as well. ctx.ViewData("message", "Hello World!") ctx.ViewData("name", ctx.Params().Get("name")) // set view data to the `Vars` template's field tmpl := &templates.Hello{ Vars: ctx.GetViewData(), } // render the template ExecuteTemplate(ctx, tmpl) } ================================================ FILE: _examples/view/quicktemplate/controllers/index.go ================================================ package controllers import ( "github.com/kataras/iris/v12/_examples/view/quicktemplate/templates" "github.com/kataras/iris/v12" ) // Index renders our ../templates/index.qtpl file using the compiled ../templates/index.qtpl.go file. func Index(ctx iris.Context) { tmpl := &templates.Index{} // render the template ExecuteTemplate(ctx, tmpl) } ================================================ FILE: _examples/view/quicktemplate/main.go ================================================ package main import ( "github.com/kataras/iris/v12/_examples/view/quicktemplate/controllers" "github.com/kataras/iris/v12" ) func newApp() *iris.Application { app := iris.New() app.Get("/", controllers.Index) app.Get("/{name}", controllers.Hello) return app } func main() { app := newApp() // http://localhost:8080 // http://localhost:8080/yourname app.Listen(":8080") } ================================================ FILE: _examples/view/quicktemplate/main_test.go ================================================ package main import ( "fmt" "testing" "github.com/kataras/iris/v12/httptest" ) func TestResponseWriterQuicktemplate(t *testing.T) { baseRawBody := ` Quicktemplate integration with Iris
    Header contents here...

    %s

    %s
    Footer contents here...
    ` expectedIndexRawBody := fmt.Sprintf(baseRawBody, "Index Page", "This is our index page's body.") name := "yourname" expectedHelloRawBody := fmt.Sprintf(baseRawBody, "Hello World!", "Hello "+name+"!") app := newApp() e := httptest.New(t, app) e.GET("/").Expect().Status(httptest.StatusOK).Body().IsEqual(expectedIndexRawBody) e.GET("/" + name).Expect().Status(httptest.StatusOK).Body().IsEqual(expectedHelloRawBody) } ================================================ FILE: _examples/view/quicktemplate/models/.gitkeep ================================================ ================================================ FILE: _examples/view/quicktemplate/templates/base.qtpl ================================================ This is our templates' base implementation. {% interface Partial { Body() } %} Template writes a template implementing the Partial interface. {% func Template(p Partial) %} Quicktemplate integration with Iris
    Header contents here...
    {%= p.Body() %}
    Footer contents here...
    {% endfunc %} Base template implementation. Other pages may inherit from it if they need overriding only certain Partial methods. {% code type Base struct {} %} {% func (b *Base) Body() %}This is the base body{% endfunc %} ================================================ FILE: _examples/view/quicktemplate/templates/base.qtpl.go ================================================ // This file is automatically generated by qtc from "base.qtpl". // See https://github.com/valyala/quicktemplate for details. // This is our templates' base implementation. // //line base.qtpl:3 package templates //line base.qtpl:3 import ( qtio422016 "io" qt422016 "github.com/valyala/quicktemplate" ) //line base.qtpl:3 var ( _ = qtio422016.Copy _ = qt422016.AcquireByteBuffer ) //line base.qtpl:4 type Partial interface { //line base.qtpl:4 Body() string //line base.qtpl:4 StreamBody(qw422016 *qt422016.Writer) //line base.qtpl:4 WriteBody(qq422016 qtio422016.Writer) //line base.qtpl:4 } // Template writes a template implementing the Partial interface. //line base.qtpl:11 func StreamTemplate(qw422016 *qt422016.Writer, p Partial) { //line base.qtpl:11 qw422016.N().S(` Quicktemplate integration with Iris
    Header contents here...
    `) //line base.qtpl:22 p.StreamBody(qw422016) //line base.qtpl:22 qw422016.N().S(`
    Footer contents here...
    `) //line base.qtpl:30 } //line base.qtpl:30 func WriteTemplate(qq422016 qtio422016.Writer, p Partial) { //line base.qtpl:30 qw422016 := qt422016.AcquireWriter(qq422016) //line base.qtpl:30 StreamTemplate(qw422016, p) //line base.qtpl:30 qt422016.ReleaseWriter(qw422016) //line base.qtpl:30 } //line base.qtpl:30 func Template(p Partial) string { //line base.qtpl:30 qb422016 := qt422016.AcquireByteBuffer() //line base.qtpl:30 WriteTemplate(qb422016, p) //line base.qtpl:30 qs422016 := string(qb422016.B) //line base.qtpl:30 qt422016.ReleaseByteBuffer(qb422016) //line base.qtpl:30 return qs422016 //line base.qtpl:30 } // Base template implementation. Other pages may inherit from it if they need // overriding only certain Partial methods. //line base.qtpl:35 type Base struct{} //line base.qtpl:36 func (b *Base) StreamBody(qw422016 *qt422016.Writer) { //line base.qtpl:36 qw422016.N().S(`This is the base body`) } //line base.qtpl:36 //line base.qtpl:36 func (b *Base) WriteBody(qq422016 qtio422016.Writer) { //line base.qtpl:36 qw422016 := qt422016.AcquireWriter(qq422016) //line base.qtpl:36 b.StreamBody(qw422016) //line base.qtpl:36 qt422016.ReleaseWriter(qw422016) //line base.qtpl:36 } //line base.qtpl:36 func (b *Base) Body() string { //line base.qtpl:36 qb422016 := qt422016.AcquireByteBuffer() //line base.qtpl:36 b.WriteBody(qb422016) //line base.qtpl:36 qs422016 := string(qb422016.B) //line base.qtpl:36 qt422016.ReleaseByteBuffer(qb422016) //line base.qtpl:36 return qs422016 //line base.qtpl:36 } ================================================ FILE: _examples/view/quicktemplate/templates/hello.qtpl ================================================ Hello template, implements the Partial's methods. {% code type Hello struct { Vars map[string]any } %} {% func (h *Hello) Body() %}

    {%v h.Vars["message"] %}

    Hello {%v h.Vars["name"] %}!
    {% endfunc %} ================================================ FILE: _examples/view/quicktemplate/templates/hello.qtpl.go ================================================ // This file is automatically generated by qtc from "hello.qtpl". // See https://github.com/valyala/quicktemplate for details. // Hello template, implements the Partial's methods. // //line hello.qtpl:3 package templates //line hello.qtpl:3 import ( qtio422016 "io" qt422016 "github.com/valyala/quicktemplate" ) //line hello.qtpl:3 var ( _ = qtio422016.Copy _ = qt422016.AcquireByteBuffer ) //line hello.qtpl:4 type Hello struct { Vars map[string]any } //line hello.qtpl:9 func (h *Hello) StreamBody(qw422016 *qt422016.Writer) { //line hello.qtpl:9 qw422016.N().S(`

    `) //line hello.qtpl:10 qw422016.E().V(h.Vars["message"]) //line hello.qtpl:10 qw422016.N().S(`

    Hello `) //line hello.qtpl:12 qw422016.E().V(h.Vars["name"]) //line hello.qtpl:12 qw422016.N().S(`!
    `) //line hello.qtpl:14 } //line hello.qtpl:14 func (h *Hello) WriteBody(qq422016 qtio422016.Writer) { //line hello.qtpl:14 qw422016 := qt422016.AcquireWriter(qq422016) //line hello.qtpl:14 h.StreamBody(qw422016) //line hello.qtpl:14 qt422016.ReleaseWriter(qw422016) //line hello.qtpl:14 } //line hello.qtpl:14 func (h *Hello) Body() string { //line hello.qtpl:14 qb422016 := qt422016.AcquireByteBuffer() //line hello.qtpl:14 h.WriteBody(qb422016) //line hello.qtpl:14 qs422016 := string(qb422016.B) //line hello.qtpl:14 qt422016.ReleaseByteBuffer(qb422016) //line hello.qtpl:14 return qs422016 //line hello.qtpl:14 } ================================================ FILE: _examples/view/quicktemplate/templates/index.qtpl ================================================ Index template, implements the Partial's methods. {% code type Index struct {} %} {% func (i *Index) Body() %}

    Index Page

    This is our index page's body.
    {% endfunc %} ================================================ FILE: _examples/view/quicktemplate/templates/index.qtpl.go ================================================ // This file is automatically generated by qtc from "index.qtpl". // See https://github.com/valyala/quicktemplate for details. // Index template, implements the Partial's methods. // //line index.qtpl:3 package templates //line index.qtpl:3 import ( qtio422016 "io" qt422016 "github.com/valyala/quicktemplate" ) //line index.qtpl:3 var ( _ = qtio422016.Copy _ = qt422016.AcquireByteBuffer ) //line index.qtpl:4 type Index struct{} //line index.qtpl:7 func (i *Index) StreamBody(qw422016 *qt422016.Writer) { //line index.qtpl:7 qw422016.N().S(`

    Index Page

    This is our index page's body.
    `) //line index.qtpl:12 } //line index.qtpl:12 func (i *Index) WriteBody(qq422016 qtio422016.Writer) { //line index.qtpl:12 qw422016 := qt422016.AcquireWriter(qq422016) //line index.qtpl:12 i.StreamBody(qw422016) //line index.qtpl:12 qt422016.ReleaseWriter(qw422016) //line index.qtpl:12 } //line index.qtpl:12 func (i *Index) Body() string { //line index.qtpl:12 qb422016 := qt422016.AcquireByteBuffer() //line index.qtpl:12 i.WriteBody(qb422016) //line index.qtpl:12 qs422016 := string(qb422016.B) //line index.qtpl:12 qt422016.ReleaseByteBuffer(qb422016) //line index.qtpl:12 return qs422016 //line index.qtpl:12 } ================================================ FILE: _examples/view/templ/go.mod ================================================ module github.com/kataras/iris/v12/_examples/view/templ go 1.25 require ( github.com/a-h/templ v0.3.977 github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd ) require ( github.com/BurntSushi/toml v1.6.0 // indirect github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect github.com/CloudyKit/jet/v6 v6.3.1 // indirect github.com/Joker/jade v1.1.3 // indirect github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 // indirect github.com/andybalholm/brotli v1.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/fatih/structs v1.1.0 // indirect github.com/flosch/pongo2/v4 v4.0.2 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/iris-contrib/schema v0.0.6 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/kataras/blocks v0.0.8 // indirect github.com/kataras/golog v0.1.12 // indirect github.com/kataras/pio v0.0.13 // indirect github.com/kataras/sitemap v0.0.6 // indirect github.com/kataras/tunnel v0.0.4 // indirect github.com/klauspost/compress v1.18.2 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mailgun/raymond/v2 v2.0.48 // indirect github.com/mailru/easyjson v0.9.1 // indirect github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/schollz/closestmatch v2.1.0+incompatible // indirect github.com/sirupsen/logrus v1.8.1 // indirect github.com/stretchr/testify v1.11.1 // indirect github.com/tdewolff/minify/v2 v2.24.8 // indirect github.com/tdewolff/parse/v2 v2.8.5 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/yosssi/ace v0.0.5 // indirect golang.org/x/crypto v0.47.0 // indirect golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect golang.org/x/net v0.49.0 // indirect golang.org/x/sys v0.40.0 // indirect golang.org/x/text v0.33.0 // indirect golang.org/x/time v0.14.0 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: _examples/view/templ/go.sum ================================================ github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= github.com/CloudyKit/jet/v6 v6.3.1 h1:6IAo5Cx21xrHVaR8zzXN5gJatKV/wO7Nf6bfCnCSbUw= github.com/CloudyKit/jet/v6 v6.3.1/go.mod h1:lf8ksdNsxZt7/yH/3n4vJQWA9RUq4wpaHtArHhGVMOw= github.com/Joker/hpp v1.0.0 h1:65+iuJYdRXv/XyN62C1uEmmOx3432rNG/rKlX6V7Kkc= github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= github.com/Joker/jade v1.1.3 h1:Qbeh12Vq6BxURXT1qZBRHsDxeURB8ztcL6f3EXSGeHk= github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKduM= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 h1:dG7/gLroJGht/jSQtHiLvT48Hxn+crbmvyItZC8cWOs= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05/go.mod h1:NYezi6wtnJtBm5btoprXc5SvAdqH0XTXWnUup0MptAI= github.com/a-h/templ v0.3.977 h1:kiKAPXTZE2Iaf8JbtM21r54A8bCNsncrfnokZZSrSDg= github.com/a-h/templ v0.3.977/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/flosch/pongo2/v4 v4.0.2 h1:gv+5Pe3vaSVmiJvh/BZa82b7/00YUGm0PIyVVLop0Hw= github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+BWPhwZD8= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a h1:l7A0loSszR5zHd/qK53ZIHMO8b3bBSmENnQ6eKnUT0A= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/iris-contrib/httpexpect/v2 v2.15.2 h1:T9THsdP1woyAqKHwjkEsbCnMefsAFvk8iJJKokcJ3Go= github.com/iris-contrib/httpexpect/v2 v2.15.2/go.mod h1:JLDgIqnFy5loDSUv1OA2j0mb6p/rDhiCqigP22Uq9xE= github.com/iris-contrib/schema v0.0.6 h1:CPSBLyx2e91H2yJzPuhGuifVRnZBBJ3pCOMbOvPZaTw= github.com/iris-contrib/schema v0.0.6/go.mod h1:iYszG0IOsuIsfzjymw1kMzTL8YQcCWlm65f3wX8J5iA= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kataras/blocks v0.0.8 h1:MrpVhoFTCR2v1iOOfGng5VJSILKeZZI+7NGfxEh3SUM= github.com/kataras/blocks v0.0.8/go.mod h1:9Jm5zx6BB+06NwA+OhTbHW1xkMOYxahnqTN5DveZ2Yg= github.com/kataras/golog v0.1.12 h1:Bu7I/G4ilJlbfzjmU39O9N+2uO1pBcMK045fzZ4ytNg= github.com/kataras/golog v0.1.12/go.mod h1:wrGSbOiBqbQSQznleVNX4epWM8rl9SJ/rmEacl0yqy4= github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd h1:oMsQgqKACbUdlaIWnN+EZzzAnHkw90hE/y/R0BSij2U= github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd/go.mod h1:yucjtDl9Vn3OctWM1fi0MuM9OckemC7kEreFa9QtPF8= github.com/kataras/pio v0.0.13 h1:x0rXVX0fviDTXOOLOmr4MUxOabu1InVSTu5itF8CXCM= github.com/kataras/pio v0.0.13/go.mod h1:k3HNuSw+eJ8Pm2lA4lRhg3DiCjVgHlP8hmXApSej3oM= github.com/kataras/sitemap v0.0.6 h1:w71CRMMKYMJh6LR2wTgnk5hSgjVNB9KL60n5e2KHvLY= github.com/kataras/sitemap v0.0.6/go.mod h1:dW4dOCNs896OR1HmG+dMLdT7JjDk7mYBzoIRwuj5jA4= github.com/kataras/tunnel v0.0.4 h1:sCAqWuJV7nPzGrlb0os3j49lk2JhILT0rID38NHNLpA= github.com/kataras/tunnel v0.0.4/go.mod h1:9FkU4LaeifdMWqZu7o20ojmW4B7hdhv2CMLwfnHGpYw= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw= github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18= github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo= github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk= github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tdewolff/minify/v2 v2.24.8 h1:58/VjsbevI4d5FGV0ZSuBrHMSSkH4MCH0sIz/eKIauE= github.com/tdewolff/minify/v2 v2.24.8/go.mod h1:0Ukj0CRpo/sW/nd8uZ4ccXaV1rEVIWA3dj8U7+Shhfw= github.com/tdewolff/parse/v2 v2.8.5 h1:ZmBiA/8Do5Rpk7bDye0jbbDUpXXbCdc3iah4VeUvwYU= github.com/tdewolff/parse/v2 v2.8.5/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo= github.com/tdewolff/test v1.0.11 h1:FdLbwQVHxqG16SlkGveC0JVyrJN62COWTRyUFzfbtBE= github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA= github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0= github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs= moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHcE= ================================================ FILE: _examples/view/templ/hello.templ ================================================ package main templ hello(name string) {
    Hello, { name }
    } ================================================ FILE: _examples/view/templ/hello_templ.go ================================================ // Code generated by templ - DO NOT EDIT. // templ: version: 0.2.476 package main //lint:file-ignore SA4006 This context is only used if a nested component is present. import "github.com/a-h/templ" import "context" import "io" import "bytes" func hello(name string) templ.Component { return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) if !templ_7745c5c3_IsBuffer { templ_7745c5c3_Buffer = templ.GetBuffer() defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) } ctx = templ.InitializeContext(ctx) templ_7745c5c3_Var1 := templ.GetChildren(ctx) if templ_7745c5c3_Var1 == nil { templ_7745c5c3_Var1 = templ.NopComponent } ctx = templ.ClearChildren(ctx) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } templ_7745c5c3_Var2 := `Hello, ` _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var3 string = name _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if !templ_7745c5c3_IsBuffer { _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) } return templ_7745c5c3_Err }) } ================================================ FILE: _examples/view/templ/main.go ================================================ package main import ( "github.com/kataras/iris/v12" ) // $ go install github.com/a-h/templ/cmd/templ@latest // $ templ generate // $ go run . func main() { component := hello("Makis") app := iris.New() app.Get("/", iris.Component(component)) app.Listen(":8080") } ================================================ FILE: _examples/view/template_ace_0/main.go ================================================ package main import ( "fmt" "github.com/kataras/iris/v12" ) func main() { app := iris.New() // Read about its markup syntax at: https://github.com/yosssi/ace tmpl := iris.Ace("./views", ".ace") // tmpl.Layout("layouts/main.ace") -> global layout for all pages. app.RegisterView(tmpl) app.Get("/", func(ctx iris.Context) { if err := ctx.View("index", iris.Map{ "Title": "Title of The Page", }); err != nil { ctx.HTML(fmt.Sprintf("

    %s

    ", err.Error())) return } }) app.Get("/layout", func(ctx iris.Context) { ctx.ViewLayout("layouts/main") // layout for that response. if err := ctx.View("index", iris.Map{ // file extension is optional. "Title": "Title of the main Page", }); err != nil { ctx.HTML(fmt.Sprintf("

    %s

    ", err.Error())) return } }) // otherGroup := app.Party("/other").Layout("layouts/other.ace") -> layout for that party. // otherGroup.Get("/", func(ctx iris.Context) { ctx.View("index.ace", [...]) }) app.Listen(":8080") } ================================================ FILE: _examples/view/template_ace_0/views/index.ace ================================================ = include partials/header.ace . h2 {{.Title}} h3 Body = include partials/footer.ace . ================================================ FILE: _examples/view/template_ace_0/views/layouts/main.ace ================================================ = doctype html html head title Main Page body h1 Layout {{ yield . }} ================================================ FILE: _examples/view/template_ace_0/views/partials/footer.ace ================================================ h1 Partial Footer ================================================ FILE: _examples/view/template_ace_0/views/partials/header.ace ================================================ h1 Partial Header ================================================ FILE: _examples/view/template_blocks_0/main.go ================================================ package main import ( "fmt" "github.com/kataras/iris/v12" ) func main() { app := iris.New() // Read about its syntax at: https://github.com/kataras/blocks app.RegisterView(iris.Blocks("./views", ".html").Reload(true)) app.Get("/", index) app.Get("/500", internalServerError) app.Listen(":8080") } func index(ctx iris.Context) { data := iris.Map{ "Title": "Page Title", } ctx.ViewLayout("main") if err := ctx.View("index", data); err != nil { ctx.HTML(fmt.Sprintf("

    %s

    ", err.Error())) return } } func internalServerError(ctx iris.Context) { ctx.StatusCode(iris.StatusInternalServerError) data := iris.Map{ "Code": iris.StatusInternalServerError, "Message": "Internal Server Error", } ctx.ViewLayout("error") if err := ctx.View("500", data); err != nil { ctx.HTML(fmt.Sprintf("

    %s

    ", err.Error())) return } } ================================================ FILE: _examples/view/template_blocks_0/views/500.html ================================================ {{ define "content" }}

    Internal Server Error

    {{ end }} {{ define "message" }}

    {{.Message}}

    {{ end }} ================================================ FILE: _examples/view/template_blocks_0/views/index.html ================================================

    Index Body

    ================================================ FILE: _examples/view/template_blocks_0/views/layouts/error.html ================================================ {{.Code}} {{ template "content" . }} {{ block "message" . }}Default Error Message{{ end }} ================================================ FILE: _examples/view/template_blocks_0/views/layouts/main.html ================================================ {{ if .Title }}{{ .Title }}{{ else }}Default Main Title{{ end }} {{ template "content" . }}
    {{ partial "partials/footer" . }}
    ================================================ FILE: _examples/view/template_blocks_0/views/partials/footer.html ================================================

    Footer Partial

    ================================================ FILE: _examples/view/template_blocks_1_embedded/bindata.go ================================================ // Code generated by go-bindata. (@generated) DO NOT EDIT. // Package main generated by go-bindata.// sources: // ../template_blocks_0/views/500.html // ../template_blocks_0/views/index.html // ../template_blocks_0/views/layouts/error.html // ../template_blocks_0/views/layouts/main.html // ../template_blocks_0/views/partials/footer.html package main import ( "bytes" "compress/gzip" "fmt" "io" "net/http" "os" "path/filepath" "strings" "time" ) func bindataRead(data []byte, name string) ([]byte, error) { gz, err := gzip.NewReader(bytes.NewBuffer(data)) if err != nil { return nil, fmt.Errorf("read %q: %v", name, err) } var buf bytes.Buffer _, err = io.Copy(&buf, gz) clErr := gz.Close() if err != nil { return nil, fmt.Errorf("read %q: %v", name, err) } if clErr != nil { return nil, err } return buf.Bytes(), nil } type asset struct { bytes []byte info os.FileInfo } type bindataFileInfo struct { name string size int64 mode os.FileMode modTime time.Time } // Name return file name func (fi bindataFileInfo) Name() string { return fi.name } // Size return file size func (fi bindataFileInfo) Size() int64 { return fi.size } // Mode return file mode func (fi bindataFileInfo) Mode() os.FileMode { return fi.mode } // ModTime return file modify time func (fi bindataFileInfo) ModTime() time.Time { return fi.modTime } // IsDir return file whether a directory func (fi bindataFileInfo) IsDir() bool { return fi.mode&os.ModeDir != 0 } // Sys return file is sys mode func (fi bindataFileInfo) Sys() any { return nil } type assetFile struct { *bytes.Reader name string childInfos []os.FileInfo childInfoOffset int } type assetOperator struct{} // Open implement http.FileSystem interface func (f *assetOperator) Open(name string) (http.File, error) { var err error if len(name) > 0 && name[0] == '/' { name = name[1:] } content, err := Asset(name) if err == nil { return &assetFile{name: name, Reader: bytes.NewReader(content)}, nil } children, err := AssetDir(name) if err == nil { childInfos := make([]os.FileInfo, 0, len(children)) for _, child := range children { childPath := filepath.Join(name, child) info, errInfo := AssetInfo(filepath.Join(name, child)) if errInfo == nil { childInfos = append(childInfos, info) } else { childInfos = append(childInfos, newDirFileInfo(childPath)) } } return &assetFile{name: name, childInfos: childInfos}, nil } else { // If the error is not found, return an error that will // result in a 404 error. Otherwise the server returns // a 500 error for files not found. if strings.Contains(err.Error(), "not found") { return nil, os.ErrNotExist } return nil, err } } // Close no need do anything func (f *assetFile) Close() error { return nil } // Readdir read dir's children file info func (f *assetFile) Readdir(count int) ([]os.FileInfo, error) { if len(f.childInfos) == 0 { return nil, os.ErrNotExist } if count <= 0 { return f.childInfos, nil } if f.childInfoOffset+count > len(f.childInfos) { count = len(f.childInfos) - f.childInfoOffset } offset := f.childInfoOffset f.childInfoOffset += count return f.childInfos[offset : offset+count], nil } // Stat read file info from asset item func (f *assetFile) Stat() (os.FileInfo, error) { if len(f.childInfos) != 0 { return newDirFileInfo(f.name), nil } return AssetInfo(f.name) } // newDirFileInfo return default dir file info func newDirFileInfo(name string) os.FileInfo { return &bindataFileInfo{ name: name, size: 0, mode: os.FileMode(2147484068), // equal os.FileMode(0644)|os.ModeDir modTime: time.Time{}} } // AssetFile return a http.FileSystem instance that data backend by asset func AssetFile() http.FileSystem { return &assetOperator{} } var __500Html = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x7c\x8f\xb1\x4e\xf3\x30\x14\x85\xf7\x48\x79\x87\xfb\x67\xe9\x8f\xd4\xa4\xea\x5a\x42\x37\x06\x06\xa6\x22\x21\x46\xc7\x3e\xad\x2d\x9c\x7b\x23\xdb\x49\x89\xa2\xbc\x3b\x6a\x5a\x04\x2c\xac\xf7\x7e\xe7\x3b\x3a\xf5\xbf\xb2\xa4\x37\xe9\x49\x2b\x26\x83\xa3\x63\x50\x2b\x01\x94\xac\x62\x12\x06\x35\x5e\xf4\x7b\x95\x67\x2f\x16\x17\x40\xf5\x3e\x2d\x77\x17\xa9\xd0\xc2\x09\x9c\x0a\x3a\x5b\xa7\x2d\x45\x2b\xbd\x37\xd4\x5c\xd2\xa0\x56\x39\xa6\x84\xb6\xf3\x2a\x61\x15\xa9\x11\x33\x56\x79\x76\x90\x35\x61\x00\x93\x3b\x92\x4b\xab\x48\xad\x8b\xd1\xf1\x89\xfe\x47\x80\x1c\x1b\x7c\x54\x36\xb5\xfe\x6e\x7d\x7d\x2b\x63\x60\x48\xf5\x49\x5a\x95\x9c\x56\xde\x8f\xd4\x8c\x4b\xc3\xe0\x70\x26\xf0\xc9\x31\xaa\x3c\x7b\xb5\x60\x1a\xa5\x27\x06\x0c\x25\xf9\x63\xce\x7a\xe1\xac\x1a\x70\xe1\x9a\x1b\x13\x3b\x68\x77\x74\x7a\x97\x67\x65\xb9\xcf\xb3\x69\xfa\x52\x7c\x0f\x9d\xe7\x3c\xab\xed\x76\xff\xc4\x09\x81\x95\xa7\x03\xc2\x80\x40\x8f\x21\x48\xa8\x37\x76\x7b\xcd\x81\xcd\x82\xfe\x92\xb4\x88\x51\x9d\x70\x93\x74\x14\xd3\xe8\xf1\x50\x68\xf1\x12\x76\x01\xe6\xbe\xd8\x4f\x53\xf5\x7c\xa5\xe6\xb9\xde\x74\x3f\x65\x9f\x01\x00\x00\xff\xff\xb3\xfa\x91\xbd\xaa\x01\x00\x00") func _500HtmlBytes() ([]byte, error) { return bindataRead( __500Html, "500.html", ) } func _500Html() (*asset, error) { bytes, err := _500HtmlBytes() if err != nil { return nil, err } info := bindataFileInfo{name: "500.html", size: 426, mode: os.FileMode(438), modTime: time.Unix(1599156854, 0)} a := &asset{bytes: bytes, info: info} return a, nil } var _indexHtml = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xb2\xc9\x30\xb4\xf3\xcc\x4b\x49\xad\x50\x70\xca\x4f\xa9\xb4\xd1\xcf\x30\xb4\x03\x04\x00\x00\xff\xff\xcc\x4b\x98\x69\x13\x00\x00\x00") func indexHtmlBytes() ([]byte, error) { return bindataRead( _indexHtml, "index.html", ) } func indexHtml() (*asset, error) { bytes, err := indexHtmlBytes() if err != nil { return nil, err } info := bindataFileInfo{name: "index.html", size: 19, mode: os.FileMode(438), modTime: time.Unix(1599156854, 0)} a := &asset{bytes: bytes, info: info} return a, nil } var _layoutsErrorHtml = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x54\x90\xbf\x4e\xf3\x40\x10\xc4\x7b\x4b\x7e\x87\xfd\xb6\xfe\x6c\x43\x47\x71\xe7\x26\x40\x0b\x45\x28\x28\x37\x77\xa3\xf8\xc4\xfd\x89\xe2\x55\x22\x74\xf2\xbb\xa3\x18\x83\x44\xb5\xda\x99\x9f\x66\x57\x63\xfe\x3d\xbe\xec\xf6\xef\xaf\x4f\x34\x69\x8a\x63\xdb\x98\xdb\xa4\x28\xf9\x68\x19\x99\x57\x05\xe2\xc7\xb6\x21\x22\x32\x09\x2a\xe4\x26\x39\xcf\x50\xcb\x6f\xfb\xe7\xee\x81\xff\x78\x59\x12\x2c\x5f\x02\xae\xa7\x72\x56\x26\x57\xb2\x22\xab\xe5\x6b\xf0\x3a\x59\x8f\x4b\x70\xe8\xd6\xe5\x3f\x85\x1c\x34\x48\xec\x66\x27\x11\xf6\xbe\xbf\xfb\xcd\xd2\xa0\x11\x63\xad\xfd\xae\x78\x2c\x8b\x19\xbe\x85\xb6\x31\xc3\xf6\x8e\x39\x14\xff\xb9\xe1\xb5\x92\x22\x9d\xa2\x28\x88\xb7\x8b\x4c\xfd\xb2\xb4\xcd\x0f\x70\x88\xc5\x7d\x10\x27\xcc\xb3\x1c\xb1\x9a\xb5\x22\xfb\x1b\x63\x86\x2d\xcb\x0c\x6b\x0b\x5f\x01\x00\x00\xff\xff\xbe\xb7\x11\x67\x15\x01\x00\x00") func layoutsErrorHtmlBytes() ([]byte, error) { return bindataRead( _layoutsErrorHtml, "layouts/error.html", ) } func layoutsErrorHtml() (*asset, error) { bytes, err := layoutsErrorHtmlBytes() if err != nil { return nil, err } info := bindataFileInfo{name: "layouts/error.html", size: 277, mode: os.FileMode(438), modTime: time.Unix(1599156854, 0)} a := &asset{bytes: bytes, info: info} return a, nil } var _layoutsMainHtml = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x54\x90\xb1\x4e\xf4\x30\x10\x84\xfb\x48\x79\x87\xf9\x5d\xff\x49\xa0\xa3\xb0\xd3\x70\xd0\x21\x28\x42\x41\xb9\x24\x1b\x62\xc9\x71\xa2\x64\xb9\x13\xb2\xfc\xee\xc8\x39\x0b\xe9\x2a\xcf\xfa\xb3\x66\xc6\xab\xff\x9d\x5e\x1f\xbb\x8f\xb7\x27\x4c\x32\xbb\xb6\x2c\x74\x3a\xe1\xc8\x7f\x19\xc5\x5e\x1d\x37\x4c\x43\x5b\x16\x00\xa0\x67\x16\x42\x3f\xd1\xb6\xb3\x18\xf5\xde\x3d\x57\x0f\xea\x86\x79\x9a\xd9\xa8\xb3\xe5\xcb\xba\x6c\xa2\xd0\x2f\x5e\xd8\x8b\x51\x17\x3b\xc8\x64\x06\x3e\xdb\x9e\xab\x63\xf8\x0f\xeb\xad\x58\x72\xd5\xde\x93\x63\x73\x5f\xdf\xfd\x79\x89\x15\xc7\x6d\x08\xb0\x23\xea\x2e\x0d\x88\x31\x84\x1b\xcd\x6e\x4f\xea\xc4\x23\x7d\x3b\xc1\x0b\x59\x8f\x03\x27\xe6\x07\xc4\xa8\x9b\xab\x4f\x59\xe8\x26\xff\x42\x7f\x2e\xc3\x4f\x4e\x09\x01\xc2\xf3\xea\x48\x18\x2a\x17\x55\xa8\x11\x63\x59\x94\x85\x1e\x97\x45\x78\x4b\x25\x56\xda\x52\x4f\xa8\x2c\xf6\xe6\xca\x14\xea\x14\x92\x1f\xa6\x94\xec\xae\x9b\x63\x9d\xbf\x01\x00\x00\xff\xff\x44\x95\x63\x98\x5e\x01\x00\x00") func layoutsMainHtmlBytes() ([]byte, error) { return bindataRead( _layoutsMainHtml, "layouts/main.html", ) } func layoutsMainHtml() (*asset, error) { bytes, err := layoutsMainHtmlBytes() if err != nil { return nil, err } info := bindataFileInfo{name: "layouts/main.html", size: 350, mode: os.FileMode(438), modTime: time.Unix(1599156854, 0)} a := &asset{bytes: bytes, info: info} return a, nil } var _partialsFooterHtml = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xb2\xc9\x30\xb6\x73\xcb\xcf\x2f\x49\x2d\x52\x08\x48\x2c\x2a\xc9\x4c\xcc\xb1\xd1\xcf\x30\xb6\x03\x04\x00\x00\xff\xff\x08\xe6\xe9\xf8\x17\x00\x00\x00") func partialsFooterHtmlBytes() ([]byte, error) { return bindataRead( _partialsFooterHtml, "partials/footer.html", ) } func partialsFooterHtml() (*asset, error) { bytes, err := partialsFooterHtmlBytes() if err != nil { return nil, err } info := bindataFileInfo{name: "partials/footer.html", size: 23, mode: os.FileMode(438), modTime: time.Unix(1599156854, 0)} a := &asset{bytes: bytes, info: info} return a, nil } // Asset loads and returns the asset for the given name. // It returns an error if the asset could not be found or // could not be loaded. func Asset(name string) ([]byte, error) { canonicalName := strings.Replace(name, "\\", "/", -1) if f, ok := _bindata[canonicalName]; ok { a, err := f() if err != nil { return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err) } return a.bytes, nil } return nil, fmt.Errorf("Asset %s not found", name) } // MustAsset is like Asset but panics when Asset would return an error. // It simplifies safe initialization of global variables. func MustAsset(name string) []byte { a, err := Asset(name) if err != nil { panic("asset: Asset(" + name + "): " + err.Error()) } return a } // AssetInfo loads and returns the asset info for the given name. // It returns an error if the asset could not be found or // could not be loaded. func AssetInfo(name string) (os.FileInfo, error) { canonicalName := strings.Replace(name, "\\", "/", -1) if f, ok := _bindata[canonicalName]; ok { a, err := f() if err != nil { return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err) } return a.info, nil } return nil, fmt.Errorf("AssetInfo %s not found", name) } // AssetNames returns the names of the assets. func AssetNames() []string { names := make([]string, 0, len(_bindata)) for name := range _bindata { names = append(names, name) } return names } // _bindata is a table, holding each asset generator, mapped to its name. var _bindata = map[string]func() (*asset, error){ "500.html": _500Html, "index.html": indexHtml, "layouts/error.html": layoutsErrorHtml, "layouts/main.html": layoutsMainHtml, "partials/footer.html": partialsFooterHtml, } // AssetDir returns the file names below a certain // directory embedded in the file by go-bindata. // For example if you run go-bindata on data/... and data contains the // following hierarchy: // // data/ // foo.txt // img/ // a.png // b.png // // then AssetDir("data") would return []string{"foo.txt", "img"} // AssetDir("data/img") would return []string{"a.png", "b.png"} // AssetDir("foo.txt") and AssetDir("nonexistent") would return an error // AssetDir("") will return []string{"data"}. func AssetDir(name string) ([]string, error) { node := _bintree if len(name) != 0 { canonicalName := strings.Replace(name, "\\", "/", -1) pathList := strings.Split(canonicalName, "/") for _, p := range pathList { node = node.Children[p] if node == nil { return nil, fmt.Errorf("Asset %s not found", name) } } } if node.Func != nil { return nil, fmt.Errorf("Asset %s not found", name) } rv := make([]string, 0, len(node.Children)) for childName := range node.Children { rv = append(rv, childName) } return rv, nil } type bintree struct { Func func() (*asset, error) Children map[string]*bintree } var _bintree = &bintree{nil, map[string]*bintree{ "500.html": {_500Html, map[string]*bintree{}}, "index.html": {indexHtml, map[string]*bintree{}}, "layouts": {nil, map[string]*bintree{ "error.html": {layoutsErrorHtml, map[string]*bintree{}}, "main.html": {layoutsMainHtml, map[string]*bintree{}}, }}, "partials": {nil, map[string]*bintree{ "footer.html": {partialsFooterHtml, map[string]*bintree{}}, }}, }} // RestoreAsset restores an asset under the given directory func RestoreAsset(dir, name string) error { data, err := Asset(name) if err != nil { return err } info, err := AssetInfo(name) if err != nil { return err } err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755)) if err != nil { return err } err = os.WriteFile(_filePath(dir, name), data, info.Mode()) if err != nil { return err } err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime()) if err != nil { return err } return nil } // RestoreAssets restores an asset under the given directory recursively func RestoreAssets(dir, name string) error { children, err := AssetDir(name) // File if err != nil { return RestoreAsset(dir, name) } // Dir for _, child := range children { err = RestoreAssets(dir, filepath.Join(name, child)) if err != nil { return err } } return nil } func _filePath(dir, name string) string { canonicalName := strings.Replace(name, "\\", "/", -1) return filepath.Join(append([]string{dir}, strings.Split(canonicalName, "/")...)...) } ================================================ FILE: _examples/view/template_blocks_1_embedded/main.go ================================================ package main import ( "fmt" "github.com/kataras/iris/v12" ) // $ go install github.com/go-bindata/go-bindata/v3/go-bindata@latest // // $ go-bindata -fs -prefix "../template_blocks_0/views" ../template_blocks_0/views/... // $ go run . // // # OR: go-bindata -fs -prefix "views" ./views/... if the views dir is rel to the executable. // # OR: go-bindata -fs -prefix "../template_blocks_0" ../template_blocks_0/views/... // # with iris.Blocks(AssetFile()).RootDir("/views") // // System files are not used, you can optionally delete the folder and run the example now. func main() { app := iris.New() app.RegisterView(iris.Blocks(AssetFile(), ".html")) app.Get("/", index) app.Get("/500", internalServerError) app.Listen(":8080") } func index(ctx iris.Context) { data := iris.Map{ "Title": "Page Title", } ctx.ViewLayout("main") if err := ctx.View("index", data); err != nil { ctx.HTML(fmt.Sprintf("

    %s

    ", err.Error())) return } } func internalServerError(ctx iris.Context) { ctx.StatusCode(iris.StatusInternalServerError) data := iris.Map{ "Code": iris.StatusInternalServerError, "Message": "Internal Server Error", } ctx.ViewLayout("error") if err := ctx.View("500", data); err != nil { ctx.HTML(fmt.Sprintf("

    %s

    ", err.Error())) return } } ================================================ FILE: _examples/view/template_blocks_2/main.go ================================================ package main import "github.com/kataras/iris/v12" // Based on https://github.com/kataras/iris/issues/2214. func main() { app := initApp() app.Listen(":8080") } func initApp() *iris.Application { app := iris.New() app.Logger().SetLevel("debug") tmpl := iris.Blocks("./src/public/html", ".html") tmpl.Layout("main") app.RegisterView(tmpl) app.Get("/list", func(ctx iris.Context) { ctx.View("files/list") }) app.Get("/menu", func(ctx iris.Context) { ctx.View("menu/menu") }) app.Get("/list2", func(ctx iris.Context) { ctx.ViewLayout("secondary") ctx.View("files/list") }) app.Get("/menu2", func(ctx iris.Context) { ctx.ViewLayout("secondary") ctx.View("menu/menu") }) return app } ================================================ FILE: _examples/view/template_blocks_2/src/public/html/files/list.html ================================================ {{ define "title"}} 222 {{ end }} {{ define "content" }}

    List Content

    {{ end }} ================================================ FILE: _examples/view/template_blocks_2/src/public/html/layouts/main.html ================================================ {{ block "title" .}}Main Default Title{{end}} {{ block "content" .}}

    Main Default Content

    {{end}} ================================================ FILE: _examples/view/template_blocks_2/src/public/html/layouts/secondary.html ================================================ {{ block "title" .}}Secondary Default Title{{end}}

    Secondary Layout

    {{ block "content" .}}

    Secondary Default Content

    {{end}} ================================================ FILE: _examples/view/template_blocks_2/src/public/html/menu/menu.html ================================================ {{ define "title" }} 111{{ end }} {{ define "content" }}

    Menu Content

    {{ end }} ================================================ FILE: _examples/view/template_django_0/main.go ================================================ package main import ( "fmt" "time" "github.com/kataras/iris/v12" // optionally, register filters like `timesince`. _ "github.com/iris-contrib/pongo2-addons/v4" ) var startTime = time.Now() func main() { app := iris.New() tmpl := iris.Django("./templates", ".html") tmpl.Reload(true) // reload templates on each request (development mode) tmpl.AddFunc("greet", func(s string) string { // {{greet(name)}} return "Greetings " + s + "!" }) // tmpl.RegisterFilter("myFilter", myFilter) // {{"simple input for filter"|myFilter}} app.RegisterView(tmpl) app.Get("/", hi) // http://localhost:8080 app.Listen(":8080") } func hi(ctx iris.Context) { // ctx.ViewData("title", "Hi Page") // ctx.ViewData("name", "iris") // ctx.ViewData("serverStartTime", startTime) // or if you set all view data in the same handler you can use the // iris.Map/pongo2.Context/map[string]any, look below: if err := ctx.View("hi.html", iris.Map{ "title": "Hi Page", "name": "iris", "serverStartTime": startTime, }); err != nil { ctx.HTML(fmt.Sprintf("

    %s

    ", err.Error())) return } } ================================================ FILE: _examples/view/template_django_0/templates/hi.html ================================================ {{title}}

    Hi {{name|capfirst}}

    {{greet(name)}}

    Server started about {{serverStartTime|timesince}}. Refresh the page to see different result

    ================================================ FILE: _examples/view/template_django_1/main.go ================================================ package main import ( "fmt" "github.com/kataras/iris/v12" ) func main() { app := iris.New() app.RegisterView(iris.Django("./views", ".html").Reload(true)) mypathRoute := app.Get("/mypath", writePathHandler) mypathRoute.Name = "my-page1" mypath2Route := app.Get("/mypath2/{paramfirst}/{paramsecond}", writePathHandler) mypath2Route.Name = "my-page2" mypath3Route := app.Get("/mypath3/{paramfirst}/statichere/{paramsecond}", writePathHandler) mypath3Route.Name = "my-page3" mypath4Route := app.Get("/mypath4/{paramfirst}/statichere/{paramsecond}/{otherparam}/{something:path}", writePathHandler) // same as: app.Get("/mypath4/:paramfirst/statichere/:paramsecond/:otherparam/*something", writePathHandler) mypath4Route.Name = "my-page4" // same with Handle/Func mypath5Route := app.Handle("GET", "/mypath5/{paramfirst}/statichere/{paramsecond}/{otherparam}/anything/{something:path}", writePathHandler) mypath5Route.Name = "my-page5" mypath6Route := app.Get("/mypath6/{paramfirst}/{paramsecond}/statichere/{paramThirdAfterStatic}", writePathHandler) mypath6Route.Name = "my-page6" app.Get("/", func(ctx iris.Context) { // for /mypath6... paramsAsArray := []string{"theParam1", "theParam2", "paramThirdAfterStatic"} ctx.ViewData("ParamsAsArray", paramsAsArray) if err := ctx.View("page.html"); err != nil { ctx.HTML(fmt.Sprintf("

    %s

    ", err.Error())) return } }) app.Get("/redirect/{namedRoute}", func(ctx iris.Context) { routeName := ctx.Params().Get("namedRoute") r := app.GetRoute(routeName) if r == nil { ctx.StatusCode(404) ctx.Writef("Route with name %s not found", routeName) return } println("The path of " + routeName + "is: " + r.Path) // if routeName == "my-page1" // prints: The path of of my-page1 is: /mypath // if it's a path which takes named parameters // then use "r.ResolvePath(paramValuesHere)" ctx.Redirect(r.Path) // http://localhost:8080/redirect/my-page1 will redirect to -> http://localhost:8080/mypath }) // http://localhost:8080 // http://localhost:8080/redirect/my-page1 app.Listen(":8080") } func writePathHandler(ctx iris.Context) { ctx.Writef("Hello from %s.", ctx.Path()) } ================================================ FILE: _examples/view/template_django_1/views/page.html ================================================ urlpath function - django /mypath

    /mypath2/{paramfirst}/{paramsecond}

    /mypath3/{paramfirst}/statichere/{paramsecond}

    /mypath4/{paramfirst}/statichere/{paramsecond}/{otherparam}/{something:path}

    /mypath5/{paramfirst}/statichere/{paramsecond}/{otherparam}/anything/{anything:path}

    /mypath6/{paramfirst}/{paramsecond}/statichere/{paramThirdAfterStatic} ================================================ FILE: _examples/view/template_handlebars_0/main.go ================================================ package main import ( "fmt" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/mvc" ) func main() { app := iris.New() app.Logger().SetLevel("debug") // Init the handlebars engine e := iris.Handlebars("./templates", ".html").Reload(true) // Register a helper. e.AddFunc("fullName", func(person map[string]string) string { return person["firstName"] + " " + person["lastName"] }) app.RegisterView(e) app.Get("/", func(ctx iris.Context) { viewData := iris.Map{ "author": map[string]string{"firstName": "Jean", "lastName": "Valjean"}, "body": "Life is difficult", "comments": []iris.Map{{ "author": map[string]string{"firstName": "Marcel", "lastName": "Beliveau"}, "body": "LOL!", }}, } if err := ctx.View("example.html", viewData); err != nil { ctx.HTML(fmt.Sprintf("

    %s

    ", err.Error())) return } }) exampleRouter := app.Party("/example") /* See context-view-data example: Set data through one or more middleware */ exampleRouter.Use(func(ctx iris.Context) { ctx.ViewData("author", map[string]string{"firstName": "Jean", "lastName": "Valjean"}) ctx.ViewData("body", "Life is difficult") ctx.ViewData("comments", []iris.Map{{ "author": map[string]string{"firstName": "Marcel", "lastName": "Beliveau"}, "body": "LOL!", }}) // OR: // ctx.ViewData("", iris.Map{ // "author": map[string]string{"firstName": "Jean", "lastName": "Valjean"}, // "body": "Life is difficult", // "comments": []iris.Map{{ // "author": map[string]string{"firstName": "Marcel", "lastName": "Beliveau"}, // "body": "LOL!", // }}, // }) ctx.Next() }) mvc.New(exampleRouter).Handle(new(controller)) // Read more about its syntax at: // https://github.com/mailgun/raymond and // https://handlebarsjs.com/guide // http://localhost:8080 // http://localhost:8080/example app.Listen(":8080") } type controller struct{} func (c *controller) Get() mvc.Result { return mvc.View{ Name: "example", Code: 200, } } ================================================ FILE: _examples/view/template_handlebars_0/templates/example.html ================================================

    By {{fullName author}}

    {{body}}

    Comments

    {{#each comments}}

    By {{fullName author}}

    {{body}}
    {{/each}}
    Read more at: https://github.com/mailgun/raymond and https://handlebarsjs.com/guide
    ================================================ FILE: _examples/view/template_html_0/main.go ================================================ package main import ( "fmt" "github.com/kataras/iris/v12" ) func main() { app := iris.New() // defaults to these tmpl := iris.HTML("./templates", ".html") tmpl.Reload(true) // reload templates on each request (development mode) // default template funcs are: // // - {{ urlpath "mynamedroute" "pathParameter_ifneeded" }} // - {{ render "header.html" . }} // - {{ render_r "header.html" . }} // partial relative path to current page // - {{ yield . }} // - {{ current . }} tmpl.AddFunc("greet", func(s string) string { return "Greetings " + s + "!" }) app.RegisterView(tmpl) app.Get("/", hi) // http://localhost:8080 app.Listen(":8080", iris.WithCharset("utf-8")) // defaults to that but you can change it. } func hi(ctx iris.Context) { ctx.ViewData("Title", "Hi Page") ctx.ViewData("Name", "iris") // {{.Name}} will render: iris // ctx.ViewData("", myCcustomStruct{}) if err := ctx.View("hi.html"); err != nil { ctx.HTML(fmt.Sprintf("

    %s

    ", err.Error())) return } } ================================================ FILE: _examples/view/template_html_0/templates/hi.html ================================================ {{.Title}}

    Hi {{.Name}}

    ================================================ FILE: _examples/view/template_html_1/main.go ================================================ package main import ( "fmt" "github.com/kataras/iris/v12" ) type mypage struct { Title string Message string } func main() { app := iris.New() app.RegisterView(iris.HTML("./templates", ".html").Layout("layout.html")) // TIP: append .Reload(true) to reload the templates on each request. app.Get("/", func(ctx iris.Context) { ctx.CompressWriter(true) ctx.ViewData("", mypage{"My Page title", "Hello world!"}) if err := ctx.View("mypage.html"); err != nil { ctx.HTML(fmt.Sprintf("

    %s

    ", err.Error())) return } // Note that: you can pass "layout" : "otherLayout.html" to bypass the config's Layout property // or view.NoLayout to disable layout on this render action. // third is an optional parameter }) // http://localhost:8080 app.Listen(":8080") } ================================================ FILE: _examples/view/template_html_1/templates/layout.html ================================================ My Layout

    [layout] Body content is below...

    {{ yield . }} ================================================ FILE: _examples/view/template_html_1/templates/mypage.html ================================================

    Title: {{.Title}}

    Message: {{.Message}}

    ================================================ FILE: _examples/view/template_html_2/README.md ================================================ ## Info This folder examines the {{render "dir/templatefilename" .}} functionality to manually render any template inside any template ================================================ FILE: _examples/view/template_html_2/main.go ================================================ package main import ( "fmt" "github.com/kataras/iris/v12" ) func main() { app := iris.New() tmpl := iris.HTML("./templates", ".html") tmpl.Layout("layouts/layout.html") tmpl.AddFunc("greet", func(s string) string { return "Greetings " + s + "!" }) app.RegisterView(tmpl) app.Get("/", func(ctx iris.Context) { if err := ctx.View("page1.html"); err != nil { ctx.StatusCode(iris.StatusInternalServerError) ctx.WriteString(err.Error()) } }) // remove the layout for a specific route app.Get("/nolayout", func(ctx iris.Context) { ctx.ViewLayout(iris.NoLayout) if err := ctx.View("page1.html"); err != nil { ctx.StatusCode(iris.StatusInternalServerError) ctx.WriteString(err.Error()) } }) // set a layout for a party, .Layout should be BEFORE any Get or other Handle party's method my := app.Party("/my").Layout("layouts/mylayout.html") { // both of these will use the layouts/mylayout.html as their layout. my.Get("/", func(ctx iris.Context) { if err := ctx.View("page1.html"); err != nil { ctx.HTML(fmt.Sprintf("

    %s

    ", err.Error())) return } }) my.Get("/other", func(ctx iris.Context) { if err := ctx.View("page1.html"); err != nil { ctx.HTML(fmt.Sprintf("

    %s

    ", err.Error())) return } }) } // http://localhost:8080 // http://localhost:8080/nolayout // http://localhost:8080/my // http://localhost:8080/my/other app.Listen(":8080") } ================================================ FILE: _examples/view/template_html_2/templates/layouts/layout.html ================================================ Layout

    This is the global layout


    {{ yield . }} ================================================ FILE: _examples/view/template_html_2/templates/layouts/mylayout.html ================================================ my Layout

    This is the layout for the /my/ and /my/other routes only


    {{ yield . }} ================================================ FILE: _examples/view/template_html_2/templates/page1.html ================================================

    Page 1 {{ greet "iris developer"}}

    {{ render "partials/page1_partial1.html" . }}
    ================================================ FILE: _examples/view/template_html_2/templates/partials/page1_partial1.html ================================================

    Page 1's Partial 1

    ================================================ FILE: _examples/view/template_html_3/main.go ================================================ // Package main an example on how to naming your routes & use the custom // 'url path' HTML Template Engine, same for other template engines. package main import ( "fmt" "github.com/kataras/iris/v12" ) func main() { app := iris.New() app.RegisterView(iris.HTML("./templates", ".html").Reload(true)) mypathRoute := app.Get("/mypath", writePathHandler) mypathRoute.Name = "my-page1" mypath2Route := app.Get("/mypath2/{paramfirst}/{paramsecond}", writePathHandler) mypath2Route.Name = "my-page2" mypath3Route := app.Get("/mypath3/{paramfirst}/statichere/{paramsecond}", writePathHandler) mypath3Route.Name = "my-page3" mypath4Route := app.Get("/mypath4/{paramfirst}/statichere/{paramsecond}/{otherparam}/{something:path}", writePathHandler) // same as: app.Get("/mypath4/:paramfirst/statichere/:paramsecond/:otherparam/*something", writePathHandler) mypath4Route.Name = "my-page4" // same with Handle/Func mypath5Route := app.Handle("GET", "/mypath5/{paramfirst}/statichere/{paramsecond}/{otherparam}/anything/{something:path}", writePathHandler) mypath5Route.Name = "my-page5" mypath6Route := app.Get("/mypath6/{paramfirst}/{paramsecond}/statichere/{paramThirdAfterStatic}", writePathHandler) mypath6Route.Name = "my-page6" app.Get("/", func(ctx iris.Context) { // for /mypath6... paramsAsArray := []string{"theParam1", "theParam2", "paramThirdAfterStatic"} ctx.ViewData("ParamsAsArray", paramsAsArray) if err := ctx.View("page.html"); err != nil { ctx.HTML(fmt.Sprintf("

    %s

    ", err.Error())) return } }) app.Get("/redirect/{namedRoute}", func(ctx iris.Context) { routeName := ctx.Params().Get("namedRoute") r := app.GetRoute(routeName) if r == nil { ctx.StatusCode(404) ctx.Writef("Route with name %s not found", routeName) return } println("The path of " + routeName + "is: " + r.Path) // if routeName == "my-page1" // prints: The path of of my-page1 is: /mypath // if it's a path which takes named parameters // then use "r.ResolvePath(paramValuesHere)" ctx.Redirect(r.Path) // http://localhost:8080/redirect/my-page1 will redirect to -> http://localhost:8080/mypath }) // http://localhost:8080 // http://localhost:8080/redirect/my-page1 app.Listen(":8080") } func writePathHandler(ctx iris.Context) { ctx.Writef("Hello from %s.", ctx.Path()) } ================================================ FILE: _examples/view/template_html_3/templates/page.html ================================================ template_html_3 /mypath

    /mypath2/{paramfirst}/{paramsecond}

    /mypath3/{paramfirst}/statichere/{paramsecond}

    /mypath4/{paramfirst}/statichere/{paramsecond}/{otherparam}/{something:path}

    /mypath5/{paramfirst}/statichere/{paramsecond}/{otherparam}/anything/{anything:path}

    /mypath6/{paramfirst}/{paramsecond}/statichere/{paramThirdAfterStatic} ================================================ FILE: _examples/view/template_html_4/hosts ================================================ # Copyright (c) 1993-2009 Microsoft Corp. # # This is a sample HOSTS file used by Microsoft TCP/IP for Windows. # # This file contains the mappings of IP addresses to host names. Each # entry should be kept on an individual line. The IP address should # be placed in the first column followed by the corresponding host name. # The IP address and the host name should be separated by at least one # space. # # Additionally, comments (such as these) may be inserted on individual # lines or following the machine name denoted by a '#' symbol. # # For example: # # 102.54.94.97 rhino.acme.com # source server # 38.25.63.10 x.acme.com # x client host # localhost name resolution is handled within DNS itself. 127.0.0.1 localhost ::1 localhost #-iris-For development machine, you have to configure your dns also for online, search google how to do it if you don't know 127.0.0.1 username1.127.0.0.1 127.0.0.1 username2.127.0.0.1 127.0.0.1 username3.127.0.0.1 127.0.0.1 username4.127.0.0.1 127.0.0.1 username5.127.0.0.1 # note that you can always use custom subdomains #-END iris- # Windows: Drive:/Windows/system32/drivers/etc/hosts, on Linux: /etc/hosts ================================================ FILE: _examples/view/template_html_4/main.go ================================================ // Package main an example on how to naming your routes & use the custom 'url' HTML Template Engine, same for other template engines. package main import ( "fmt" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/core/router" ) const ( host = "127.0.0.1:8080" ) func main() { app := iris.New() // create a custom path reverser, iris let you define your own host and scheme // which is useful when you have nginx or caddy in front of iris. rv := router.NewRoutePathReverser(app, router.WithHost(host), router.WithScheme("http")) // locate and define our templates as usual. templates := iris.HTML("./templates", ".html") // add a custom func of "url" and pass the rv.URL as its template function body, // so {{url "routename" "paramsOrSubdomainAsFirstArgument"}} will work inside our templates. templates.AddFunc("url", rv.URL) app.RegisterView(templates) // wildcard subdomain, will catch username1.... username2.... username3... username4.... username5... // that our below links are providing via page.html's first argument which is the subdomain. subdomain := app.WildcardSubdomain() mypathRoute := subdomain.Get("/mypath", emptyHandler) mypathRoute.Name = "my-page1" mypath2Route := subdomain.Get("/mypath2/{paramfirst}/{paramsecond}", emptyHandler) mypath2Route.Name = "my-page2" mypath3Route := subdomain.Get("/mypath3/{paramfirst}/statichere/{paramsecond}", emptyHandler) mypath3Route.Name = "my-page3" mypath4Route := subdomain.Get("/mypath4/{paramfirst}/statichere/{paramsecond}/{otherparam}/{something:path}", emptyHandler) mypath4Route.Name = "my-page4" mypath5Route := subdomain.Handle("GET", "/mypath5/{paramfirst}/statichere/{paramsecond}/{otherparam}/anything/{something:path}", emptyHandler) mypath5Route.Name = "my-page5" mypath6Route := subdomain.Get("/mypath6/{paramfirst}/{paramsecond}/staticParam/{paramThirdAfterStatic}", emptyHandler) mypath6Route.Name = "my-page6" app.Get("/", func(ctx iris.Context) { // for username5./mypath6... paramsAsArray := []string{"username5", "theParam1", "theParam2", "paramThirdAfterStatic"} ctx.ViewData("ParamsAsArray", paramsAsArray) if err := ctx.View("page.html"); err != nil { ctx.HTML(fmt.Sprintf("

    %s

    ", err.Error())) return } }) // simple path so you can test it without host mapping and subdomains, // at view it make uses of {{urlpath ...}} // in order to showcase you that you can use it // if you don't want the entire scheme and host to be part of the url. app.Get("/mypath7/{paramfirst}/{paramsecond}/static/{paramthird}", emptyHandler).Name = "my-page7" // http://127.0.0.1:8080 app.Listen(host) } func emptyHandler(ctx iris.Context) { ctx.Writef("Hello from subdomain: %s , you're in path: %s", ctx.Subdomain(), ctx.Path()) } // Note: // If you got an empty string on {{ url }} or {{ urlpath }} it means that // args length are not aligned with the route's parameters length // or the route didn't found by the passed name. ================================================ FILE: _examples/view/template_html_4/templates/page.html ================================================ username1.127.0.0.1:8080/mypath

    username2.127.0.0.1:8080/mypath2/{paramfirst}/{paramsecond}

    username3.127.0.0.1:8080/mypath3/{paramfirst}/statichere/{paramsecond}

    username4.127.0.0.1:8080/mypath4/{paramfirst}/statichere/{paramsecond}/{otherParam}/{something:path}

    username5.127.0.0.1:8080/mypath5/{paramfirst}/statichere/{paramsecond}/{otherparam}/anything/{something:path}

    username5.127.0.0.1:8080/mypath6/{paramfirst}/{paramsecond}/staticParam/{paramThirdAfterStatic}

    mypath7/{paramfirst}/{paramsecond}/static/{paramthird}

    ================================================ FILE: _examples/view/template_html_5/main.go ================================================ package main import ( "fmt" "github.com/kataras/iris/v12" ) func main() { app := iris.New() app.RegisterView(iris.HTML("./views", ".html").Layout("layout.html")) // TIP: append .Reload(true) to reload the templates on each request. app.Get("/home", func(ctx iris.Context) { ctx.ViewData("title", "Home page") if err := ctx.View("home.html"); err != nil { ctx.HTML(fmt.Sprintf("

    %s

    ", err.Error())) return } // Note that: you can pass "layout" : "otherLayout.html" to bypass the config's Layout property // or view.NoLayout to disable layout on this render action. // third is an optional parameter }) app.Get("/about", func(ctx iris.Context) { if err := ctx.View("about.html"); err != nil { ctx.HTML(fmt.Sprintf("

    %s

    ", err.Error())) return } }) app.Get("/user/index", func(ctx iris.Context) { if err := ctx.View("user/index.html"); err != nil { ctx.HTML(fmt.Sprintf("

    %s

    ", err.Error())) return } }) // http://localhost:8080 app.Listen(":8080") } ================================================ FILE: _examples/view/template_html_5/views/about.html ================================================ {{ define "about-head"}} about page {{ end }} {{ define "about-body"}} extend body content in layout. {{ end }}
    Hello about page
    ================================================ FILE: _examples/view/template_html_5/views/home.html ================================================ {{ define "home-head"}} {{.title}} {{ end }}
    Hello home page
    ================================================ FILE: _examples/view/template_html_5/views/layout.html ================================================ {{ part "head"}}

    [layout] Body content is below...

    {{ part "body" . }} {{ yield . }} ================================================ FILE: _examples/view/template_html_5/views/user/index.html ================================================ {{ define "user/index-head"}} {{ end }}
    Hello user index page
    ================================================ FILE: _examples/view/template_jet_0/README.md ================================================ # Jet Engine Example This example is a fork of to work with Iris, so you can notice the differences side by side. Read more at: https://github.com/CloudyKit/jet/blob/master/docs/syntax.md > The Iris Jet View Engine fixes some bugs that the underline [jet template parser](https://github.com/CloudyKit/jet) currently has. Continue by learning how you can [serve embedded templates](../template_jet_1_embedded). ================================================ FILE: _examples/view/template_jet_0/main.go ================================================ // Package main shows how to use jet template parser with ease using the Iris built-in Jet view engine. // This example is customized fork of https://github.com/CloudyKit/jet/tree/master/examples/todos, so you can // notice the differences side by side. package main import ( "bytes" "encoding/base64" "fmt" "os" "reflect" "strings" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/view" ) type tTODO struct { Text string Done bool } type doneTODOs struct { list map[string]*tTODO keys []string len int i int } func (dt *doneTODOs) New(todos map[string]*tTODO) *doneTODOs { dt.len = len(todos) for k := range todos { dt.keys = append(dt.keys, k) } dt.list = todos return dt } // Range satisfies the jet.Ranger interface and only returns TODOs that are done, // even when the list contains TODOs that are not done. func (dt *doneTODOs) Range() (reflect.Value, reflect.Value, bool) { for dt.i < dt.len { key := dt.keys[dt.i] dt.i++ if dt.list[key].Done { return reflect.ValueOf(key), reflect.ValueOf(dt.list[key]), false } } return reflect.Value{}, reflect.Value{}, true } // Note: jet version 4 requires this. func (dt *doneTODOs) ProvidesIndex() bool { return true } func (dt *doneTODOs) Render(r *view.JetRuntime) { r.Write([]byte("custom renderer")) } // Render implements jet.Renderer interface func (t *tTODO) Render(r *view.JetRuntime) { done := "yes" if !t.Done { done = "no" } r.Write([]byte(fmt.Sprintf("TODO: %s (done: %s)", t.Text, done))) } func main() { // // Type aliases: // view.JetRuntimeVars = jet.VarMap // view.JetRuntime = jet.Runtime // view.JetArguments = jet.Arguments // // Iris also gives you the ability to put runtime variables // from middlewares as well, by: // view.AddJetRuntimeVars(ctx, vars) // or tmpl.AddRuntimeVars(ctx, vars) app := iris.New() tmpl := iris.Jet("./views", ".jet") // <-- tmpl.Reload(true) // remove in production. tmpl.AddFunc("base64", func(a view.JetArguments) reflect.Value { a.RequireNumOfArguments("base64", 1, 1) buffer := bytes.NewBuffer(nil) fmt.Fprint(buffer, a.Get(0)) return reflect.ValueOf(base64.URLEncoding.EncodeToString(buffer.Bytes())) }) app.RegisterView(tmpl) // <-- todos := map[string]*tTODO{ "example-todo-1": {Text: "Add an show todo page to the example project", Done: true}, "example-todo-2": {Text: "Add an add todo page to the example project"}, "example-todo-3": {Text: "Add an update todo page to the example project"}, "example-todo-4": {Text: "Add an delete todo page to the example project", Done: true}, } app.Get("/", func(ctx iris.Context) { err := ctx.View("todos/index.jet", todos) // <-- // Note that the `ctx.View` already logs the error if logger level is allowing it and returns the error. if err != nil { ctx.StopWithText(iris.StatusInternalServerError, "Templates not rendered!") } }) app.Get("/todo", func(ctx iris.Context) { id := ctx.URLParam("id") todo, ok := todos[id] if !ok { ctx.Redirect("/") return } ctx.ViewData("title", "Show TODO") if err := ctx.View("todos/show.jet", todo); err != nil { ctx.HTML(fmt.Sprintf("

    %s

    ", err.Error())) return } }) app.Get("/all-done", func(ctx iris.Context) { // vars := make(view.JetRuntimeVars) // vars.Set("showingAllDone", true) // vars.Set("title", "Todos - All Done") // view.AddJetRuntimeVars(ctx, vars) // ctx.View("todos/index.jet", (&doneTODOs{}).New(todos)) // // OR ctx.ViewData("showingAllDone", true) ctx.ViewData("title", "Todos - All Done") // Use ctx.ViewData("_jet", jetData) // if using as middleware and you want // to pre-set the value or even change it later on from another next middleware. // ctx.ViewData("_jet", (&doneTODOs{}).New(todos)) // and ctx.View("todos/index.jet") // OR if err := ctx.View("todos/index.jet", (&doneTODOs{}).New(todos)); err != nil { ctx.HTML(fmt.Sprintf("

    %s

    ", err.Error())) return } }) port := os.Getenv("PORT") if len(port) == 0 { port = ":8080" } else if !strings.HasPrefix(":", port) { port = ":" + port } app.Listen(port) } ================================================ FILE: _examples/view/template_jet_0/views/layouts/application.jet ================================================ {{ isset(title) ? title : "Default" }} {{block documentBody()}}{{end}} ================================================ FILE: _examples/view/template_jet_0/views/todos/index.jet ================================================ {{extends "../layouts/application.jet"}} {{block button(label, href="javascript:void(0)")}} {{ label }} {{end}} {{block ul()}}
      {{yield content}}
    {{end}} {{block documentBody()}}

    List of TODOs

    {{if isset(showingAllDone) && showingAllDone}}

    Showing only TODOs that are done

    {{else}}

    Show only TODOs that are done

    {{end}} {{yield ul() content}} {{range id, value := .}}
  • {{ value.Text }} {{yield button(label="UP", href="/update/?id="+base64(id))}} - {{yield button(href="/delete/?id="+id, label="DL")}}
  • {{end}} {{end}} {{end}} ================================================ FILE: _examples/view/template_jet_0/views/todos/show.jet ================================================ {{extends "../layouts/application.jet"}} {{block documentBody()}}

    Show TODO

    This uses a custom renderer by implementing the Renderer interface.

    {{.}}

    {{end}} ================================================ FILE: _examples/view/template_jet_1_embedded/bindata.go ================================================ // Code generated by go-bindata. (@generated) DO NOT EDIT. // Package main generated by go-bindata.// sources: // views/includes/_partial.jet // views/includes/blocks.jet // views/index.jet // views/layouts/application.jet package main import ( "bytes" "compress/gzip" "fmt" "io" "net/http" "os" "path/filepath" "strings" "time" ) func bindataRead(data []byte, name string) ([]byte, error) { gz, err := gzip.NewReader(bytes.NewBuffer(data)) if err != nil { return nil, fmt.Errorf("read %q: %v", name, err) } var buf bytes.Buffer _, err = io.Copy(&buf, gz) clErr := gz.Close() if err != nil { return nil, fmt.Errorf("read %q: %v", name, err) } if clErr != nil { return nil, err } return buf.Bytes(), nil } type asset struct { bytes []byte info os.FileInfo } type bindataFileInfo struct { name string size int64 mode os.FileMode modTime time.Time } // Name return file name func (fi bindataFileInfo) Name() string { return fi.name } // Size return file size func (fi bindataFileInfo) Size() int64 { return fi.size } // Mode return file mode func (fi bindataFileInfo) Mode() os.FileMode { return fi.mode } // ModTime return file modify time func (fi bindataFileInfo) ModTime() time.Time { return fi.modTime } // IsDir return file whether a directory func (fi bindataFileInfo) IsDir() bool { return fi.mode&os.ModeDir != 0 } // Sys return file is sys mode func (fi bindataFileInfo) Sys() any { return nil } type assetFile struct { *bytes.Reader name string childInfos []os.FileInfo childInfoOffset int } type assetOperator struct{} // Open implement http.FileSystem interface func (f *assetOperator) Open(name string) (http.File, error) { var err error if len(name) > 0 && name[0] == '/' { name = name[1:] } content, err := Asset(name) if err == nil { return &assetFile{name: name, Reader: bytes.NewReader(content)}, nil } children, err := AssetDir(name) if err == nil { childInfos := make([]os.FileInfo, 0, len(children)) for _, child := range children { childPath := filepath.Join(name, child) info, errInfo := AssetInfo(filepath.Join(name, child)) if errInfo == nil { childInfos = append(childInfos, info) } else { childInfos = append(childInfos, newDirFileInfo(childPath)) } } return &assetFile{name: name, childInfos: childInfos}, nil } else { // If the error is not found, return an error that will // result in a 404 error. Otherwise the server returns // a 500 error for files not found. if strings.Contains(err.Error(), "not found") { return nil, os.ErrNotExist } return nil, err } } // Close no need do anything func (f *assetFile) Close() error { return nil } // Readdir read dir's children file info func (f *assetFile) Readdir(count int) ([]os.FileInfo, error) { if len(f.childInfos) == 0 { return nil, os.ErrNotExist } if count <= 0 { return f.childInfos, nil } if f.childInfoOffset+count > len(f.childInfos) { count = len(f.childInfos) - f.childInfoOffset } offset := f.childInfoOffset f.childInfoOffset += count return f.childInfos[offset : offset+count], nil } // Stat read file info from asset item func (f *assetFile) Stat() (os.FileInfo, error) { if len(f.childInfos) != 0 { return newDirFileInfo(f.name), nil } return AssetInfo(f.name) } // newDirFileInfo return default dir file info func newDirFileInfo(name string) os.FileInfo { return &bindataFileInfo{ name: name, size: 0, mode: os.FileMode(2147484068), // equal os.FileMode(0644)|os.ModeDir modTime: time.Time{}} } // AssetFile return a http.FileSystem instance that data backend by asset func AssetFile() http.FileSystem { return &assetOperator{} } var _includes_partialJet = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xb2\x29\xb0\xf3\xcc\x4b\xce\x29\x4d\x49\x4d\x51\x28\x48\x2c\x2a\xc9\x4c\xcc\xb1\xd1\x2f\xb0\xe3\xe5\x02\x04\x00\x00\xff\xff\x90\x62\x4f\xfb\x19\x00\x00\x00") func includes_partialJetBytes() ([]byte, error) { return bindataRead( _includes_partialJet, "includes/_partial.jet", ) } func includes_partialJet() (*asset, error) { bytes, err := includes_partialJetBytes() if err != nil { return nil, err } info := bindataFileInfo{name: "includes/_partial.jet", size: 25, mode: os.FileMode(438), modTime: time.Unix(1599156854, 0)} a := &asset{bytes: bytes, info: info} return a, nil } var _includesBlocksJet = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xaa\xae\x4e\xca\xc9\x4f\xce\x56\xc8\x4d\xcd\x2b\xd5\xd0\xac\xad\xe5\xe5\x52\x50\xb0\x29\xb0\x0b\xc9\x48\x05\x0b\x29\x40\x64\xcb\x13\x8b\x15\x32\xf3\xca\xf2\xb3\x53\x53\xf4\x6c\xf4\x0b\xec\x78\xb9\xaa\xab\x53\xf3\x52\x40\xca\x01\x01\x00\x00\xff\xff\xa0\xd9\xd9\x5d\x41\x00\x00\x00") func includesBlocksJetBytes() ([]byte, error) { return bindataRead( _includesBlocksJet, "includes/blocks.jet", ) } func includesBlocksJet() (*asset, error) { bytes, err := includesBlocksJetBytes() if err != nil { return nil, err } info := bindataFileInfo{name: "includes/blocks.jet", size: 65, mode: os.FileMode(438), modTime: time.Unix(1599156854, 0)} a := &asset{bytes: bytes, info: info} return a, nil } var _indexJet = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x64\x90\xb1\x6a\xf3\x30\x14\x85\xf7\x1f\xfe\x77\x38\xf5\xd2\x74\xa9\xc9\x5a\x8c\x87\x42\x86\x2e\x5d\xfa\x00\x45\xb6\x6e\xb0\x1a\x59\x57\xf8\x5e\xd7\x36\x22\xef\x5e\x2c\x9b\x10\xda\xed\x48\xe7\x3b\xe8\x43\x29\xd1\xac\x14\xac\xa0\xf0\x66\xe1\x51\xa5\x34\x31\x7a\xd7\x1a\x75\x1c\x9e\xbf\x48\x8b\xeb\xf5\xff\xbf\x94\x5c\x1f\x79\x50\x14\x2e\xb4\x7e\xb4\x24\x65\xe3\xb9\xbd\xc8\x8d\x58\x99\x7c\x05\xcb\xed\xd8\x53\xd0\x57\xb6\xcb\xe1\x69\xed\x80\xaa\x3b\xd6\xa7\xbe\x21\x6b\xc9\x82\x66\xd3\x47\x4f\x55\xd9\x1d\xeb\x75\x08\x54\xc1\x7c\xd7\x6b\x00\x52\x5a\x1c\x79\x8b\x9e\xc2\x78\x5b\x97\x5b\xbf\xe6\x94\x76\x83\x3b\x95\xcf\x68\x06\x75\xc6\xdf\xc9\x64\xf0\x8c\x87\x9d\x79\x3b\x9f\x66\x27\x2a\x87\xc2\x32\xc9\x3b\x6b\x3e\x66\x7e\x7f\x03\xa8\x62\xfd\xd1\xf1\x24\xe8\x78\xfa\x33\xc4\xc4\xc3\x45\x5e\xf0\x7b\x8e\xc9\x08\x02\x2b\x76\xde\xa2\xa1\xd6\x8c\x42\x70\x9a\xe1\xf0\xa8\xa0\x4c\x57\x65\xac\x37\x31\x0a\x76\xfb\xd4\x3d\xfc\x04\x00\x00\xff\xff\x28\x5a\x9d\x42\x85\x01\x00\x00") func indexJetBytes() ([]byte, error) { return bindataRead( _indexJet, "index.jet", ) } func indexJet() (*asset, error) { bytes, err := indexJetBytes() if err != nil { return nil, err } info := bindataFileInfo{name: "index.jet", size: 389, mode: os.FileMode(438), modTime: time.Unix(1599156854, 0)} a := &asset{bytes: bytes, info: info} return a, nil } var _layoutsApplicationJet = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x3c\x8e\xbd\xae\xc2\x30\x0c\x85\xf7\x4a\x7d\x07\xdf\x4c\x97\x01\x75\x65\x70\x3b\x00\x65\x85\xa1\x0c\x8c\x69\x6d\x11\x44\x7e\x10\x18\x89\x2a\xca\xbb\x23\xda\xc0\x64\xcb\x3e\xdf\xa7\x83\x7f\xdb\xfd\xa6\x3b\x1d\x5a\x30\xe2\x6c\x53\x16\xf8\x99\x60\xb5\x3f\xd7\x8a\xbd\x6a\xca\x02\x00\x0d\x6b\x9a\x36\x00\x74\x2c\x1a\x06\xa3\xef\x0f\x96\x5a\x1d\xbb\xdd\x72\xa5\xbe\x3f\xb9\x88\xe5\xa6\x75\x3d\x13\x31\x01\xbf\xb4\xbb\x59\xc6\x6a\xbe\x4f\xaa\xea\xe7\xc2\x3e\xd0\x98\xc9\x18\x7b\x1b\x86\x2b\x50\x18\x9e\x8e\xbd\xac\x03\x8d\xff\x8b\x94\x62\x64\x4f\x29\xcd\x64\xce\x63\x95\xab\xbe\x03\x00\x00\xff\xff\xee\xc2\x94\xa4\xbc\x00\x00\x00") func layoutsApplicationJetBytes() ([]byte, error) { return bindataRead( _layoutsApplicationJet, "layouts/application.jet", ) } func layoutsApplicationJet() (*asset, error) { bytes, err := layoutsApplicationJetBytes() if err != nil { return nil, err } info := bindataFileInfo{name: "layouts/application.jet", size: 188, mode: os.FileMode(438), modTime: time.Unix(1599156854, 0)} a := &asset{bytes: bytes, info: info} return a, nil } // Asset loads and returns the asset for the given name. // It returns an error if the asset could not be found or // could not be loaded. func Asset(name string) ([]byte, error) { canonicalName := strings.Replace(name, "\\", "/", -1) if f, ok := _bindata[canonicalName]; ok { a, err := f() if err != nil { return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err) } return a.bytes, nil } return nil, fmt.Errorf("Asset %s not found", name) } // MustAsset is like Asset but panics when Asset would return an error. // It simplifies safe initialization of global variables. func MustAsset(name string) []byte { a, err := Asset(name) if err != nil { panic("asset: Asset(" + name + "): " + err.Error()) } return a } // AssetInfo loads and returns the asset info for the given name. // It returns an error if the asset could not be found or // could not be loaded. func AssetInfo(name string) (os.FileInfo, error) { canonicalName := strings.Replace(name, "\\", "/", -1) if f, ok := _bindata[canonicalName]; ok { a, err := f() if err != nil { return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err) } return a.info, nil } return nil, fmt.Errorf("AssetInfo %s not found", name) } // AssetNames returns the names of the assets. func AssetNames() []string { names := make([]string, 0, len(_bindata)) for name := range _bindata { names = append(names, name) } return names } // _bindata is a table, holding each asset generator, mapped to its name. var _bindata = map[string]func() (*asset, error){ "includes/_partial.jet": includes_partialJet, "includes/blocks.jet": includesBlocksJet, "index.jet": indexJet, "layouts/application.jet": layoutsApplicationJet, } // AssetDir returns the file names below a certain // directory embedded in the file by go-bindata. // For example if you run go-bindata on data/... and data contains the // following hierarchy: // // data/ // foo.txt // img/ // a.png // b.png // // then AssetDir("data") would return []string{"foo.txt", "img"} // AssetDir("data/img") would return []string{"a.png", "b.png"} // AssetDir("foo.txt") and AssetDir("nonexistent") would return an error // AssetDir("") will return []string{"data"}. func AssetDir(name string) ([]string, error) { node := _bintree if len(name) != 0 { canonicalName := strings.Replace(name, "\\", "/", -1) pathList := strings.Split(canonicalName, "/") for _, p := range pathList { node = node.Children[p] if node == nil { return nil, fmt.Errorf("Asset %s not found", name) } } } if node.Func != nil { return nil, fmt.Errorf("Asset %s not found", name) } rv := make([]string, 0, len(node.Children)) for childName := range node.Children { rv = append(rv, childName) } return rv, nil } type bintree struct { Func func() (*asset, error) Children map[string]*bintree } var _bintree = &bintree{nil, map[string]*bintree{ "includes": {nil, map[string]*bintree{ "_partial.jet": {includes_partialJet, map[string]*bintree{}}, "blocks.jet": {includesBlocksJet, map[string]*bintree{}}, }}, "index.jet": {indexJet, map[string]*bintree{}}, "layouts": {nil, map[string]*bintree{ "application.jet": {layoutsApplicationJet, map[string]*bintree{}}, }}, }} // RestoreAsset restores an asset under the given directory func RestoreAsset(dir, name string) error { data, err := Asset(name) if err != nil { return err } info, err := AssetInfo(name) if err != nil { return err } err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755)) if err != nil { return err } err = os.WriteFile(_filePath(dir, name), data, info.Mode()) if err != nil { return err } err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime()) if err != nil { return err } return nil } // RestoreAssets restores an asset under the given directory recursively func RestoreAssets(dir, name string) error { children, err := AssetDir(name) // File if err != nil { return RestoreAsset(dir, name) } // Dir for _, child := range children { err = RestoreAssets(dir, filepath.Join(name, child)) if err != nil { return err } } return nil } func _filePath(dir, name string) string { canonicalName := strings.Replace(name, "\\", "/", -1) return filepath.Join(append([]string{dir}, strings.Split(canonicalName, "/")...)...) } ================================================ FILE: _examples/view/template_jet_1_embedded/main.go ================================================ // Package main shows how to use jet templates embedded in your application with ease using the Iris built-in Jet view engine. // This example is a customized fork of https://github.com/CloudyKit/jet/tree/master/examples/asset_packaging, so you can // notice the differences side by side. For example, you don't have to use any external package inside your application, // Iris manually builds the template loader for binary data when Asset and AssetNames are available via tools like the go-bindata. package main import ( "fmt" "os" "github.com/kataras/iris/v12" ) // $ go install github.com/go-bindata/go-bindata/v3/go-bindata@latest // // $ go-bindata -fs -prefix "views" ./views/... // $ go run . // // System files are not used, you can optionally delete the folder and run the example now. func main() { app := iris.New() tmpl := iris.Jet(AssetFile(), ".jet") app.RegisterView(tmpl) app.Get("/", func(ctx iris.Context) { if err := ctx.View("index.jet"); err != nil { ctx.HTML(fmt.Sprintf("

    %s

    ", err.Error())) return } }) addr := ":8080" if port := os.Getenv("PORT"); port != "" { addr = ":" + port } app.Listen(addr) } ================================================ FILE: _examples/view/template_jet_1_embedded/views/includes/_partial.jet ================================================

    Included partial

    ================================================ FILE: _examples/view/template_jet_1_embedded/views/includes/blocks.jet ================================================ {{block menu()}}

    The menu block was invoked.

    {{end}} ================================================ FILE: _examples/view/template_jet_1_embedded/views/index.jet ================================================ {{extends "layouts/application.jet"}} {{import "includes/blocks.jet"}} {{block documentBody()}}

    Embedded example

    {{include "includes/_partial.jet"}} {{if !includeIfExists("doesNotExist.jet")}}

    Shows how !includeIfExists works: doesNotExist.jet was not included because it doesn't exist.

    {{end}} {{end}} ================================================ FILE: _examples/view/template_jet_1_embedded/views/layouts/application.jet ================================================ Embedded example {{block documentBody()}}{{end}} ================================================ FILE: _examples/view/template_jet_2/main.go ================================================ // Package main an example on how to naming your routes & use the custom 'url path' Jet Template Engine. package main import ( "fmt" "github.com/kataras/iris/v12" ) func main() { app := iris.New() app.RegisterView(iris.Jet("./views", ".jet").Reload(true)) mypathRoute := app.Get("/mypath", writePathHandler) mypathRoute.Name = "my-page1" mypath2Route := app.Get("/mypath2/{paramfirst}/{paramsecond}", writePathHandler) mypath2Route.Name = "my-page2" mypath3Route := app.Get("/mypath3/{paramfirst}/statichere/{paramsecond}", writePathHandler) mypath3Route.Name = "my-page3" mypath4Route := app.Get("/mypath4/{paramfirst}/statichere/{paramsecond}/{otherparam}/{something:path}", writePathHandler) // same as: app.Get("/mypath4/:paramfirst/statichere/:paramsecond/:otherparam/*something", writePathHandler) mypath4Route.Name = "my-page4" // same with Handle/Func mypath5Route := app.Handle("GET", "/mypath5/{paramfirst:int}/statichere/{paramsecond}/{otherparam}/anything/{something:path}", writePathHandlerPage5) mypath5Route.Name = "my-page5" mypath6Route := app.Get("/mypath6/{paramfirst}/{paramsecond}/statichere/{paramThirdAfterStatic}", writePathHandler) mypath6Route.Name = "my-page6" app.Get("/", func(ctx iris.Context) { // for /mypath6... paramsAsArray := []string{"theParam1", "theParam2", "paramThirdAfterStatic"} ctx.ViewData("ParamsAsArray", paramsAsArray) if err := ctx.View("page.jet"); err != nil { ctx.HTML(fmt.Sprintf("

    %s

    ", err.Error())) return } }) app.Get("/redirect/{namedRoute}", func(ctx iris.Context) { routeName := ctx.Params().Get("namedRoute") r := app.GetRoute(routeName) if r == nil { ctx.StatusCode(iris.StatusNotFound) ctx.Writef("Route with name %s not found", routeName) return } println("The path of " + routeName + "is: " + r.Path) // if routeName == "my-page1" // prints: The path of of my-page1 is: /mypath // if it's a path which takes named parameters // then use "r.ResolvePath(paramValuesHere)" ctx.Redirect(r.Path) // http://localhost:8080/redirect/my-page1 will redirect to -> http://localhost:8080/mypath }) // http://localhost:8080 // http://localhost:8080/redirect/my-page1 app.Listen(":8080") } func writePathHandler(ctx iris.Context) { ctx.Writef("Hello from %s.", ctx.Path()) } func writePathHandlerPage5(ctx iris.Context) { ctx.Writef("Hello from %s.\nparamfirst(int)=%d", ctx.Path(), ctx.Params().GetIntDefault("paramfirst", 0)) } ================================================ FILE: _examples/view/template_jet_2/views/page.jet ================================================ /mypath

    /mypath2/{paramfirst}/{paramsecond}

    /mypath3/{paramfirst}/statichere/{paramsecond}

    /mypath4/{paramfirst}/statichere/{paramsecond}/{otherparam}/{something:path}

    /mypath5/{paramfirst}/statichere/{paramsecond}/{otherparam}/anything/{anything:path}

    /mypath6/{paramfirst}/{paramsecond}/statichere/{paramThirdAfterStatic} ================================================ FILE: _examples/view/template_jet_3/main.go ================================================ package main import ( "fmt" "reflect" "strings" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/view" ) func main() { tmpl := iris.Jet("./views", ".jet") tmpl.Reload(true) val := reflect.ValueOf(ViewBuiler{}) fns := val.Type() for i := 0; i < fns.NumMethod(); i++ { method := fns.Method(i) tmpl.AddFunc(strings.ToLower(method.Name), val.Method(i).Interface()) } app := iris.New() app.RegisterView(tmpl) app.Get("/", func(ctx iris.Context) { if err := ctx.View("index.jet"); err != nil { ctx.HTML(fmt.Sprintf("

    %s

    ", err.Error())) return } }) app.Listen(":8080") } type ViewBuiler struct { } func (ViewBuiler) Asset(a view.JetArguments) reflect.Value { path := a.Get(0).String() // fmt.Println(os.Getenv("APP_URL")) return reflect.ValueOf(path) } func (ViewBuiler) Style(a view.JetArguments) reflect.Value { path := a.Get(0).String() s := fmt.Sprintf(``, path) return reflect.ValueOf(s) } func (ViewBuiler) Script(a view.JetArguments) reflect.Value { path := a.Get(0).String() s := fmt.Sprintf(``, path) return reflect.ValueOf(s) } ================================================ FILE: _examples/view/template_jet_3/views/index.jet ================================================ {{ asset("./myasset.mp3")}}
    {{ style("my-stle.css") | raw}}
    {{ script("my-script.js") | raw}} ./myasset.mp3
    <link href="my-stle.css" rel="stylesheet">
    <script src="my-script.js"></script>
    ================================================ FILE: _examples/view/template_pug_0/main.go ================================================ // Package main shows an example of pug actions based on https://github.com/Joker/jade/tree/master/example/actions package main import ( "fmt" "github.com/kataras/iris/v12" ) type Person struct { Name string Age int Emails []string Jobs []*Job } type Job struct { Employer string Role string } func main() { app := iris.New() tmpl := iris.Pug("./templates", ".pug") app.RegisterView(tmpl) app.Get("/", index) // http://localhost:8080 app.Listen(":8080") } func index(ctx iris.Context) { job1 := Job{Employer: "Monash B", Role: "Honorary"} job2 := Job{Employer: "Box Hill", Role: "Head of HE"} person := Person{ Name: "jan", Age: 50, Emails: []string{"jan@newmarch.name", "jan.newmarch@gmail.com"}, Jobs: []*Job{&job1, &job2}, } if err := ctx.View("index.pug", person); err != nil { ctx.HTML(fmt.Sprintf("

    %s

    ", err.Error())) return } } ================================================ FILE: _examples/view/template_pug_0/templates/index.pug ================================================ doctype html html(lang="en") head meta(charset="utf-8") title Title body p ads ul li The name is {{.Name}}. li The age is {{.Age}}. each _,_ in .Emails div An email is {{.}} | {{ with .Jobs }} each _,_ in . div An employer is {{.Employer}} and the role is {{.Role}} | {{ end }} ================================================ FILE: _examples/view/template_pug_1/main.go ================================================ package main import ( "fmt" "html/template" "github.com/kataras/iris/v12" ) func main() { app := iris.New() tmpl := iris.Pug("./templates", ".pug") tmpl.Reload(true) // reload templates on each request (development mode) tmpl.AddFunc("bold", func(s string) (template.HTML, error) { // add your template func here. return template.HTML("" + s + ""), nil }) app.RegisterView(tmpl) app.Get("/", index) // http://localhost:8080 app.Listen(":8080") } func index(ctx iris.Context) { if err := ctx.View("index.pug"); err != nil { ctx.HTML(fmt.Sprintf("

    %s

    ", err.Error())) return } } ================================================ FILE: _examples/view/template_pug_1/templates/footer.pug ================================================ #footer p Copyright (c) foobar ================================================ FILE: _examples/view/template_pug_1/templates/header.pug ================================================ head title My Site ================================================ FILE: _examples/view/template_pug_1/templates/index.pug ================================================ doctype html html include header.pug body h1 My Site p {{ bold "Welcome to my super lame site."}} include footer.pug ================================================ FILE: _examples/view/template_pug_2_embedded/bindata.go ================================================ // Code generated by go-bindata. (@generated) DO NOT EDIT. // Package main generated by go-bindata.// sources: // templates/index.pug // templates/layout.pug package main import ( "bytes" "compress/gzip" "fmt" "io" "net/http" "os" "path/filepath" "strings" "time" ) func bindataRead(data []byte, name string) ([]byte, error) { gz, err := gzip.NewReader(bytes.NewBuffer(data)) if err != nil { return nil, fmt.Errorf("read %q: %v", name, err) } var buf bytes.Buffer _, err = io.Copy(&buf, gz) clErr := gz.Close() if err != nil { return nil, fmt.Errorf("read %q: %v", name, err) } if clErr != nil { return nil, err } return buf.Bytes(), nil } type asset struct { bytes []byte info os.FileInfo } type bindataFileInfo struct { name string size int64 mode os.FileMode modTime time.Time } // Name return file name func (fi bindataFileInfo) Name() string { return fi.name } // Size return file size func (fi bindataFileInfo) Size() int64 { return fi.size } // Mode return file mode func (fi bindataFileInfo) Mode() os.FileMode { return fi.mode } // ModTime return file modify time func (fi bindataFileInfo) ModTime() time.Time { return fi.modTime } // IsDir return file whether a directory func (fi bindataFileInfo) IsDir() bool { return fi.mode&os.ModeDir != 0 } // Sys return file is sys mode func (fi bindataFileInfo) Sys() any { return nil } type assetFile struct { *bytes.Reader name string childInfos []os.FileInfo childInfoOffset int } type assetOperator struct{} // Open implement http.FileSystem interface func (f *assetOperator) Open(name string) (http.File, error) { var err error if len(name) > 0 && name[0] == '/' { name = name[1:] } content, err := Asset(name) if err == nil { return &assetFile{name: name, Reader: bytes.NewReader(content)}, nil } children, err := AssetDir(name) if err == nil { childInfos := make([]os.FileInfo, 0, len(children)) for _, child := range children { childPath := filepath.Join(name, child) info, errInfo := AssetInfo(filepath.Join(name, child)) if errInfo == nil { childInfos = append(childInfos, info) } else { childInfos = append(childInfos, newDirFileInfo(childPath)) } } return &assetFile{name: name, childInfos: childInfos}, nil } else { // If the error is not found, return an error that will // result in a 404 error. Otherwise the server returns // a 500 error for files not found. if strings.Contains(err.Error(), "not found") { return nil, os.ErrNotExist } return nil, err } } // Close no need do anything func (f *assetFile) Close() error { return nil } // Readdir read dir's children file info func (f *assetFile) Readdir(count int) ([]os.FileInfo, error) { if len(f.childInfos) == 0 { return nil, os.ErrNotExist } if count <= 0 { return f.childInfos, nil } if f.childInfoOffset+count > len(f.childInfos) { count = len(f.childInfos) - f.childInfoOffset } offset := f.childInfoOffset f.childInfoOffset += count return f.childInfos[offset : offset+count], nil } // Stat read file info from asset item func (f *assetFile) Stat() (os.FileInfo, error) { if len(f.childInfos) != 0 { return newDirFileInfo(f.name), nil } return AssetInfo(f.name) } // newDirFileInfo return default dir file info func newDirFileInfo(name string) os.FileInfo { return &bindataFileInfo{ name: name, size: 0, mode: os.FileMode(2147484068), // equal os.FileMode(0644)|os.ModeDir modTime: time.Time{}} } // AssetFile return a http.FileSystem instance that data backend by asset func AssetFile() http.FileSystem { return &assetOperator{} } var _indexPug = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x4a\xad\x28\x49\xcd\x4b\x29\x56\xc8\x49\xac\xcc\x2f\x2d\xd1\x2b\x28\x4d\xe7\xe5\xe2\xe5\x4a\xca\xc9\x4f\xce\x56\x28\xc9\x2c\xc9\x49\xe5\xe5\x52\x80\x30\x14\x1c\x8b\x4a\x32\x93\x73\x52\x15\x42\x20\xc2\x30\x55\xc9\xf9\x79\x25\xa9\x79\x25\x20\x75\x19\x86\x0a\xbe\x95\x30\x75\x80\x00\x00\x00\xff\xff\xa6\xfd\x18\x8c\x5a\x00\x00\x00") func indexPugBytes() ([]byte, error) { return bindataRead( _indexPug, "index.pug", ) } func indexPug() (*asset, error) { bytes, err := indexPugBytes() if err != nil { return nil, err } info := bindataFileInfo{name: "index.pug", size: 90, mode: os.FileMode(438), modTime: time.Unix(1599156854, 0)} a := &asset{bytes: bytes, info: info} return a, nil } var _layoutPug = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x4a\xc9\x4f\x2e\xa9\x2c\x48\x55\xc8\x28\xc9\xcd\xe1\xe5\x82\x90\x0a\x0a\x19\xa9\x89\x29\x20\x5a\x41\x21\x29\x27\x3f\x39\x5b\xa1\x24\xb3\x24\x27\x15\x22\xa0\x00\xe1\x28\xb8\xa4\xa6\x25\x96\xe6\x94\x20\xa4\x92\xf2\x53\x2a\x91\xf5\x24\xe7\xe7\x95\xa4\xe6\x95\x00\x02\x00\x00\xff\xff\x5f\xa5\x93\xf9\x61\x00\x00\x00") func layoutPugBytes() ([]byte, error) { return bindataRead( _layoutPug, "layout.pug", ) } func layoutPug() (*asset, error) { bytes, err := layoutPugBytes() if err != nil { return nil, err } info := bindataFileInfo{name: "layout.pug", size: 97, mode: os.FileMode(438), modTime: time.Unix(1599156854, 0)} a := &asset{bytes: bytes, info: info} return a, nil } // Asset loads and returns the asset for the given name. // It returns an error if the asset could not be found or // could not be loaded. func Asset(name string) ([]byte, error) { canonicalName := strings.Replace(name, "\\", "/", -1) if f, ok := _bindata[canonicalName]; ok { a, err := f() if err != nil { return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err) } return a.bytes, nil } return nil, fmt.Errorf("Asset %s not found", name) } // MustAsset is like Asset but panics when Asset would return an error. // It simplifies safe initialization of global variables. func MustAsset(name string) []byte { a, err := Asset(name) if err != nil { panic("asset: Asset(" + name + "): " + err.Error()) } return a } // AssetInfo loads and returns the asset info for the given name. // It returns an error if the asset could not be found or // could not be loaded. func AssetInfo(name string) (os.FileInfo, error) { canonicalName := strings.Replace(name, "\\", "/", -1) if f, ok := _bindata[canonicalName]; ok { a, err := f() if err != nil { return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err) } return a.info, nil } return nil, fmt.Errorf("AssetInfo %s not found", name) } // AssetNames returns the names of the assets. func AssetNames() []string { names := make([]string, 0, len(_bindata)) for name := range _bindata { names = append(names, name) } return names } // _bindata is a table, holding each asset generator, mapped to its name. var _bindata = map[string]func() (*asset, error){ "index.pug": indexPug, "layout.pug": layoutPug, } // AssetDir returns the file names below a certain // directory embedded in the file by go-bindata. // For example if you run go-bindata on data/... and data contains the // following hierarchy: // // data/ // foo.txt // img/ // a.png // b.png // // then AssetDir("data") would return []string{"foo.txt", "img"} // AssetDir("data/img") would return []string{"a.png", "b.png"} // AssetDir("foo.txt") and AssetDir("nonexistent") would return an error // AssetDir("") will return []string{"data"}. func AssetDir(name string) ([]string, error) { node := _bintree if len(name) != 0 { canonicalName := strings.Replace(name, "\\", "/", -1) pathList := strings.Split(canonicalName, "/") for _, p := range pathList { node = node.Children[p] if node == nil { return nil, fmt.Errorf("Asset %s not found", name) } } } if node.Func != nil { return nil, fmt.Errorf("Asset %s not found", name) } rv := make([]string, 0, len(node.Children)) for childName := range node.Children { rv = append(rv, childName) } return rv, nil } type bintree struct { Func func() (*asset, error) Children map[string]*bintree } var _bintree = &bintree{nil, map[string]*bintree{ "index.pug": {indexPug, map[string]*bintree{}}, "layout.pug": {layoutPug, map[string]*bintree{}}, }} // RestoreAsset restores an asset under the given directory func RestoreAsset(dir, name string) error { data, err := Asset(name) if err != nil { return err } info, err := AssetInfo(name) if err != nil { return err } err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755)) if err != nil { return err } err = os.WriteFile(_filePath(dir, name), data, info.Mode()) if err != nil { return err } err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime()) if err != nil { return err } return nil } // RestoreAssets restores an asset under the given directory recursively func RestoreAssets(dir, name string) error { children, err := AssetDir(name) // File if err != nil { return RestoreAsset(dir, name) } // Dir for _, child := range children { err = RestoreAssets(dir, filepath.Join(name, child)) if err != nil { return err } } return nil } func _filePath(dir, name string) string { canonicalName := strings.Replace(name, "\\", "/", -1) return filepath.Join(append([]string{dir}, strings.Split(canonicalName, "/")...)...) } ================================================ FILE: _examples/view/template_pug_2_embedded/main.go ================================================ package main import ( "fmt" "github.com/kataras/iris/v12" ) // $ go install github.com/go-bindata/go-bindata/v3/go-bindata@latest // // $ go-bindata -fs -prefix "templates" ./templates/... // $ go run . // // System files are not used, you can optionally delete the folder and run the example now. func main() { app := iris.New() tmpl := iris.Pug(AssetFile(), ".pug") app.RegisterView(tmpl) app.Get("/", index) // http://localhost:8080 app.Listen(":8080") } func index(ctx iris.Context) { if err := ctx.View("index.pug"); err != nil { ctx.HTML(fmt.Sprintf("

    %s

    ", err.Error())) return } } ================================================ FILE: _examples/view/template_pug_2_embedded/templates/index.pug ================================================ extends layout.pug block title title Article Title block content h1 My Article ================================================ FILE: _examples/view/template_pug_2_embedded/templates/layout.pug ================================================ doctype html html head block title title Default title body block content ================================================ FILE: _examples/view/write-to/main.go ================================================ package main import ( "os" "github.com/kataras/iris/v12" ) type mailData struct { Title string Body string RefTitle string RefLink string } func main() { app := iris.New() app.Logger().SetLevel("debug") app.RegisterView(iris.HTML("./views", ".html")) // you need to call `app.Build` manually before using the `app.View` func, // so templates are built in that state. app.Build() // Or a string-buffered writer to use its body to send an e-mail // for sending e-mails you can use the https://github.com/kataras/go-mailer // or any other third-party package you like. // // The template's parsed result will be written to that writer. writer := os.Stdout err := app.View(writer, "email/simple.html", "shared/email.html", mailData{ Title: "This is my e-mail title", Body: "This is my e-mail body", RefTitle: "Iris web framework", RefLink: "https://iris-go.com", }) if err != nil { app.Logger().Errorf("error from app.View: %v", err) } app.Listen(":8080") } ================================================ FILE: _examples/view/write-to/views/email/simple.html ================================================ {{.Body}} ================================================ FILE: _examples/view/write-to/views/shared/email.html ================================================

    {{.Title}}

    {{yield}}

    {{.RefTitle}} ================================================ FILE: _examples/webassembly/client/go-wasm-runtime.js ================================================ // Copyright 2018 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. (() => { // Map web browser API and Node.js API to a single common API (preferring web standards over Node.js API). const isNodeJS = typeof process !== "undefined"; if (isNodeJS) { global.require = require; global.fs = require("fs"); const nodeCrypto = require("crypto"); global.crypto = { getRandomValues(b) { nodeCrypto.randomFillSync(b); }, }; global.performance = { now() { const [sec, nsec] = process.hrtime(); return sec * 1000 + nsec / 1000000; }, }; const util = require("util"); global.TextEncoder = util.TextEncoder; global.TextDecoder = util.TextDecoder; } else { window.global = window; let outputBuf = ""; global.fs = { constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1, O_NONBLOCK: -1, O_SYNC: -1 }, // unused writeSync(fd, buf) { outputBuf += decoder.decode(buf); const nl = outputBuf.lastIndexOf("\n"); if (nl != -1) { console.log(outputBuf.substr(0, nl)); outputBuf = outputBuf.substr(nl + 1); } return buf.length; }, openSync(path, flags, mode) { const err = new Error("not implemented"); err.code = "ENOSYS"; throw err; }, }; } const encoder = new TextEncoder("utf-8"); const decoder = new TextDecoder("utf-8"); global.Go = class { constructor() { this.argv = ["js"]; this.env = {}; this.exit = (code) => { if (code !== 0) { console.warn("exit code:", code); } }; this._callbackTimeouts = new Map(); this._nextCallbackTimeoutID = 1; const mem = () => { // The buffer may change when requesting more memory. return new DataView(this._inst.exports.mem.buffer); } const setInt64 = (addr, v) => { mem().setUint32(addr + 0, v, true); mem().setUint32(addr + 4, Math.floor(v / 4294967296), true); } const getInt64 = (addr) => { const low = mem().getUint32(addr + 0, true); const high = mem().getInt32(addr + 4, true); return low + high * 4294967296; } const loadValue = (addr) => { const f = mem().getFloat64(addr, true); if (!isNaN(f)) { return f; } const id = mem().getUint32(addr, true); return this._values[id]; } const storeValue = (addr, v) => { if (typeof v === "number") { if (isNaN(v)) { mem().setUint32(addr + 4, 0x7FF80000, true); // NaN mem().setUint32(addr, 0, true); return; } mem().setFloat64(addr, v, true); return; } mem().setUint32(addr + 4, 0x7FF80000, true); // NaN switch (v) { case undefined: mem().setUint32(addr, 1, true); return; case null: mem().setUint32(addr, 2, true); return; case true: mem().setUint32(addr, 3, true); return; case false: mem().setUint32(addr, 4, true); return; } if (typeof v === "string") { let ref = this._stringRefs.get(v); if (ref === undefined) { ref = this._values.length; this._values.push(v); this._stringRefs.set(v, ref); } mem().setUint32(addr, ref, true); return; } if (typeof v === "symbol") { let ref = this._symbolRefs.get(v); if (ref === undefined) { ref = this._values.length; this._values.push(v); this._symbolRefs.set(v, ref); } mem().setUint32(addr, ref, true); return; } let ref = v[this._refProp]; if (ref === undefined) { ref = this._values.length; this._values.push(v); v[this._refProp] = ref; } mem().setUint32(addr, ref, true); } const loadSlice = (addr) => { const array = getInt64(addr + 0); const len = getInt64(addr + 8); return new Uint8Array(this._inst.exports.mem.buffer, array, len); } const loadSliceOfValues = (addr) => { const array = getInt64(addr + 0); const len = getInt64(addr + 8); const a = new Array(len); for (let i = 0; i < len; i++) { a[i] = loadValue(array + i * 8); } return a; } const loadString = (addr) => { const saddr = getInt64(addr + 0); const len = getInt64(addr + 8); return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len)); } const timeOrigin = Date.now() - performance.now(); this.importObject = { go: { // func wasmExit(code int32) "runtime.wasmExit": (sp) => { this.exited = true; this.exit(mem().getInt32(sp + 8, true)); }, // func wasmWrite(fd uintptr, p unsafe.Pointer, n int32) "runtime.wasmWrite": (sp) => { const fd = getInt64(sp + 8); const p = getInt64(sp + 16); const n = mem().getInt32(sp + 24, true); fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n)); }, // func nanotime() int64 "runtime.nanotime": (sp) => { setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000); }, // func walltime() (sec int64, nsec int32) "runtime.walltime": (sp) => { const msec = (new Date).getTime(); setInt64(sp + 8, msec / 1000); mem().setInt32(sp + 16, (msec % 1000) * 1000000, true); }, // func scheduleCallback(delay int64) int32 "runtime.scheduleCallback": (sp) => { const id = this._nextCallbackTimeoutID; this._nextCallbackTimeoutID++; this._callbackTimeouts.set(id, setTimeout( () => { this._resolveCallbackPromise(); }, getInt64(sp + 8) + 1, // setTimeout has been seen to fire up to 1 millisecond early )); mem().setInt32(sp + 16, id, true); }, // func clearScheduledCallback(id int32) "runtime.clearScheduledCallback": (sp) => { const id = mem().getInt32(sp + 8, true); clearTimeout(this._callbackTimeouts.get(id)); this._callbackTimeouts.delete(id); }, // func getRandomData(r []byte) "runtime.getRandomData": (sp) => { crypto.getRandomValues(loadSlice(sp + 8)); }, // func stringVal(value string) ref "syscall/js.stringVal": (sp) => { storeValue(sp + 24, loadString(sp + 8)); }, // func valueGet(v ref, p string) ref "syscall/js.valueGet": (sp) => { storeValue(sp + 32, Reflect.get(loadValue(sp + 8), loadString(sp + 16))); }, // func valueSet(v ref, p string, x ref) "syscall/js.valueSet": (sp) => { Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32)); }, // func valueIndex(v ref, i int) ref "syscall/js.valueIndex": (sp) => { storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16))); }, // valueSetIndex(v ref, i int, x ref) "syscall/js.valueSetIndex": (sp) => { Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24)); }, // func valueCall(v ref, m string, args []ref) (ref, bool) "syscall/js.valueCall": (sp) => { try { const v = loadValue(sp + 8); const m = Reflect.get(v, loadString(sp + 16)); const args = loadSliceOfValues(sp + 32); storeValue(sp + 56, Reflect.apply(m, v, args)); mem().setUint8(sp + 64, 1); } catch (err) { storeValue(sp + 56, err); mem().setUint8(sp + 64, 0); } }, // func valueInvoke(v ref, args []ref) (ref, bool) "syscall/js.valueInvoke": (sp) => { try { const v = loadValue(sp + 8); const args = loadSliceOfValues(sp + 16); storeValue(sp + 40, Reflect.apply(v, undefined, args)); mem().setUint8(sp + 48, 1); } catch (err) { storeValue(sp + 40, err); mem().setUint8(sp + 48, 0); } }, // func valueNew(v ref, args []ref) (ref, bool) "syscall/js.valueNew": (sp) => { try { const v = loadValue(sp + 8); const args = loadSliceOfValues(sp + 16); storeValue(sp + 40, Reflect.construct(v, args)); mem().setUint8(sp + 48, 1); } catch (err) { storeValue(sp + 40, err); mem().setUint8(sp + 48, 0); } }, // func valueLength(v ref) int "syscall/js.valueLength": (sp) => { setInt64(sp + 16, parseInt(loadValue(sp + 8).length)); }, // valuePrepareString(v ref) (ref, int) "syscall/js.valuePrepareString": (sp) => { const str = encoder.encode(String(loadValue(sp + 8))); storeValue(sp + 16, str); setInt64(sp + 24, str.length); }, // valueLoadString(v ref, b []byte) "syscall/js.valueLoadString": (sp) => { const str = loadValue(sp + 8); loadSlice(sp + 16).set(str); }, // func valueInstanceOf(v ref, t ref) bool "syscall/js.valueInstanceOf": (sp) => { mem().setUint8(sp + 24, loadValue(sp + 8) instanceof loadValue(sp + 16)); }, "debug": (value) => { console.log(value); }, } }; } async run(instance) { this._inst = instance; this._values = [ // TODO: garbage collection NaN, undefined, null, true, false, global, this._inst.exports.mem, () => { // resolveCallbackPromise if (this.exited) { throw new Error("bad callback: Go program has already exited"); } setTimeout(this._resolveCallbackPromise, 0); // make sure it is asynchronous }, ]; this._stringRefs = new Map(); this._symbolRefs = new Map(); this._refProp = Symbol(); this.exited = false; const mem = new DataView(this._inst.exports.mem.buffer) // Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory. let offset = 4096; const strPtr = (str) => { let ptr = offset; new Uint8Array(mem.buffer, offset, str.length + 1).set(encoder.encode(str + "\0")); offset += str.length + (8 - (str.length % 8)); return ptr; }; const argc = this.argv.length; const argvPtrs = []; this.argv.forEach((arg) => { argvPtrs.push(strPtr(arg)); }); const keys = Object.keys(this.env).sort(); argvPtrs.push(keys.length); keys.forEach((key) => { argvPtrs.push(strPtr(`${key}=${this.env[key]}`)); }); const argv = offset; argvPtrs.forEach((ptr) => { mem.setUint32(offset, ptr, true); mem.setUint32(offset + 4, 0, true); offset += 8; }); while (true) { const callbackPromise = new Promise((resolve) => { this._resolveCallbackPromise = resolve; }); this._inst.exports.run(argc, argv); if (this.exited) { break; } await callbackPromise; } } } if (isNodeJS) { if (process.argv.length < 3) { process.stderr.write("usage: go_js_wasm_exec [wasm binary] [arguments]\n"); process.exit(1); } const go = new Go(); go.argv = process.argv.slice(2); go.env = process.env; go.exit = process.exit; WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then((result) => { process.on("exit", () => { // Node.js exits if no callback is pending if (!go.exited) { console.error("error: all goroutines asleep and no JavaScript callback pending - deadlock!"); process.exit(1); } }); return go.run(result.instance); }).catch((err) => { console.error(err); go.exited = true; process.exit(1); }); } })(); ================================================ FILE: _examples/webassembly/client/hello.html ================================================ Hello Webassembly + Iris (Go)
    ================================================ FILE: _examples/webassembly/client/hello_go116.go ================================================ //go:build js // +build js package main import ( "fmt" "syscall/js" "time" ) func main() { // GOARCH=wasm GOOS=js /home/$yourusername/go1.16/bin/go build -mod=mod -o hello.wasm hello_go116.go js.Global().Get("console").Call("log", "Hello Webassembly!") message := fmt.Sprintf("Hello, the current time is: %s", time.Now().String()) js.Global().Get("document").Call("getElementById", "hello").Set("innerText", message) } ================================================ FILE: _examples/webassembly/client/main.js ================================================ import './go-wasm-runtime.js'; if (!WebAssembly.instantiateStreaming) { // polyfill WebAssembly.instantiateStreaming = async (resp, importObject) => { const source = await (await resp).arrayBuffer(); return await WebAssembly.instantiate(source, importObject); }; } const go = new Go(); WebAssembly.instantiateStreaming(fetch("hello.wasm"), go.importObject).then((result) => { return WebAssembly.instantiate(result.module, go.importObject); }).then(instance => go.run(instance)); ================================================ FILE: _examples/webassembly/main.go ================================================ package main import ( "github.com/kataras/iris/v12" ) /* You need to build the hello.wasm first, download the go1.16 and execute the below command: $ cd client && GOARCH=wasm GOOS=js /home/$yourname/go1.16/bin/go build -mod=mod -o hello.wasm hello_go116.go */ func main() { app := iris.New() // we could serve your assets like this the sake of the example, // never include the .go files there in production. app.HandleDir("/", iris.Dir("./client")) app.Get("/", func(ctx iris.Context) { // ctx.CompressWriter(true) ctx.ServeFile("./client/hello.html") }) // visit http://localhost:8080 // you should get an html output like this: // Hello, the current time is: 2018-07-09 05:54:12.564 +0000 UTC m=+0.003900161 app.Listen(":8080") } ================================================ FILE: _examples/websocket/README.md ================================================ # Websocket [WebSocket](https://wikipedia.org/wiki/WebSocket) is a protocol that enables two-way persistent communication channels over TCP connections. It is used for applications such as chat, stock tickers, games, anywhere you want real-time functionality in a web application. Iris websocket library is now merged with the [neffos real-time framework](https://github.com/kataras/neffos) and Iris-specific helpers and type aliases live on the [iris/websocket](https://github.com/kataras/iris/tree/main/websocket) subpackage. Learn neffos from its [wiki](https://github.com/kataras/neffos#learning-neffos). Helpers and type aliases improves your code speed when writing a websocket module. For example, instead of importing both `kataras/iris/websocket` - in order to use its `websocket.Handler` - and `github.com/kataras/neffos` - to create a new websocket server `neffos.New` - you can use the `websocket.New` instead, another example is the `neffos.Conn` which can be declared as `websocket.Conn`. All neffos and its subpackage's types and package-level functions exist as type aliases on the `kataras/iris/websocket` package too, there are too many of those and there is no need to write each one of those here, some common types: - `github.com/kataras/neffos/#Conn` -> `github.com/kataras/iris/websocket/#Conn` - `github.com/kataras/neffos/gorilla/#DefaultUpgrader` -> `github.com/kataras/iris/websocket/#DefaultGorillaUpgrader` - `github.com/kataras/neffos/stackexchange/redis/#NewStackExchange` -> `github.com/kataras/iris/websocket/#NewRedisStackExchange` ================================================ FILE: _examples/websocket/basic/README.md ================================================ # Basic Example At the end of this example you will be able to run a websocket server and clients for all platforms (Go, Browser and Nodejs). ![](overview.png) Open as many clients as you want to try out and start typing. This example contains only the basics, however, the library supports rooms, native websocket messages, any data can be sent and received (i.e protobufs, json) and all kinds of broadcasting and connections collections. ## How to run ### Server Open a terminal window instance and execute: ```sh $ go mod tidy -compat=1.17 $ go run server.go # start the websocket server. ``` ### Client (Go) Start a new terminal instance and execute: ```sh $ cd ./go-client $ go mod tidy -compat=1.17 $ go run client.go # start the websocket client. # start typing... ``` ### Client (Browser) Navigate to and start typing. The `./browser/index.html` should be served, it contains the client-side code. ### Client (Browserify) Install [NPM](https://nodejs.org) first, then start a new terminal instance and execute: ```sh $ cd ./browserify $ npm install # build the modern browser-side client: # embed the neffos.js node-module and app.js # into a single ./browserify/bundle.js file # which ./browserify/client.html imports. $ npm run-script build ``` Navigate to and start typing. ### Client (Nodejs) Install [NPM](https://nodejs.org) if you haven't already and then, start a new terminal instance and execute: ```sh $ cd nodejs-client $ npm install $ node client.js # start the websocket client. # start typing. ``` ================================================ FILE: _examples/websocket/basic/browser/index.html ================================================
    
    
    
    
    
    ================================================
    FILE: _examples/websocket/basic/browserify/app.js
    ================================================
    const neffos = require('neffos.js');
    
    var scheme = document.location.protocol == "https:" ? "wss" : "ws";
    var port = document.location.port ? ":" + document.location.port : "";
    
    var wsURL = scheme + "://" + document.location.hostname + port + "/echo";
    
    const enableJWT = true;
    if (enableJWT) {
      // This is just a signature and a payload of an example content, 
      // please replace this with your logic.
      //
      // Add a random letter in front of the token to make it
      // invalid and see that this client is not allowed to dial the websocket server.
      const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjozMjEzMjF9.8waEX7-vPKACa-Soi1pQvW3Rl8QY-SUFcHKTLZI4mvU";
      wsURL += "?token=" + token;
    }
    
    var outputTxt = document.getElementById("output");
    
    function addMessage(msg) {
      outputTxt.innerHTML += msg + "\n";
    }
    
    function handleError(reason) {
      console.log(reason);
      window.alert(reason);
    }
    
    function handleNamespaceConnectedConn(nsConn) {
      nsConn.emit("chat", "Hello from browser(ify) client-side!");
    
      const inputTxt = document.getElementById("input");
      const sendBtn = document.getElementById("sendBtn");
    
      sendBtn.disabled = false;
      sendBtn.onclick = function () {
        const input = inputTxt.value;
        inputTxt.value = "";
    
        nsConn.emit("chat", input);
        addMessage("Me: " + input);
      };
    }
    
    async function runExample() {
      try {
        const conn = await neffos.dial(wsURL, {
          default: { // "default" namespace.
            _OnNamespaceConnected: function (nsConn, msg) {
              addMessage("connected to namespace: " + msg.Namespace);
              handleNamespaceConnectedConn(nsConn);
            },
            _OnNamespaceDisconnect: function (nsConn, msg) {
              addMessage("disconnected from namespace: " + msg.Namespace);
            },
            chat: function (nsConn, msg) { // "chat" event.
              addMessage(msg.Body);
            }
          }
        });
    
        // You can either wait to conenct or just conn.connect("connect")
        // and put the `handleNamespaceConnectedConn` inside `_OnNamespaceConnected` callback instead.
        // const nsConn = await conn.connect("default");
        // handleNamespaceConnectedConn(nsConn);
        // nsConn.emit(...); handleNamespaceConnectedConn(nsConn);
        conn.connect("default");
    
      } catch (err) {
        handleError(err);
      }
    }
    
    runExample();
    
    ================================================
    FILE: _examples/websocket/basic/browserify/bundle.js
    ================================================
    (function(){function d(s,e,n){function f(u,i){if(!e[u]){if(!s[u]){var t="function"==typeof require&&require;if(!i&&t)return t(u,!0);if(o)return o(u,!0);var r=new Error("Cannot find module '"+u+"'");throw r.code="MODULE_NOT_FOUND",r}var a=e[u]={exports:{}};s[u][0].call(a.exports,function(e){var d=s[u][1][e];return f(d||e)},a,a.exports,d,s,e,n)}return e[u].exports}for(var o="function"==typeof require&&require,u=0;uu||57343=u)s.push(65533);else if(55296<=u&&56319>=u)if(n===o-1)s.push(65533);else{var a=f.charCodeAt(n+1);if(56320<=a&&57343>=a){s.push(65536+((1023&u)<<10)+(1023&a)),n+=1}else s.push(65533)}n+=1}return s}function s(e){for(var n=String.fromCharCode,d="",f=0,u;f=u?d+=n(u):(u-=65536,d+=n((u>>10)+55296,(1023&u)+56320));return d}function a(e){return 0<=e&&127>=e}function t(e){this.tokens=[].slice.call(e),this.tokens.reverse()}function r(e,n){if(e)throw TypeError("Decoder error");return n||65533}function c(e){throw TypeError("The code point "+e+" could not be encoded.")}function l(e){return e=(e+"").trim().toLowerCase(),Object.prototype.hasOwnProperty.call(G,e)?G[e]:null}function p(e,n){return n?n[e]||null:null}function m(e,n){var d=n.indexOf(e);return-1===d?null:d}function g(e){if(!("encoding-indexes"in d))throw Error("Indexes missing. Did you forget to include encoding-indexes.js first?");return d["encoding-indexes"][e]}function h(e){if(39419e||1237575>6*d)+f];0>6*(d-1);u.push(128|63&o),d-=1}return u}}function I(e,n){var d=n.fatal;this.handler=function(n,i){if(i===-1)return-1;if(a(i))return i;var f=e[i-128];return null===f?r(d):f}}function k(e,n){n.fatal;this.handler=function(n,d){if(-1===d)return-1;if(z(d))return d;var i=m(d,e);return null===i&&c(d),i+128}}function x(e){var n=e.fatal,d=0,f=0,u=0;this.handler=function(e,o){if(o===-1&&0===d&&0===f&&0===u)return-1;o===-1&&(0!==d||0!==f||0!==u)&&(d=0,f=0,u=0,r(n));var s;if(0!==u){s=null,i(o,48,57)&&(s=h(10*(126*(10*(d-129)+f-48)+u-129)+o-48));var t=[f,u,o];return d=0,f=0,u=0,null===s?(e.prepend(t),r(n)):s}if(0!==f)return i(o,129,254)?(u=o,null):(e.prepend([f,o]),d=0,f=0,r(n));if(0!==d){if(i(o,48,57))return f=o,null;var c=d,l=null;d=0;var m=127>o?64:65;return(i(o,64,126)||i(o,128,254))&&(l=190*(c-129)+(o-m)),s=null===l?null:p(l,g("gb18030")),null===s&&a(o)&&e.prepend(o),null===s?r(n):s}return a(o)?o:128===o?8364:i(o,129,254)?(d=o,null):r(n)}}function N(e,n){e.fatal;this.handler=function(e,d){if(d===-1)return-1;if(z(d))return d;if(58853===d)return c(d);if(n&&8364===d)return 128;var i=m(d,g("gb18030"));if(null!==i){var f=W(i/190)+129,u=i%190,o=63>u?64:65;return[f,u+o]}if(n)return c(d);i=y(d);var s=W(i/10/126/10);i-=10*(126*(10*s));var a=W(i/10/126);i-=126*(10*a);var t=W(i/10),r=i-10*t;return[s+129,a+48,t+129,r+48]}}function C(e){var n=e.fatal,d=0;this.handler=function(e,f){if(-1===f&&0!==d)return d=0,r(n);if(-1===f&&0===d)return-1;if(0!==d){var u=d,o=null;d=0;var s=127>f?64:98;switch((i(f,64,126)||i(f,161,254))&&(o=157*(u-129)+(f-s)),o){case 1133:return[202,772];case 1135:return[202,780];case 1164:return[234,772];case 1166:return[234,780];}var t=null===o?null:p(o,g("big5"));return null===t&&a(f)&&e.prepend(f),null===t?r(n):t}return a(f)?f:i(f,129,254)?(d=f,null):r(n)}}function R(e){e.fatal;this.handler=function(e,n){if(n===-1)return-1;if(z(n))return n;var d=_(n);if(null===d)return c(n);var i=W(d/157)+129;if(161>i)return c(n);var f=d%157,u=63>f?64:98;return[i,f+u]}}function S(e){var n=e.fatal,d=!1,f=0;this.handler=function(e,u){if(-1===u&&0!==f)return f=0,r(n);if(-1===u&&0===f)return-1;if(142===f&&i(u,161,223))return f=0,65216+u;if(143===f&&i(u,161,254))return d=!0,f=u,null;if(0!==f){var o=f;f=0;var s=null;return i(o,161,254)&&i(u,161,254)&&(s=p(94*(o-161)+(u-161),g(d?"jis0212":"jis0208"))),d=!1,i(u,161,254)||e.prepend(u),null===s?r(n):s}return a(u)?u:142===u||143===u||i(u,161,254)?(f=u,null):r(n)}}function L(e){e.fatal;this.handler=function(e,n){if(n===-1)return-1;if(z(n))return n;if(165===n)return 92;if(8254===n)return 126;if(i(n,65377,65439))return[142,n-65377+161];8722===n&&(n=65293);var d=m(n,g("jis0208"));if(null===d)return c(n);var f=W(d/94)+161;return[f,d%94+161]}}function B(e){var n=e.fatal,d={ASCII:0,Roman:1,Katakana:2,LeadByte:3,TrailByte:4,EscapeStart:5,Escape:6},f=d.ASCII,u=d.ASCII,o=0,s=!1;this.handler=function(e,a){switch(f){default:case d.ASCII:return 27===a?(f=d.EscapeStart,null):i(a,0,127)&&14!==a&&15!==a&&27!==a?(s=!1,a):-1===a?-1:(s=!1,r(n));case d.Roman:return 27===a?(f=d.EscapeStart,null):92===a?(s=!1,165):126===a?(s=!1,8254):i(a,0,127)&&14!==a&&15!==a&&27!==a&&92!==a&&126!==a?(s=!1,a):-1===a?-1:(s=!1,r(n));case d.Katakana:return 27===a?(f=d.EscapeStart,null):i(a,33,95)?(s=!1,65344+a):-1===a?-1:(s=!1,r(n));case d.LeadByte:return 27===a?(f=d.EscapeStart,null):i(a,33,126)?(s=!1,o=a,f=d.TrailByte,null):-1===a?-1:(s=!1,r(n));case d.TrailByte:if(27===a)return f=d.EscapeStart,r(n);if(i(a,33,126)){f=d.LeadByte;var t=94*(o-33)+a-33,c=p(t,g("jis0208"));return null===c?r(n):c}return-1===a?(f=d.LeadByte,e.prepend(a),r(n)):(f=d.LeadByte,r(n));case d.EscapeStart:return 36===a||40===a?(o=a,f=d.Escape,null):(e.prepend(a),s=!1,f=u,r(n));case d.Escape:var l=o;o=0;var m=null;if(40===l&&66===a&&(m=d.ASCII),40===l&&74===a&&(m=d.Roman),40===l&&73===a&&(m=d.Katakana),36===l&&(64===a||66===a)&&(m=d.LeadByte),null!==m){f=f=m;var h=s;return s=!0,h?r(n):null}return e.prepend([l,a]),s=!1,f=u,r(n);}}}function j(e){var n=e.fatal,d={ASCII:0,Roman:1,jis0208:2},i=d.ASCII;this.handler=function(e,n){if(-1===n&&i!==d.ASCII)return e.prepend(n),i=d.ASCII,[27,40,66];if(-1===n&&i===d.ASCII)return-1;if((i===d.ASCII||i===d.Roman)&&(14===n||15===n||27===n))return c(65533);if(i===d.ASCII&&z(n))return n;if(i===d.Roman&&(z(n)&&92!==n&&126!==n||165==n||8254==n)){if(z(n))return n;if(165===n)return 92;if(8254===n)return 126}if(z(n)&&i!==d.ASCII)return e.prepend(n),i=d.ASCII,[27,40,66];if((165===n||8254===n)&&i!==d.Roman)return e.prepend(n),i=d.Roman,[27,40,74];8722===n&&(n=65293);var f=m(n,g("jis0208"));if(null===f)return c(n);if(i!==d.jis0208)return e.prepend(n),i=d.jis0208,[27,36,66];var u=W(f/94)+33;return[u,f%94+33]}}function A(e){var n=e.fatal,d=0;this.handler=function(e,f){if(-1===f&&0!==d)return d=0,r(n);if(-1===f&&0===d)return-1;if(0!==d){var u=d,o=null;d=0;var s=127>f?64:65,t=160>u?129:193;if((i(f,64,126)||i(f,128,252))&&(o=188*(u-t)+f-s),i(o,8836,10715))return 48508+o;var c=null===o?null:p(o,g("jis0208"));return null===c&&a(f)&&e.prepend(f),null===c?r(n):c}return a(f)||128===f?f:i(f,161,223)?65216+f:i(f,129,159)||i(f,224,252)?(d=f,null):r(n)}}function T(e){e.fatal;this.handler=function(e,n){if(n===-1)return-1;if(z(n)||128===n)return n;if(165===n)return 92;if(8254===n)return 126;if(i(n,65377,65439))return n-65377+161;8722===n&&(n=65293);var d=w(n);if(null===d)return c(n);var f=W(d/188),u=31>f?129:193,o=d%188,s=63>o?64:65;return[f+u,o+s]}}function M(e){var n=e.fatal,d=0;this.handler=function(e,f){if(-1===f&&0!==d)return d=0,r(n);if(-1===f&&0===d)return-1;if(0!==d){var u=d,o=null;d=0,i(f,65,254)&&(o=190*(u-129)+(f-65));var s=null===o?null:p(o,g("euc-kr"));return null===o&&a(f)&&e.prepend(f),null===s?r(n):s}return a(f)?f:i(f,129,254)?(d=f,null):r(n)}}function D(e){e.fatal;this.handler=function(e,n){if(n===-1)return-1;if(z(n))return n;var d=m(n,g("euc-kr"));if(null===d)return c(n);var i=W(d/190)+129;return[i,d%190+65]}}function J(e,n){var d=e>>8,i=255&e;return n?[d,i]:[i,d]}function U(e,n){var d=n.fatal,f=null,u=null;this.handler=function(n,o){if(-1===o&&(null!==f||null!==u))return r(d);if(-1===o&&null===f&&null===u)return-1;if(null===f)return f=o,null;var s;if(s=e?(f<<8)+o:(o<<8)+f,f=null,null!==u){var a=u;return(u=null,i(s,56320,57343))?65536+1024*(a-55296)+(s-56320):(n.prepend(J(s,e)),r(d))}return i(s,55296,56319)?(u=s,null):i(s,56320,57343)?r(d):s}}function P(e,n){n.fatal;this.handler=function(n,d){if(d===-1)return-1;if(i(d,0,65535))return J(d,e);var f=J((d-65536>>10)+55296,e),u=J((1023&d-65536)+56320,e);return f.concat(u)}}function F(e){e.fatal;this.handler=function(e,n){return-1===n?-1:a(n)?n:63360+n-128}}function K(e){e.fatal;this.handler=function(e,n){return-1===n?-1:z(n)?n:i(n,63360,63487)?n-63360+128:c(n)}}"undefined"!=typeof n&&n.exports&&!d["encoding-indexes"]&&(d["encoding-indexes"]=e("./encoding-indexes.js")["encoding-indexes"]);var W=Math.floor,z=a,H=-1;t.prototype={endOfStream:function(){return!this.tokens.length},read:function(){return this.tokens.length?this.tokens.pop():H},prepend:function(e){if(Array.isArray(e))for(var n=e;n.length;)this.tokens.push(n.pop());else this.tokens.push(e)},push:function(e){if(Array.isArray(e))for(var n=e;n.length;)this.tokens.unshift(n.shift());else this.tokens.unshift(e)}};(function(){}).prototype={handler:function(){}},function(){}.prototype={handler:function(){}};var q=[{encodings:[{labels:["unicode-1-1-utf-8","utf-8","utf8"],name:"UTF-8"}],heading:"The Encoding"},{encodings:[{labels:["866","cp866","csibm866","ibm866"],name:"IBM866"},{labels:["csisolatin2","iso-8859-2","iso-ir-101","iso8859-2","iso88592","iso_8859-2","iso_8859-2:1987","l2","latin2"],name:"ISO-8859-2"},{labels:["csisolatin3","iso-8859-3","iso-ir-109","iso8859-3","iso88593","iso_8859-3","iso_8859-3:1988","l3","latin3"],name:"ISO-8859-3"},{labels:["csisolatin4","iso-8859-4","iso-ir-110","iso8859-4","iso88594","iso_8859-4","iso_8859-4:1988","l4","latin4"],name:"ISO-8859-4"},{labels:["csisolatincyrillic","cyrillic","iso-8859-5","iso-ir-144","iso8859-5","iso88595","iso_8859-5","iso_8859-5:1988"],name:"ISO-8859-5"},{labels:["arabic","asmo-708","csiso88596e","csiso88596i","csisolatinarabic","ecma-114","iso-8859-6","iso-8859-6-e","iso-8859-6-i","iso-ir-127","iso8859-6","iso88596","iso_8859-6","iso_8859-6:1987"],name:"ISO-8859-6"},{labels:["csisolatingreek","ecma-118","elot_928","greek","greek8","iso-8859-7","iso-ir-126","iso8859-7","iso88597","iso_8859-7","iso_8859-7:1987","sun_eu_greek"],name:"ISO-8859-7"},{labels:["csiso88598e","csisolatinhebrew","hebrew","iso-8859-8","iso-8859-8-e","iso-ir-138","iso8859-8","iso88598","iso_8859-8","iso_8859-8:1988","visual"],name:"ISO-8859-8"},{labels:["csiso88598i","iso-8859-8-i","logical"],name:"ISO-8859-8-I"},{labels:["csisolatin6","iso-8859-10","iso-ir-157","iso8859-10","iso885910","l6","latin6"],name:"ISO-8859-10"},{labels:["iso-8859-13","iso8859-13","iso885913"],name:"ISO-8859-13"},{labels:["iso-8859-14","iso8859-14","iso885914"],name:"ISO-8859-14"},{labels:["csisolatin9","iso-8859-15","iso8859-15","iso885915","iso_8859-15","l9"],name:"ISO-8859-15"},{labels:["iso-8859-16"],name:"ISO-8859-16"},{labels:["cskoi8r","koi","koi8","koi8-r","koi8_r"],name:"KOI8-R"},{labels:["koi8-ru","koi8-u"],name:"KOI8-U"},{labels:["csmacintosh","mac","macintosh","x-mac-roman"],name:"macintosh"},{labels:["dos-874","iso-8859-11","iso8859-11","iso885911","tis-620","windows-874"],name:"windows-874"},{labels:["cp1250","windows-1250","x-cp1250"],name:"windows-1250"},{labels:["cp1251","windows-1251","x-cp1251"],name:"windows-1251"},{labels:["ansi_x3.4-1968","ascii","cp1252","cp819","csisolatin1","ibm819","iso-8859-1","iso-ir-100","iso8859-1","iso88591","iso_8859-1","iso_8859-1:1987","l1","latin1","us-ascii","windows-1252","x-cp1252"],name:"windows-1252"},{labels:["cp1253","windows-1253","x-cp1253"],name:"windows-1253"},{labels:["cp1254","csisolatin5","iso-8859-9","iso-ir-148","iso8859-9","iso88599","iso_8859-9","iso_8859-9:1989","l5","latin5","windows-1254","x-cp1254"],name:"windows-1254"},{labels:["cp1255","windows-1255","x-cp1255"],name:"windows-1255"},{labels:["cp1256","windows-1256","x-cp1256"],name:"windows-1256"},{labels:["cp1257","windows-1257","x-cp1257"],name:"windows-1257"},{labels:["cp1258","windows-1258","x-cp1258"],name:"windows-1258"},{labels:["x-mac-cyrillic","x-mac-ukrainian"],name:"x-mac-cyrillic"}],heading:"Legacy single-byte encodings"},{encodings:[{labels:["chinese","csgb2312","csiso58gb231280","gb2312","gb_2312","gb_2312-80","gbk","iso-ir-58","x-gbk"],name:"GBK"},{labels:["gb18030"],name:"gb18030"}],heading:"Legacy multi-byte Chinese (simplified) encodings"},{encodings:[{labels:["big5","big5-hkscs","cn-big5","csbig5","x-x-big5"],name:"Big5"}],heading:"Legacy multi-byte Chinese (traditional) encodings"},{encodings:[{labels:["cseucpkdfmtjapanese","euc-jp","x-euc-jp"],name:"EUC-JP"},{labels:["csiso2022jp","iso-2022-jp"],name:"ISO-2022-JP"},{labels:["csshiftjis","ms932","ms_kanji","shift-jis","shift_jis","sjis","windows-31j","x-sjis"],name:"Shift_JIS"}],heading:"Legacy multi-byte Japanese encodings"},{encodings:[{labels:["cseuckr","csksc56011987","euc-kr","iso-ir-149","korean","ks_c_5601-1987","ks_c_5601-1989","ksc5601","ksc_5601","windows-949"],name:"EUC-KR"}],heading:"Legacy multi-byte Korean encodings"},{encodings:[{labels:["csiso2022kr","hz-gb-2312","iso-2022-cn","iso-2022-cn-ext","iso-2022-kr"],name:"replacement"},{labels:["utf-16be"],name:"UTF-16BE"},{labels:["utf-16","utf-16le"],name:"UTF-16LE"},{labels:["x-user-defined"],name:"x-user-defined"}],heading:"Legacy miscellaneous encodings"}],G={};q.forEach(function(e){e.encodings.forEach(function(e){e.labels.forEach(function(n){G[n]=e})})});var X={},Q={},$,Y;Object.defineProperty&&(Object.defineProperty(b.prototype,"encoding",{get:function(){return this._encoding.name.toLowerCase()}}),Object.defineProperty(b.prototype,"fatal",{get:function(){return"fatal"===this._error_mode}}),Object.defineProperty(b.prototype,"ignoreBOM",{get:function(){return this._ignoreBOM}})),b.prototype.decode=function(e,n){var d;d="object"==typeof e&&e instanceof ArrayBuffer?new Uint8Array(e):"object"==typeof e&&"buffer"in e&&e.buffer instanceof ArrayBuffer?new Uint8Array(e.buffer,e.byteOffset,e.byteLength):new Uint8Array(0),n=u(n),this._do_not_flush||(this._decoder=Q[this._encoding.name]({fatal:"fatal"===this._error_mode}),this._BOMseen=!1),this._do_not_flush=!!n.stream;for(var i=new t(d),o=[],a,r;(r=i.read(),r!==H)&&!(a=this._decoder.handler(i,r),-1===a);)null!==a&&(Array.isArray(a)?o.push.apply(o,a):o.push(a));if(!this._do_not_flush){do{if(a=this._decoder.handler(i,i.read()),-1===a)break;if(null===a)continue;Array.isArray(a)?o.push.apply(o,a):o.push(a)}while(!i.endOfStream());this._decoder=null}return function(e){return!f(["UTF-8","UTF-16LE","UTF-16BE"],this._encoding.name)||this._ignoreBOM||this._BOMseen||(0=a)return r.close(),null;var d=new Map;r.connectedNamespaces.forEach(function(e,n){var i=[];!w(e.rooms)&&0a[0]&&d[1]
    
    
    
    
    
    
    
    
    
    
    
    
    ================================================
    FILE: _examples/websocket/basic/browserify/package.json
    ================================================
    {
        "name": "neffos.js.example.browserify",
        "version": "0.0.1",
        "scripts": {
            "browserify": "browserify ./app.js -o ./bundle.js",
            "minifyES6": "minify ./bundle.js --outFile ./bundle.js",
            "build": "npm run-script browserify && npm run-script minifyES6"
        },
        "dependencies": {
            "neffos.js": "latest"
        },
        "devDependencies": {
            "browserify": "^16.2.3",
            "babel-minify": "^0.5.0"
        }
    }
    
    
    ================================================
    FILE: _examples/websocket/basic/go-client/client.go
    ================================================
    package main
    
    import (
    	"bufio"
    	"bytes"
    	"context"
    	"fmt"
    	"log"
    	"os"
    	"time"
    
    	"github.com/kataras/iris/v12/websocket"
    )
    
    const (
    	endpoint              = "ws://localhost:8080/echo"
    	namespace             = "default"
    	dialAndConnectTimeout = 5 * time.Second
    )
    
    // this can be shared with the server.go's.
    // `NSConn.Conn` has the `IsClient() bool` method which can be used to
    // check if that's is a client or a server-side callback.
    var clientEvents = websocket.Namespaces{
    	namespace: websocket.Events{
    		websocket.OnNamespaceConnected: func(c *websocket.NSConn, msg websocket.Message) error {
    			log.Printf("connected to namespace: %s", msg.Namespace)
    			return nil
    		},
    		websocket.OnNamespaceDisconnect: func(c *websocket.NSConn, msg websocket.Message) error {
    			log.Printf("disconnected from namespace: %s", msg.Namespace)
    			return nil
    		},
    		"chat": func(c *websocket.NSConn, msg websocket.Message) error {
    			log.Printf("%s", string(msg.Body))
    			return nil
    		},
    	},
    }
    
    func main() {
    	ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(dialAndConnectTimeout))
    	defer cancel()
    
    	// username := "my_username"
    	// dialer := websocket.GobwasDialer(websocket.GobwasDialerOptions{Header: websocket.GobwasHeader{"X-Username": []string{username}}})
    	dialer := websocket.DefaultGobwasDialer
    	client, err := websocket.Dial(ctx, dialer, endpoint, clientEvents)
    	if err != nil {
    		panic(err)
    	}
    	defer client.Close()
    
    	c, err := client.Connect(ctx, namespace)
    	if err != nil {
    		panic(err)
    	}
    
    	c.Emit("chat", []byte("Hello from Go client side!"))
    
    	fmt.Fprint(os.Stdout, ">> ")
    	scanner := bufio.NewScanner(os.Stdin)
    	for {
    		if !scanner.Scan() {
    			log.Printf("ERROR: %v", scanner.Err())
    			return
    		}
    
    		text := scanner.Bytes()
    
    		if bytes.Equal(text, []byte("exit")) {
    			if err := c.Disconnect(nil); err != nil {
    				log.Printf("reply from server: %v", err)
    			}
    			break
    		}
    
    		ok := c.Emit("chat", text)
    		if !ok {
    			break
    		}
    
    		fmt.Fprint(os.Stdout, ">> ")
    	}
    } // try running this program twice or/and run the server's http://localhost:8080 to check the browser client as well.
    
    
    ================================================
    FILE: _examples/websocket/basic/go.mod
    ================================================
    module github.com/kataras/iris/_examples/websocket/basic
    
    go 1.25
    
    require (
    	github.com/iris-contrib/middleware/jwt v0.0.0-20251225090426-92c6f28facda
    	github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd
    )
    
    require (
    	github.com/BurntSushi/toml v1.6.0 // indirect
    	github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect
    	github.com/CloudyKit/jet/v6 v6.3.1 // indirect
    	github.com/Joker/jade v1.1.3 // indirect
    	github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 // indirect
    	github.com/andybalholm/brotli v1.2.0 // indirect
    	github.com/aymerick/douceur v0.2.0 // indirect
    	github.com/fatih/structs v1.1.0 // indirect
    	github.com/flosch/pongo2/v4 v4.0.2 // indirect
    	github.com/gobwas/httphead v0.1.0 // indirect
    	github.com/gobwas/pool v0.2.1 // indirect
    	github.com/gobwas/ws v1.4.0 // indirect
    	github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
    	github.com/golang/snappy v1.0.0 // indirect
    	github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a // indirect
    	github.com/google/uuid v1.6.0 // indirect
    	github.com/gorilla/css v1.0.1 // indirect
    	github.com/gorilla/websocket v1.5.3 // indirect
    	github.com/iris-contrib/schema v0.0.6 // indirect
    	github.com/josharian/intern v1.0.0 // indirect
    	github.com/kataras/blocks v0.0.8 // indirect
    	github.com/kataras/golog v0.1.12 // indirect
    	github.com/kataras/neffos v0.0.24 // indirect
    	github.com/kataras/pio v0.0.13 // indirect
    	github.com/kataras/sitemap v0.0.6 // indirect
    	github.com/kataras/tunnel v0.0.4 // indirect
    	github.com/klauspost/compress v1.18.2 // indirect
    	github.com/kr/text v0.2.0 // indirect
    	github.com/mailgun/raymond/v2 v2.0.48 // indirect
    	github.com/mailru/easyjson v0.9.1 // indirect
    	github.com/mediocregopher/radix/v3 v3.8.1 // indirect
    	github.com/microcosm-cc/bluemonday v1.0.27 // indirect
    	github.com/nats-io/nats.go v1.40.1 // indirect
    	github.com/nats-io/nkeys v0.4.9 // indirect
    	github.com/nats-io/nuid v1.0.1 // indirect
    	github.com/russross/blackfriday/v2 v2.1.0 // indirect
    	github.com/schollz/closestmatch v2.1.0+incompatible // indirect
    	github.com/sirupsen/logrus v1.8.1 // indirect
    	github.com/tdewolff/minify/v2 v2.24.8 // indirect
    	github.com/tdewolff/parse/v2 v2.8.5 // indirect
    	github.com/valyala/bytebufferpool v1.0.0 // indirect
    	github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
    	github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
    	github.com/yosssi/ace v0.0.5 // indirect
    	golang.org/x/crypto v0.47.0 // indirect
    	golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect
    	golang.org/x/net v0.49.0 // indirect
    	golang.org/x/sys v0.40.0 // indirect
    	golang.org/x/text v0.33.0 // indirect
    	golang.org/x/time v0.14.0 // indirect
    	golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
    	google.golang.org/protobuf v1.36.11 // indirect
    	gopkg.in/ini.v1 v1.67.0 // indirect
    	gopkg.in/yaml.v3 v3.0.1 // indirect
    )
    
    
    ================================================
    FILE: _examples/websocket/basic/go.sum
    ================================================
    github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
    github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
    github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c=
    github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno=
    github.com/CloudyKit/jet/v6 v6.3.1 h1:6IAo5Cx21xrHVaR8zzXN5gJatKV/wO7Nf6bfCnCSbUw=
    github.com/CloudyKit/jet/v6 v6.3.1/go.mod h1:lf8ksdNsxZt7/yH/3n4vJQWA9RUq4wpaHtArHhGVMOw=
    github.com/Joker/hpp v1.0.0 h1:65+iuJYdRXv/XyN62C1uEmmOx3432rNG/rKlX6V7Kkc=
    github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY=
    github.com/Joker/jade v1.1.3 h1:Qbeh12Vq6BxURXT1qZBRHsDxeURB8ztcL6f3EXSGeHk=
    github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKduM=
    github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 h1:dG7/gLroJGht/jSQtHiLvT48Hxn+crbmvyItZC8cWOs=
    github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05/go.mod h1:NYezi6wtnJtBm5btoprXc5SvAdqH0XTXWnUup0MptAI=
    github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
    github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
    github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
    github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
    github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
    github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
    github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
    github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
    github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
    github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
    github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
    github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
    github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
    github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
    github.com/flosch/pongo2/v4 v4.0.2 h1:gv+5Pe3vaSVmiJvh/BZa82b7/00YUGm0PIyVVLop0Hw=
    github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+BWPhwZD8=
    github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
    github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
    github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
    github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
    github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
    github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
    github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
    github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
    github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
    github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
    github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
    github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
    github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a h1:l7A0loSszR5zHd/qK53ZIHMO8b3bBSmENnQ6eKnUT0A=
    github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
    github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
    github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
    github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
    github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
    github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
    github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
    github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
    github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
    github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
    github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
    github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk=
    github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA=
    github.com/iris-contrib/httpexpect/v2 v2.15.2 h1:T9THsdP1woyAqKHwjkEsbCnMefsAFvk8iJJKokcJ3Go=
    github.com/iris-contrib/httpexpect/v2 v2.15.2/go.mod h1:JLDgIqnFy5loDSUv1OA2j0mb6p/rDhiCqigP22Uq9xE=
    github.com/iris-contrib/middleware/jwt v0.0.0-20251225090426-92c6f28facda h1:1VquMwe3hyozXkURE0uJLqMCiTmJpTYwLFCEZSkXzs0=
    github.com/iris-contrib/middleware/jwt v0.0.0-20251225090426-92c6f28facda/go.mod h1:gvZR1e7IFVaT5ph6WFdleMXYSsocncDG+0BvKyJerTc=
    github.com/iris-contrib/schema v0.0.6 h1:CPSBLyx2e91H2yJzPuhGuifVRnZBBJ3pCOMbOvPZaTw=
    github.com/iris-contrib/schema v0.0.6/go.mod h1:iYszG0IOsuIsfzjymw1kMzTL8YQcCWlm65f3wX8J5iA=
    github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
    github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
    github.com/kataras/blocks v0.0.8 h1:MrpVhoFTCR2v1iOOfGng5VJSILKeZZI+7NGfxEh3SUM=
    github.com/kataras/blocks v0.0.8/go.mod h1:9Jm5zx6BB+06NwA+OhTbHW1xkMOYxahnqTN5DveZ2Yg=
    github.com/kataras/golog v0.1.12 h1:Bu7I/G4ilJlbfzjmU39O9N+2uO1pBcMK045fzZ4ytNg=
    github.com/kataras/golog v0.1.12/go.mod h1:wrGSbOiBqbQSQznleVNX4epWM8rl9SJ/rmEacl0yqy4=
    github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd h1:oMsQgqKACbUdlaIWnN+EZzzAnHkw90hE/y/R0BSij2U=
    github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd/go.mod h1:yucjtDl9Vn3OctWM1fi0MuM9OckemC7kEreFa9QtPF8=
    github.com/kataras/neffos v0.0.24 h1:S3lHqJopCfXN285VdlbGeOj+Id83u4xdQKToa+w1vW0=
    github.com/kataras/neffos v0.0.24/go.mod h1:/3K9zQ0yEC5/xUiSQx46ToWa3xneGfUo/nMit/F5g+U=
    github.com/kataras/pio v0.0.13 h1:x0rXVX0fviDTXOOLOmr4MUxOabu1InVSTu5itF8CXCM=
    github.com/kataras/pio v0.0.13/go.mod h1:k3HNuSw+eJ8Pm2lA4lRhg3DiCjVgHlP8hmXApSej3oM=
    github.com/kataras/sitemap v0.0.6 h1:w71CRMMKYMJh6LR2wTgnk5hSgjVNB9KL60n5e2KHvLY=
    github.com/kataras/sitemap v0.0.6/go.mod h1:dW4dOCNs896OR1HmG+dMLdT7JjDk7mYBzoIRwuj5jA4=
    github.com/kataras/tunnel v0.0.4 h1:sCAqWuJV7nPzGrlb0os3j49lk2JhILT0rID38NHNLpA=
    github.com/kataras/tunnel v0.0.4/go.mod h1:9FkU4LaeifdMWqZu7o20ojmW4B7hdhv2CMLwfnHGpYw=
    github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
    github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
    github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
    github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
    github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
    github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
    github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
    github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
    github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw=
    github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18=
    github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8=
    github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
    github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
    github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
    github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
    github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
    github.com/mediocregopher/radix/v3 v3.8.1 h1:rOkHflVuulFKlwsLY01/M2cM2tWCjDoETcMqKbAWu1M=
    github.com/mediocregopher/radix/v3 v3.8.1/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8=
    github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
    github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
    github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
    github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
    github.com/nats-io/nats.go v1.40.1 h1:MLjDkdsbGUeCMKFyCFoLnNn/HDTqcgVa3EQm+pMNDPk=
    github.com/nats-io/nats.go v1.40.1/go.mod h1:wV73x0FSI/orHPSYoyMeJB+KajMDoWyXmFaRrrYaaTo=
    github.com/nats-io/nkeys v0.4.9 h1:qe9Faq2Gxwi6RZnZMXfmGMZkg3afLLOtrU+gDZJ35b0=
    github.com/nats-io/nkeys v0.4.9/go.mod h1:jcMqs+FLG+W5YO36OX6wFIFcmpdAns+w1Wm6D3I/evE=
    github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
    github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
    github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
    github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
    github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
    github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
    github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
    github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
    github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
    github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo=
    github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U=
    github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk=
    github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g=
    github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
    github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
    github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
    github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
    github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
    github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
    github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
    github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
    github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
    github.com/tdewolff/minify/v2 v2.24.8 h1:58/VjsbevI4d5FGV0ZSuBrHMSSkH4MCH0sIz/eKIauE=
    github.com/tdewolff/minify/v2 v2.24.8/go.mod h1:0Ukj0CRpo/sW/nd8uZ4ccXaV1rEVIWA3dj8U7+Shhfw=
    github.com/tdewolff/parse/v2 v2.8.5 h1:ZmBiA/8Do5Rpk7bDye0jbbDUpXXbCdc3iah4VeUvwYU=
    github.com/tdewolff/parse/v2 v2.8.5/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo=
    github.com/tdewolff/test v1.0.11 h1:FdLbwQVHxqG16SlkGveC0JVyrJN62COWTRyUFzfbtBE=
    github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
    github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
    github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
    github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
    github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
    github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
    github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
    github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
    github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
    github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
    github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
    github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
    github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
    github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
    github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
    github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY=
    github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI=
    github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA=
    github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0=
    github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA=
    github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
    github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M=
    github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
    github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
    golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
    golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
    golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
    golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
    golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
    golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
    golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
    golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
    golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
    golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
    golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
    golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
    golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
    golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
    golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
    golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
    golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
    golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
    golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
    golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
    golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
    golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
    golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
    golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
    golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
    golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
    golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
    golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
    golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
    golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
    golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
    golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
    golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
    golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
    golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
    golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
    golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
    golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
    golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
    golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
    golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
    golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
    google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
    google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
    gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
    gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
    gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
    gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
    gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
    gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
    gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
    gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
    gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
    gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
    gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
    moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs=
    moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHcE=
    
    
    ================================================
    FILE: _examples/websocket/basic/nodejs-client/client.js
    ================================================
    const neffos = require('neffos.js');
    const stdin = process.openStdin();
    
    const wsURL = "ws://localhost:8080/echo";
    
    async function runExample() {
      try {
        const conn = await neffos.dial(wsURL, {
          default: { // "default" namespace.
            _OnNamespaceConnected: function (nsConn, msg) {
              console.log("connected to namespace: " + msg.Namespace);
            },
            _OnNamespaceDisconnect: function (nsConn, msg) {
              console.log("disconnected from namespace: " + msg.Namespace);
            },
            chat: function (nsConn, msg) { // "chat" event.
              console.log(msg.Body);
            }
          }
        });
    
        const nsConn = await conn.connect("default");
        nsConn.emit("chat", "Hello from Nodejs client side!");
    
        stdin.addListener("data", function (data) {
          const text = data.toString().trim();
          nsConn.emit("chat", text);
        });
    
      } catch (err) {
        console.error(err);
      }
    }
    
    runExample();
    
    
    ================================================
    FILE: _examples/websocket/basic/nodejs-client/package.json
    ================================================
    {
        "name": "neffos.js.example.nodejsclient",
        "version": "0.0.1",
        "main": "client.js",
        "dependencies": {
            "neffos.js": "latest"
        }
    }
    
    
    ================================================
    FILE: _examples/websocket/basic/server.go
    ================================================
    package main
    
    import (
    	"log"
    
    	"github.com/kataras/iris/v12"
    	"github.com/kataras/iris/v12/websocket"
    
    	// Used when "enableJWT" constant is true:
    	"github.com/iris-contrib/middleware/jwt"
    )
    
    // values should match with the client sides as well.
    const enableJWT = true
    const namespace = "default"
    
    // if namespace is empty then simply websocket.Events{...} can be used instead.
    var serverEvents = websocket.Namespaces{
    	namespace: websocket.Events{
    		websocket.OnNamespaceConnected: func(nsConn *websocket.NSConn, msg websocket.Message) error {
    			// with `websocket.GetContext` you can retrieve the Iris' `Context`.
    			ctx := websocket.GetContext(nsConn.Conn)
    
    			log.Printf("[%s] connected to namespace [%s] with IP [%s]",
    				nsConn, msg.Namespace,
    				ctx.RemoteAddr())
    			return nil
    		},
    		websocket.OnNamespaceDisconnect: func(nsConn *websocket.NSConn, msg websocket.Message) error {
    			log.Printf("[%s] disconnected from namespace [%s]", nsConn, msg.Namespace)
    			return nil
    		},
    		"chat": func(nsConn *websocket.NSConn, msg websocket.Message) error {
    			// room.String() returns -> NSConn.String() returns -> Conn.String() returns -> Conn.ID()
    			log.Printf("[%s] sent: %s", nsConn, string(msg.Body))
    
    			// Write message back to the client message owner with:
    			// nsConn.Emit("chat", msg)
    			// Write message to all except this client with:
    			nsConn.Conn.Server().Broadcast(nsConn, msg)
    			return nil
    		},
    	},
    }
    
    func main() {
    	app := iris.New()
    	websocketServer := websocket.New(
    		websocket.DefaultGorillaUpgrader, /* DefaultGobwasUpgrader can be used too. */
    		serverEvents)
    
    	j := jwt.New(jwt.Config{
    		// Extract by the "token" url,
    		// so the client should dial with ws://localhost:8080/echo?token=$token
    		Extractor: jwt.FromParameter("token"),
    
    		ValidationKeyGetter: func(token *jwt.Token) (any, error) {
    			return []byte("My Secret"), nil
    		},
    
    		// When set, the middleware verifies that tokens are signed
    		// with the specific signing algorithm
    		// If the signing method is not constant the
    		// `Config.ValidationKeyGetter` callback field can be used
    		// to implement additional checks
    		// Important to avoid security issues described here:
    		// https://auth0.com/blog/2015/03/31/critical-vulnerabilities-in-json-web-token-libraries/
    		SigningMethod: jwt.SigningMethodHS256,
    	})
    
    	idGen := func(ctx iris.Context) string {
    		if username := ctx.GetHeader("X-Username"); username != "" {
    			return username
    		}
    
    		return websocket.DefaultIDGenerator(ctx)
    	}
    
    	// serves the endpoint of ws://localhost:8080/echo
    	// with optional custom ID generator.
    	websocketRoute := app.Get("/echo", websocket.Handler(websocketServer, idGen))
    
    	if enableJWT {
    		// Register the jwt middleware (on handshake):
    		websocketRoute.Use(j.Serve)
    		// OR
    		//
    		// Check for token through the jwt middleware
    		// on websocket connection or on any event:
    		/* websocketServer.OnConnect = func(c *websocket.Conn) error {
    		ctx := websocket.GetContext(c)
    		if err := j.CheckJWT(ctx); err != nil {
    			// will send the above error on the client
    			// and will not allow it to connect to the websocket server at all.
    			return err
    		}
    
    		user := ctx.Values().Get("jwt").(*jwt.Token)
    		// or just: user := j.Get(ctx)
    
    		log.Printf("This is an authenticated request\n")
    		log.Printf("Claim content:")
    		log.Printf("%#+v\n", user.Claims)
    
    		log.Printf("[%s] connected to the server", c.ID())
    
    		return nil
    		} */
    	}
    
    	// serves the browser-based websocket client.
    	app.Get("/", func(ctx iris.Context) {
    		ctx.ServeFile("./browser/index.html")
    	})
    
    	app.Get("/other", func(ctx iris.Context) {
    		ctx.WriteString("Other route")
    	})
    
    	// serves the npm browser websocket client usage example.
    	app.HandleDir("/browserify", iris.Dir("./browserify"))
    
    	// http://localhost:8080
    	// http://localhost:8080/browserify/client.html
    	app.Listen(":8080")
    }
    
    
    ================================================
    FILE: _examples/websocket/gorilla-filewatch/go.mod
    ================================================
    module gorilla-filewatch-example
    
    go 1.25
    
    require (
    	github.com/gorilla/websocket v1.5.3
    	github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd
    )
    
    require (
    	github.com/BurntSushi/toml v1.6.0 // indirect
    	github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect
    	github.com/CloudyKit/jet/v6 v6.3.1 // indirect
    	github.com/Joker/jade v1.1.3 // indirect
    	github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 // indirect
    	github.com/andybalholm/brotli v1.2.0 // indirect
    	github.com/aymerick/douceur v0.2.0 // indirect
    	github.com/fatih/structs v1.1.0 // indirect
    	github.com/flosch/pongo2/v4 v4.0.2 // indirect
    	github.com/golang/snappy v1.0.0 // indirect
    	github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a // indirect
    	github.com/google/uuid v1.6.0 // indirect
    	github.com/gorilla/css v1.0.1 // indirect
    	github.com/iris-contrib/schema v0.0.6 // indirect
    	github.com/josharian/intern v1.0.0 // indirect
    	github.com/kataras/blocks v0.0.8 // indirect
    	github.com/kataras/golog v0.1.12 // indirect
    	github.com/kataras/pio v0.0.13 // indirect
    	github.com/kataras/sitemap v0.0.6 // indirect
    	github.com/kataras/tunnel v0.0.4 // indirect
    	github.com/klauspost/compress v1.18.2 // indirect
    	github.com/kr/pretty v0.3.1 // indirect
    	github.com/mailgun/raymond/v2 v2.0.48 // indirect
    	github.com/mailru/easyjson v0.9.1 // indirect
    	github.com/microcosm-cc/bluemonday v1.0.27 // indirect
    	github.com/rogpeppe/go-internal v1.10.0 // indirect
    	github.com/russross/blackfriday/v2 v2.1.0 // indirect
    	github.com/schollz/closestmatch v2.1.0+incompatible // indirect
    	github.com/sirupsen/logrus v1.8.1 // indirect
    	github.com/stretchr/testify v1.11.1 // indirect
    	github.com/tdewolff/minify/v2 v2.24.8 // indirect
    	github.com/tdewolff/parse/v2 v2.8.5 // indirect
    	github.com/valyala/bytebufferpool v1.0.0 // indirect
    	github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
    	github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
    	github.com/yosssi/ace v0.0.5 // indirect
    	golang.org/x/crypto v0.47.0 // indirect
    	golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect
    	golang.org/x/net v0.49.0 // indirect
    	golang.org/x/sys v0.40.0 // indirect
    	golang.org/x/text v0.33.0 // indirect
    	golang.org/x/time v0.14.0 // indirect
    	google.golang.org/protobuf v1.36.11 // indirect
    	gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
    	gopkg.in/ini.v1 v1.67.0 // indirect
    	gopkg.in/yaml.v3 v3.0.1 // indirect
    )
    
    
    ================================================
    FILE: _examples/websocket/gorilla-filewatch/go.sum
    ================================================
    github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
    github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
    github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c=
    github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno=
    github.com/CloudyKit/jet/v6 v6.3.1 h1:6IAo5Cx21xrHVaR8zzXN5gJatKV/wO7Nf6bfCnCSbUw=
    github.com/CloudyKit/jet/v6 v6.3.1/go.mod h1:lf8ksdNsxZt7/yH/3n4vJQWA9RUq4wpaHtArHhGVMOw=
    github.com/Joker/hpp v1.0.0 h1:65+iuJYdRXv/XyN62C1uEmmOx3432rNG/rKlX6V7Kkc=
    github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY=
    github.com/Joker/jade v1.1.3 h1:Qbeh12Vq6BxURXT1qZBRHsDxeURB8ztcL6f3EXSGeHk=
    github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKduM=
    github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 h1:dG7/gLroJGht/jSQtHiLvT48Hxn+crbmvyItZC8cWOs=
    github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05/go.mod h1:NYezi6wtnJtBm5btoprXc5SvAdqH0XTXWnUup0MptAI=
    github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
    github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
    github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
    github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
    github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
    github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
    github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
    github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
    github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
    github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
    github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
    github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
    github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
    github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
    github.com/flosch/pongo2/v4 v4.0.2 h1:gv+5Pe3vaSVmiJvh/BZa82b7/00YUGm0PIyVVLop0Hw=
    github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+BWPhwZD8=
    github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
    github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
    github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
    github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
    github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a h1:l7A0loSszR5zHd/qK53ZIHMO8b3bBSmENnQ6eKnUT0A=
    github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
    github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
    github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
    github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
    github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
    github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
    github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
    github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
    github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
    github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
    github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
    github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk=
    github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA=
    github.com/iris-contrib/httpexpect/v2 v2.15.2 h1:T9THsdP1woyAqKHwjkEsbCnMefsAFvk8iJJKokcJ3Go=
    github.com/iris-contrib/httpexpect/v2 v2.15.2/go.mod h1:JLDgIqnFy5loDSUv1OA2j0mb6p/rDhiCqigP22Uq9xE=
    github.com/iris-contrib/schema v0.0.6 h1:CPSBLyx2e91H2yJzPuhGuifVRnZBBJ3pCOMbOvPZaTw=
    github.com/iris-contrib/schema v0.0.6/go.mod h1:iYszG0IOsuIsfzjymw1kMzTL8YQcCWlm65f3wX8J5iA=
    github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
    github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
    github.com/kataras/blocks v0.0.8 h1:MrpVhoFTCR2v1iOOfGng5VJSILKeZZI+7NGfxEh3SUM=
    github.com/kataras/blocks v0.0.8/go.mod h1:9Jm5zx6BB+06NwA+OhTbHW1xkMOYxahnqTN5DveZ2Yg=
    github.com/kataras/golog v0.1.12 h1:Bu7I/G4ilJlbfzjmU39O9N+2uO1pBcMK045fzZ4ytNg=
    github.com/kataras/golog v0.1.12/go.mod h1:wrGSbOiBqbQSQznleVNX4epWM8rl9SJ/rmEacl0yqy4=
    github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd h1:oMsQgqKACbUdlaIWnN+EZzzAnHkw90hE/y/R0BSij2U=
    github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd/go.mod h1:yucjtDl9Vn3OctWM1fi0MuM9OckemC7kEreFa9QtPF8=
    github.com/kataras/pio v0.0.13 h1:x0rXVX0fviDTXOOLOmr4MUxOabu1InVSTu5itF8CXCM=
    github.com/kataras/pio v0.0.13/go.mod h1:k3HNuSw+eJ8Pm2lA4lRhg3DiCjVgHlP8hmXApSej3oM=
    github.com/kataras/sitemap v0.0.6 h1:w71CRMMKYMJh6LR2wTgnk5hSgjVNB9KL60n5e2KHvLY=
    github.com/kataras/sitemap v0.0.6/go.mod h1:dW4dOCNs896OR1HmG+dMLdT7JjDk7mYBzoIRwuj5jA4=
    github.com/kataras/tunnel v0.0.4 h1:sCAqWuJV7nPzGrlb0os3j49lk2JhILT0rID38NHNLpA=
    github.com/kataras/tunnel v0.0.4/go.mod h1:9FkU4LaeifdMWqZu7o20ojmW4B7hdhv2CMLwfnHGpYw=
    github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
    github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
    github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
    github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
    github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
    github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
    github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
    github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
    github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
    github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw=
    github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18=
    github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8=
    github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
    github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
    github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
    github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
    github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
    github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
    github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
    github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
    github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
    github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
    github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
    github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
    github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
    github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
    github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
    github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
    github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
    github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
    github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo=
    github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U=
    github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk=
    github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g=
    github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
    github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
    github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
    github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
    github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
    github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
    github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
    github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
    github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
    github.com/tdewolff/minify/v2 v2.24.8 h1:58/VjsbevI4d5FGV0ZSuBrHMSSkH4MCH0sIz/eKIauE=
    github.com/tdewolff/minify/v2 v2.24.8/go.mod h1:0Ukj0CRpo/sW/nd8uZ4ccXaV1rEVIWA3dj8U7+Shhfw=
    github.com/tdewolff/parse/v2 v2.8.5 h1:ZmBiA/8Do5Rpk7bDye0jbbDUpXXbCdc3iah4VeUvwYU=
    github.com/tdewolff/parse/v2 v2.8.5/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo=
    github.com/tdewolff/test v1.0.11 h1:FdLbwQVHxqG16SlkGveC0JVyrJN62COWTRyUFzfbtBE=
    github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
    github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
    github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
    github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
    github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
    github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
    github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
    github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
    github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
    github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
    github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
    github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
    github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
    github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
    github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
    github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY=
    github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI=
    github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA=
    github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0=
    github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA=
    github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
    github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M=
    github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
    github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
    golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
    golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
    golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
    golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
    golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
    golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
    golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
    golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
    golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
    golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
    golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
    golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
    golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
    golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
    golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
    golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
    golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
    golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
    golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
    golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
    golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
    golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
    golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
    golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
    golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
    golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
    golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
    golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
    golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
    golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
    golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
    golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
    golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
    golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
    golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
    golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
    golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
    google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
    google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
    gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
    gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
    gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
    gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
    gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
    gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
    gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
    gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
    gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
    gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
    gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
    moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs=
    moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHcE=
    
    
    ================================================
    FILE: _examples/websocket/gorilla-filewatch/main.go
    ================================================
    // Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
    // Use of this source code is governed by a BSD-style
    // license that can be found in the LICENSE file.
    //
    // This example shows a use case with gorilla webscokets package
    // sends a file to the browser client for display whenever the file is modified.
    //
    // $ go run main.go 
    // # Open http://localhost:8080/ .
    // # Modify the file to see it update in the browser.
    package main
    
    import (
    	"flag"
    	"fmt"
    	"log"
    	"os"
    	"strconv"
    	"time"
    
    	"github.com/gorilla/websocket"
    	"github.com/kataras/iris/v12"
    )
    
    const (
    	// Time allowed to write the file to the client.
    	writeWait = 10 * time.Second
    
    	// Time allowed to read the next pong message from the client.
    	pongWait = 60 * time.Second
    
    	// Send pings to client with this period. Must be less than pongWait.
    	pingPeriod = (pongWait * 9) / 10
    
    	// Poll file for changes with this period.
    	filePeriod = 10 * time.Second
    )
    
    var (
    	addr     = flag.String("addr", ":8080", "http service address")
    	filename string
    	upgrader = websocket.Upgrader{
    		ReadBufferSize:  1024,
    		WriteBufferSize: 1024,
    	}
    )
    
    func readFileIfModified(lastMod time.Time) ([]byte, time.Time, error) {
    	fi, err := os.Stat(filename)
    	if err != nil {
    		return nil, lastMod, err
    	}
    	if !fi.ModTime().After(lastMod) {
    		return nil, lastMod, nil
    	}
    	p, err := os.ReadFile(filename)
    	if err != nil {
    		return nil, fi.ModTime(), err
    	}
    	return p, fi.ModTime(), nil
    }
    
    func reader(ws *websocket.Conn) {
    	defer ws.Close()
    	ws.SetReadLimit(512)
    	ws.SetReadDeadline(time.Now().Add(pongWait))
    	ws.SetPongHandler(func(string) error { ws.SetReadDeadline(time.Now().Add(pongWait)); return nil })
    	for {
    		_, _, err := ws.ReadMessage()
    		if err != nil {
    			break
    		}
    	}
    }
    
    func writer(ws *websocket.Conn, lastMod time.Time) {
    	lastError := ""
    	pingTicker := time.NewTicker(pingPeriod)
    	fileTicker := time.NewTicker(filePeriod)
    	defer func() {
    		pingTicker.Stop()
    		fileTicker.Stop()
    		ws.Close()
    	}()
    	for {
    		select {
    		case <-fileTicker.C:
    			var p []byte
    			var err error
    
    			p, lastMod, err = readFileIfModified(lastMod)
    
    			if err != nil {
    				if s := err.Error(); s != lastError {
    					lastError = s
    					p = []byte(lastError)
    				}
    			} else {
    				lastError = ""
    			}
    
    			if p != nil {
    				ws.SetWriteDeadline(time.Now().Add(writeWait))
    				if err := ws.WriteMessage(websocket.TextMessage, p); err != nil {
    					return
    				}
    			}
    		case <-pingTicker.C:
    			ws.SetWriteDeadline(time.Now().Add(writeWait))
    			if err := ws.WriteMessage(websocket.PingMessage, []byte{}); err != nil {
    				return
    			}
    		}
    	}
    }
    
    func serveWs(ctx iris.Context) {
    	ws, err := upgrader.Upgrade(ctx.ResponseWriter(), ctx.Request(), nil)
    	if err != nil {
    		if _, ok := err.(websocket.HandshakeError); !ok {
    			log.Println(err)
    		}
    		return
    	}
    
    	var lastMod time.Time
    	if n, err := strconv.ParseInt(ctx.FormValue("lastMod"), 16, 64); err == nil {
    		lastMod = time.Unix(0, n)
    	}
    
    	go writer(ws, lastMod)
    	reader(ws)
    }
    
    func serveHome(ctx iris.Context) {
    	p, lastMod, err := readFileIfModified(time.Time{})
    	if err != nil {
    		p = []byte(err.Error())
    		lastMod = time.Unix(0, 0)
    	}
    	var v = struct {
    		Host    string
    		Data    string
    		LastMod string
    	}{
    		ctx.Host(),
    		string(p),
    		strconv.FormatInt(lastMod.UnixNano(), 16),
    	}
    	if err := ctx.View("home.html", v); err != nil {
    		ctx.HTML(fmt.Sprintf("

    %s

    ", err.Error())) return } } // $ go get github.com/gorilla/websocket // $ go run main.go testfile.txt func main() { flag.Parse() if flag.NArg() != 1 { log.Fatal("filename not specified") } filename = flag.Args()[0] app := iris.New() app.RegisterView(iris.HTML("./views", ".html")) app.Get("/", serveHome) app.Any("/ws", serveWs) app.Listen(*addr) } ================================================ FILE: _examples/websocket/gorilla-filewatch/testfile.txt ================================================ Some Contents # you can edit this file locally # and http://localhost:8080 # will render the new contents through the live websocket connection. ================================================ FILE: _examples/websocket/gorilla-filewatch/views/home.html ================================================ WebSocket Example
    {{.Data}}
    ================================================ FILE: _examples/websocket/native-messages/main.go ================================================ package main import ( "fmt" "log" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/websocket" ) type clientPage struct { Title string Host string } func main() { app := iris.New() app.RegisterView(iris.HTML("./templates", ".html")) // select the html engine to serve templates // Almost all features of neffos are disabled because no custom message can pass // when app expects to accept and send only raw websocket native messages. // When only allow native messages is a fact? // When the registered namespace is just one and it's empty // and contains only one registered event which is the `OnNativeMessage`. // When `Events{...}` is used instead of `Namespaces{ "namespaceName": Events{...}}` // then the namespace is empty "". ws := websocket.New(websocket.DefaultGorillaUpgrader, websocket.Events{ websocket.OnNativeMessage: func(nsConn *websocket.NSConn, msg websocket.Message) error { log.Printf("Server got: %s from [%s]", msg.Body, nsConn.Conn.ID()) nsConn.Conn.Server().Broadcast(nsConn, msg) return nil }, }) ws.OnConnect = func(c *websocket.Conn) error { log.Printf("[%s] Connected to server!", c.ID()) return nil } ws.OnDisconnect = func(c *websocket.Conn) { log.Printf("[%s] Disconnected from server", c.ID()) } app.HandleDir("/js", iris.Dir("./static/js")) // serve our custom javascript code. // register the server on an endpoint. // see the inline javascript code i the websockets.html, this endpoint is used to connect to the server. app.Get("/my_endpoint", websocket.Handler(ws)) app.Get("/", func(ctx iris.Context) { if err := ctx.View("client.html", clientPage{"Client Page", "localhost:8080"}); err != nil { ctx.HTML(fmt.Sprintf("

    %s

    ", err.Error())) return } }) // Target some browser windows/tabs to http://localhost:8080 and send some messages, // see the static/js/chat.js, // note that the client is using only the browser's native WebSocket API instead of the neffos one. app.Listen(":8080") } ================================================ FILE: _examples/websocket/native-messages/static/js/chat.js ================================================ var messageTxt = document.getElementById("messageTxt"); var messages = document.getElementById("messages"); var sendBtn = document.getElementById("sendBtn") w = new WebSocket("ws://" + HOST + "/my_endpoint"); w.onopen = function () { console.log("Websocket connection enstablished"); }; w.onclose = function () { appendMessage("

    Disconnected

    "); }; w.onmessage = function (message) { appendMessage("
    " + message.data + "
    "); }; sendBtn.onclick = function () { myText = messageTxt.value; messageTxt.value = ""; appendMessage("
    me: " + myText + "
    "); w.send(myText); }; messageTxt.addEventListener("keyup", function (e) { if (e.keyCode === 13) { e.preventDefault(); sendBtn.click(); } }); function appendMessage(messageDivHTML) { messages.insertAdjacentHTML('afterbegin', messageDivHTML); } ================================================ FILE: _examples/websocket/native-messages/templates/client.html ================================================ {{ .Title}}
    ================================================ FILE: _examples/websocket/online-visitors/main.go ================================================ package main import ( "fmt" "sync/atomic" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/websocket" ) var events = websocket.Namespaces{ "default": websocket.Events{ websocket.OnRoomJoined: onRoomJoined, websocket.OnRoomLeft: onRoomLeft, }, } func main() { // init the web application instance // app := iris.New() app := iris.Default() // load templates app.RegisterView(iris.HTML("./templates", ".html").Reload(true)) // setup the websocket server ws := websocket.New(websocket.DefaultGorillaUpgrader, events) app.Get("/my_endpoint", websocket.Handler(ws)) // register static assets request path and system directory app.HandleDir("/js", iris.Dir("./static/assets/js")) h := func(ctx iris.Context) { ctx.ViewData("", page{PageID: "index page"}) if err := ctx.View("index.html"); err != nil { ctx.HTML(fmt.Sprintf("

    %s

    ", err.Error())) return } } h2 := func(ctx iris.Context) { ctx.ViewData("", page{PageID: "other page"}) if err := ctx.View("other.html"); err != nil { ctx.HTML(fmt.Sprintf("

    %s

    ", err.Error())) return } } // Open some browser tabs/or windows // and navigate to // http://localhost:8080/ and http://localhost:8080/other multiple times. // Each page has its own online-visitors counter. app.Get("/", h) app.Get("/other", h2) app.Listen(":8080") } type page struct { PageID string } type pageView struct { source string count uint64 } func (v *pageView) increment() { atomic.AddUint64(&v.count, 1) } func (v *pageView) decrement() { atomic.AddUint64(&v.count, ^uint64(0)) } func (v *pageView) getCount() uint64 { return atomic.LoadUint64(&v.count) } type ( pageViews []pageView ) func (v *pageViews) Add(source string) { args := *v n := len(args) for i := 0; i < n; i++ { kv := &args[i] if kv.source == source { kv.increment() return } } c := cap(args) if c > n { args = args[:n+1] kv := &args[n] kv.source = source kv.count = 1 *v = args return } kv := pageView{} kv.source = source kv.count = 1 *v = append(args, kv) } func (v *pageViews) Get(source string) *pageView { args := *v n := len(args) for i := 0; i < n; i++ { kv := &args[i] if kv.source == source { return kv } } return nil } func (v *pageViews) Reset() { *v = (*v)[:0] } var v pageViews func viewsCountBytes(viewsCount uint64) []byte { // * there are other methods to convert uint64 to []byte return []byte(fmt.Sprintf("%d", viewsCount)) } func onRoomJoined(ns *websocket.NSConn, msg websocket.Message) error { // the roomName here is the source. pageSource := string(msg.Room) v.Add(pageSource) viewsCount := v.Get(pageSource).getCount() if viewsCount == 0 { viewsCount++ // count should be always > 0 here } // fire the "onNewVisit" client event // on each connection joined to this room (source page) // and notify of the new visit, // including this connection (see nil on first input arg). ns.Conn.Server().Broadcast(nil, websocket.Message{ Namespace: msg.Namespace, Room: pageSource, Event: "onNewVisit", // fire the "onNewVisit" client event. Body: viewsCountBytes(viewsCount), }) return nil } func onRoomLeft(ns *websocket.NSConn, msg websocket.Message) error { // the roomName here is the source. pageV := v.Get(msg.Room) if pageV == nil { return nil // for any case that this room is not a pageView source } // decrement -1 the specific counter for this page source. pageV.decrement() // fire the "onNewVisit" client event // on each connection joined to this room (source page) // and notify of the new, decremented by one, visits count. ns.Conn.Server().Broadcast(nil, websocket.Message{ Namespace: msg.Namespace, Room: msg.Room, Event: "onNewVisit", Body: viewsCountBytes(pageV.getCount()), }) return nil } ================================================ FILE: _examples/websocket/online-visitors/static/assets/js/visitors.js ================================================ (function () { var events = { default: { _OnNamespaceConnected: function (ns, msg) { ns.joinRoom(PAGE_SOURCE); }, _OnNamespaceDisconnect: function (ns, msg) { document.getElementById("online_views").innerHTML = "you've been disconnected"; }, onNewVisit: function (ns, msg) { var text = "1 online view"; var onlineViews = Number(msg.Body); if (onlineViews > 1) { text = onlineViews + " online views"; } document.getElementById("online_views").innerHTML = text; } } }; neffos.dial("ws://localhost:8080/my_endpoint", events).then(function (client) { client.connect("default"); }); })(); ================================================ FILE: _examples/websocket/online-visitors/templates/index.html ================================================ Online visitors example
    1 online view
    ================================================ FILE: _examples/websocket/online-visitors/templates/other.html ================================================ Different page, different results 1 online view ================================================ FILE: _examples/websocket/secure/README.md ================================================ # Secure Websockets 1. Run your server through `https://` (`iris.Run(iris.TLS)` or `iris.Run(iris.AutoTLS)` or a custom `iris.Listener(...)`) 2. Nothing changes inside the whole app, including the websocket side 3. The clients must dial the websocket server endpoint (i.e `/echo`) via `wss://` prefix (instead of the non-secure `ws://`), for example `wss://example.com/echo` 4. Ready to GO. ================================================ FILE: _examples/websocket/socketio/asset/index.html ================================================ Socket.IO chat
      ================================================ FILE: _examples/websocket/socketio/go.mod ================================================ module github.com/kataras/iris/_examples/websocket/socketio go 1.25 require ( github.com/googollee/go-socket.io v1.7.0 github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd ) require ( github.com/BurntSushi/toml v1.6.0 // indirect github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect github.com/CloudyKit/jet/v6 v6.3.1 // indirect github.com/Joker/jade v1.1.3 // indirect github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 // indirect github.com/andybalholm/brotli v1.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/fatih/structs v1.1.0 // indirect github.com/flosch/pongo2/v4 v4.0.2 // indirect github.com/gofrs/uuid v4.0.0+incompatible // indirect github.com/golang/snappy v1.0.0 // indirect github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a // indirect github.com/gomodule/redigo v1.8.4 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/iris-contrib/schema v0.0.6 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/kataras/blocks v0.0.8 // indirect github.com/kataras/golog v0.1.12 // indirect github.com/kataras/pio v0.0.13 // indirect github.com/kataras/sitemap v0.0.6 // indirect github.com/kataras/tunnel v0.0.4 // indirect github.com/klauspost/compress v1.18.2 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mailgun/raymond/v2 v2.0.48 // indirect github.com/mailru/easyjson v0.9.1 // indirect github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/schollz/closestmatch v2.1.0+incompatible // indirect github.com/sirupsen/logrus v1.8.1 // indirect github.com/stretchr/testify v1.11.1 // indirect github.com/tdewolff/minify/v2 v2.24.8 // indirect github.com/tdewolff/parse/v2 v2.8.5 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/yosssi/ace v0.0.5 // indirect golang.org/x/crypto v0.47.0 // indirect golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect golang.org/x/net v0.49.0 // indirect golang.org/x/sys v0.40.0 // indirect golang.org/x/text v0.33.0 // indirect golang.org/x/time v0.14.0 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: _examples/websocket/socketio/go.sum ================================================ github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= github.com/CloudyKit/jet/v6 v6.3.1 h1:6IAo5Cx21xrHVaR8zzXN5gJatKV/wO7Nf6bfCnCSbUw= github.com/CloudyKit/jet/v6 v6.3.1/go.mod h1:lf8ksdNsxZt7/yH/3n4vJQWA9RUq4wpaHtArHhGVMOw= github.com/Joker/hpp v1.0.0 h1:65+iuJYdRXv/XyN62C1uEmmOx3432rNG/rKlX6V7Kkc= github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= github.com/Joker/jade v1.1.3 h1:Qbeh12Vq6BxURXT1qZBRHsDxeURB8ztcL6f3EXSGeHk= github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKduM= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 h1:dG7/gLroJGht/jSQtHiLvT48Hxn+crbmvyItZC8cWOs= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05/go.mod h1:NYezi6wtnJtBm5btoprXc5SvAdqH0XTXWnUup0MptAI= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/flosch/pongo2/v4 v4.0.2 h1:gv+5Pe3vaSVmiJvh/BZa82b7/00YUGm0PIyVVLop0Hw= github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+BWPhwZD8= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a h1:l7A0loSszR5zHd/qK53ZIHMO8b3bBSmENnQ6eKnUT0A= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/gomodule/redigo v1.8.4 h1:Z5JUg94HMTR1XpwBaSH4vq3+PNSIykBLxMdglbw10gg= github.com/gomodule/redigo v1.8.4/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googollee/go-socket.io v1.7.0 h1:ODcQSAvVIPvKozXtUGuJDV3pLwdpBLDs1Uoq/QHIlY8= github.com/googollee/go-socket.io v1.7.0/go.mod h1:0vGP8/dXR9SZUMMD4+xxaGo/lohOw3YWMh2WRiWeKxg= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/iris-contrib/httpexpect/v2 v2.15.2 h1:T9THsdP1woyAqKHwjkEsbCnMefsAFvk8iJJKokcJ3Go= github.com/iris-contrib/httpexpect/v2 v2.15.2/go.mod h1:JLDgIqnFy5loDSUv1OA2j0mb6p/rDhiCqigP22Uq9xE= github.com/iris-contrib/schema v0.0.6 h1:CPSBLyx2e91H2yJzPuhGuifVRnZBBJ3pCOMbOvPZaTw= github.com/iris-contrib/schema v0.0.6/go.mod h1:iYszG0IOsuIsfzjymw1kMzTL8YQcCWlm65f3wX8J5iA= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kataras/blocks v0.0.8 h1:MrpVhoFTCR2v1iOOfGng5VJSILKeZZI+7NGfxEh3SUM= github.com/kataras/blocks v0.0.8/go.mod h1:9Jm5zx6BB+06NwA+OhTbHW1xkMOYxahnqTN5DveZ2Yg= github.com/kataras/golog v0.1.12 h1:Bu7I/G4ilJlbfzjmU39O9N+2uO1pBcMK045fzZ4ytNg= github.com/kataras/golog v0.1.12/go.mod h1:wrGSbOiBqbQSQznleVNX4epWM8rl9SJ/rmEacl0yqy4= github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd h1:oMsQgqKACbUdlaIWnN+EZzzAnHkw90hE/y/R0BSij2U= github.com/kataras/iris/v12 v12.2.11-0.20251225090712-30c1c6b10edd/go.mod h1:yucjtDl9Vn3OctWM1fi0MuM9OckemC7kEreFa9QtPF8= github.com/kataras/pio v0.0.13 h1:x0rXVX0fviDTXOOLOmr4MUxOabu1InVSTu5itF8CXCM= github.com/kataras/pio v0.0.13/go.mod h1:k3HNuSw+eJ8Pm2lA4lRhg3DiCjVgHlP8hmXApSej3oM= github.com/kataras/sitemap v0.0.6 h1:w71CRMMKYMJh6LR2wTgnk5hSgjVNB9KL60n5e2KHvLY= github.com/kataras/sitemap v0.0.6/go.mod h1:dW4dOCNs896OR1HmG+dMLdT7JjDk7mYBzoIRwuj5jA4= github.com/kataras/tunnel v0.0.4 h1:sCAqWuJV7nPzGrlb0os3j49lk2JhILT0rID38NHNLpA= github.com/kataras/tunnel v0.0.4/go.mod h1:9FkU4LaeifdMWqZu7o20ojmW4B7hdhv2CMLwfnHGpYw= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw= github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18= github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo= github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk= github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tdewolff/minify/v2 v2.24.8 h1:58/VjsbevI4d5FGV0ZSuBrHMSSkH4MCH0sIz/eKIauE= github.com/tdewolff/minify/v2 v2.24.8/go.mod h1:0Ukj0CRpo/sW/nd8uZ4ccXaV1rEVIWA3dj8U7+Shhfw= github.com/tdewolff/parse/v2 v2.8.5 h1:ZmBiA/8Do5Rpk7bDye0jbbDUpXXbCdc3iah4VeUvwYU= github.com/tdewolff/parse/v2 v2.8.5/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo= github.com/tdewolff/test v1.0.11 h1:FdLbwQVHxqG16SlkGveC0JVyrJN62COWTRyUFzfbtBE= github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA= github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0= github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs= moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHcE= ================================================ FILE: _examples/websocket/socketio/main.go ================================================ // Package main runs a go-socket.io based websocket server. // An Iris compatible clone of: https://github.com/googollee/go-socket.io#example, // use of `iris.FromStd` to convert its handler. package main import ( "fmt" socketio "github.com/googollee/go-socket.io" "github.com/kataras/iris/v12" ) func main() { app := iris.New() server := socketio.NewServer(nil) server.OnConnect("/", func(s socketio.Conn) error { s.SetContext("") fmt.Println("connected:", s.ID()) return nil }) server.OnEvent("/", "notice", func(s socketio.Conn, msg string) { fmt.Println("notice:", msg) s.Emit("reply", "have "+msg) }) server.OnEvent("/chat", "msg", func(s socketio.Conn, msg string) string { s.SetContext(msg) return "recv " + msg }) server.OnEvent("/", "bye", func(s socketio.Conn) string { last := s.Context().(string) s.Emit("bye", last) s.Close() return last }) server.OnError("/", func(s socketio.Conn, e error) { fmt.Println("meet error:", e) }) server.OnDisconnect("/", func(s socketio.Conn, reason string) { fmt.Println("closed", reason) }) go server.Serve() defer server.Close() app.HandleMany("GET POST", "/socket.io/{any:path}", iris.FromStd(server)) app.HandleDir("/", iris.Dir("./asset")) app.Listen(":8000", iris.WithoutPathCorrection) } /* If you want to enable CORS in your websocket handler, please follow this post: https://github.com/googollee/go-socket.io/issues/242 */ ================================================ FILE: _proposals/route_builder.md ================================================ ```go package main import ( "fmt" "strings" "github.com/kataras/iris/v12/macro" ) func main() { path := NewRouteBuilder(). Path("/user"). String("name", "prefix(ma)", "suffix(kis)"). Int("age"). Path("/friends"). Wildcard("rest"). Build() fmt.Println(path) } type RouteBuilder struct { path string } func NewRouteBuilder() *RouteBuilder { return &RouteBuilder{ path: "/", } } func (r *RouteBuilder) Path(path string) *RouteBuilder { if path[0] != '/' { path = "/" + path } r.path = strings.TrimSuffix(r.path, "/") + path return r } type StaticPathBuilder interface { Path(path string) *RouteBuilder } func (r *RouteBuilder) Param(param ParamBuilder) *RouteBuilder { // StaticPathBuilder { path := "" // keep it here, a single call to r.Path must be done. if len(r.path) == 0 || r.path[len(r.path)-1] != '/' { path += "/" // if for some reason no prior Path("/") was called for delimeter between path parameter. } path += fmt.Sprintf("{%s:%s", param.GetName(), param.GetParamType().Indent()) if funcs := param.GetFuncs(); len(funcs) > 0 { path += fmt.Sprintf(" %s", strings.Join(funcs, " ")) } path += "}" return r.Path(path) } func (r *RouteBuilder) String(name string, funcs ...string) *RouteBuilder { return r.Param(Param(macro.String, name, funcs...)) } func (r *RouteBuilder) Int(name string, funcs ...string) *RouteBuilder { return r.Param(Param(macro.Int, name, funcs...)) } func (r *RouteBuilder) Int8(name string, funcs ...string) *RouteBuilder { return r.Param(Param(macro.Int8, name, funcs...)) } func (r *RouteBuilder) Int16(name string, funcs ...string) *RouteBuilder { return r.Param(Param(macro.Int16, name, funcs...)) } func (r *RouteBuilder) Int32(name string, funcs ...string) *RouteBuilder { return r.Param(Param(macro.Int32, name, funcs...)) } func (r *RouteBuilder) Int64(name string, funcs ...string) *RouteBuilder { return r.Param(Param(macro.Int64, name, funcs...)) } func (r *RouteBuilder) Uint(name string, funcs ...string) *RouteBuilder { return r.Param(Param(macro.Uint, name, funcs...)) } func (r *RouteBuilder) Uint8(name string, funcs ...string) *RouteBuilder { return r.Param(Param(macro.Uint8, name, funcs...)) } func (r *RouteBuilder) Uint16(name string, funcs ...string) *RouteBuilder { return r.Param(Param(macro.Uint16, name, funcs...)) } func (r *RouteBuilder) Uint32(name string, funcs ...string) *RouteBuilder { return r.Param(Param(macro.Uint32, name, funcs...)) } func (r *RouteBuilder) Uint64(name string, funcs ...string) *RouteBuilder { return r.Param(Param(macro.Uint64, name, funcs...)) } func (r *RouteBuilder) Bool(name string, funcs ...string) *RouteBuilder { return r.Param(Param(macro.Bool, name, funcs...)) } func (r *RouteBuilder) Alphabetical(name string, funcs ...string) *RouteBuilder { return r.Param(Param(macro.Alphabetical, name, funcs...)) } func (r *RouteBuilder) File(name string, funcs ...string) *RouteBuilder { return r.Param(Param(macro.File, name, funcs...)) } func (r *RouteBuilder) Wildcard(name string, funcs ...string) *RouteBuilder { return r.Param(Param(macro.Path, name, funcs...)) } func (r *RouteBuilder) UUID(name string, funcs ...string) *RouteBuilder { return r.Param(Param(macro.UUID, name, funcs...)) } func (r *RouteBuilder) Mail(name string, funcs ...string) *RouteBuilder { return r.Param(Param(macro.Mail, name, funcs...)) } func (r *RouteBuilder) Email(name string, funcs ...string) *RouteBuilder { return r.Param(Param(macro.Email, name, funcs...)) } func (r *RouteBuilder) Date(name string, funcs ...string) *RouteBuilder { return r.Param(Param(macro.Date, name, funcs...)) } func (r *RouteBuilder) Weekday(name string, funcs ...string) *RouteBuilder { return r.Param(Param(macro.Weekday, name, funcs...)) } func (r *RouteBuilder) Build() string { return r.path } type ParamBuilder interface { GetName() string GetFuncs() []string GetParamType() *macro.Macro } type pathParam struct { Name string Funcs []string ParamType *macro.Macro } var _ ParamBuilder = (*pathParam)(nil) func Param(paramType *macro.Macro, name string, funcs ...string) ParamBuilder { return &pathParam{ Name: name, ParamType: paramType, Funcs: funcs, } } func (p *pathParam) GetName() string { return p.Name } func (p *pathParam) GetParamType() *macro.Macro { return p.ParamType } func (p *pathParam) GetFuncs() []string { return p.Funcs } ``` ================================================ FILE: _proposals/xerrors_party.md ================================================ ```go app.PartyConfigure("/api", errors.NewParty[CreateRequest, CreateResponse, ListFilter](). Create(service.Create). Update(service.Update). Delete(service.DeleteWithFeedback). List(service.ListPaginated). Get(service.GetByID).Validation(validateCreateRequest)) ``` ```go type Party[T, R, F any] struct { validators []ContextRequestFunc[T] filterValidators []ContextRequestFunc[F] filterIntercepters []ContextResponseFunc[F, R] intercepters []ContextResponseFunc[T, R] serviceCreateFunc func(stdContext.Context, T) (R, error) serviceUpdateFunc func(stdContext.Context, T) (bool, error) serviceDeleteFunc func(stdContext.Context, string) (bool, error) serviceListFunc func(stdContext.Context, pagination.ListOptions, F /* filter options */) ([]R, int, error) serviceGetFunc func(stdContext.Context, string) (R, error) } func (p *Party[T, R, F]) Configure(r router.Party) { if p.serviceCreateFunc != nil { r.Post("/", Validation(p.validators...), Intercept(p.intercepters...), CreateHandler(p.serviceCreateFunc)) } if p.serviceUpdateFunc != nil { r.Put("/{id:string}", Validation(p.validators...), Intercept(p.intercepters...), NoContentOrNotModifiedHandler(p.serviceUpdateFunc)) } if p.serviceListFunc != nil { r.Post("/list", Validation(p.filterValidators...), Intercept(p.filterIntercepters...), ListHandler(p.serviceListFunc)) } if p.serviceDeleteFunc != nil { r.Delete("/{id:string}", NoContentOrNotModifiedHandler(p.serviceDeleteFunc, PathParam[string]("id"))) } if p.serviceGetFunc != nil { r.Get("/{id:string}", Handler(p.serviceGetFunc, PathParam[string]("id"))) } } func NewParty[T, R, F any]() *Party[T, R, F] { return &Party[T, R, F]{} } func (p *Party[T, R, F]) Validation(validators ...ContextRequestFunc[T]) *Party[T, R, F] { p.validators = append(p.validators, validators...) return p } func (p *Party[T, R, F]) FilterValidation(filterValidators ...ContextRequestFunc[F]) *Party[T, R, F] { p.filterValidators = append(p.filterValidators, filterValidators...) return p } func (p *Party[T, R, F]) Intercept(intercepters ...ContextResponseFunc[T, R]) *Party[T, R, F] { p.intercepters = append(p.intercepters, intercepters...) return p } func (p *Party[T, R, F]) FilterIntercept(filterIntercepters ...ContextResponseFunc[F, R]) *Party[T, R, F] { p.filterIntercepters = append(p.filterIntercepters, filterIntercepters...) return p } func (p *Party[T, R, F]) Create(fn func(stdContext.Context, T) (R, error)) *Party[T, R, F] { p.serviceCreateFunc = fn return p } func (p *Party[T, R, F]) Update(fn func(stdContext.Context, T) (bool, error)) *Party[T, R, F] { p.serviceUpdateFunc = fn return p } func (p *Party[T, R, F]) Delete(fn func(stdContext.Context, string) (bool, error)) *Party[T, R, F] { p.serviceDeleteFunc = fn return p } func (p *Party[T, R, F]) List(fn func(stdContext.Context, pagination.ListOptions, F /* filter options */) ([]R, int, error)) *Party[T, R, F] { p.serviceListFunc = fn return p } func (p *Party[T, R, F]) Get(fn func(stdContext.Context, string) (R, error)) *Party[T, R, F] { p.serviceGetFunc = fn return p } ``` ================================================ FILE: aliases.go ================================================ package iris import ( "io/fs" "net/http" "net/url" "path" "regexp" "strings" "time" "github.com/kataras/iris/v12/cache" "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/core/handlerconv" "github.com/kataras/iris/v12/core/host" "github.com/kataras/iris/v12/core/router" "github.com/kataras/iris/v12/hero" "github.com/kataras/iris/v12/view" ) var ( // BuildRevision holds the vcs commit id information of the program's build. // To display the Iris' version please use the iris.Version constant instead. // Available at go version 1.18+ BuildRevision = context.BuildRevision // BuildTime holds the vcs commit time information of the program's build. // Available at go version 1.18+ BuildTime = context.BuildTime ) // SameSite attributes. const ( SameSiteDefaultMode = http.SameSiteDefaultMode SameSiteLaxMode = http.SameSiteLaxMode SameSiteStrictMode = http.SameSiteStrictMode SameSiteNoneMode = http.SameSiteNoneMode ) type ( // Context is the middle-man server's "object" for the clients. // // A New context is being acquired from a sync.Pool on each connection. // The Context is the most important thing on the iris's http flow. // // Developers send responses to the client's request through a Context. // Developers get request information from the client's request by a Context. Context = *context.Context // ViewEngine is an alias of `context.ViewEngine`. // See HTML, Blocks, Django, Jet, Pug, Ace, Handlebars and e.t.c. ViewEngine = context.ViewEngine // UnmarshalerFunc a shortcut, an alias for the `context#UnmarshalerFunc` type // which implements the `context#Unmarshaler` interface for reading request's body // via custom decoders, most of them already implement the `context#UnmarshalerFunc` // like the json.Unmarshal, xml.Unmarshal, yaml.Unmarshal and every library which // follows the best practises and is aligned with the Go standards. // // See 'context#UnmarshalBody` for more. // // Example: https://github.com/kataras/iris/blob/main/_examples/request-body/read-custom-via-unmarshaler/main.go UnmarshalerFunc = context.UnmarshalerFunc // DecodeFunc is a generic type of decoder function. // When the returned error is not nil the decode operation // is terminated and the error is received by the ReadJSONStream method, // otherwise it continues to read the next available object. // Look the `Context.ReadJSONStream` method. // // Example: https://github.com/kataras/iris/blob/main/_examples/request-body/read-json-stream. DecodeFunc = context.DecodeFunc // A Handler responds to an HTTP request. // It writes reply headers and data to the Context.ResponseWriter() and then return. // Returning signals that the request is finished; // it is not valid to use the Context after or concurrently with the completion of the Handler call. // // Depending on the HTTP client software, HTTP protocol version, // and any intermediaries between the client and the iris server, // it may not be possible to read from the Context.Request().Body after writing to the context.ResponseWriter(). // Cautious handlers should read the Context.Request().Body first, and then reply. // // Except for reading the body, handlers should not modify the provided Context. // // If Handler panics, the server (the caller of Handler) assumes that the effect of the panic was isolated to the active request. // It recovers the panic, logs a stack trace to the server error log, and hangs up the connection. Handler = context.Handler // Filter is just a type of func(Context) bool which reports whether an action must be performed // based on the incoming request. // // See `NewConditionalHandler` for more. // An alias for the `context/Filter`. Filter = context.Filter // A Map is an alias of map[string]any. Map = context.Map // User is a generic view of an authorized client. // See `Context.User` and `SetUser` methods for more. // An alias for the `context/User` type. User = context.User // SimpleUser is a simple implementation of the User interface. SimpleUser = context.SimpleUser // Problem Details for HTTP APIs. // Pass a Problem value to `context.Problem` to // write an "application/problem+json" response. // // Read more at: https://github.com/kataras/iris/blob/main/_examples/routing/http-errors. // // It is an alias of the `context#Problem` type. Problem = context.Problem // ProblemOptions the optional settings when server replies with a Problem. // See `Context.Problem` method and `Problem` type for more details. // // It is an alias of the `context#ProblemOptions` type. ProblemOptions = context.ProblemOptions // JSON the optional settings for JSON renderer. // // It is an alias of the `context#JSON` type. JSON = context.JSON // JSONReader holds the JSON decode options of the `Context.ReadJSON, ReadBody` methods. // // It is an alias of the `context#JSONReader` type. JSONReader = context.JSONReader // JSONP the optional settings for JSONP renderer. // // It is an alias of the `context#JSONP` type. JSONP = context.JSONP // ProtoMarshalOptions is a type alias for protojson.MarshalOptions. ProtoMarshalOptions = context.ProtoMarshalOptions // ProtoUnmarshalOptions is a type alias for protojson.UnmarshalOptions. ProtoUnmarshalOptions = context.ProtoUnmarshalOptions // XML the optional settings for XML renderer. // // It is an alias of the `context#XML` type. XML = context.XML // Markdown the optional settings for Markdown renderer. // See `Context.Markdown` for more. // // It is an alias of the `context#Markdown` type. Markdown = context.Markdown // Supervisor is a shortcut of the `host#Supervisor`. // Used to add supervisor configurators on common Runners // without the need of importing the `core/host` package. Supervisor = host.Supervisor // Party is just a group joiner of routes which have the same prefix and share same middleware(s) also. // Party could also be named as 'Join' or 'Node' or 'Group' , Party chosen because it is fun. // // Look the `core/router#APIBuilder` for its implementation. // // A shortcut for the `core/router#Party`, useful when `PartyFunc` is being used. Party = router.Party // APIContainer is a wrapper of a common `Party` featured by Dependency Injection. // See `Party.ConfigureContainer` for more. // // A shortcut for the `core/router#APIContainer`. APIContainer = router.APIContainer // ResultHandler describes the function type which should serve the "v" struct value. // See `APIContainer.UseResultHandler`. ResultHandler = hero.ResultHandler // DirOptions contains the optional settings that // `FileServer` and `Party#HandleDir` can use to serve files and assets. // A shortcut for the `router.DirOptions`, useful when `FileServer` or `HandleDir` is being used. DirOptions = router.DirOptions // DirCacheOptions holds the options for the cached file system. // See `DirOptions`. DirCacheOptions = router.DirCacheOptions // DirListRichOptions the options for the `DirListRich` helper function. // A shortcut for the `router.DirListRichOptions`. // Useful when `DirListRich` function is passed to `DirOptions.DirList` field. DirListRichOptions = router.DirListRichOptions // Attachments options for files to be downloaded and saved locally by the client. // See `DirOptions`. Attachments = router.Attachments // Dir implements FileSystem using the native file system restricted to a // specific directory tree, can be passed to the `FileServer` function // and `HandleDir` method. It's an alias of `http.Dir`. Dir = http.Dir // ExecutionRules gives control to the execution of the route handlers outside of the handlers themselves. // Usage: // Party#SetExecutionRules(ExecutionRules { // Done: ExecutionOptions{Force: true}, // }) // // See `core/router/Party#SetExecutionRules` for more. // Example: https://github.com/kataras/iris/tree/main/_examples/mvc/middleware/without-ctx-next ExecutionRules = router.ExecutionRules // ExecutionOptions is a set of default behaviors that can be changed in order to customize the execution flow of the routes' handlers with ease. // // See `ExecutionRules` and `core/router/Party#SetExecutionRules` for more. ExecutionOptions = router.ExecutionOptions // CookieOption is the type of function that is accepted on // context's methods like `SetCookieKV`, `RemoveCookie` and `SetCookie` // as their (last) variadic input argument to amend the end cookie's form. // // Any custom or builtin `CookieOption` is valid, // see `CookiePath`, `CookieCleanPath`, `CookieExpires` and `CookieHTTPOnly` for more. // // An alias for the `context.CookieOption`. CookieOption = context.CookieOption // Cookie is a type alias for the standard net/http Cookie struct type. // See `Context.SetCookie`. Cookie = http.Cookie // N is a struct which can be passed on the `Context.Negotiate` method. // It contains fields which should be filled based on the `Context.Negotiation()` // server side values. If no matched mime then its "Other" field will be sent, // which should be a string or []byte. // It completes the `context/context.ContentSelector` interface. // // An alias for the `context.N`. N = context.N // Locale describes the i18n locale. // An alias for the `context.Locale`. Locale = context.Locale // ErrPrivate if provided then the error saved in context // should NOT be visible to the client no matter what. // An alias for the `context.ErrPrivate`. ErrPrivate = context.ErrPrivate ) // Constants for input argument at `router.RouteRegisterRule`. // See `Party#SetRegisterRule`. const ( // RouteOverride replaces an existing route with the new one, the default rule. RouteOverride = router.RouteOverride // RouteSkip keeps the original route and skips the new one. RouteSkip = router.RouteSkip // RouteError log when a route already exists, shown after the `Build` state, // server never starts. RouteError = router.RouteError // RouteOverlap will overlap the new route to the previous one. // If the route stopped and its response can be reset then the new route will be execute. RouteOverlap = router.RouteOverlap ) // Contains the enum values of the `Context.GetReferrer()` method, // shortcuts of the context subpackage. const ( ReferrerInvalid = context.ReferrerInvalid ReferrerIndirect = context.ReferrerIndirect ReferrerDirect = context.ReferrerDirect ReferrerEmail = context.ReferrerEmail ReferrerSearch = context.ReferrerSearch ReferrerSocial = context.ReferrerSocial ReferrerNotGoogleSearch = context.ReferrerNotGoogleSearch ReferrerGoogleOrganicSearch = context.ReferrerGoogleOrganicSearch ReferrerGoogleAdwords = context.ReferrerGoogleAdwords ) // NoLayout to disable layout for a particular template file // A shortcut for the `view#NoLayout`. const NoLayout = view.NoLayout var ( // HTML view engine. // Shortcut of the view.HTML. HTML = view.HTML // Blocks view engine. // Can be used as a faster alternative of the HTML engine. // Shortcut of the view.Blocks. Blocks = view.Blocks // Django view engine. // Shortcut of the view.Django. Django = view.Django // Handlebars view engine. // Shortcut of the view.Handlebars. Handlebars = view.Handlebars // Pug view engine. // Shortcut of the view.Pug. Pug = view.Pug // Jet view engine. // Shortcut of the view.Jet. Jet = view.Jet // Ace view engine. // Shortcut of the view.Ace. Ace = view.Ace ) type ( // ErrViewNotExist reports whether a template was not found in the parsed templates tree. ErrViewNotExist = context.ErrViewNotExist // FallbackViewFunc is a function that can be registered // to handle view fallbacks. It accepts the Context and // a special error which contains information about the previous template error. // It implements the FallbackViewProvider interface. // // See `Context.View` method. FallbackViewFunc = context.FallbackViewFunc // FallbackView is a helper to register a single template filename as a fallback // when the provided tempate filename was not found. FallbackView = context.FallbackView // FallbackViewLayout is a helper to register a single template filename as a fallback // layout when the provided layout filename was not found. FallbackViewLayout = context.FallbackViewLayout ) // Component returns a new Handler which can be registered as a main handler for a route. // It's a shortcut handler that renders the given component as HTML through Context.RenderComponent. func Component(component context.Component) Handler { return func(ctx Context) { ctx.RenderComponent(component) } } // PrefixDir returns a new FileSystem that opens files // by adding the given "prefix" to the directory tree of "fs". // // Useful when having templates and static files in the same // bindata AssetFile method. This way you can select // which one to serve as static files and what for templates. // All view engines have a `RootDir` method for that reason too // but alternatively, you can wrap the given file system with this `PrefixDir`. // // Example: https://github.com/kataras/iris/blob/main/_examples/file-server/single-page-application/embedded-single-page-application/main.go func PrefixDir(prefix string, fs http.FileSystem) http.FileSystem { return &prefixedDir{prefix, fs} } // PrefixFS same as "PrefixDir" but for `fs.FS` type. func PrefixFS(fileSystem fs.FS, dir string) (fs.FS, error) { return fs.Sub(fileSystem, dir) } type prefixedDir struct { prefix string fs http.FileSystem } func (p *prefixedDir) Open(name string) (http.File, error) { // Don't do this: as this is responsibility of the underline fs. // _, filename, ok, err := context.SafeFilename("", name) destPath := path.Join(p.prefix, name) return p.fs.Open(destPath) } type partyConfiguratorMiddleware struct { handlers []Handler } func (p *partyConfiguratorMiddleware) Configure(r Party) { r.Use(p.handlers...) } // ConfigureMiddleware is a PartyConfigurator which can be used // as a shortcut to add middlewares on Party.PartyConfigure("/path", WithMiddleware(handler), new(example.API)). func ConfigureMiddleware(handlers ...Handler) router.PartyConfigurator { return &partyConfiguratorMiddleware{handlers: handlers} } // Compression is a middleware which enables // writing and reading using the best offered compression. // Usage: // app.Use (for matched routes) // app.UseRouter (for both matched and 404s or other HTTP errors). func Compression(ctx Context) { ctx.CompressWriter(true) ctx.CompressReader(true) ctx.Next() } var ( // AllowQuerySemicolons returns a middleware that serves requests by converting any // unescaped semicolons(;) in the URL query to ampersands(&). // // This restores the pre-Go 1.17 behavior of splitting query parameters on both // semicolons and ampersands. // (See golang.org/issue/25192 and https://github.com/kataras/iris/issues/1875). // Note that this behavior doesn't match that of many proxies, // and the mismatch can lead to security issues. // // AllowQuerySemicolons should be invoked before any Context read query or // form methods are called. // // To skip HTTP Server logging for this type of warning: // app.Listen/Run(..., iris.WithoutServerError(iris.ErrURLQuerySemicolon)). AllowQuerySemicolons = func(ctx Context) { // clopy of net/http.AllowQuerySemicolons. r := ctx.Request() if s := r.URL.RawQuery; strings.Contains(s, ";") { r2 := new(http.Request) *r2 = *r r2.URL = new(url.URL) *r2.URL = *r.URL r2.URL.RawQuery = strings.ReplaceAll(s, ";", "&") ctx.ResetRequest(r2) } ctx.Next() } // MatchImagesAssets is a simple regex expression // that can be passed to the DirOptions.Cache.CompressIgnore field // in order to skip compression on already-compressed file types // such as images and pdf. MatchImagesAssets = regexp.MustCompile("((.*).pdf|(.*).jpg|(.*).jpeg|(.*).gif|(.*).tif|(.*).tiff)$") // MatchCommonAssets is a simple regex expression which // can be used on `DirOptions.PushTargetsRegexp`. // It will match and Push // all available js, css, font and media files. // Ideal for Single Page Applications. MatchCommonAssets = regexp.MustCompile("((.*).js|(.*).css|(.*).ico|(.*).png|(.*).ttf|(.*).svg|(.*).webp|(.*).gif)$") ) var ( // RegisterOnInterrupt registers a global function to call when CTRL+C/CMD+C pressed or a unix kill command received. // // A shortcut for the `host#RegisterOnInterrupt`. RegisterOnInterrupt = host.RegisterOnInterrupt // LimitRequestBodySize is a middleware which sets a request body size limit // for all next handlers in the chain. // // A shortcut for the `context#LimitRequestBodySize`. LimitRequestBodySize = context.LimitRequestBodySize // NewConditionalHandler returns a single Handler which can be registered // as a middleware. // Filter is just a type of Handler which returns a boolean. // Handlers here should act like middleware, they should contain `ctx.Next` to proceed // to the next handler of the chain. Those "handlers" are registered to the per-request context. // // // It checks the "filter" and if passed then // it, correctly, executes the "handlers". // // If passed, this function makes sure that the Context's information // about its per-request handler chain based on the new "handlers" is always updated. // // If not passed, then simply the Next handler(if any) is executed and "handlers" are ignored. // Example can be found at: _examples/routing/conditional-chain. // // A shortcut for the `context#NewConditionalHandler`. NewConditionalHandler = context.NewConditionalHandler // FileServer returns a Handler which serves files from a specific system, phyisical, directory // or an embedded one. // The first parameter is the directory, relative to the executable program. // The second optional parameter is any optional settings that the caller can use. // // See `Party#HandleDir` too. // Examples can be found at: https://github.com/kataras/iris/tree/main/_examples/file-server // A shortcut for the `router.FileServer`. FileServer = router.FileServer // DirList is the default `DirOptions.DirList` field. // Read more at: `core/router.DirList`. DirList = router.DirList // DirListRich can be passed to `DirOptions.DirList` field // to override the default file listing appearance. // Read more at: `core/router.DirListRich`. DirListRich = router.DirListRich // StripPrefix returns a handler that serves HTTP requests // by removing the given prefix from the request URL's Path // and invoking the handler h. StripPrefix handles a // request for a path that doesn't begin with prefix by // replying with an HTTP 404 not found error. // // Usage: // fileserver := iris.FileServer("./static_files", DirOptions {...}) // h := iris.StripPrefix("/static", fileserver) // app.Get("/static/{file:path}", h) // app.Head("/static/{file:path}", h) StripPrefix = router.StripPrefix // FromStd converts native http.Handler, http.HandlerFunc & func(w, r, next) to context.Handler. // // Supported form types: // .FromStd(h http.Handler) // .FromStd(func(w http.ResponseWriter, r *http.Request)) // .FromStd(func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc)) // // A shortcut for the `handlerconv#FromStd`. FromStd = handlerconv.FromStd // Cache is a middleware providing server-side cache functionalities // to the next handlers, can be used as: `app.Get("/", iris.Cache, aboutHandler)`. // It should be used after Static methods. // See `iris#Cache304` for an alternative, faster way. // // Examples can be found at: https://github.com/kataras/iris/tree/main/_examples/#caching Cache = cache.Handler // NoCache is a middleware which overrides the Cache-Control, Pragma and Expires headers // in order to disable the cache during the browser's back and forward feature. // // A good use of this middleware is on HTML routes; to refresh the page even on "back" and "forward" browser's arrow buttons. // // See `iris#StaticCache` for the opposite behavior. // // A shortcut of the `cache#NoCache` NoCache = cache.NoCache // StaticCache middleware for caching static files by sending the "Cache-Control" and "Expires" headers to the client. // It accepts a single input parameter, the "cacheDur", a time.Duration that it's used to calculate the expiration. // // If "cacheDur" <=0 then it returns the `NoCache` middleware instaed to disable the caching between browser's "back" and "forward" actions. // // Usage: `app.Use(iris.StaticCache(24 * time.Hour))` or `app.Use(iris.StaticCache(-1))`. // A middleware, which is a simple Handler can be called inside another handler as well, example: // cacheMiddleware := iris.StaticCache(...) // func(ctx iris.Context){ // cacheMiddleware(ctx) // [...] // } // // A shortcut of the `cache#StaticCache` StaticCache = cache.StaticCache // Cache304 sends a `StatusNotModified` (304) whenever // the "If-Modified-Since" request header (time) is before the // time.Now() + expiresEvery (always compared to their UTC values). // Use this, which is a shortcut of the, `chache#Cache304` instead of the "github.com/kataras/iris/v12/cache" or iris.Cache // for better performance. // Clients that are compatible with the http RCF (all browsers are and tools like postman) // will handle the caching. // The only disadvantage of using that instead of server-side caching // is that this method will send a 304 status code instead of 200, // So, if you use it side by side with other micro services // you have to check for that status code as well for a valid response. // // Developers are free to extend this method's behavior // by watching system directories changes manually and use of the `ctx.WriteWithExpiration` // with a "modtime" based on the file modified date, // similar to the `HandleDir`(which sends status OK(200) and browser disk caching instead of 304). // // A shortcut of the `cache#Cache304`. Cache304 = cache.Cache304 // CookieOverride is a CookieOption which overrides the cookie explicitly to the given "cookie". // // A shortcut for the `context#CookieOverride`. CookieOverride = context.CookieOverride // CookieDomain is a CookieOption which sets the cookie's Domain field. // If empty then the current domain is used. // // A shortcut for the `context#CookieDomain`. CookieDomain = context.CookieDomain // CookieAllowReclaim accepts the Context itself. // If set it will add the cookie to (on `CookieSet`, `CookieSetKV`, `CookieUpsert`) // or remove the cookie from (on `CookieRemove`) the Request object too. // // A shortcut for the `context#CookieAllowReclaim`. CookieAllowReclaim = context.CookieAllowReclaim // CookieAllowSubdomains set to the Cookie Options // in order to allow subdomains to have access to the cookies. // It sets the cookie's Domain field (if was empty) and // it also sets the cookie's SameSite to lax mode too. // // A shortcut for the `context#CookieAllowSubdomains`. CookieAllowSubdomains = context.CookieAllowSubdomains // CookieSameSite sets a same-site rule for cookies to set. // SameSite allows a server to define a cookie attribute making it impossible for // the browser to send this cookie along with cross-site requests. The main // goal is to mitigate the risk of cross-origin information leakage, and provide // some protection against cross-site request forgery attacks. // // See https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00 for details. // // A shortcut for the `context#CookieSameSite`. CookieSameSite = context.CookieSameSite // CookieSecure sets the cookie's Secure option if the current request's // connection is using TLS. See `CookieHTTPOnly` too. // // A shortcut for the `context#CookieSecure`. CookieSecure = context.CookieSecure // CookieHTTPOnly is a `CookieOption`. // Use it to set the cookie's HttpOnly field to false or true. // HttpOnly field defaults to true for `RemoveCookie` and `SetCookieKV`. // // A shortcut for the `context#CookieHTTPOnly`. CookieHTTPOnly = context.CookieHTTPOnly // CookiePath is a `CookieOption`. // Use it to change the cookie's Path field. // // A shortcut for the `context#CookiePath`. CookiePath = context.CookiePath // CookieCleanPath is a `CookieOption`. // Use it to clear the cookie's Path field, exactly the same as `CookiePath("")`. // // A shortcut for the `context#CookieCleanPath`. CookieCleanPath = context.CookieCleanPath // CookieExpires is a `CookieOption`. // Use it to change the cookie's Expires and MaxAge fields by passing the lifetime of the cookie. // // A shortcut for the `context#CookieExpires`. CookieExpires = context.CookieExpires // CookieEncoding accepts a value which implements `Encode` and `Decode` methods. // It calls its `Encode` on `Context.SetCookie, UpsertCookie, and SetCookieKV` methods. // And on `Context.GetCookie` method it calls its `Decode`. // // A shortcut for the `context#CookieEncoding`. CookieEncoding = context.CookieEncoding // IsErrEmptyJSON reports whether the given "err" is caused by a // Context.ReadJSON call when the request body // didn't start with { or it was totally empty. IsErrEmptyJSON = context.IsErrEmptyJSON // IsErrPath can be used at `context#ReadForm` and `context#ReadQuery`. // It reports whether the incoming error is type of `schema.ErrPath`, // which can be ignored when server allows unknown post values to be sent by the client. // // A shortcut for the `context#IsErrPath`. IsErrPath = context.IsErrPath // IsErrCanceled reports whether the "err" is caused by a cancellation or timeout. // // A shortcut for the `context#IsErrCanceled`. IsErrCanceled = context.IsErrCanceled // ErrEmptyForm is the type error which API users can make use of // to check if a form was empty on `Context.ReadForm`. // // A shortcut for the `context#ErrEmptyForm`. ErrEmptyForm = context.ErrEmptyForm // ErrEmptyFormField reports whether if form value is empty. // An alias of `context.ErrEmptyFormField`. ErrEmptyFormField = context.ErrEmptyFormField // ErrNotFound reports whether a key was not found, useful // on post data, versioning feature and others. // An alias of `context.ErrNotFound`. ErrNotFound = context.ErrNotFound // NewProblem returns a new Problem. // Head over to the `Problem` type godoc for more. // // A shortcut for the `context#NewProblem`. NewProblem = context.NewProblem // XMLMap wraps a map[string]any to compatible xml marshaler, // in order to be able to render maps as XML on the `Context.XML` method. // // Example: `Context.XML(XMLMap("Root", map[string]any{...})`. // // A shortcut for the `context#XMLMap`. XMLMap = context.XMLMap // ErrStopExecution if returned from a hero middleware or a request-scope dependency // stops the handler's execution, see _examples/dependency-injection/basic/middleware. ErrStopExecution = hero.ErrStopExecution // ErrHijackNotSupported is returned by the Hijack method to // indicate that Hijack feature is not available. // // A shortcut for the `context#ErrHijackNotSupported`. ErrHijackNotSupported = context.ErrHijackNotSupported // ErrPushNotSupported is returned by the Push method to // indicate that HTTP/2 Push support is not available. // // A shortcut for the `context#ErrPushNotSupported`. ErrPushNotSupported = context.ErrPushNotSupported // PrivateError accepts an error and returns a wrapped private one. // A shortcut for the `context#PrivateError` function. PrivateError = context.PrivateError // TrimParamFilePart is a middleware which trims any last part after a dot (.) character // of the current route's dynamic path parameters. // A shortcut for the `context#TrimParamFilePart` function. TrimParamFilePart Handler = context.TrimParamFilePart ) // HTTP Methods copied from `net/http`. const ( MethodGet = http.MethodGet MethodPost = http.MethodPost MethodPut = http.MethodPut MethodDelete = http.MethodDelete MethodConnect = http.MethodConnect MethodHead = http.MethodHead MethodPatch = http.MethodPatch MethodOptions = http.MethodOptions MethodTrace = http.MethodTrace // MethodNone is an iris-specific "virtual" method // to store the "offline" routes. MethodNone = router.MethodNone ) // HTTP status codes as registered with IANA. // See: http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml. // Raw Copy from the future(tip) net/http std package in order to recude the import path of "net/http" for the users. const ( StatusContinue = http.StatusContinue // RFC 7231, 6.2.1 StatusSwitchingProtocols = http.StatusSwitchingProtocols // RFC 7231, 6.2.2 StatusProcessing = http.StatusProcessing // RFC 2518, 10.1 StatusEarlyHints = http.StatusEarlyHints // RFC 8297 StatusOK = http.StatusOK // RFC 7231, 6.3.1 StatusCreated = http.StatusCreated // RFC 7231, 6.3.2 StatusAccepted = http.StatusAccepted // RFC 7231, 6.3.3 StatusNonAuthoritativeInfo = http.StatusNonAuthoritativeInfo // RFC 7231, 6.3.4 StatusNoContent = http.StatusNoContent // RFC 7231, 6.3.5 StatusResetContent = http.StatusResetContent // RFC 7231, 6.3.6 StatusPartialContent = http.StatusPartialContent // RFC 7233, 4.1 StatusMultiStatus = http.StatusMultiStatus // RFC 4918, 11.1 StatusAlreadyReported = http.StatusAlreadyReported // RFC 5842, 7.1 StatusIMUsed = http.StatusIMUsed // RFC 3229, 10.4.1 StatusMultipleChoices = http.StatusMultipleChoices // RFC 7231, 6.4.1 StatusMovedPermanently = http.StatusMovedPermanently // RFC 7231, 6.4.2 StatusFound = http.StatusFound // RFC 7231, 6.4.3 StatusSeeOther = http.StatusSeeOther // RFC 7231, 6.4.4 StatusNotModified = http.StatusNotModified // RFC 7232, 4.1 StatusUseProxy = http.StatusUseProxy // RFC 7231, 6.4.5 _ = 306 // RFC 7231, 6.4.6 (Unused) StatusTemporaryRedirect = http.StatusTemporaryRedirect // RFC 7231, 6.4.7 StatusPermanentRedirect = http.StatusPermanentRedirect // RFC 7538, 3 StatusBadRequest = http.StatusBadRequest // RFC 7231, 6.5.1 StatusUnauthorized = http.StatusUnauthorized // RFC 7235, 3.1 StatusPaymentRequired = http.StatusPaymentRequired // RFC 7231, 6.5.2 StatusForbidden = http.StatusForbidden // RFC 7231, 6.5.3 StatusNotFound = http.StatusNotFound // RFC 7231, 6.5.4 StatusMethodNotAllowed = http.StatusMethodNotAllowed // RFC 7231, 6.5.5 StatusNotAcceptable = http.StatusNotAcceptable // RFC 7231, 6.5.6 StatusProxyAuthRequired = http.StatusProxyAuthRequired // RFC 7235, 3.2 StatusRequestTimeout = http.StatusRequestTimeout // RFC 7231, 6.5.7 StatusConflict = http.StatusConflict // RFC 7231, 6.5.8 StatusGone = http.StatusGone // RFC 7231, 6.5.9 StatusLengthRequired = http.StatusLengthRequired // RFC 7231, 6.5.10 StatusPreconditionFailed = http.StatusPreconditionFailed // RFC 7232, 4.2 StatusRequestEntityTooLarge = http.StatusRequestEntityTooLarge // RFC 7231, 6.5.11 StatusRequestURITooLong = http.StatusRequestURITooLong // RFC 7231, 6.5.12 StatusUnsupportedMediaType = http.StatusUnsupportedMediaType // RFC 7231, 6.5.13 StatusRequestedRangeNotSatisfiable = http.StatusRequestedRangeNotSatisfiable // RFC 7233, 4.4 StatusExpectationFailed = http.StatusExpectationFailed // RFC 7231, 6.5.14 StatusTeapot = http.StatusTeapot // RFC 7168, 2.3.3 StatusMisdirectedRequest = http.StatusMisdirectedRequest // RFC 7540, 9.1.2 StatusUnprocessableEntity = http.StatusUnprocessableEntity // RFC 4918, 11.2 StatusLocked = http.StatusLocked // RFC 4918, 11.3 StatusFailedDependency = http.StatusFailedDependency // RFC 4918, 11.4 StatusTooEarly = http.StatusTooEarly // RFC 8470, 5.2. StatusUpgradeRequired = http.StatusUpgradeRequired // RFC 7231, 6.5.15 StatusPreconditionRequired = http.StatusPreconditionRequired // RFC 6585, 3 StatusTooManyRequests = http.StatusTooManyRequests // RFC 6585, 4 StatusRequestHeaderFieldsTooLarge = http.StatusRequestHeaderFieldsTooLarge // RFC 6585, 5 StatusUnavailableForLegalReasons = http.StatusUnavailableForLegalReasons // RFC 7725, 3 // Unofficial Client Errors. StatusPageExpired = context.StatusPageExpired StatusBlockedByWindowsParentalControls = context.StatusBlockedByWindowsParentalControls StatusInvalidToken = context.StatusInvalidToken StatusTokenRequired = context.StatusTokenRequired // StatusInternalServerError = http.StatusInternalServerError // RFC 7231, 6.6.1 StatusNotImplemented = http.StatusNotImplemented // RFC 7231, 6.6.2 StatusBadGateway = http.StatusBadGateway // RFC 7231, 6.6.3 StatusServiceUnavailable = http.StatusServiceUnavailable // RFC 7231, 6.6.4 StatusGatewayTimeout = http.StatusGatewayTimeout // RFC 7231, 6.6.5 StatusHTTPVersionNotSupported = http.StatusHTTPVersionNotSupported // RFC 7231, 6.6.6 StatusVariantAlsoNegotiates = http.StatusVariantAlsoNegotiates // RFC 2295, 8.1 StatusInsufficientStorage = http.StatusInsufficientStorage // RFC 4918, 11.5 StatusLoopDetected = http.StatusLoopDetected // RFC 5842, 7.2 StatusNotExtended = http.StatusNotExtended // RFC 2774, 7 StatusNetworkAuthenticationRequired = http.StatusNetworkAuthenticationRequired // RFC 6585, 6 // Unofficial Server Errors. StatusBandwidthLimitExceeded = context.StatusBandwidthLimitExceeded StatusInvalidSSLCertificate = context.StatusInvalidSSLCertificate StatusSiteOverloaded = context.StatusSiteOverloaded StatusSiteFrozen = context.StatusSiteFrozen StatusNetworkReadTimeout = context.StatusNetworkReadTimeout ) var ( // StatusText returns a text for the HTTP status code. It returns the empty // string if the code is unknown. // // Shortcut for core/router#StatusText. StatusText = context.StatusText // RegisterMethods adds custom http methods to the "AllMethods" list. // Use it on initialization of your program. // // Shortcut for core/router#RegisterMethods. RegisterMethods = router.RegisterMethods // WebDAVMethods contains a list of WebDAV HTTP Verbs. // Register using RegiterMethods package-level function or // through HandleMany party-level method. WebDAVMethods = []string{ MethodGet, MethodHead, MethodPatch, MethodPut, MethodPost, MethodDelete, MethodOptions, MethodConnect, MethodTrace, "MKCOL", "COPY", "MOVE", "LOCK", "UNLOCK", "PROPFIND", "PROPPATCH", "LINK", "UNLINK", "PURGE", "VIEW", } ) var globalPatches = &GlobalPatches{ contextPatches: &ContextPatches{ writers: &ContextWriterPatches{}, }, } // GlobalPatches is a singleton features a uniform way to apply global/package-level modifications. // // See the `Patches` package-level function. type GlobalPatches struct { contextPatches *ContextPatches } // Patches returns the singleton of GlobalPatches, an easy way to modify // global(package-level) configuration for Iris applications. // // See its `Context` method. // // Example: https://github.com/kataras/iris/blob/main/_examples/response-writer/json-third-party/main.go func Patches() *GlobalPatches { // singleton. return globalPatches } // Context returns the available context patches. func (p *GlobalPatches) Context() *ContextPatches { return p.contextPatches } // ContextPatches contains the available global Iris context modifications. type ContextPatches struct { writers *ContextWriterPatches } // Writers returns the available global Iris context modifications for REST writers. func (cp *ContextPatches) Writers() *ContextWriterPatches { return cp.writers } // GetDomain modifies the way a domain is fetched from `Context#Domain` method, // which is used on subdomain redirect feature, i18n's language cookie for subdomain sharing // and the rewrite middleware. func (cp *ContextPatches) GetDomain(patchFunc func(hostport string) string) { context.GetDomain = patchFunc } // SetCookieKVExpiration modifies the default cookie expiration time on `Context#SetCookieKV` method. func (cp *ContextPatches) SetCookieKVExpiration(patch time.Duration) { context.SetCookieKVExpiration = patch } // ResolveHTTPFS modifies the default way to resolve a filesystem by any type of value. // It affects the Application's API Builder's `HandleDir` method. func (cp *ContextPatches) ResolveHTTPFS(patchFunc func(fsOrDir any) http.FileSystem) { context.ResolveHTTPFS = patchFunc } // ResolveHTTPFS modifies the default way to resolve a filesystem by any type of value. // It affects the view engine's filesystem resolver. func (cp *ContextPatches) ResolveFS(patchFunc func(fsOrDir any) fs.FS) { context.ResolveFS = patchFunc } // ContextWriterPatches features the context's writers patches. type ContextWriterPatches struct{} // JSON sets a custom function which runs and overrides the default behavior of the `Context#JSON` method. func (cwp *ContextWriterPatches) JSON(patchFunc func(ctx Context, v any, options *JSON) error) { context.WriteJSON = patchFunc } // JSONP sets a custom function which runs and overrides the default behavior of the `Context#JSONP` method. func (cwp *ContextWriterPatches) JSONP(patchFunc func(ctx Context, v any, options *JSONP) error) { context.WriteJSONP = patchFunc } // XML sets a custom function which runs and overrides the default behavior of the `Context#XML` method. func (cwp *ContextWriterPatches) XML(patchFunc func(ctx Context, v any, options *XML) error) { context.WriteXML = patchFunc } // Markdown sets a custom function which runs and overrides the default behavior of the `Context#Markdown` method. func (cwp *ContextWriterPatches) Markdown(patchFunc func(ctx Context, v []byte, options *Markdown) error) { context.WriteMarkdown = patchFunc } // YAML sets a custom function which runs and overrides the default behavior of the `Context#YAML` method. func (cwp *ContextWriterPatches) YAML(patchFunc func(ctx Context, v any, indentSpace int) error) { context.WriteYAML = patchFunc } // Singleton is a structure which can be used as an embedded field on // struct/controllers that should be marked as singletons on `PartyConfigure` or `MVC` Applications. type Singleton struct{} // Singleton returns true as this controller is a singleton. func (c Singleton) Singleton() bool { return true } ================================================ FILE: apps/README.md ================================================ # The `apps` package Package `github.com/kataras/iris/v12/apps` provides a globally scoped control over all registered Iris Applications of the same Program. [Example Application](https://github.com/kataras/iris/tree/main/_examples/routing/subdomains/redirect/multi-instances) Below you will find a use case for each feature. ## The `Get` function The `Get` function returns an Iris Application based on its "appName". It returns nil when no application was found with the given exact name. If "appName" parameter is missing then it returns the last registered one. When no application is registered yet then it creates a new on-fly with a "Default" name and returns that instead. The "Default" one can be used across multiple Go packages of the same Program too. Applications of the same program are registered automatically. To check if at least one application is registered or not, use the `GetAll() []*iris.Application` function instead. ```go func Get(appName ...string) *iris.Application ``` ### Features - Access an Iris Application globally from all of your Go packages of the same Program without a global parameter and import cycle. ```go /* myserver/api/user.go */ package userapi import ( "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/apps" ) func init() { app := apps.Get() app.Get("/", list) } func list(ctx iris.Context) { // [...] ctx.WriteString("list users") } ``` ```go /* myserver/main.go */ package main import ( _ "myserver/api/user" "github.com/kataras/iris/v12/apps" ) func main() { app := apps.Get() app.Listen(":80") } ``` The empty `_` import statement will call the `userapi.init` function (before `main`) which initializes an Iris Application, if not already exists, and registers it to the internal global store for further use across packages. - Reference one or more Iris Applications based on their names. The `Application.SetName` method sets a unique name to this Iris Application. It sets a child prefix for the current Application's Logger. Its `Application.String` method returns the given name. It returns itself. ```go func (app *Application) SetName(appName string) *Application ``` ```go /* myserver/main.go */ package main import "github.com/kataras/iris/v12" func main() { app := iris.New().SetName("app.company.com") } ``` ```go /* myserver/pkg/something.go */ package pkg import "github.com/kataras/iris/v12/apps" func DoSomething() { app := apps.Get("app.company.com") } ``` The `main` function creates and registers an Iris Application on `"app.company.com"` name, after that declaration every package of the same Program can retrieve that specific Application instance through its name. ## The `Switch` function The `Switch` function returns a new Application with the sole purpose of routing the matched Applications through the "provided cases". Read below about the available SwitchProviders and how you can create and use your own one. The cases are filtered in order of their registration. ```go func Switch(provider SwitchProvider, options ...SwitchOption) *iris.Application ``` Example Code: ```go switcher := Switch(Hosts{ {Pattern: "mydomain.com", Target: app}, {Pattern: "test.mydomain.com", Target: testSubdomainApp}, {Pattern: "otherdomain.com", Target: "appName"}, }) switcher.Listen(":80") ``` Note that this is NOT an alternative for a load balancer. The filters are executed by registration order and a matched Application handles the request, that's all it does. The returned Switch Iris Application can register routes that will run when neither of the registered Applications is responsible to handle the incoming request against the provided filters. The returned Switch Iris Application can also register custom error code handlers, e.g. to inject the 404 on not responsible Application was found. It can also be wrapped with its `WrapRouter` method, which is really useful for logging and statistics. ### The `SwitchProvider` interface This is the first required input argument for the `Switch` function. A `SwitchProvider` should return one or more `SwitchCase` values. ```go type SwitchProvider interface { GetSwitchCases() []SwitchCase } ``` The `SwitchCase` structure contains the Filter and the target Iris Application. ```go type SwitchCase struct { Filter func(ctx iris.Context) bool App *iris.Application } ``` ### The `SwitchOptions` structure This is the last variadic (optional) input argument for the `Switch` function. ```go type SwitchOptions struct { // RequestModifiers holds functions to run // if and only if at least one Filter passed. // They are used to modify the request object // of the matched Application, e.g. modify the host. // // See `SetHost` option too. RequestModifiers []func(*http.Request) } ``` The `SetHost` function is a SwitchOption. It force sets a Host field for the matched Application's request object. Extremely useful when used with Hosts SwitchProvider. Usecase: www. to root domain without redirection (SEO reasons) and keep the same internal request Host for both of them so the root app's handlers will always work with a single host no matter what the real request Host was. ```go func SetHost(hostField string) SwitchOptionFunc ``` Example Code: ```go cases := Hosts{ {"^(www.)?mydomain.com$", rootApp}, } switcher := Switch(cases, SetHost("mydomain.com")) ``` ### Join different type of SwitchProviders Wrap with the `Join` slice to pass more than one provider at the same time. Example Code: ```go Switch(Join{ SwitchCase{ Filter: customFilter, App: myapp, }, Hosts{ {Pattern: "^test.*$", Target: myapp}, }, }) ``` ================================================ FILE: apps/apps.go ================================================ // Package apps is responsible to control many Iris Applications. // This package directly imports the iris root package and cannot be used // inside Iris' codebase itself. Only external packages/programs can make use of it. package apps import ( "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/context" ) // Get returns an Iris Application based on its "appName". // It returns nil when no application was found with the given exact name. // // If "appName" parameter is missing then it returns the last registered one. // When no application is registered yet then it creates a new on-fly // with a "Default" name and returns that instead. // The "Default" one can be used across multiple Go packages // of the same Program too. // // Applications of the same program are registered automatically. // // To check if at least one application is registered or not // use the `GetAll` function instead. func Get(appName ...string) *iris.Application { if len(appName) == 0 || appName[0] == "" { if app := context.LastApplication(); app != nil { return app.(*iris.Application) } return iris.New().SetName("Default") } if app, ok := context.GetApplication(appName[0]); ok { return app.(*iris.Application) } return nil } // GetAll returns a slice of all registered Iris Applications. func GetAll() []*iris.Application { appsReadOnly := context.GetApplications() apps := make([]*iris.Application, 0, len(appsReadOnly)) for _, app := range appsReadOnly { apps = append(apps, app.(*iris.Application)) } return apps } // OnApplicationRegistered adds a function which fires when a new application // is registered. func OnApplicationRegistered(listeners ...func(app *iris.Application)) { appListeners := make([]func(context.Application), 0, len(listeners)) for i := range listeners { appListeners = append(appListeners, func(ctxApp context.Application) { listeners[i](ctxApp.(*iris.Application)) }) } context.OnApplicationRegistered(appListeners...) } ================================================ FILE: apps/switch.go ================================================ package apps import ( "strings" "github.com/kataras/iris/v12" ) // Switch returns a new Application // with the sole purpose of routing the // matched Applications through the "provided cases". // // The cases are filtered in order of their registration. // // Example Code: // // switcher := Switch(Hosts{ // { Pattern: "mydomain.com", Target: app }, // { Pattern: "test.mydomain.com", Target: testSubdomainApp }, // { Pattern: "otherdomain.com", "Target: appName" }, // }) // switcher.Listen(":80") // // Note that this is NOT an alternative for a load balancer. // The filters are executed by registration order and a matched Application // handles the request, that's all it does. // // The returned Switch Iris Application can register routes that will run // when neither of the registered Applications is responsible // to handle the incoming request against the provided filters. // The returned Switch Iris Application can also register custom error code handlers, // e.g. to inject the 404 on not responsible Application was found. // It can also be wrapped with its `WrapRouter` method, // which is really useful for logging and statistics. // // Wrap with the `Join` slice to pass // more than one provider at the same time. // // An alternative way for manually embedding an Iris Application to another one is: // // app := iris.New() // root app. // myOtherApp := api.NewServer(otherServerConfiguration) // embedded app. // // myOtherApp.Logger().SetLevel("debug") // // if err := myOtherApp.Build(); err != nil { // panic(err) // } // // app.Any("/api/identity/{p:path}", func(ctx iris.Context) { // apiPath := "/" + ctx.Params().Get("p") // r := ctx.Request() // r.URL.Path = apiPath // r.URL.RawPath = apiPath // ctx.Params().Remove("p") // // myOtherApp.ServeHTTPC(ctx) // }) // // app.Listen(":80") func Switch(provider SwitchProvider, options ...SwitchOption) *iris.Application { cases := provider.GetSwitchCases() if len(cases) == 0 { panic("iris: switch: empty cases") } var friendlyAddrs []string if fp, ok := provider.(FriendlyNameProvider); ok { if friendlyName := fp.GetFriendlyName(); friendlyName != "" { friendlyAddrs = append(friendlyAddrs, friendlyName) } } opts := DefaultSwitchOptions() for _, opt := range options { if opt == nil { continue } opt.Apply(&opts) } app := iris.New() // Try to build the cases apps on app.Build/Listen/Run so // end-developers don't worry about it. app.OnBuild = func() error { for _, c := range cases { if err := c.App.Build(); err != nil { return err } } return nil } // If we have a request to support // middlewares in that switcher app then // we can use app.Get("{p:path}"...) instead. app.UseRouter(func(ctx iris.Context) { for _, c := range cases { if c.Filter(ctx) { w := ctx.ResponseWriter() r := ctx.Request() for _, reqMod := range opts.RequestModifiers { reqMod(r) } c.App.ServeHTTP(w, r) // if c.App.Downgraded() { // c.App.ServeHTTP(w, r) // } else { // Note(@kataras): don't ever try something like that; // the context pool is the switcher's one. // ctx.SetApplication(c.App) // c.App.ServeHTTPC(ctx) // ctx.SetApplication(app) // } return } } // let the "switch app" handle it or fire a custom 404 error page, // next is the switch app's router. ctx.Next() }) // Configure the switcher's supervisor. app.ConfigureHost(func(su *iris.Supervisor) { if len(friendlyAddrs) > 0 { su.FriendlyAddr = strings.Join(friendlyAddrs, ", ") } }) return app } type ( // SwitchCase contains the filter // and the matched Application instance. SwitchCase struct { Filter iris.Filter // Filter runs against the Switcher. App *iris.Application // App is the main target application responsible to handle the request. } // A SwitchProvider should return the switch cases. // It's an interface instead of a direct slice because // we want to make available different type of structures // without wrapping. SwitchProvider interface { GetSwitchCases() []SwitchCase } // FriendlyNameProvider can be optionally implemented by providers // to customize the Switcher's Supervisor.FriendlyAddr field (Startup log). FriendlyNameProvider interface { GetFriendlyName() string } // Join returns a new slice which joins different type of switch cases. Join []SwitchProvider ) var _ SwitchProvider = SwitchCase{} // GetSwitchCases completes the SwitchProvider, it returns itself. func (sc SwitchCase) GetSwitchCases() []SwitchCase { return []SwitchCase{sc} } var _ SwitchProvider = Join{} // GetSwitchCases completes the switch provider. func (j Join) GetSwitchCases() (cases []SwitchCase) { for _, p := range j { if p == nil { continue } cases = append(cases, p.GetSwitchCases()...) } return } ================================================ FILE: apps/switch_hosts.go ================================================ package apps import ( "fmt" "net/http" "net/url" "regexp" "strings" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/context" ) type ( // Host holds the pattern for the SwitchCase filter // and the Target host or application. Host struct { // Pattern is the incoming host matcher regexp or a literal. Pattern string // Target is the target Host that incoming requests will be redirected on pattern match // or an Application's Name that will handle the incoming request matched the Pattern. Target any // It was a string in my initial design but let's do that any, we may support more types here in the future, until generics are in, keep it any. } // Hosts is a switch provider. // It can be used as input argument to the `Switch` function // to map host to existing Iris Application instances, e.g. // { "www.mydomain.com": "mydomainApp" } . // It can accept regexp as a host too, e.g. // { "^my.*$": "mydomainApp" } . Hosts []Host // Good by we need order and map can't provide it for us // (e.g. "fallback" regexp } // Hosts map[string]*iris.Application ) var _ SwitchProvider = Hosts{} // AnyDomain is a regexp that matches any domain. // It can be used as the Pattern field of a Host. // // Example: // // apps.Switch(apps.Hosts{ // { // Pattern: "^id.*$", Target: identityApp, // }, // { // Pattern: apps.AnyDomain, Target: app, // }, // }).Listen(":80") const AnyDomain = `^(([a-zA-Z]{1})|([a-zA-Z]{1}[a-zA-Z]{1})|([a-zA-Z]{1}[0-9]{1})|([0-9]{1}[a-zA-Z]{1})|([a-zA-Z0-9][a-zA-Z0-9-_]{1,61}[a-zA-Z0-9]))\.([a-zA-Z]{2,6}|[a-zA-Z0-9-]{2,30}\.[a-zA-Z ]{2,3})$` // GetSwitchCases completes the SwitchProvider. // It returns a slice of SwitchCase which // if passed on `Switch` function, they act // as a router between matched domains and subdomains // between existing Iris Applications. func (hosts Hosts) GetSwitchCases() []SwitchCase { cases := make([]SwitchCase, 0, len(hosts)) for _, host := range hosts { cases = append(cases, SwitchCase{ Filter: hostFilter(host.Pattern), App: hostApp(host), }) } return cases } // GetFriendlyName implements the FriendlyNameProvider. func (hosts Hosts) GetFriendlyName() string { var patterns []string for _, host := range hosts { if strings.TrimSpace(host.Pattern) != "" { patterns = append(patterns, host.Pattern) } } return strings.Join(patterns, ", ") } func hostApp(host Host) *iris.Application { if host.Target == nil { return nil } switch target := host.Target.(type) { case context.Application: return target.(*iris.Application) case string: // Check if the given target is an application name, if so // we must not redirect (loop) we must serve the request // using that app. if targetApp, ok := context.GetApplication(target); ok { // It's always iris.Application so we are totally safe here. return targetApp.(*iris.Application) } // If it's a real host, warn the user of invalid input. u, err := url.Parse(target) if err == nil && u.IsAbs() { // remember, we redirect hosts, not full URLs here. panic(fmt.Sprintf(`iris: switch: hosts: invalid target host: "%s"`, target)) } if regex := regexp.MustCompile(host.Pattern); regex.MatchString(target) { panic(fmt.Sprintf(`iris: switch: hosts: loop detected between expression: "%s" and target host: "%s"`, host.Pattern, host.Target)) } return newHostRedirectApp(target, HostsRedirectCode) default: panic(fmt.Sprintf("iris: switch: hosts: invalid target type: %T", target)) } } func hostFilter(expr string) iris.Filter { regex := regexp.MustCompile(expr) return func(ctx iris.Context) bool { return regex.MatchString(ctx.Host()) } } // HostsRedirectCode is the default status code is used // to redirect a matching host to a url. var HostsRedirectCode = iris.StatusMovedPermanently func newHostRedirectApp(targetHost string, code int) *iris.Application { app := iris.New() app.Downgrade(func(w http.ResponseWriter, r *http.Request) { if targetHost == context.GetHost(r) { // Note(@kataras): // this should never happen as the HostsRedirect // carefully checks if the expression already matched the "redirectTo" // to avoid the redirect loops at all. // iris: switch: hosts redirect: loop detected between expression: "^my.*$" and target host: "mydomain.com" http.Error(w, iris.StatusText(iris.StatusTooManyRequests), iris.StatusTooManyRequests) return } r.Host = targetHost r.URL.Host = targetHost // r.URL.User = nil http.Redirect(w, r, r.URL.String(), code) }) return app } ================================================ FILE: apps/switch_hosts_test.go ================================================ package apps import ( "fmt" "net/url" "testing" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/httptest" ) type testRequests map[string]map[string]int // url -> path -> status code func TestSwitchHosts(t *testing.T) { var ( expected = func(app context.Application, host string) string { return fmt.Sprintf("App Name: %s\nHost: %s", app, host) } index = func(ctx iris.Context) { ctx.WriteString(expected(ctx.Application(), ctx.Host())) } ) testdomain1 := iris.New().SetName("test 1 domain") testdomain1.Get("/", index) // should match host matching with "testdomain1.com". testdomain2 := iris.New().SetName("test 2 domain") testdomain2.Get("/", index) // should match host matching with "testdomain2.com". mydomain := iris.New().SetName("my domain") mydomain.OnErrorCode(iris.StatusNotFound, func(ctx iris.Context) { ctx.WriteString(ctx.Host() + " custom not found") }) mydomain.Get("/", index) // should match ALL hosts starting with "my". tests := []struct { Pattern string Target *iris.Application Requests testRequests }{ { "testdomain1.com", testdomain1, testRequests{ "http://testdomain1.com": { "/": iris.StatusOK, }, }, }, { "testdomain2.com", testdomain2, testRequests{ "http://testdomain2.com": { "/": iris.StatusOK, }, }, }, { "^my.*$", mydomain, testRequests{ "http://mydomain.com": { "/": iris.StatusOK, "/nf": iris.StatusNotFound, }, "http://myotherdomain.com": { "/": iris.StatusOK, }, "http://mymy.com": { "/": iris.StatusOK, }, "http://nmy.com": { "/": iris.StatusBadGateway, /* 404 hijacked by switch.OnErrorCode */ }, }, }, } var hosts Hosts for _, tt := range tests { hosts = append(hosts, Host{tt.Pattern, tt.Target}) } switcher := Switch(hosts) switcher.OnErrorCode(iris.StatusNotFound, func(ctx iris.Context) { // inject the 404 to 502. // tests the ctx.Next inside the Hosts switch provider. ctx.StatusCode(iris.StatusBadGateway) ctx.WriteString("Switcher: Bad Gateway") }) e := httptest.New(t, switcher) for i, tt := range tests { for URL, paths := range tt.Requests { u, err := url.Parse(URL) if err != nil { t.Fatalf("[%d] %v", i, err) } targetHost := u.Host for requestPath, statusCode := range paths { // url := fmt.Sprintf("http://%s", requestHost) body := expected(tt.Target, targetHost) switch statusCode { case 404: body = targetHost + " custom not found" case 502: body = "Switcher: Bad Gateway" } e.GET(requestPath).WithURL(URL).Expect().Status(statusCode).Body().IsEqual(body) } } } } func TestSwitchHostsRedirect(t *testing.T) { var ( expected = func(appName, host, path string) string { return fmt.Sprintf("App Name: %s\nHost: %s\nPath: %s", appName, host, path) } index = func(ctx iris.Context) { ctx.WriteString(expected(ctx.Application().String(), ctx.Host(), ctx.Path())) } ) mydomain := iris.New().SetName("mydomain") mydomain.OnAnyErrorCode(func(ctx iris.Context) { ctx.WriteString("custom: " + iris.StatusText(ctx.GetStatusCode())) }) mydomain.Get("/", index) mydomain.Get("/f", index) tests := []struct { Pattern string Target string Requests testRequests }{ { "www.mydomain.com", "mydomain", testRequests{ "http://www.mydomain.com": { "/": iris.StatusOK, "/f": iris.StatusOK, "/nf": iris.StatusNotFound, }, }, }, { "^test.*$", "mydomain", testRequests{ "http://testdomain.com": { "/": iris.StatusOK, "/f": iris.StatusOK, "/nf": iris.StatusNotFound, }, }, }, // Something like this will panic to protect users: // { // ..., // "^my.*$", // "mydomain.com", // ... // { "^www.*$", "google.com", testRequests{ "http://www.mydomain.com": { "/": iris.StatusOK, }, "http://www.golang.org": { "/": iris.StatusNotFound, // should give not found because this is not a switcher's web app. }, }, }, } var hostsRedirect Hosts for _, tt := range tests { hostsRedirect = append(hostsRedirect, Host{tt.Pattern, tt.Target}) } switcher := Switch(hostsRedirect) e := httptest.New(t, switcher) for i, tt := range tests { for requestURL, paths := range tt.Requests { u, err := url.Parse(requestURL) if err != nil { t.Fatalf("[%d] %v", i, err) } targetHost := u.Host for requestPath, statusCode := range paths { body := expected(mydomain.String(), targetHost, requestPath) if statusCode != 200 { if tt.Target != mydomain.String() { // it's external. body = "Not Found" } else { body = "custom: " + iris.StatusText(statusCode) } } e.GET(requestPath).WithURL(requestURL).Expect().Status(statusCode).Body().IsEqual(body) } } } } ================================================ FILE: apps/switch_options.go ================================================ package apps import "net/http" type ( // SwitchOptions holds configuration // for the switcher application. SwitchOptions struct { // RequestModifiers holds functions to run // if and only if at least one Filter passed. // They are used to modify the request object // of the matched Application, e.g. modify the host. // // See `SetHost` option too. RequestModifiers []func(*http.Request) // Note(@kataras): I though a lot of API designs for that one and the current is the safest to use. // I skipped the idea of returning a wrapped Application to have functions like app.UseFilter // or the idea of accepting a chain of Iris Handlers here because the Context belongs // to the switcher application and a new one is acquired on the matched Application level, // so communication between them is not possible although // we can make it possible but lets not complicate the code here, unless otherwise requested. } // SwitchOption should be implemented by all options // passed to the `Switch` package-level last variadic input argument. SwitchOption interface { Apply(*SwitchOptions) } // SwitchOptionFunc provides a functional way to pass options // to the `Switch` package-level function's last variadic input argument. SwitchOptionFunc func(*SwitchOptions) ) // Apply completes the `SwitchOption` interface. func (f SwitchOptionFunc) Apply(opts *SwitchOptions) { f(opts) } // DefaultSwitchOptions returns a fresh SwitchOptions // struct value with its fields set to their defaults. func DefaultSwitchOptions() SwitchOptions { return SwitchOptions{ RequestModifiers: nil, } } // Apply completes the `SwitchOption` interface. // It does copies values from "o" to "opts" when necessary. func (o SwitchOptions) Apply(opts *SwitchOptions) { if v := o.RequestModifiers; len(v) > 0 { opts.RequestModifiers = v // override, not append. } } // SetHost is a SwitchOption. // It force sets a Host field for the matched Application's request object. // Extremely useful when used with Hosts SwitchProvider. // Usecase: www. to root domain without redirection (SEO reasons) // and keep the same internal request Host for both of them so // the root app's handlers will always work with a single host no matter // what the real request Host was. func SetHost(hostField string) SwitchOptionFunc { if hostField == "" { return nil } setHost := func(r *http.Request) { r.Host = hostField r.URL.Host = hostField // note: the URL.String builds the uri based on that. } return func(opts *SwitchOptions) { opts.RequestModifiers = append(opts.RequestModifiers, setHost) } } ================================================ FILE: apps/switch_options_test.go ================================================ package apps import ( "testing" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/httptest" ) func TestSetHost(t *testing.T) { var ( index = func(ctx iris.Context) { ctx.Header("Server", ctx.Application().String()) ctx.WriteString(ctx.Host()) } forceHost = "www.mydomain.com" ) rootApp := iris.New().SetName("My Server") rootApp.Get("/", index) switcher := Switch(Hosts{ {"^(www.)?mydomain.com$", rootApp}, }, SetHost(forceHost)) e := httptest.New(t, switcher) tests := []*httptest.Request{ e.GET("/").WithURL("http://mydomain.com"), e.GET("/").WithURL("http://www.mydomain.com"), } for _, tt := range tests { ex := tt.Expect().Status(iris.StatusOK) ex.Header("Server").Equal(rootApp.String()) ex.Body().IsEqual(forceHost) } } ================================================ FILE: apps/switch_scheme.go ================================================ package apps ================================================ FILE: apps/switch_test.go ================================================ package apps import ( "fmt" "testing" "github.com/kataras/iris/v12" ) func TestSwitchJoin(t *testing.T) { myapp := iris.New() customFilter := func(ctx iris.Context) bool { pass, _ := ctx.URLParamBool("filter") return pass } joinedCases := Join{ SwitchCase{ Filter: customFilter, App: myapp, }, Hosts{{Pattern: "^test.*$", Target: myapp}}, } cases := []SwitchCase{ { Filter: customFilter, App: myapp, }, {Filter: hostFilter("^test.*$"), App: myapp}, } if expected, got := fmt.Sprintf("%#+v", cases), fmt.Sprintf("%#+v", joinedCases.GetSwitchCases()); expected != got { t.Fatalf("join does not match with the expected slice of cases, expected:\n%s\nbut got:\n%s", expected, got) } } ================================================ FILE: auth/auth.go ================================================ //go:build go1.18 // +build go1.18 package auth import ( stdContext "context" "fmt" "net/http" "net/url" "strings" "time" "github.com/kataras/iris/v12/context" "github.com/google/uuid" "github.com/gorilla/securecookie" "github.com/kataras/jwt" ) type ( // Auth holds the necessary functionality to authorize and optionally authenticating // users to access and perform actions against the resource server (Iris API). // It completes a secure and fast JSON Web Token signer and verifier which, // based on the custom application needs, can be further customized. // Each Auth of T instance can sign and verify a single custom instance, // more Auth instances can share the same configuration to support multiple custom user types. // Initialize a new Auth of T instance using the New or MustLoad package-level functions. // Most important methods of the instance are: // - AddProvider // - SigninHandler // - VerifyHandler // - SignoutHandler // - SignoutAllHandler // // Example can be found at: https://github.com/kataras/iris/tree/main/_examples/auth/auth/main.go. Auth[T User] struct { // Holds the configuration passed through the New and MustLoad // package-level functions. One or more Auth instance can share the // same configuration's values. config Configuration // Holds the result of the config.KeysConfiguration. keys jwt.Keys // This is an Iris cookie option used to encrypt and decrypt a cookie when // the config.Cookie.Hash & Block are not empty. securecookie context.SecureCookie // Defaults to an empty list, which cannot sign any tokens. // One or more custom providers should be registered through // the AddProvider or WithProviderAndErrorHandler methods. providers []Provider[T] // at least one. // Always not nil, set to custom error handler on SetErrorHandler. errorHandler ErrorHandler // Not nil if a transformer is registered. transformer Transformer[T] // Not nil if a custom claims provider is registered. claimsProvider ClaimsProvider // True if KIDRefresh on config.Keys. refreshEnabled bool } // VerifyUserFunc is passed on Verify and VerifyHandler method // to, optionally, further validate a T user value. VerifyUserFunc[T User] func(t T) error // SigninRequest is the request body the server expects // on SignHandler. The Password and Username or Email should be filled. SigninRequest struct { Username string `json:"username" form:"username,omitempty"` // username OR email, username has priority over email. Email string `json:"email" form:"email,omitempty"` // email OR username. Password string `json:"password" form:"password"` } // SigninResponse is the response body the server sends // to the client on the SignHandler. It contains a pair of the access token // and the refresh token if the refresh jwt token id exists in the configuration. SigninResponse struct { AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token,omitempty"` } // RefreshRequest is the request body the server expects // on VerifyHandler to renew an access and refresh token pair. RefreshRequest struct { RefreshToken string `json:"refresh_token"` } ) // MustLoad binds a filename (fullpath) configuration yaml or json // and constructs a new Auth instance. It panics on error. func MustLoad[T User](filename string) *Auth[T] { var config Configuration if err := config.BindFile(filename); err != nil { panic(err) } return Must(New[T](config)) } // Must is a helper that wraps a call to a function returning (*Auth[T], error) // and panics if the error is non-nil. It is intended for use in variable // initializations such as // // var s = auth.Must(auth.New[MyUser](config)) func Must[T User](s *Auth[T], err error) *Auth[T] { if err != nil { panic(err) } return s } // New initializes a new Auth instance typeof T and returns it. // The T generic can be any custom struct. // It accepts a Configuration value which can be constructed // manually or through a configuration file using the // MustGenerateConfiguration or MustLoadConfiguration // or LoadConfiguration or MustLoad package-level functions. // // Example can be found at: https://github.com/kataras/iris/tree/main/_examples/auth/auth/main.go. func New[T User](config Configuration) (*Auth[T], error) { keys, err := config.validate() if err != nil { return nil, err } _, refreshEnabled := keys[KIDRefresh] s := &Auth[T]{ config: config, keys: keys, securecookie: securecookie.New([]byte(config.Cookie.Hash), []byte(config.Cookie.Block)), refreshEnabled: refreshEnabled, // providers: []Provider[T]{newProvider[T]()}, errorHandler: new(DefaultErrorHandler), } return s, nil } // WithProviderAndErrorHandler registers a provider (if not nil) and // an error handler (if not nil) and returns this "s" Auth instance. // It's the same as calling AddProvider and SetErrorHandler at once. // It's really useful when registering an Auth instance using the iris.Party.PartyConfigure // method when a Provider of T and ErrorHandler is available through the registered Party's dependencies. // // Usage Example: // // api := app.Party("/api") // api.EnsureStaticBindings().RegisterDependency( // NewAuthProviderErrorHandler(), // NewAuthCustomerProvider, // auth.Must(auth.New[Customer](authConfig)).WithProviderAndErrorHandler, // ) func (s *Auth[T]) WithProviderAndErrorHandler(provider Provider[T], errHandler ErrorHandler) *Auth[T] { if provider != nil { for i := range s.providers { s.providers[i] = nil } s.providers = nil s.providers = make([]Provider[T], 0, 1) s.AddProvider(provider) } if errHandler != nil { s.SetErrorHandler(errHandler) } return s } // AddProvider registers one or more providers to this Auth of T instance and returns itself. // Look the Provider godoc for more. func (s *Auth[T]) AddProvider(providers ...Provider[T]) *Auth[T] { // A provider can also implement both transformer and // error handler if that's the design option of the end-developer. for _, p := range providers { if s.transformer == nil { if transformer, ok := p.(Transformer[T]); ok { s.SetTransformer(transformer) } } if errHandler, ok := p.(ErrorHandler); ok { s.SetErrorHandler(errHandler) } if s.claimsProvider == nil { if claimsProvider, ok := p.(ClaimsProvider); ok { s.claimsProvider = claimsProvider } } } s.providers = append(s.providers, providers...) return s } // SetErrorHandler sets a custom error handler to this Auth of T instance and returns itself. // Look the Provider and ErrorHandler godoc for more. func (s *Auth[T]) SetErrorHandler(errHandler ErrorHandler) *Auth[T] { s.errorHandler = errHandler return s } // SetTransformer sets a custom transformer to this Auth of T instance and returns itself. // Look the Provider and Transformer godoc for more. func (s *Auth[T]) SetTransformer(transformer Transformer[T]) *Auth[T] { s.transformer = transformer return s } // SetTransformerFunc like SetTransformer method but accepts a function instead. func (s *Auth[T]) SetTransformerFunc(transfermerFunc func(ctx stdContext.Context, tok *VerifiedToken) (T, error)) *Auth[T] { s.transformer = TransformerFunc[T](transfermerFunc) return s } // Signin signs a token based on the provided username and password // and returns a pair of access and refresh tokens. // // Signin calls the Provider.Signin method to check if a user // is authenticated by the given username and password combination. func (s *Auth[T]) Signin(ctx stdContext.Context, username, password string) ([]byte, []byte, error) { var t T // get "t" from a valid provider. if n := len(s.providers); n > 0 { for i := 0; i < n; i++ { p := s.providers[i] v, err := p.Signin(ctx, username, password) if err != nil { if i == n-1 { // last provider errored. return nil, nil, fmt.Errorf("auth: signin: %w", err) } // keep searching. continue } // found. t = v break } } else { return nil, nil, fmt.Errorf("auth: signin: no provider") } // sign the tokens. accessToken, refreshToken, err := s.sign(t) if err != nil { return nil, nil, fmt.Errorf("auth: signin: %w", err) } return accessToken, refreshToken, nil } func (s *Auth[T]) sign(t T) ([]byte, []byte, error) { // sign the tokens. var ( accessStdClaims StandardClaims refreshStdClaims StandardClaims ) if s.claimsProvider != nil { accessStdClaims = s.claimsProvider.GetAccessTokenClaims() refreshStdClaims = s.claimsProvider.GetRefreshTokenClaims(accessStdClaims) } iat := jwt.Clock().Unix() if accessStdClaims.IssuedAt == 0 { accessStdClaims.IssuedAt = iat } if accessStdClaims.ID == "" { accessStdClaims.ID = uuid.NewString() } if refreshStdClaims.IssuedAt == 0 { refreshStdClaims.IssuedAt = iat } if refreshStdClaims.ID == "" { refreshStdClaims.ID = uuid.NewString() } if refreshStdClaims.OriginID == "" { // keep a reference of the access token the refresh token is created, // if that access token is invalidated then // its refresh token should be too so the user can force-login. refreshStdClaims.OriginID = accessStdClaims.ID } accessToken, err := s.keys.SignToken(KIDAccess, t, accessStdClaims) if err != nil { return nil, nil, fmt.Errorf("access: %w", err) } var refreshToken []byte if s.refreshEnabled { refreshToken, err = s.keys.SignToken(KIDRefresh, t, refreshStdClaims) if err != nil { return nil, nil, fmt.Errorf("refresh: %w", err) } } return accessToken, refreshToken, nil } // SignHandler generates and sends a pair of access and refresh token to the client // as JSON body of `SigninResponse` and cookie (if cookie setting was provided). // See `Signin` method for more. func (s *Auth[T]) SigninHandler(ctx *context.Context) { // No, let the developer decide it based on a middleware, e.g. iris.LimitRequestBodySize. // ctx.SetMaxRequestBodySize(s.maxRequestBodySize) var ( req SigninRequest err error ) switch ctx.GetContentTypeRequested() { case context.ContentFormHeaderValue, context.ContentFormMultipartHeaderValue: err = ctx.ReadForm(&req) default: err = ctx.ReadJSON(&req) } if err != nil { s.errorHandler.InvalidArgument(ctx, err) return } if req.Username == "" { req.Username = req.Email } accessTokenBytes, refreshTokenBytes, err := s.Signin(ctx, req.Username, req.Password) if err != nil { s.tryRemoveCookie(ctx) // remove cookie on invalidated. s.errorHandler.Unauthenticated(ctx, err) return } accessToken := jwt.BytesToString(accessTokenBytes) refreshToken := jwt.BytesToString(refreshTokenBytes) s.trySetCookie(ctx, accessToken) resp := SigninResponse{ AccessToken: accessToken, RefreshToken: refreshToken, } ctx.JSON(resp) } // Verify accepts a token and verifies it. // It returns the token's custom and standard JWT claims. func (s *Auth[T]) Verify(ctx stdContext.Context, token []byte, verifyFuncs ...VerifyUserFunc[T]) (T, StandardClaims, error) { t, claims, err := s.verify(ctx, token) if err != nil { return t, StandardClaims{}, fmt.Errorf("auth: verify: %w", err) } for _, verify := range verifyFuncs { if verify == nil { continue } if err = verify(t); err != nil { return t, StandardClaims{}, fmt.Errorf("auth: verify: %w", err) } } return t, claims, nil } func (s *Auth[T]) verify(ctx stdContext.Context, token []byte) (T, StandardClaims, error) { var t T if len(token) == 0 { // should never happen at this state. return t, StandardClaims{}, jwt.ErrMissing } verifiedToken, err := jwt.VerifyWithHeaderValidator(nil, nil, token, s.keys.ValidateHeader, jwt.Future(time.Minute), jwt.Leeway(time.Minute)) if err != nil { return t, StandardClaims{}, err } if s.transformer != nil { if t, err = s.transformer.Transform(ctx, verifiedToken); err != nil { return t, StandardClaims{}, err } } else { if err = verifiedToken.Claims(&t); err != nil { return t, StandardClaims{}, err } } standardClaims := verifiedToken.StandardClaims if n := len(s.providers); n > 0 { for i := 0; i < n; i++ { p := s.providers[i] err := p.ValidateToken(ctx, standardClaims, t) if err != nil { if i == n-1 { // last provider errored. return t, StandardClaims{}, err } // keep searching. continue } // token is allowed. break } } else { // return t, StandardClaims{}, fmt.Errorf("no provider") } return t, standardClaims, nil } // VerifyHandler verifies and sets the necessary information about the user(claims) and // the verified token to the Iris Context and calls the Context's Next method. // This information is available through auth.GetAccessToken, auth.GetStandardClaims and // auth.GetUser[T] package-level functions. // // See `Verify` method for more. func (s *Auth[T]) VerifyHandler(verifyFuncs ...VerifyUserFunc[T]) context.Handler { return func(ctx *context.Context) { accessToken := s.ExtractAccessToken(ctx) if accessToken == "" { // if empty, fire 401. s.errorHandler.Unauthenticated(ctx, jwt.ErrMissing) return } t, claims, err := s.Verify(ctx, []byte(accessToken), verifyFuncs...) if err != nil { s.errorHandler.Unauthenticated(ctx, err) return } ctx.SetUser(t) // store the user to the request. ctx.Values().Set(accessTokenContextKey, accessToken) ctx.Values().Set(standardClaimsContextKey, claims) ctx.Values().Set(userContextKey, t) ctx.Next() } } // ExtractAccessToken extracts the access token from the request's header or cookie. func (s *Auth[T]) ExtractAccessToken(ctx *context.Context) string { // first try from authorization: bearer header. accessToken := s.extractTokenFromHeader(ctx) // then if no header, try try extract from cookie. if accessToken == "" { if cookieName := s.config.Cookie.Name; cookieName != "" { accessToken = ctx.GetCookie(cookieName, context.CookieEncoding(s.securecookie)) } } return accessToken } // Refresh accepts a previously generated refresh token (from SigninHandler) and // returns a new access and refresh token pair. func (s *Auth[T]) Refresh(ctx stdContext.Context, refreshToken []byte) ([]byte, []byte, error) { if !s.refreshEnabled { return nil, nil, fmt.Errorf("auth: refresh: disabled") } t, _, err := s.verify(ctx, refreshToken) if err != nil { return nil, nil, fmt.Errorf("auth: refresh: %w", err) } // refresh the tokens, both refresh & access tokens will be renew to prevent // malicious 😈 users that may hold a refresh token. accessTok, refreshTok, err := s.sign(t) if err != nil { return nil, nil, fmt.Errorf("auth: refresh: %w", err) } return accessTok, refreshTok, nil } // RefreshHandler reads the request body which should include data for `RefreshRequest` structure // and sends a new access and refresh token pair, // also sets the cookie to the new encrypted access token value. // See `Refresh` method for more. func (s *Auth[T]) RefreshHandler(ctx *context.Context) { var req RefreshRequest err := ctx.ReadJSON(&req) if err != nil { s.errorHandler.InvalidArgument(ctx, err) return } accessTokenBytes, refreshTokenBytes, err := s.Refresh(ctx, []byte(req.RefreshToken)) if err != nil { // s.tryRemoveCookie(ctx) s.errorHandler.Unauthenticated(ctx, err) return } accessToken := jwt.BytesToString(accessTokenBytes) refreshToken := jwt.BytesToString(refreshTokenBytes) s.trySetCookie(ctx, accessToken) resp := SigninResponse{ AccessToken: accessToken, RefreshToken: refreshToken, } ctx.JSON(resp) } // Signout accepts the access token and a boolean which reports whether // the signout should be applied to all tokens generated for a specific user (logout from all devices) // or just the provided token's one. // It calls the Provider's InvalidateToken(all=false) or InvalidateTokens (all=true). func (s *Auth[T]) Signout(ctx stdContext.Context, token []byte, all bool) error { t, standardClaims, err := s.verify(ctx, token) if err != nil { return fmt.Errorf("auth: signout: verify: %w", err) } for i, n := 0, len(s.providers)-1; i <= n; i++ { p := s.providers[i] if all { err = p.InvalidateTokens(ctx, t) } else { err = p.InvalidateToken(ctx, standardClaims, t) } if err != nil { if i == n { // last provider errored. return err } // keep trying. continue } // token is marked as invalidated by a provider. break } return nil } // SignoutHandler verifies the request's access token and invalidates it, calling // the Provider's InvalidateToken method. // See `Signout` method too. func (s *Auth[T]) SignoutHandler(ctx *context.Context) { s.signoutHandler(ctx, false) } // SignoutAllHandler verifies the request's access token and // should invalidate all the tokens generated previously calling // the Provider's InvalidateTokens method. // See `Signout` method too. func (s *Auth[T]) SignoutAllHandler(ctx *context.Context) { s.signoutHandler(ctx, true) } func (s *Auth[T]) signoutHandler(ctx *context.Context, all bool) { accessToken := s.ExtractAccessToken(ctx) if accessToken == "" { s.errorHandler.Unauthenticated(ctx, jwt.ErrMissing) return } err := s.Signout(ctx, []byte(accessToken), all) if err != nil { s.errorHandler.Unauthenticated(ctx, err) return } s.tryRemoveCookie(ctx) ctx.SetUser(nil) ctx.Values().Remove(accessTokenContextKey) ctx.Values().Remove(standardClaimsContextKey) ctx.Values().Remove(userContextKey) } func (s *Auth[T]) extractTokenFromHeader(ctx *context.Context) string { for _, headerKey := range s.config.Headers { headerValue := ctx.GetHeader(headerKey) if headerValue == "" { continue } // pure check: authorization header format must be Bearer {token} authHeaderParts := strings.Split(headerValue, " ") if len(authHeaderParts) != 2 || strings.ToLower(authHeaderParts[0]) != "bearer" { continue } return authHeaderParts[1] } return "" } func (s *Auth[T]) trySetCookie(ctx *context.Context, accessToken string) { if cookieName := s.config.Cookie.Name; cookieName != "" { maxAge := s.keys[KIDAccess].MaxAge if maxAge == 0 { maxAge = context.SetCookieKVExpiration } cookie := &http.Cookie{ Name: cookieName, Value: url.QueryEscape(accessToken), Path: "/", HttpOnly: true, Secure: s.config.Cookie.Secure || ctx.IsSSL(), Domain: ctx.Domain(), SameSite: http.SameSiteLaxMode, Expires: time.Now().Add(maxAge), MaxAge: int(maxAge.Seconds()), } ctx.SetCookie(cookie, context.CookieEncoding(s.securecookie), context.CookieAllowReclaim()) } } func (s *Auth[T]) tryRemoveCookie(ctx *context.Context) { if cookieName := s.config.Cookie.Name; cookieName != "" { ctx.RemoveCookie(cookieName) } } ================================================ FILE: auth/configuration.go ================================================ //go:build go1.18 // +build go1.18 package auth import ( "encoding/json" "errors" "fmt" "os" "path/filepath" "time" "github.com/gorilla/securecookie" "github.com/kataras/jwt" "gopkg.in/yaml.v3" ) const ( // The JWT Key ID for access tokens. KIDAccess = "IRIS_AUTH_ACCESS" // The JWT Key ID for refresh tokens. KIDRefresh = "IRIS_AUTH_REFRESH" ) type ( // Configuration holds the necessary information for Iris Auth & Single-Sign-On feature. // // See the `New` package-level function. Configuration struct { // The authorization header keys that server should read the access token from. // // Defaults to: // - Authorization // - X-Authorization Headers []string `json:"headers" yaml:"Headers" toml:"Headers" ini:"Headers"` // Cookie optional configuration. // A Cookie.Name holds the access token value fully encrypted. Cookie CookieConfiguration `json:"cookie" yaml:"Cookie" toml:"Cookie" ini:"cookie"` // Keys MUST define the jwt keys configuration for access, // and optionally, for refresh tokens signing and verification. Keys jwt.KeysConfiguration `json:"keys" yaml:"Keys" toml:"Keys" ini:"keys"` } // CookieConfiguration holds the necessary information for cookie client storage. CookieConfiguration struct { // Name defines the cookie's name. Name string `json:"cookie" yaml:"Name" toml:"Name" ini:"name"` // Secure if true then "; Secure" is appended to the Set-Cookie header. // By setting the secure to true, the web browser will prevent the // transmission of a cookie over an unencrypted channel. // // Defaults to false but it's true when the request is under iris.Context.IsSSL(). Secure bool `json:"secure" yaml:"Secure" toml:"Secure" ini:"secure"` // Hash is optional, it is used to authenticate cookie value using HMAC. // It is recommended to use a key with 32 or 64 bytes. Hash string `json:"hash" yaml:"Hash" toml:"Hash" ini:"hash"` // Block is optional, used to encrypt cookie value. // The key length must correspond to the block size // of the encryption algorithm. For AES, used by default, valid lengths are // 16, 24, or 32 bytes to select AES-128, AES-192, or AES-256. Block string `json:"block" yaml:"Block" toml:"Block" ini:"block"` } ) func (c *Configuration) validate() (jwt.Keys, error) { if len(c.Headers) == 0 { return nil, fmt.Errorf("auth: configuration: headers slice is empty") } if c.Cookie.Name != "" { if c.Cookie.Hash == "" || c.Cookie.Block == "" { return nil, fmt.Errorf("auth: configuration: cookie block and cookie hash are required for security reasons when cookie is used") } } keys, err := c.Keys.Load() if err != nil { return nil, fmt.Errorf("auth: configuration: %w", err) } if _, ok := keys[KIDAccess]; !ok { return nil, fmt.Errorf("auth: configuration: %s access token is missing from the configuration", KIDAccess) } // Let's keep refresh optional. // if _, ok := keys[KIDRefresh]; !ok { // return nil, fmt.Errorf("auth: configuration: %s refresh token is missing from the configuration", KIDRefresh) // } return keys, nil } // BindRandom binds the "c" configuration to random values for keys and cookie security. // Keys will not be persisted between restarts, // a more persistent storage should be considered for production applications, // see BindFile method and LoadConfiguration/MustLoadConfiguration package-level functions. func (c *Configuration) BindRandom() error { accessPublic, accessPrivate, err := jwt.GenerateEdDSA() if err != nil { return err } refreshPublic, refreshPrivate, err := jwt.GenerateEdDSA() if err != nil { return err } *c = Configuration{ Headers: []string{ "Authorization", "X-Authorization", }, Cookie: CookieConfiguration{ Name: "iris_auth_cookie", Secure: false, Hash: string(securecookie.GenerateRandomKey(64)), Block: string(securecookie.GenerateRandomKey(32)), }, Keys: jwt.KeysConfiguration{ { ID: KIDAccess, Alg: jwt.EdDSA.Name(), MaxAge: 2 * time.Hour, Public: string(accessPublic), Private: string(accessPrivate), }, { ID: KIDRefresh, Alg: jwt.EdDSA.Name(), MaxAge: 720 * time.Hour, Public: string(refreshPublic), Private: string(refreshPrivate), EncryptionKey: string(jwt.MustGenerateRandom(32)), }, }, } return nil } // BindFile binds a filename (fullpath) to "c" Configuration. // The file format is either JSON or YAML and it should be suffixed // with .json or .yml/.yaml. func (c *Configuration) BindFile(filename string) error { switch filepath.Ext(filename) { case ".json": contents, err := os.ReadFile(filename) if err != nil { if errors.Is(err, os.ErrNotExist) { generatedConfig := MustGenerateConfiguration() if generatedYAML, gErr := generatedConfig.ToJSON(); gErr == nil { err = fmt.Errorf("%w: example:\n\n%s", err, generatedYAML) } } return err } return json.Unmarshal(contents, c) default: contents, err := os.ReadFile(filename) if err != nil { if errors.Is(err, os.ErrNotExist) { generatedConfig := MustGenerateConfiguration() if generatedYAML, gErr := generatedConfig.ToYAML(); gErr == nil { err = fmt.Errorf("%w: example:\n\n%s", err, generatedYAML) } } return err } return yaml.Unmarshal(contents, c) } } // ToYAML returns the "c" Configuration's contents as raw yaml byte slice. func (c *Configuration) ToYAML() ([]byte, error) { return yaml.Marshal(c) } // ToJSON returns the "c" Configuration's contents as raw json byte slice. func (c *Configuration) ToJSON() ([]byte, error) { return json.Marshal(c) } // MustGenerateConfiguration calls the Configuration's BindRandom // method and returns the result. It panics on errors. func MustGenerateConfiguration() (c Configuration) { if err := c.BindRandom(); err != nil { panic(err) } return } // MustLoadConfiguration same as LoadConfiguration package-level function // but it panics on error. func MustLoadConfiguration(filename string) Configuration { c, err := LoadConfiguration(filename) if err != nil { panic(err) } return c } // LoadConfiguration reads a filename (fullpath) // and returns a Configuration binded to the contents of the given filename. // See Configuration.BindFile method too. func LoadConfiguration(filename string) (c Configuration, err error) { err = c.BindFile(filename) return } ================================================ FILE: auth/provider.go ================================================ //go:build go1.18 // +build go1.18 package auth import ( stdContext "context" "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/x/errors" "github.com/kataras/jwt" ) // VerifiedToken holds the information about a verified token. type VerifiedToken = jwt.VerifiedToken // Provider is an interface of T which MUST be completed // by a custom value type to provide user information to the Auth's // JWT Token Signer and Verifier. // // A provider can optionally complete the Transformer, ClaimsProvider and // ErrorHandler all in once when necessary. // Set a provider using the AddProvider method of Auth type. // // Example can be found at: https://github.com/kataras/iris/tree/main/_examples/auth/auth/user_provider.go. type Provider[T User] interface { // Signin accepts a username (or email) and a password and should // return a valid T value or an error describing // the user authentication or verification reason of failure. // // The first input argument standard context can be // casted to iris.Context if executed through Auth.SigninHandler. // // It's called on Auth.SigninHandler. Signin(ctx stdContext.Context, username, password string) (T, error) // ValidateToken accepts the standard JWT claims and the T value obtained // by the Signin method and should return a nil error on validation success // or a non-nil error for validation failure. // It is mostly used to perform checks of the T value's struct fields or // the standard claim's (e.g. origin jwt token id). // It can be an empty method too which returns a nil error. // // The first input argument standard context can be // casted to iris.Context if executed through Auth.VerifyHandler. // // It's caleld on Auth.VerifyHandler. ValidateToken(ctx stdContext.Context, standardClaims StandardClaims, t T) error // InvalidateToken is optional and can be used to allow tokens to be invalidated // from server-side. Commonly, implement when a token and user pair is saved // on a persistence storage and server can decide which token is valid or invalid. // It can be an empty method too which returns a nil error. // // The first input argument standard context can be // casted to iris.Context if executed through Auth.SignoutHandler. // // It's called on Auth.SignoutHandler. InvalidateToken(ctx stdContext.Context, standardClaims StandardClaims, t T) error // InvalidateTokens is like InvalidateToken but it should invalidate // all tokens generated for a specific T value. // It can be an empty method too which returns a nil error. // // The first input argument standard context can be // casted to iris.Context if executed through Auth.SignoutAllHandler. // // It's called on Auth.SignoutAllHandler. InvalidateTokens(ctx stdContext.Context, t T) error } // ClaimsProvider is an optional interface, which may not be used at all. // If implemented by a Provider, it signs the jwt token // using these claims to each of the following token types. type ClaimsProvider interface { GetAccessTokenClaims() StandardClaims GetRefreshTokenClaims(accessClaims StandardClaims) StandardClaims } // Transformer is an optional interface which can be implemented by a Provider as well. // Set a Transformer through Auth.SetTransformer or Auth.SetTransformerFunc or by implementing // the Transform method inside a Provider which can be registered through the Auth.AddProvider // method. // // A transformer is called on Auth.VerifyHandler before Provider.ValidateToken and it can // be used to modify the T value based on the token's contents. It is mostly used // to convert the json claims to T value manually, when they differ. // // The first input argument standard context can be // casted to iris.Context if executed through Auth.VerifyHandler. type Transformer[T User] interface { Transform(ctx stdContext.Context, tok *VerifiedToken) (T, error) } // TransformerFunc completes the Transformer interface. type TransformerFunc[T User] func(ctx stdContext.Context, tok *VerifiedToken) (T, error) // Transform calls itself. func (fn TransformerFunc[T]) Transform(ctx stdContext.Context, tok *VerifiedToken) (T, error) { return fn(ctx, tok) } // ErrorHandler is an optional interface which can be implemented by a Provider as well. // // ErrorHandler is the interface which controls the HTTP errors on // Auth.SigninHandler, Auth.VerifyHandler, Auth.SignoutHandler and // Auth.SignoutAllHandler handelrs. type ErrorHandler interface { // InvalidArgument should handle any 400 (bad request) errors, // e.g. invalid request body. InvalidArgument(ctx *context.Context, err error) // Unauthenticated should handle any 401 (unauthenticated) errors, // e.g. user not found or invalid credentials. Unauthenticated(ctx *context.Context, err error) } // DefaultErrorHandler is the default registered ErrorHandler which can be // replaced through the Auth.SetErrorHandler method. type DefaultErrorHandler struct{} // InvalidArgument sends 400 (bad request) with "unable to parse body" as its message // and the "err" value as its details. func (e *DefaultErrorHandler) InvalidArgument(ctx *context.Context, err error) { errors.InvalidArgument.Details(ctx, "unable to parse body", err.Error()) } // Unauthenticated sends 401 (unauthenticated) with the "err" value as its message. func (e *DefaultErrorHandler) Unauthenticated(ctx *context.Context, err error) { errors.Unauthenticated.Err(ctx, err) } ================================================ FILE: auth/user.go ================================================ //go:build go1.18 // +build go1.18 package auth import ( "github.com/kataras/iris/v12/context" "github.com/kataras/jwt" ) type ( // StandardClaims is an alias of jwt.Claims, it holds the standard JWT claims. StandardClaims = jwt.Claims // User is an alias of an empty interface, it's here to declare the typeof T, // which can be any custom struct type. // // Example can be found at: https://github.com/kataras/iris/tree/main/_examples/auth/auth/user.go. User = any ) const accessTokenContextKey = "iris.auth.context.access_token" // GetAccessToken accepts the iris Context and returns the raw access token value. // It's only available after Auth.VerifyHandler is executed. func GetAccessToken(ctx *context.Context) string { return ctx.Values().GetString(accessTokenContextKey) } const standardClaimsContextKey = "iris.auth.context.standard_claims" // GetStandardClaims accepts the iris Context and returns the standard token's claims. // It's only available after Auth.VerifyHandler is executed. func GetStandardClaims(ctx *context.Context) StandardClaims { if v := ctx.Values().Get(standardClaimsContextKey); v != nil { if c, ok := v.(StandardClaims); ok { return c } } return StandardClaims{} } const userContextKey = "iris.auth.context.user" // GetUser is the package-level function of the Auth.GetUser method. // It returns the T user value after Auth.VerifyHandler is executed. func GetUser[T User](ctx *context.Context) T { if v := ctx.Values().Get(userContextKey); v != nil { if t, ok := v.(T); ok { return t } } var empty T return empty } // GetUser accepts the iris Context and returns the T custom user/claims struct value. // It's only available after Auth.VerifyHandler is executed. func (s *Auth[T]) GetUser(ctx *context.Context) T { return GetUser[T](ctx) } ================================================ FILE: cache/browser.go ================================================ package cache import ( "strconv" "time" "github.com/kataras/iris/v12/cache/client" "github.com/kataras/iris/v12/context" ) // CacheControlHeaderValue is the header value of the // "Cache-Control": "private, no-cache, max-age=0, must-revalidate, no-store, proxy-revalidate, s-maxage=0". // // It can be overridden. var CacheControlHeaderValue = "private, no-cache, max-age=0, must-revalidate, no-store, proxy-revalidate, s-maxage=0" const ( // PragmaHeaderKey is the header key of "Pragma". PragmaHeaderKey = "Pragma" // PragmaNoCacheHeaderValue is the header value of "Pragma": "no-cache". PragmaNoCacheHeaderValue = "no-cache" // ExpiresHeaderKey is the header key of "Expires". ExpiresHeaderKey = "Expires" // ExpiresNeverHeaderValue is the header value of "ExpiresHeaderKey": "0". ExpiresNeverHeaderValue = "0" ) // NoCache is a middleware which overrides the Cache-Control, Pragma and Expires headers // in order to disable the cache during the browser's back and forward feature. // // A good use of this middleware is on HTML routes; to refresh the page even on "back" and "forward" browser's arrow buttons. // // See `cache#StaticCache` for the opposite behavior. var NoCache = func(ctx *context.Context) { ctx.Header(context.CacheControlHeaderKey, CacheControlHeaderValue) ctx.Header(PragmaHeaderKey, PragmaNoCacheHeaderValue) ctx.Header(ExpiresHeaderKey, ExpiresNeverHeaderValue) // Add the X-No-Cache header as well, for any customized case, i.e `cache#Handler` or `cache#Cache`. client.NoCache(ctx) ctx.Next() } // StaticCache middleware for caching static files by sending the "Cache-Control" and "Expires" headers to the client. // It accepts a single input parameter, the "cacheDur", a time.Duration that it's used to calculate the expiration. // // If "cacheDur" <=0 then it returns the `NoCache` middleware instaed to disable the caching between browser's "back" and "forward" actions. // // Usage: `app.Use(cache.StaticCache(24 * time.Hour))` or `app.Use(cache.Staticcache(-1))`. // A middleware, which is a simple Handler can be called inside another handler as well, example: // cacheMiddleware := cache.StaticCache(...) // // func(ctx iris.Context){ // cacheMiddleware(ctx) // [...] // } var StaticCache = func(cacheDur time.Duration) context.Handler { if int64(cacheDur) <= 0 { return NoCache } cacheControlHeaderValue := "public, max-age=" + strconv.Itoa(int(cacheDur.Seconds())) return func(ctx *context.Context) { cacheUntil := time.Now().Add(cacheDur).Format(ctx.Application().ConfigurationReadOnly().GetTimeFormat()) ctx.Header(ExpiresHeaderKey, cacheUntil) ctx.Header(context.CacheControlHeaderKey, cacheControlHeaderValue) ctx.Next() } } const ifNoneMatchHeaderKey = "If-None-Match" // ETag is another browser & server cache request-response feature. // It can be used side by side with the `StaticCache`, usually `StaticCache` middleware should go first. // This should be used on routes that serves static files only. // The key of the `ETag` is the `ctx.Request().URL.Path`, invalidation of the not modified cache method // can be made by other request handler as well. // // In typical usage, when a URL is retrieved, the web server will return the resource's current // representation along with its corresponding ETag value, // which is placed in an HTTP response header "ETag" field: // // ETag: "/mypath" // // The client may then decide to cache the representation, along with its ETag. // Later, if the client wants to retrieve the same URL resource again, // it will first determine whether the local cached version of the URL has expired // (through the Cache-Control (`StaticCache` method) and the Expire headers). // If the URL has not expired, it will retrieve the local cached resource. // If it determined that the URL has expired (is stale), then the client will contact the server // and send its previously saved copy of the ETag along with the request in a "If-None-Match" field. // // Usage with combination of `StaticCache`: // assets := app.Party("/assets", cache.StaticCache(24 * time.Hour), ETag) // assets.HandleDir("/", iris.Dir("./assets")) // // Similar to `Cache304` but it doesn't depends on any "modified date", it uses just the ETag and If-None-Match headers. // // Read more at: https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching and // https://en.wikipedia.org/wiki/HTTP_ETag var ETag = func(ctx *context.Context) { key := ctx.Request().URL.Path ctx.Header(context.ETagHeaderKey, key) if match := ctx.GetHeader(ifNoneMatchHeaderKey); match == key { ctx.WriteNotModified() return } ctx.Next() } // Cache304 sends a `StatusNotModified` (304) whenever // the "If-Modified-Since" request header (time) is before the // time.Now() + expiresEvery (always compared to their UTC values). // Use this `cache#Cache304` instead of the "github.com/kataras/iris/v12/cache" or iris.Cache // for better performance. // Clients that are compatible with the http RCF (all browsers are and tools like postman) // will handle the caching. // The only disadvantage of using that instead of server-side caching // is that this method will send a 304 status code instead of 200, // So, if you use it side by side with other micro services // you have to check for that status code as well for a valid response. // // Developers are free to extend this method's behavior // by watching system directories changes manually and use of the `ctx.WriteWithExpiration` // with a "modtime" based on the file modified date, // can be used on Party's that contains a static handler, // i.e `HandleDir`. var Cache304 = func(expiresEvery time.Duration) context.Handler { return func(ctx *context.Context) { now := time.Now() if modified, err := ctx.CheckIfModifiedSince(now.Add(-expiresEvery)); !modified && err == nil { ctx.WriteNotModified() return } ctx.SetLastModified(now) ctx.Next() } } ================================================ FILE: cache/browser_test.go ================================================ package cache_test import ( "strconv" "testing" "time" "github.com/kataras/iris/v12/cache" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/httptest" ) func TestNoCache(t *testing.T) { app := iris.New() app.Get("/", cache.NoCache, func(ctx iris.Context) { ctx.WriteString("no_cache") }) // tests e := httptest.New(t, app) r := e.GET("/").Expect().Status(httptest.StatusOK) r.Body().IsEqual("no_cache") r.Header(context.CacheControlHeaderKey).Equal(cache.CacheControlHeaderValue) r.Header(cache.PragmaHeaderKey).Equal(cache.PragmaNoCacheHeaderValue) r.Header(cache.ExpiresHeaderKey).Equal(cache.ExpiresNeverHeaderValue) } func TestStaticCache(t *testing.T) { // test change the time format, which is not recommended but can be done. app := iris.New().Configure(iris.WithTimeFormat("02 Jan 2006 15:04:05 GMT")) cacheDur := 30 * (24 * time.Hour) var expectedTime time.Time app.Get("/", cache.StaticCache(cacheDur), func(ctx iris.Context) { expectedTime = time.Now() ctx.WriteString("static_cache") }) // tests e := httptest.New(t, app) r := e.GET("/").Expect().Status(httptest.StatusOK) r.Body().IsEqual("static_cache") r.Header(cache.ExpiresHeaderKey).Equal(expectedTime.Add(cacheDur).Format(app.ConfigurationReadOnly().GetTimeFormat())) cacheControlHeaderValue := "public, max-age=" + strconv.Itoa(int(cacheDur.Seconds())) r.Header(context.CacheControlHeaderKey).Equal(cacheControlHeaderValue) } func TestCache304(t *testing.T) { // t.Parallel() app := iris.New() expiresEvery := 4 * time.Second app.Get("/", cache.Cache304(expiresEvery), func(ctx iris.Context) { ctx.WriteString("send") }) // handlers e := httptest.New(t, app) // when 304, content type, content length and if ETagg is there are removed from the headers. insideCacheTimef := time.Now().Add(-expiresEvery).UTC().Format(app.ConfigurationReadOnly().GetTimeFormat()) r := e.GET("/").WithHeader(context.IfModifiedSinceHeaderKey, insideCacheTimef).Expect().Status(httptest.StatusNotModified) r.Headers().NotContainsKey(context.ContentTypeHeaderKey).NotContainsKey(context.ContentLengthHeaderKey).NotContainsKey("ETag") r.Body().IsEqual("") // continue to the handler itself. cacheInvalidatedTimef := time.Now().Add(expiresEvery).UTC().Format(app.ConfigurationReadOnly().GetTimeFormat()) // after ~5seconds. r = e.GET("/").WithHeader(context.LastModifiedHeaderKey, cacheInvalidatedTimef).Expect().Status(httptest.StatusOK) r.Body().IsEqual("send") // now without header, it should continue to the handler itself as well. r = e.GET("/").Expect().Status(httptest.StatusOK) r.Body().IsEqual("send") } func TestETag(t *testing.T) { // t.Parallel() app := iris.New() n := "_" app.Get("/", cache.ETag, func(ctx iris.Context) { ctx.WriteString(n) n += "_" }) // the first and last test writes the content with status OK without cache, // the rest tests the cache headers and status 304 and return, so body should be "". e := httptest.New(t, app) r := e.GET("/").Expect().Status(httptest.StatusOK) r.Header("ETag").Equal("/") // test if header set. r.Body().IsEqual("_") e.GET("/").WithHeader("ETag", "/").WithHeader("If-None-Match", "/").Expect(). Status(httptest.StatusNotModified).Body().IsEqual("") // browser is responsible, no the test engine. r = e.GET("/").Expect().Status(httptest.StatusOK) r.Header("ETag").Equal("/") // test if header set. r.Body().IsEqual("__") } ================================================ FILE: cache/cache.go ================================================ /* Package cache provides server-side caching capabilities with rich support of options and rules. Use it for server-side caching, see the `iris#Cache304` for an alternative approach that may fit your needs most. Example code: import ( "time" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/cache" ) func main(){ app := iris.Default() middleware := cache.Handler(2 *time.Minute) app.Get("/hello", middleware, h) app.Listen(":8080") } func h(ctx iris.Context) { ctx.HTML("

      Hello, this should be cached. Every 2 minutes it will be refreshed, check your browser's inspector

      ") } */ package cache import ( "time" "github.com/kataras/iris/v12/cache/client" "github.com/kataras/iris/v12/context" ) // WithKey sets a custom entry key for cached pages. // Should be prepended to the cache handler. // // Usage: // app.Get("/", cache.WithKey("custom-key"), cache.Handler(time.Minute), mainHandler) func WithKey(key string) context.Handler { return func(ctx *context.Context) { client.SetKey(ctx, key) ctx.Next() } } // DefaultMaxAge is a function which returns the // `context#MaxAge` as time.Duration. // It's the default expiration function for the cache handler. var DefaultMaxAge = func(ctx *context.Context) time.Duration { return time.Duration(ctx.MaxAge()) * time.Second } // MaxAge is a shortcut to set a simple duration as a MaxAgeFunc. // // Usage: // app.Get("/", cache.Cache(cache.MaxAge(1*time.Minute), mainHandler) func MaxAge(dur time.Duration) client.MaxAgeFunc { return func(*context.Context) time.Duration { return dur } } // Cache accepts the cache expiration duration. // If the "maxAgeFunc" input argument is nil, // then expiration is taken by the "cache-control's maxage" header. // Returns a Handler structure which you can use to customize cache further. // // All types of response can be cached, templates, json, text, anything. // // Use it for server-side caching, see the `iris#Cache304` for an alternative approach that // may be more suited to your needs. // // You can add validators with this function. func Cache(maxAgeFunc client.MaxAgeFunc) *client.Handler { if maxAgeFunc == nil { maxAgeFunc = DefaultMaxAge } return client.NewHandler(maxAgeFunc) } // Handler like `Cache` but returns an Iris Handler to be used as a middleware. // For more options use the `Cache`. // // Examples can be found at: https://github.com/kataras/iris/tree/main/_examples/response-writer/cache func Handler(expiration time.Duration) context.Handler { maxAgeFunc := func(*context.Context) time.Duration { return expiration } h := Cache(maxAgeFunc).ServeHTTP return h } ================================================ FILE: cache/cache_test.go ================================================ package cache_test import ( "fmt" "net/http" "sync/atomic" "testing" "time" "github.com/kataras/iris/v12/cache" "github.com/kataras/iris/v12/cache/client" "github.com/kataras/iris/v12/cache/client/rule" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/context" "github.com/iris-contrib/httpexpect/v2" "github.com/kataras/iris/v12/httptest" ) var ( cacheDuration = 2 * time.Second expectedBodyStr = "Imagine it as a big message to achieve x20 response performance!" ) type testError struct { expected int got uint32 } func (h *testError) Error() string { return fmt.Sprintf("expected the main handler to be executed %d times instead of %d", h.expected, h.got) } func runTest(e *httpexpect.Expect, path string, counterPtr *uint32, expectedBodyStr string, nocache string) error { e.GET(path).Expect().Status(http.StatusOK).Body().IsEqual(expectedBodyStr) time.Sleep(cacheDuration / 5) // lets wait for a while, cache should be saved and ready e.GET(path).Expect().Status(http.StatusOK).Body().IsEqual(expectedBodyStr) counter := atomic.LoadUint32(counterPtr) if counter > 1 { // n should be 1 because it doesn't changed after the first call return &testError{1, counter} } time.Sleep(cacheDuration) // cache should be cleared now e.GET(path).Expect().Status(http.StatusOK).Body().IsEqual(expectedBodyStr) time.Sleep(cacheDuration / 5) // let's call again , the cache should be saved e.GET(path).Expect().Status(http.StatusOK).Body().IsEqual(expectedBodyStr) counter = atomic.LoadUint32(counterPtr) if counter != 2 { return &testError{2, counter} } // we have cache response saved for the path, we have some time more here, but here // we will make the requestS with some of the deniers options e.GET(path).WithHeader("max-age", "0").Expect().Status(http.StatusOK).Body().IsEqual(expectedBodyStr) e.GET(path).WithHeader("Authorization", "basic or anything").Expect().Status(http.StatusOK).Body().IsEqual(expectedBodyStr) counter = atomic.LoadUint32(counterPtr) if counter != 4 { return &testError{4, counter} } if nocache != "" { // test the NoCache, first sleep to pass the cache expiration, // second add to the cache with a valid request and response // third, do it with the "/nocache" path (static for now, pure test design) given by the consumer time.Sleep(cacheDuration) // cache should be cleared now, this should work because we are not in the "nocache" path e.GET("/").Expect().Status(http.StatusOK).Body().IsEqual(expectedBodyStr) // counter = 5 time.Sleep(cacheDuration / 5) // let's call the "nocache", the expiration is not passed so but the "nocache" // route's path has the cache.NoCache so it should be not cached and the counter should be ++ e.GET(nocache).Expect().Status(http.StatusOK).Body().IsEqual(expectedBodyStr) // counter should be 6 counter = atomic.LoadUint32(counterPtr) if counter != 6 { // 4 before, 5 with the first call to store the cache, and six with the no cache, again original handler executation return &testError{6, counter} } // let's call again the path the expiration is not passed so it should be cached e.GET(path).Expect().Status(http.StatusOK).Body().IsEqual(expectedBodyStr) counter = atomic.LoadUint32(counterPtr) if counter != 6 { return &testError{6, counter} } // but now check for the No } return nil } func TestClientNoCache(t *testing.T) { app := iris.New() var n uint32 app.Get("/", cache.Handler(cacheDuration), func(ctx *context.Context) { atomic.AddUint32(&n, 1) ctx.Write([]byte(expectedBodyStr)) }) app.Get("/nocache", cache.Handler(cacheDuration), func(ctx *context.Context) { client.NoCache(ctx) // <---- atomic.AddUint32(&n, 1) ctx.Write([]byte(expectedBodyStr)) }) e := httptest.New(t, app) if err := runTest(e, "/", &n, expectedBodyStr, "/nocache"); err != nil { t.Fatalf(t.Name()+": %v", err) } } func TestCache(t *testing.T) { app := iris.New() var n uint32 app.Use(cache.Handler(cacheDuration)) app.Get("/", func(ctx *context.Context) { atomic.AddUint32(&n, 1) ctx.Write([]byte(expectedBodyStr)) }) var ( n2 uint32 expectedBodyStr2 = "This is the other" ) app.Get("/other", func(ctx *context.Context) { atomic.AddUint32(&n2, 1) ctx.Write([]byte(expectedBodyStr2)) }) e := httptest.New(t, app) if err := runTest(e, "/", &n, expectedBodyStr, ""); err != nil { t.Fatalf(t.Name()+": %v", err) } if err := runTest(e, "/other", &n2, expectedBodyStr2, ""); err != nil { t.Fatalf(t.Name()+" other: %v", err) } } // This works but we have issue on golog.SetLevel and get golog.Level on httptest.New // when tests are running in parallel and the loggers are used. // // TODO: Fix it on golog repository or here, we'll see. // func TestCacheHandlerParallel(t *testing.T) { // t.Parallel() // TestCache(t) // } func TestCacheValidator(t *testing.T) { app := iris.New() var n uint32 h := func(ctx *context.Context) { atomic.AddUint32(&n, 1) ctx.Write([]byte(expectedBodyStr)) } validCache := cache.Handler(cacheDuration) app.Get("/", validCache, h) managedCache := cache.Cache(cache.MaxAge(cacheDuration)) managedCache.AddRule(rule.Validator([]rule.PreValidator{ func(ctx *context.Context) bool { // should always invalid for cache, don't bother to go to try to get or set cache return ctx.Request().URL.Path != "/invalid" }, }, nil)) managedCache2 := cache.Cache(cache.MaxAge(cacheDuration)) managedCache2.AddRule(rule.Validator(nil, []rule.PostValidator{ func(ctx *context.Context) bool { // it's passed the Claim and now Valid checks if the response contains a header of "DONT" return ctx.ResponseWriter().Header().Get("DONT") == "" }, }, )) app.Get("/valid", validCache, h) app.Get("/invalid", managedCache.ServeHTTP, h) app.Get("/invalid2", managedCache2.ServeHTTP, func(ctx *context.Context) { atomic.AddUint32(&n, 1) ctx.Header("DONT", "DO not cache that response even if it was claimed") ctx.Write([]byte(expectedBodyStr)) }) e := httptest.New(t, app) // execute from cache the next time e.GET("/valid").Expect().Status(http.StatusOK).Body().IsEqual(expectedBodyStr) time.Sleep(cacheDuration / 5) // lets wait for a while, cache should be saved and ready e.GET("/valid").Expect().Status(http.StatusOK).Body().IsEqual(expectedBodyStr) counter := atomic.LoadUint32(&n) if counter > 1 { // n should be 1 because it doesn't changed after the first call t.Fatalf("%s: %v", t.Name(), &testError{1, counter}) } // don't execute from cache, execute the original, counter should ++ here e.GET("/invalid").Expect().Status(http.StatusOK).Body().IsEqual(expectedBodyStr) // counter = 2 e.GET("/invalid2").Expect().Status(http.StatusOK).Body().IsEqual(expectedBodyStr) // counter = 3 counter = atomic.LoadUint32(&n) if counter != 3 { // n should be 1 because it doesn't changed after the first call t.Fatalf("%s: %v", t.Name(), &testError{3, counter}) } } ================================================ FILE: cache/cfg/cfg.go ================================================ package cfg import "time" // The constants be used by both client and server var ( FailStatus = 400 SuccessStatus = 200 ContentHTML = "text/html; charset=utf-8" ContentTypeHeader = "Content-Type" StatusCodeHeader = "Status" QueryCacheKey = "cache_key" QueryCacheDuration = "cache_duration" QueryCacheStatusCode = "cache_status_code" QueryCacheContentType = "cache_content_type" RequestCacheTimeout = 5 * time.Second ) // NoCacheHeader is the static header key which is set to the response when NoCache is called, // used inside nethttp and fhttp Skippers. var NoCacheHeader = "X-No-Cache" // MinimumCacheDuration is the minimum duration from time.Now // which is allowed between cache save and cache clear var MinimumCacheDuration = 2 * time.Second ================================================ FILE: cache/client/client.go ================================================ package client import ( "bytes" "io" "net/http" "time" "github.com/kataras/iris/v12/cache/cfg" "github.com/kataras/iris/v12/cache/client/rule" "github.com/kataras/iris/v12/cache/uri" "github.com/kataras/iris/v12/context" ) // ClientHandler is the client-side handler // for each of the cached route paths's response // register one client handler per route. // // it's just calls a remote cache service server/handler, which may lives on other, external machine. type ClientHandler struct { // bodyHandler the original route's handler bodyHandler context.Handler // Rule optional validators for pre cache and post cache actions // // See more at ruleset.go rule rule.Rule life time.Duration remoteHandlerURL string } // NewClientHandler returns a new remote client handler // which asks the remote handler the cached entry's response // with a GET request, or add a response with POST request // these all are done automatically, users can use this // handler as they use the local.go/NewHandler // // the ClientHandler is useful when user // wants to apply horizontal scaling to the app and // has a central http server which handles func NewClientHandler(bodyHandler context.Handler, life time.Duration, remote string) *ClientHandler { return &ClientHandler{ bodyHandler: bodyHandler, rule: DefaultRuleSet, life: life, remoteHandlerURL: remote, } } // Rule sets the ruleset for this handler, // see internal/net/http/ruleset.go for more information. // // returns itself. func (h *ClientHandler) Rule(r rule.Rule) *ClientHandler { if r == nil { // if nothing passed then use the allow-everything rule r = rule.Satisfied() } h.rule = r return h } // AddRule adds a rule in the chain, the default rules are executed first. // // returns itself. func (h *ClientHandler) AddRule(r rule.Rule) *ClientHandler { if r == nil { return h } h.rule = rule.Chained(h.rule, r) return h } // Client is used inside the global Request function // this client is an exported to give you a freedom of change its Transport, Timeout and so on(in case of ssl) var Client = &http.Client{Timeout: cfg.RequestCacheTimeout} // ServeHTTP , or remote cache client whatever you like, it's the client-side function of the ServeHTTP // sends a request to the server-side remote cache Service and sends the cached response to the frontend client // it is used only when you achieved something like horizontal scaling (separate machines) // look ../remote/remote.ServeHTTP for more // // if cache din't find then it sends a POST request and save the bodyHandler's body to the remote cache. // // It takes 3 parameters // the first is the remote address (it's the address you started your http server which handled by the Service.ServeHTTP) // the second is the handler (or the mux) you want to cache // and the third is the, optionally, cache expiration, // which is used to set cache duration of this specific cache entry to the remote cache service // if <=minimumAllowedCacheDuration then the server will try to parse from "cache-control" header // // client-side function func (h *ClientHandler) ServeHTTP(ctx *context.Context) { // check for deniers, if at least one of them return true // for this specific request, then skip the whole cache if !h.rule.Claim(ctx) { h.bodyHandler(ctx) return } uri := &uri.URIBuilder{} uri.ServerAddr(h.remoteHandlerURL).ClientURI(ctx.Request().URL.RequestURI()).ClientMethod(ctx.Request().Method) // set the full url here because below we have other issues, probably net/http bugs request, err := http.NewRequest(http.MethodGet, uri.String(), nil) if err != nil { //// println("error when requesting to the remote service: " + err.Error()) // somehing very bad happens, just execute the user's handler and return h.bodyHandler(ctx) return } // println("GET Do to the remote cache service with the url: " + request.URL.String()) response, err := Client.Do(request) if err != nil || response.StatusCode == cfg.FailStatus { // if not found on cache, then execute the handler and save the cache to the remote server recorder := ctx.Recorder() h.bodyHandler(ctx) // check if it's a valid response, if it's not then just return. if !h.rule.Valid(ctx) { return } // save to the remote cache // we re-create the request for any case body := recorder.Body()[0:] if len(body) == 0 { //// println("Request: len body is zero, do nothing") return } uri.StatusCode(recorder.StatusCode()) uri.Lifetime(h.life) uri.ContentType(recorder.Header().Get(cfg.ContentTypeHeader)) request, err = http.NewRequest(http.MethodPost, uri.String(), bytes.NewBuffer(body)) // yes new buffer every time // println("POST Do to the remote cache service with the url: " + request.URL.String()) if err != nil { //// println("Request: error on method Post of request to the remote: " + err.Error()) return } // go Client.Do(request) resp, err := Client.Do(request) if err != nil { return } resp.Body.Close() } else { // get the status code , content type and the write the response body ctx.ContentType(response.Header.Get(cfg.ContentTypeHeader)) ctx.StatusCode(response.StatusCode) responseBody, err := io.ReadAll(response.Body) response.Body.Close() if err != nil { return } _, _ = ctx.Write(responseBody) } } ================================================ FILE: cache/client/handler.go ================================================ package client import ( "net/http" "strings" "time" "github.com/kataras/iris/v12/cache/client/rule" "github.com/kataras/iris/v12/cache/entry" "github.com/kataras/iris/v12/context" ) func init() { context.SetHandlerName("iris/cache/client.(*Handler).ServeHTTP-fm", "iris.cache") } // Handler the local cache service handler contains // the original response, the memory cache entry and // the validator for each of the incoming requests and post responses type Handler struct { // Rule optional validators for pre cache and post cache actions // // See more at ruleset.go rule rule.Rule // when expires. maxAgeFunc MaxAgeFunc // entries the memory cache stored responses. entryPool *entry.Pool entryStore entry.Store } type MaxAgeFunc func(*context.Context) time.Duration // NewHandler returns a new Server-side cached handler for the "bodyHandler" // which expires every "expiration". func NewHandler(maxAgeFunc MaxAgeFunc) *Handler { return &Handler{ rule: DefaultRuleSet, maxAgeFunc: maxAgeFunc, entryPool: entry.NewPool(), entryStore: entry.NewMemStore(), } } // Rule sets the ruleset for this handler. // // returns itself. func (h *Handler) Rule(r rule.Rule) *Handler { if r == nil { // if nothing passed then use the allow-everything rule r = rule.Satisfied() } h.rule = r return h } // AddRule adds a rule in the chain, the default rules are executed first. // // returns itself. func (h *Handler) AddRule(r rule.Rule) *Handler { if r == nil { return h } h.rule = rule.Chained(h.rule, r) return h } // Store sets a custom store for this handler. func (h *Handler) Store(store entry.Store) *Handler { h.entryStore = store return h } // MaxAge customizes the expiration duration for this handler. func (h *Handler) MaxAge(fn MaxAgeFunc) *Handler { h.maxAgeFunc = fn return h } var emptyHandler = func(ctx *context.Context) { ctx.StopWithText(500, "cache: empty body handler") } const entryKeyContextKey = "iris.cache.server.entry.key" // SetKey sets a custom entry key for cached pages. // See root package-level `WithKey` instead. func SetKey(ctx *context.Context, key string) { ctx.Values().Set(entryKeyContextKey, key) } // GetKey returns the entry key for the current page. func GetKey(ctx *context.Context) string { return ctx.Values().GetString(entryKeyContextKey) } func getOrSetKey(ctx *context.Context) string { if key := GetKey(ctx); key != "" { return key } // Note: by-default the rules(ruleset pkg) // explicitly ignores the cache handler // execution on authenticated requests // and immediately runs the next handler: // if !h.rule.Claim(ctx) ...see `Handler` method. // So the below two lines are useless, // however we add it for cases // that the end-developer messedup with the rules // and by accident allow authenticated cached results. username, password, _ := ctx.Request().BasicAuth() authPart := username + strings.Repeat("*", len(password)) key := ctx.Method() + authPart u := ctx.Request().URL if !u.IsAbs() { key += ctx.Scheme() + ctx.Host() } key += u.String() SetKey(ctx, key) return key } func (h *Handler) ServeHTTP(ctx *context.Context) { // check for pre-cache validators, if at least one of them return false // for this specific request, then skip the whole cache bodyHandler := ctx.NextHandler() if bodyHandler == nil { emptyHandler(ctx) return } // skip prepares the context to move to the next handler if the "nextHandler" has a ctx.Next() inside it, // even if it's not executed because it's cached. ctx.Skip() if !h.rule.Claim(ctx) { bodyHandler(ctx) return } key := getOrSetKey(ctx) // unique per subdomains and paths with different url query. e := h.entryStore.Get(key) if e == nil { // if it's expired, then execute the original handler // with our custom response recorder response writer // because the net/http doesn't give us // a builtin way to get the status code & body recorder := ctx.Recorder() bodyHandler(ctx) // now that we have recordered the response, // we are ready to check if that specific response is valid to be stored. // check if it's a valid response, if it's not then just return. if !h.rule.Valid(ctx) { return } // no need to copy the body, its already done inside body := recorder.Body() if len(body) == 0 { // if no body then just exit. return } // fmt.Printf("reset cache entry\n") // fmt.Printf("key: %s\n", key) // fmt.Printf("content type: %s\n", recorder.Header().Get(cfg.ContentTypeHeader)) // fmt.Printf("body len: %d\n", len(body)) r := entry.NewResponse(recorder.StatusCode(), recorder.Header(), body) e = h.entryPool.Acquire(h.maxAgeFunc(ctx), r, func() { h.entryStore.Delete(key) }) h.entryStore.Set(key, e) return } // if it's valid then just write the cached results r := e.Response() // if !ok { // // it shouldn't be happen because if it's not valid (= expired) // // then it shouldn't be found on the store, we return as it is, the body was written. // return // } copyHeaders(ctx.ResponseWriter().Header(), r.Headers()) ctx.SetLastModified(e.LastModified) ctx.StatusCode(r.StatusCode()) ctx.Write(r.Body()) // fmt.Printf("key: %s\n", key) // fmt.Printf("write content type: %s\n", response.Headers()["ContentType"]) // fmt.Printf("write body len: %d\n", len(response.Body())) } func copyHeaders(dst, src http.Header) { // Clone returns a copy of h or nil if h is nil. if src == nil { return } // Find total number of values. nv := 0 for _, vv := range src { nv += len(vv) } sv := make([]string, nv) // shared backing array for headers' values for k, vv := range src { if vv == nil { // Preserve nil values. ReverseProxy distinguishes // between nil and zero-length header values. dst[k] = nil continue } n := copy(sv, vv) dst[k] = sv[:n:n] sv = sv[n:] } } ================================================ FILE: cache/client/rule/chained.go ================================================ package rule import ( "github.com/kataras/iris/v12/context" ) // chainedRule is a Rule with next Rule type chainedRule struct { Rule next Rule } var _ Rule = &chainedRule{} // chainedSingle returns a new rule witch has a next rule too func chainedSingle(rule Rule, next Rule) Rule { if next == nil { next = Satisfied() } return &chainedRule{ Rule: rule, next: next, } } // Chained returns a new rule which has more than one coming next ruleset func Chained(rule Rule, next ...Rule) Rule { if len(next) == 0 { return chainedSingle(rule, nil) } c := chainedSingle(rule, next[0]) for i := 1; i < len(next); i++ { c = chainedSingle(c, next[i]) } return c } // Claim validator func (c *chainedRule) Claim(ctx *context.Context) bool { if !c.Rule.Claim(ctx) { return false } return c.next.Claim(ctx) } // Valid validator func (c *chainedRule) Valid(ctx *context.Context) bool { if !c.Rule.Valid(ctx) { return false } return c.next.Valid(ctx) } ================================================ FILE: cache/client/rule/conditional.go ================================================ package rule import ( "github.com/kataras/iris/v12/context" ) // Conditional is a Rule witch adds a predicate in order to its methods to execute type conditionalRule struct { claimPredicate func() bool validPredicate func() bool } var emptyConditionalPredicate = func() bool { return true } var _ Rule = &conditionalRule{} // Conditional returns a new rule witch has conditionals func Conditional(claimPredicate func() bool, validPredicate func() bool) Rule { if claimPredicate == nil { claimPredicate = emptyConditionalPredicate } if validPredicate == nil { validPredicate = emptyConditionalPredicate } return &conditionalRule{ claimPredicate: claimPredicate, validPredicate: validPredicate, } } // Claim validator func (c *conditionalRule) Claim(ctx *context.Context) bool { return c.claimPredicate() } // Valid validator func (c *conditionalRule) Valid(ctx *context.Context) bool { return c.validPredicate() } ================================================ FILE: cache/client/rule/header.go ================================================ package rule import ( "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/cache/ruleset" ) // The HeaderPredicate should be alived on each of $package/rule BUT GOLANG DOESN'T SUPPORT type alias and I don't want to have so many copies around // read more at ../../ruleset.go // headerRule is a Rule witch receives and checks for a header predicates // request headers on Claim and response headers on Valid. type headerRule struct { claim ruleset.HeaderPredicate valid ruleset.HeaderPredicate } var _ Rule = &headerRule{} // Header returns a new rule witch claims and execute the post validations trough headers func Header(claim ruleset.HeaderPredicate, valid ruleset.HeaderPredicate) Rule { if claim == nil { claim = ruleset.EmptyHeaderPredicate } if valid == nil { valid = ruleset.EmptyHeaderPredicate } return &headerRule{ claim: claim, valid: valid, } } // HeaderClaim returns a header rule which cares only about claiming (pre-validation) func HeaderClaim(claim ruleset.HeaderPredicate) Rule { return Header(claim, nil) } // HeaderValid returns a header rule which cares only about valid (post-validation) func HeaderValid(valid ruleset.HeaderPredicate) Rule { return Header(nil, valid) } // Claim validator func (h *headerRule) Claim(ctx *context.Context) bool { return h.claim(ctx.Request().Header.Get) } // Valid validator func (h *headerRule) Valid(ctx *context.Context) bool { return h.valid(ctx.ResponseWriter().Header().Get) } ================================================ FILE: cache/client/rule/not_satisfied.go ================================================ package rule import ( "github.com/kataras/iris/v12/context" ) type notSatisfiedRule struct{} var _ Rule = ¬SatisfiedRule{} // NotSatisfied returns a rule which allows nothing func NotSatisfied() Rule { return ¬SatisfiedRule{} } func (n *notSatisfiedRule) Claim(*context.Context) bool { return false } func (n *notSatisfiedRule) Valid(*context.Context) bool { return false } ================================================ FILE: cache/client/rule/rule.go ================================================ package rule import "github.com/kataras/iris/v12/context" // Rule a superset of validators type Rule interface { Claim(ctx *context.Context) bool Valid(ctx *context.Context) bool } ================================================ FILE: cache/client/rule/satisfied.go ================================================ package rule import ( "github.com/kataras/iris/v12/context" ) type satisfiedRule struct{} var _ Rule = &satisfiedRule{} // Satisfied returns a rule which allows anything, // it's usually the last rule on chained rules if no next rule is given, // but it can be used outside of a chain too as a default allow-all rule. func Satisfied() Rule { return &satisfiedRule{} } func (n *satisfiedRule) Claim(*context.Context) bool { return true } func (n *satisfiedRule) Valid(*context.Context) bool { return true } ================================================ FILE: cache/client/rule/validator.go ================================================ package rule import "github.com/kataras/iris/v12/context" // Validators are introduced to implement the RFC about cache (https://tools.ietf.org/html/rfc7234#section-1.1). // PreValidator like middleware, executes before the cache action begins, if a callback returns false // then this specific cache action, with specific request, is ignored and the real (original) // handler is executed instead. // // I'll not add all specifications here I'll give the opportunity (public API in the httpcache package-level) // to the end-user to specify her/his ignore rules too (ignore-only for now). // // Each package, nethttp and fhttp should implement their own encapsulations because of different request object. // // One function, accepts the request and returns false if should be denied/ignore, otherwise true. // if at least one return false then the original handler will execute as it's // and the whole cache action(set & get) should be ignored, it will be never go to the step of post-cache validations. type PreValidator func(*context.Context) bool // PostValidator type is is introduced to implement the second part of the RFC about cache. // // Q: What's the difference between this and a PreValidator? // A: PreValidator runs BEFORE trying to get the cache, it cares only for the request // // and if at least one PreValidator returns false then it just runs the original handler and stop there, at the other hand // a PostValidator runs if all PreValidators returns true and original handler is executed but with a response recorder, // also the PostValidator should return true to store the cached response. // Last, a PostValidator accepts a context // in order to be able to catch the original handler's response, // the PreValidator checks only for request. // // If a function of type of PostValidator returns true then the (shared-always) cache is allowed to be stored. type PostValidator func(*context.Context) bool // validatorRule is a rule witch receives PreValidators and PostValidators // it's a 'complete set of rules', you can call it as a Responsible Validator, // it's used when you the user wants to check for special things inside a request and a response. type validatorRule struct { // preValidators a list of PreValidator functions, execute before real cache begins // if at least one of them returns false then the original handler will execute as it's // and the whole cache action(set & get) will be skipped for this specific client's request. // // Read-only 'runtime' preValidators []PreValidator // postValidators a list of PostValidator functions, execute after the original handler is executed with the response recorder // and exactly before this cached response is saved, // if at least one of them returns false then the response will be not saved for this specific client's request. // // Read-only 'runtime' postValidators []PostValidator } var _ Rule = &validatorRule{} // DefaultValidator returns a new validator which contains the default pre and post cache validators func DefaultValidator() Rule { return Validator(nil, nil) } // Validator receives the preValidators and postValidators and returns a new Validator rule func Validator(preValidators []PreValidator, postValidators []PostValidator) Rule { return &validatorRule{ preValidators: preValidators, postValidators: postValidators, } } // Claim returns true if incoming request can claim for a cached handler // the original handler should run as it is and exit func (v *validatorRule) Claim(ctx *context.Context) bool { // check for pre-cache validators, if at least one of them return false // for this specific request, then skip the whole cache for _, shouldProcess := range v.preValidators { if !shouldProcess(ctx) { return false } } return true } // Valid returns true if incoming request and post-response from the original handler // is valid to be store to the cache, if not(false) then the consumer should just exit // otherwise(true) the consumer should store the cached response func (v *validatorRule) Valid(ctx *context.Context) bool { // check if it's a valid response, if it's not then just return. for _, valid := range v.postValidators { if !valid(ctx) { return false } } return true } ================================================ FILE: cache/client/ruleset.go ================================================ package client import ( "github.com/kataras/iris/v12/cache/cfg" "github.com/kataras/iris/v12/cache/client/rule" "github.com/kataras/iris/v12/cache/ruleset" "github.com/kataras/iris/v12/context" ) // DefaultRuleSet is a list of the default pre-cache validators // which exists in ALL handlers, local and remote. var DefaultRuleSet = rule.Chained( // #1 A shared cache MUST NOT use a cached response to a request with an // Authorization header field rule.HeaderClaim(ruleset.AuthorizationRule), // #2 "must-revalidate" and/or // "s-maxage" response directives are not allowed to be served stale // (Section 4.2.4) by shared caches. In particular, a response with // either "max-age=0, must-revalidate" or "s-maxage=0" cannot be used to // satisfy a subsequent request without revalidating it on the origin // server. rule.HeaderClaim(ruleset.MustRevalidateRule), rule.HeaderClaim(ruleset.ZeroMaxAgeRule), // #3 custom No-Cache header used inside this library // for BOTH request and response (after get-cache action) rule.Header(ruleset.NoCacheRule, ruleset.NoCacheRule), ) // NoCache disables the cache for a particular request, // can be used as a middleware or called manually from the handler. func NoCache(ctx *context.Context) { ctx.Header(cfg.NoCacheHeader, "true") } ================================================ FILE: cache/entry/entry.go ================================================ package entry import ( "time" "github.com/kataras/iris/v12/core/memstore" ) // Entry is the cache entry // contains the expiration datetime and the response type Entry struct { // ExpiresAt is the time which this cache will not be available lifeTime *memstore.LifeTime // when `Reset` this value is reseting to time.Now(), // it's used to send the "Last-Modified" header, // some clients may need it. LastModified time.Time // Response the response should be served to the client response *Response // but we need the key to invalidate manually...xmm // let's see for that later, maybe we make a slice instead // of store map } // reset called each time a new entry is acquired from the pool. func (e *Entry) reset(lt *memstore.LifeTime, r *Response) { e.response = r e.LastModified = lt.Begun } // Response returns the cached response as it's. func (e *Entry) Response() *Response { return e.response } // // Response gets the cache response contents // // if it's valid returns them with a true value // // otherwise returns nil, false // func (e *Entry) Response() (*Response, bool) { // if !e.isValid() { // // it has been expired // return nil, false // } // return e.response, true // } // // isValid reports whether this entry's response is still valid or expired. // // If the entry exists in the store then it should be valid anyways. // func (e *Entry) isValid() bool { // return !e.lifeTime.HasExpired() // } ================================================ FILE: cache/entry/pool.go ================================================ package entry import ( "sync" "time" "github.com/kataras/iris/v12/cache/cfg" "github.com/kataras/iris/v12/core/memstore" ) // Pool is the context pool, it's used inside router and the framework by itself. type Pool struct { pool *sync.Pool } // NewPool creates and returns a new context pool. func NewPool() *Pool { return &Pool{pool: &sync.Pool{New: func() any { return &Entry{} }}} } // func NewPool(newFunc func() any) *Pool { // return &Pool{pool: &sync.Pool{New: newFunc}} // } // Acquire returns an Entry from pool. // See Release. func (c *Pool) Acquire(lifeDuration time.Duration, r *Response, onExpire func()) *Entry { // If the given duration is not <=0 (which means finds from the headers) // then we should check for the MinimumCacheDuration here if lifeDuration >= 0 && lifeDuration < cfg.MinimumCacheDuration { lifeDuration = cfg.MinimumCacheDuration } e := c.pool.Get().(*Entry) lt := memstore.NewLifeTime() lt.Begin(lifeDuration, func() { onExpire() c.release(e) }) e.reset(lt, r) return e } // Release puts an Entry back to its pull, this function releases its resources. // See Acquire. func (c *Pool) release(e *Entry) { e.response.body = nil e.response.headers = nil e.response.statusCode = 0 e.response = nil // do not call it, it contains a lock too, release is controlled only inside the Acquire itself when the entry is expired. // if e.lifeTime != nil { // e.lifeTime.ExpireNow() // stop any opening timers if force released. // } c.pool.Put(e) } // Release can be called by custom stores to release an entry. func (c *Pool) Release(e *Entry) { if e.lifeTime != nil { e.lifeTime.ExpireNow() // stop any opening timers if force released. } c.release(e) } ================================================ FILE: cache/entry/response.go ================================================ package entry import ( "io" "net/http" ) // Response is the cached response will be send to the clients // its fields set at runtime on each of the non-cached executions // non-cached executions = first execution, and each time after // cache expiration datetime passed. type Response struct { // statusCode for the response cache handler. statusCode int // body is the contents will be served by the cache handler. body []byte // the total headers of the response, including content type. headers http.Header } // NewResponse returns a new cached Response. func NewResponse(statusCode int, headers http.Header, body []byte) *Response { r := new(Response) r.SetStatusCode(statusCode) r.SetHeaders(headers) r.SetBody(body) return r } // SetStatusCode sets a valid status code. func (r *Response) SetStatusCode(statusCode int) { if statusCode <= 0 { statusCode = http.StatusOK } r.statusCode = statusCode } // StatusCode returns a valid status code. func (r *Response) StatusCode() int { return r.statusCode } // ContentType returns a valid content type // func (r *Response) ContentType() string { // if r.headers == "" { // r.contentType = "text/html; charset=utf-8" // } // return r.contentType // } // SetHeaders sets a clone of headers of the cached response. func (r *Response) SetHeaders(h http.Header) { r.headers = h.Clone() } // Headers returns the total headers of the cached response. func (r *Response) Headers() http.Header { return r.headers } // SetBody consumes "b" and sets the body of the cached response. func (r *Response) SetBody(body []byte) { r.body = make([]byte, len(body)) copy(r.body, body) } // Body returns contents will be served by the cache handler. func (r *Response) Body() []byte { return r.body } // Read implements the io.Reader interface. func (r *Response) Read(b []byte) (int, error) { if len(r.body) == 0 { return 0, io.EOF } n := copy(b, r.body) r.body = r.body[n:] return n, nil } // Bytes returns a copy of the cached response body. func (r *Response) Bytes() []byte { return append([]byte(nil), r.body...) } ================================================ FILE: cache/entry/store.go ================================================ package entry import ( "sync" ) // Store is the interface which is responsible to store the cache entries. type Store interface { // Get returns an entry based on its key. Get(key string) *Entry // Set sets an entry based on its key. Set(key string, e *Entry) // Delete deletes an entry based on its key. Delete(key string) } // memStore is the default in-memory store for the cache entries. type memStore struct { entries map[string]*Entry mu sync.RWMutex } var _ Store = (*memStore)(nil) // NewMemStore returns a new in-memory store for the cache entries. func NewMemStore() Store { return &memStore{ entries: make(map[string]*Entry), } } // Get returns an entry based on its key. func (s *memStore) Get(key string) *Entry { s.mu.RLock() e := s.entries[key] s.mu.RUnlock() return e } // Set sets an entry based on its key. func (s *memStore) Set(key string, e *Entry) { s.mu.Lock() s.entries[key] = e s.mu.Unlock() } // Delete deletes an entry based on its key. func (s *memStore) Delete(key string) { s.mu.Lock() delete(s.entries, key) s.mu.Unlock() } ================================================ FILE: cache/ruleset/ruleset.go ================================================ // Package ruleset provides the basics rules which are being extended by rules. package ruleset // The shared header-mostly rules for both nethttp and fasthttp var ( AuthorizationRule = func(header GetHeader) bool { return header("Authorization") == "" && header("Proxy-Authenticate") == "" } MustRevalidateRule = func(header GetHeader) bool { return header("Must-Revalidate") == "" } ZeroMaxAgeRule = func(header GetHeader) bool { return header("S-Maxage") != "0" && header("Max-Age") != "0" } NoCacheRule = func(header GetHeader) bool { return header("No-Cache") != "true" } ) // THESE ARE HERE BECAUSE THE GOLANG DOESN'T SUPPORTS THE F.... INTERFACE ALIAS, THIS SHOULD EXISTS ONLY ON /$package/rule // or somehow move interface generic rules (such as conditional, header) here, because the code sharing is exactly THE SAME // except the -end interface, this on other language can be designing very very nice but here no OOP so we stuck on this, // it's better than before but not as I wanted to be. // They will make me to forget my software design skills, // they (the language's designers) rollback the feature of type alias, BLOG POSTING that is an UNUSEFUL feature..... // GetHeader is a type of func which receives a func of string which returns a string // used to get headers values, both request's and response's. type GetHeader func(string) string // HeaderPredicate is a type of func which receives a func of string which returns a string and a boolean, // used to get headers values, both request's and response's. type HeaderPredicate func(GetHeader) bool // EmptyHeaderPredicate returns always true var EmptyHeaderPredicate = func(GetHeader) bool { return true } ================================================ FILE: cache/uri/uribuilder.go ================================================ package uri import ( "net/url" "strconv" "strings" "time" "github.com/kataras/iris/v12/cache/cfg" ) // URIBuilder is the requested url builder // which keeps all the information the server // should know to save or retrieve a cached entry's response // used on client-side only // both from net/http and valyala/fasthttp type URIBuilder struct { serverAddr, clientMethod, clientURI string cacheLifetime time.Duration cacheStatuscode int cacheContentType string } // ServerAddr sets the server address for the final request url func (r *URIBuilder) ServerAddr(s string) *URIBuilder { r.serverAddr = s return r } // ClientMethod sets the method which the client should call the remote server's handler // used to build the final request url too func (r *URIBuilder) ClientMethod(s string) *URIBuilder { r.clientMethod = s return r } // ClientURI sets the client path for the final request url func (r *URIBuilder) ClientURI(s string) *URIBuilder { r.clientURI = s return r } // Lifetime sets the cache lifetime for the final request url func (r *URIBuilder) Lifetime(d time.Duration) *URIBuilder { r.cacheLifetime = d return r } // StatusCode sets the cache status code for the final request url func (r *URIBuilder) StatusCode(code int) *URIBuilder { r.cacheStatuscode = code return r } // ContentType sets the cache content type for the final request url func (r *URIBuilder) ContentType(s string) *URIBuilder { r.cacheContentType = s return r } // String returns the full url which should be passed to get a cache entry response back // (it could be set by server too but we need some client-freedom on the requested key) // in order to be sure that the registered cache entries are unique among different clients with the same key // note1: we do it manually*, // note2: on fasthttp that is not required because the query args added as expected but we will use it there too to be align with net/http func (r URIBuilder) String() string { return r.build() } func (r URIBuilder) build() string { remoteURL := r.serverAddr // fasthttp appends the "/" in the last uri (with query args also, that's probably a fasthttp bug which I'll fix later) // for now lets make that check: if !strings.HasSuffix(remoteURL, "/") { remoteURL += "/" } scheme := "http://" // validate the remoteURL, should contains a scheme, if not then check if the client has given a scheme and if so // use that for the server too if !strings.Contains(remoteURL, "://") { if strings.Contains(remoteURL, ":443") || strings.Contains(remoteURL, ":https") { remoteURL = "https://" + remoteURL } else { remoteURL = scheme + "://" + remoteURL } } var cacheDurationStr, statusCodeStr string if r.cacheLifetime.Seconds() > 0 { cacheDurationStr = strconv.Itoa(int(r.cacheLifetime.Seconds())) } if r.cacheStatuscode > 0 { statusCodeStr = strconv.Itoa(r.cacheStatuscode) } s := remoteURL + "?" + cfg.QueryCacheKey + "=" + url.QueryEscape(r.clientMethod+scheme+r.clientURI) if cacheDurationStr != "" { s += "&" + cfg.QueryCacheDuration + "=" + url.QueryEscape(cacheDurationStr) } if statusCodeStr != "" { s += "&" + cfg.QueryCacheStatusCode + "=" + url.QueryEscape(statusCodeStr) } if r.cacheContentType != "" { s += "&" + cfg.QueryCacheContentType + "=" + url.QueryEscape(r.cacheContentType) } return s } ================================================ FILE: cli.go ================================================ package iris // +------------------------------------------------------------+ // | Bridge code between iris-cli and iris web application | // | https://github.com/kataras/iris-cli | // +------------------------------------------------------------+ import ( "bytes" "fmt" "os" "path/filepath" "strings" "github.com/kataras/iris/v12/context" "gopkg.in/yaml.v3" ) // injectLiveReload tries to check if this application // runs under https://github.com/kataras/iris-cli and if so // then it checks if the livereload is enabled and then injects // the watch listener (js script) on every HTML response. // It has a slight performance cost but // this (iris-cli with watch and livereload enabled) // is meant to be used only in development mode. // It does a full reload at the moment and if the port changed // at runtime it will fire 404 instead of redirecting to the correct port (that's a TODO). // // tryInjectLiveReload runs right before Build -> BuildRouter. func injectLiveReload(r Party) (bool, error) { conf := struct { Running bool `yaml:"Running,omitempty"` LiveReload struct { Disable bool `yaml:"Disable"` Port int `yaml:"Port"` } `yaml:"LiveReload"` }{} // defaults to disabled here. conf.LiveReload.Disable = true wd, err := os.Getwd() if err != nil { return false, err } for _, path := range []string{".iris.yml" /*, "../.iris.yml", "../../.iris.yml" */} { path = filepath.Join(wd, path) if _, err := os.Stat(path); err == nil { inFile, err := os.OpenFile(path, os.O_RDONLY, 0600) if err != nil { return false, err } dec := yaml.NewDecoder(inFile) err = dec.Decode(&conf) inFile.Close() if err != nil { return false, err } break } } if !conf.Running || conf.LiveReload.Disable { return false, nil } scriptReloadJS := []byte(fmt.Sprintf(``, conf.LiveReload.Port)) bodyCloseTag := []byte("") r.UseRouter(func(ctx Context) { rec := ctx.Recorder() // Record everything and write all in once at the Context release. ctx.Next() // call the next, so this is a 'done' handler. if strings.HasPrefix(ctx.GetContentType(), "text/html") { // delete(rec.Header(), context.ContentLengthHeaderKey) body := rec.Body() if idx := bytes.LastIndex(body, bodyCloseTag); idx > 0 { // add the script right before last . body = append(body[:idx], bytes.Replace(body[idx:], bodyCloseTag, append(scriptReloadJS, bodyCloseTag...), 1)...) rec.SetBody(body) } else { // Just append it. rec.Write(scriptReloadJS) // nolint:errcheck } if _, has := rec.Header()[context.ContentLengthHeaderKey]; has { rec.Header().Set(context.ContentLengthHeaderKey, fmt.Sprintf("%d", len(rec.Body()))) } } }) return true, nil } ================================================ FILE: configuration.go ================================================ package iris import ( "fmt" "os" "os/user" "path/filepath" "runtime" "strings" "time" "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/core/netutil" "github.com/BurntSushi/toml" "github.com/kataras/golog" "github.com/kataras/sitemap" "github.com/kataras/tunnel" "gopkg.in/yaml.v3" ) const globalConfigurationKeyword = "~" // homeConfigurationFilename returns the physical location of the global configuration(yaml or toml) file. // This is useful when we run multiple iris servers that share the same // configuration, even with custom values at its "Other" field. // It will return a file location // which targets to $HOME or %HOMEDRIVE%+%HOMEPATH% + "iris" + the given "ext". func homeConfigurationFilename(ext string) string { return filepath.Join(homeDir(), "iris"+ext) } func homeDir() (home string) { u, err := user.Current() if u != nil && err == nil { home = u.HomeDir } if home == "" { home = os.Getenv("HOME") } if home == "" { switch runtime.GOOS { case "plan9": home = os.Getenv("home") case "windows": home = os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH") if home == "" { home = os.Getenv("USERPROFILE") } } } return } func parseYAML(filename string) (Configuration, error) { c := DefaultConfiguration() // get the abs // which will try to find the 'filename' from current workind dir too. yamlAbsPath, err := filepath.Abs(filename) if err != nil { return c, fmt.Errorf("parse yaml: %w", err) } // read the raw contents of the file data, err := os.ReadFile(yamlAbsPath) if err != nil { return c, fmt.Errorf("parse yaml: %w", err) } // put the file's contents as yaml to the default configuration(c) if err := yaml.Unmarshal(data, &c); err != nil { return c, fmt.Errorf("parse yaml: %w", err) } return c, nil } // YAML reads Configuration from a configuration.yml file. // // Accepts the absolute path of the cfg.yml. // An error will be shown to the user via panic with the error message. // Error may occur when the cfg.yml does not exist or is not formatted correctly. // // Note: if the char '~' passed as "filename" then it tries to load and return // the configuration from the $home_directory + iris.yml, // see `WithGlobalConfiguration` for more information. // // Usage: // app.Configure(iris.WithConfiguration(iris.YAML("myconfig.yml"))) or // app.Run([iris.Runner], iris.WithConfiguration(iris.YAML("myconfig.yml"))). func YAML(filename string) Configuration { // check for globe configuration file and use that, otherwise // return the default configuration if file doesn't exist. if filename == globalConfigurationKeyword { filename = homeConfigurationFilename(".yml") if _, err := os.Stat(filename); os.IsNotExist(err) { panic("default configuration file '" + filename + "' does not exist") } } c, err := parseYAML(filename) if err != nil { panic(err) } return c } // TOML reads Configuration from a toml-compatible document file. // Read more about toml's implementation at: // https://github.com/toml-lang/toml // // Accepts the absolute path of the configuration file. // An error will be shown to the user via panic with the error message. // Error may occur when the file does not exist or is not formatted correctly. // // Note: if the char '~' passed as "filename" then it tries to load and return // the configuration from the $home_directory + iris.tml, // see `WithGlobalConfiguration` for more information. // // Usage: // app.Configure(iris.WithConfiguration(iris.TOML("myconfig.tml"))) or // app.Run([iris.Runner], iris.WithConfiguration(iris.TOML("myconfig.tml"))). func TOML(filename string) Configuration { c := DefaultConfiguration() // check for globe configuration file and use that, otherwise // return the default configuration if file doesn't exist. if filename == globalConfigurationKeyword { filename = homeConfigurationFilename(".tml") if _, err := os.Stat(filename); os.IsNotExist(err) { panic("default configuration file '" + filename + "' does not exist") } } // get the abs // which will try to find the 'filename' from current workind dir too. tomlAbsPath, err := filepath.Abs(filename) if err != nil { panic(fmt.Errorf("toml: %w", err)) } // read the raw contents of the file data, err := os.ReadFile(tomlAbsPath) if err != nil { panic(fmt.Errorf("toml :%w", err)) } // put the file's contents as toml to the default configuration(c) if _, err := toml.Decode(string(data), &c); err != nil { panic(fmt.Errorf("toml :%w", err)) } // Author's notes: // The toml's 'usual thing' for key naming is: the_config_key instead of TheConfigKey // but I am always prefer to use the specific programming language's syntax // and the original configuration name fields for external configuration files // so we do 'toml: "TheConfigKeySameAsTheConfigField" instead. return c } // Configurator is just an interface which accepts the framework instance. // // It can be used to register a custom configuration with `Configure` in order // to modify the framework instance. // // Currently Configurator is being used to describe the configuration's fields values. type Configurator func(*Application) // WithGlobalConfiguration will load the global yaml configuration file // from the home directory and it will set/override the whole app's configuration // to that file's contents. The global configuration file can be modified by user // and be used by multiple iris instances. // // This is useful when we run multiple iris servers that share the same // configuration, even with custom values at its "Other" field. // // Usage: `app.Configure(iris.WithGlobalConfiguration)` or `app.Run([iris.Runner], iris.WithGlobalConfiguration)`. var WithGlobalConfiguration = func(app *Application) { app.Configure(WithConfiguration(YAML(globalConfigurationKeyword))) } // WithLogLevel sets the `Configuration.LogLevel` field. func WithLogLevel(level string) Configurator { return func(app *Application) { if app.logger == nil { app.logger = golog.Default } app.logger.SetLevel(level) // can be fired through app.Configure. app.config.LogLevel = level } } // WithSocketSharding sets the `Configuration.SocketSharding` field to true. func WithSocketSharding(app *Application) { // Note(@kataras): It could be a host Configurator but it's an application setting in order // to configure it through yaml/toml files as well. app.config.SocketSharding = true } // WithKeepAlive sets the `Configuration.KeepAlive` field to the given duration. func WithKeepAlive(keepAliveDur time.Duration) Configurator { return func(app *Application) { app.config.KeepAlive = keepAliveDur } } // WithTimeout sets the `Configuration.Timeout` field to the given duration. func WithTimeout(timeoutDur time.Duration, htmlBody ...string) Configurator { return func(app *Application) { app.config.Timeout = timeoutDur if len(htmlBody) > 0 { app.config.TimeoutMessage = htmlBody[0] } } } // NonBlocking sets the `Configuration.NonBlocking` field to true. func NonBlocking() Configurator { return func(app *Application) { app.config.NonBlocking = true } } // WithoutServerError will cause to ignore the matched "errors" // from the main application's `Run/Listen` function. // // Usage: // err := app.Listen(":8080", iris.WithoutServerError(iris.ErrServerClosed)) // will return `nil` if the server's error was `http/iris#ErrServerClosed`. // // See `Configuration#IgnoreServerErrors []string` too. // // Example: https://github.com/kataras/iris/tree/main/_examples/http-server/listen-addr/omit-server-errors func WithoutServerError(errors ...error) Configurator { return func(app *Application) { if len(errors) == 0 { return } errorsAsString := make([]string, len(errors)) for i, e := range errors { errorsAsString[i] = e.Error() } app.config.IgnoreServerErrors = append(app.config.IgnoreServerErrors, errorsAsString...) } } // WithoutStartupLog turns off the information send, once, to the terminal when the main server is open. var WithoutStartupLog = func(app *Application) { app.config.DisableStartupLog = true } // WithoutBanner is a conversion for the `WithoutStartupLog` option. // // Turns off the information send, once, to the terminal when the main server is open. var WithoutBanner = WithoutStartupLog // WithoutInterruptHandler disables the automatic graceful server shutdown // when control/cmd+C pressed. var WithoutInterruptHandler = func(app *Application) { app.config.DisableInterruptHandler = true } // WithoutPathCorrection disables the PathCorrection setting. // // See `Configuration`. var WithoutPathCorrection = func(app *Application) { app.config.DisablePathCorrection = true } // WithPathIntelligence enables the EnablePathIntelligence setting. // // See `Configuration`. var WithPathIntelligence = func(app *Application) { app.config.EnablePathIntelligence = true } // WithoutPathCorrectionRedirection disables the PathCorrectionRedirection setting. // // See `Configuration`. var WithoutPathCorrectionRedirection = func(app *Application) { app.config.DisablePathCorrection = false app.config.DisablePathCorrectionRedirection = true } // WithoutBodyConsumptionOnUnmarshal disables BodyConsumptionOnUnmarshal setting. // // See `Configuration`. var WithoutBodyConsumptionOnUnmarshal = func(app *Application) { app.config.DisableBodyConsumptionOnUnmarshal = true } // WithEmptyFormError enables the setting `FireEmptyFormError`. // // See `Configuration`. var WithEmptyFormError = func(app *Application) { app.config.FireEmptyFormError = true } // WithPathEscape sets the EnablePathEscape setting to true. // // See `Configuration`. var WithPathEscape = func(app *Application) { app.config.EnablePathEscape = true } // WithLowercaseRouting enables for lowercase routing by // setting the `ForceLowercaseRoutes` to true. // // See `Configuration`. var WithLowercaseRouting = func(app *Application) { app.config.ForceLowercaseRouting = true } // WithDynamicHandler enables for dynamic routing by // setting the `EnableDynamicHandler` to true. // // See `Configuration`. var WithDynamicHandler = func(app *Application) { app.config.EnableDynamicHandler = true } // WithOptimizations can force the application to optimize for the best performance where is possible. // // See `Configuration`. var WithOptimizations = func(app *Application) { app.config.EnableOptimizations = true } // WithProtoJSON enables the proto marshaler on Context.JSON method. // // See `Configuration` for more. var WithProtoJSON = func(app *Application) { app.config.EnableProtoJSON = true } // WithEasyJSON enables the fast easy json marshaler on Context.JSON method. // // See `Configuration` for more. var WithEasyJSON = func(app *Application) { app.config.EnableEasyJSON = true } // WithFireMethodNotAllowed enables the FireMethodNotAllowed setting. // // See `Configuration`. var WithFireMethodNotAllowed = func(app *Application) { app.config.FireMethodNotAllowed = true } // WithoutAutoFireStatusCode sets the DisableAutoFireStatusCode setting to true. // // See `Configuration`. var WithoutAutoFireStatusCode = func(app *Application) { app.config.DisableAutoFireStatusCode = true } // WithResetOnFireErrorCode sets the ResetOnFireErrorCode setting to true. // // See `Configuration`. var WithResetOnFireErrorCode = func(app *Application) { app.config.ResetOnFireErrorCode = true } // WithURLParamSeparator sets the URLParamSeparator setting to "sep". // // See `Configuration`. var WithURLParamSeparator = func(sep string) Configurator { return func(app *Application) { app.config.URLParamSeparator = &sep } } // WithTimeFormat sets the TimeFormat setting. // // See `Configuration`. func WithTimeFormat(timeformat string) Configurator { return func(app *Application) { app.config.TimeFormat = timeformat } } // WithCharset sets the Charset setting. // // See `Configuration`. func WithCharset(charset string) Configurator { return func(app *Application) { app.config.Charset = charset } } // WithPostMaxMemory sets the maximum post data size // that a client can send to the server, this differs // from the overall request body size which can be modified // by the `context#SetMaxRequestBodySize` or `iris#LimitRequestBodySize`. // // Defaults to 32MB or 32 << 20 or 32*iris.MB if you prefer. func WithPostMaxMemory(limit int64) Configurator { return func(app *Application) { app.config.PostMaxMemory = limit } } // WithRemoteAddrHeader adds a new request header name // that can be used to validate the client's real IP. func WithRemoteAddrHeader(header ...string) Configurator { return func(app *Application) { for _, h := range header { exists := false for _, v := range app.config.RemoteAddrHeaders { if v == h { exists = true } } if !exists { app.config.RemoteAddrHeaders = append(app.config.RemoteAddrHeaders, h) } } } } // WithoutRemoteAddrHeader removes an existing request header name // that can be used to validate and parse the client's real IP. // // Look `context.RemoteAddr()` for more. func WithoutRemoteAddrHeader(headerName string) Configurator { return func(app *Application) { tmp := app.config.RemoteAddrHeaders[:0] for _, v := range app.config.RemoteAddrHeaders { if v != headerName { tmp = append(tmp, v) } } app.config.RemoteAddrHeaders = tmp } } // WithRemoteAddrPrivateSubnet adds a new private sub-net to be excluded from `context.RemoteAddr`. // See `WithRemoteAddrHeader` too. func WithRemoteAddrPrivateSubnet(startIP, endIP string) Configurator { return func(app *Application) { app.config.RemoteAddrPrivateSubnets = append(app.config.RemoteAddrPrivateSubnets, netutil.IPRange{ Start: startIP, End: endIP, }) } } // WithSSLProxyHeader sets a SSLProxyHeaders key value pair. // Example: WithSSLProxyHeader("X-Forwarded-Proto", "https"). // See `Context.IsSSL` for more. func WithSSLProxyHeader(headerKey, headerValue string) Configurator { return func(app *Application) { if app.config.SSLProxyHeaders == nil { app.config.SSLProxyHeaders = make(map[string]string) } app.config.SSLProxyHeaders[headerKey] = headerValue } } // WithHostProxyHeader sets a HostProxyHeaders key value pair. // Example: WithHostProxyHeader("X-Host"). // See `Context.Host` for more. func WithHostProxyHeader(headers ...string) Configurator { return func(app *Application) { if app.config.HostProxyHeaders == nil { app.config.HostProxyHeaders = make(map[string]bool) } for _, k := range headers { app.config.HostProxyHeaders[k] = true } } } // WithOtherValue adds a value based on a key to the Other setting. // // See `Configuration.Other`. func WithOtherValue(key string, val any) Configurator { return func(app *Application) { if app.config.Other == nil { app.config.Other = make(map[string]any) } app.config.Other[key] = val } } // WithSitemap enables the sitemap generator. // Use the Route's `SetLastMod`, `SetChangeFreq` and `SetPriority` to modify // the sitemap's URL child element properties. // Excluded routes: // - dynamic // - subdomain // - offline // - ExcludeSitemap method called // // It accepts a "startURL" input argument which // is the prefix for the registered routes that will be included in the sitemap. // // If more than 50,000 static routes are registered then sitemaps will be splitted and a sitemap index will be served in // /sitemap.xml. // // If `Application.I18n.Load/LoadAssets` is called then the sitemap will contain translated links for each static route. // // If the result does not complete your needs you can take control // and use the github.com/kataras/sitemap package to generate a customized one instead. // // Example: https://github.com/kataras/iris/tree/main/_examples/sitemap. func WithSitemap(startURL string) Configurator { sitemaps := sitemap.New(startURL) return func(app *Application) { var defaultLang string if tags := app.I18n.Tags(); len(tags) > 0 { defaultLang = tags[0].String() sitemaps.DefaultLang(defaultLang) } for _, r := range app.GetRoutes() { if !r.IsStatic() || r.Subdomain != "" || !r.IsOnline() || r.NoSitemap { continue } loc := r.StaticPath() var translatedLinks []sitemap.Link for _, tag := range app.I18n.Tags() { lang := tag.String() langPath := lang href := "" if lang == defaultLang { // http://domain.com/en-US/path to just http://domain.com/path if en-US is the default language. langPath = "" } if app.I18n.PathRedirect { // then use the path prefix. // e.g. http://domain.com/el-GR/path if langPath == "" { // fix double slashes http://domain.com// when self-included default language. href = loc } else { href = "/" + langPath + loc } } else if app.I18n.Subdomain { // then use the subdomain. // e.g. http://el.domain.com/path scheme := netutil.ResolveSchemeFromVHost(startURL) host := strings.TrimLeft(startURL, scheme) if langPath != "" { href = scheme + strings.Split(langPath, "-")[0] + "." + host + loc } else { href = loc } } else if p := app.I18n.URLParameter; p != "" { // then use the URL parameter. // e.g. http://domain.com/path?lang=el-GR href = loc + "?" + p + "=" + lang } else { // then skip it, we can't generate the link at this state. continue } translatedLinks = append(translatedLinks, sitemap.Link{ Rel: "alternate", Hreflang: lang, Href: href, }) } sitemaps.URL(sitemap.URL{ Loc: loc, LastMod: r.LastMod, ChangeFreq: r.ChangeFreq, Priority: r.Priority, Links: translatedLinks, }) } for _, s := range sitemaps.Build() { contentCopy := make([]byte, len(s.Content)) copy(contentCopy, s.Content) handler := func(ctx Context) { ctx.ContentType(context.ContentXMLHeaderValue) ctx.Write(contentCopy) // nolint:errcheck } if app.builded { routes := app.CreateRoutes([]string{MethodGet, MethodHead, MethodOptions}, s.Path, handler) for _, r := range routes { if err := app.Router.AddRouteUnsafe(r); err != nil { app.Logger().Errorf("sitemap route: %v", err) } } } else { app.HandleMany("GET HEAD OPTIONS", s.Path, handler) } } } } // WithTunneling is the `iris.Configurator` for the `iris.Configuration.Tunneling` field. // It's used to enable http tunneling for an Iris Application, per registered host // // Alternatively use the `iris.WithConfiguration(iris.Configuration{Tunneling: iris.TunnelingConfiguration{ ...}}}`. var WithTunneling = func(app *Application) { conf := TunnelingConfiguration{ Tunnels: []Tunnel{{}}, // create empty tunnel, its addr and name are set right before host serve. } app.config.Tunneling = conf } type ( // TunnelingConfiguration contains configuration // for the optional tunneling through ngrok feature. // Note that the ngrok should be already installed at the host machine. TunnelingConfiguration = tunnel.Configuration // Tunnel is the Tunnels field of the TunnelingConfiguration structure. Tunnel = tunnel.Tunnel ) // Configuration holds the necessary settings for an Iris Application instance. // All fields are optionally, the default values will work for a common web application. // // A Configuration value can be passed through `WithConfiguration` Configurator. // Usage: // conf := iris.Configuration{ ... } // app := iris.New() // app.Configure(iris.WithConfiguration(conf)) OR // app.Run/Listen(..., iris.WithConfiguration(conf)). type Configuration struct { // VHost lets you customize the trusted domain this server should run on. // Its value will be used as the return value of Context.Domain() too. // It can be retrieved by the context if needed (i.e router for subdomains) VHost string `ini:"v_host" json:"vHost" yaml:"VHost" toml:"VHost" env:"V_HOST"` // LogLevel is the log level the application should use to output messages. // Logger, by default, is mostly used on Build state but it is also possible // that debug error messages could be thrown when the app is running, e.g. // when malformed data structures try to be sent on Client (i.e Context.JSON/JSONP/XML...). // // Defaults to "info". Possible values are: // * "disable" // * "fatal" // * "error" // * "warn" // * "info" // * "debug" LogLevel string `ini:"log_level" json:"logLevel" yaml:"LogLevel" toml:"LogLevel" env:"LOG_LEVEL"` // SocketSharding enables SO_REUSEPORT (or SO_REUSEADDR for windows) // on all registered Hosts. // This option allows linear scaling server performance on multi-CPU servers. // // Please read the following: // 1. https://stackoverflow.com/a/14388707 // 2. https://stackoverflow.com/a/59692868 // 3. https://www.nginx.com/blog/socket-sharding-nginx-release-1-9-1/ // 4. (BOOK) Learning HTTP/2: A Practical Guide for Beginners: // Page 37, To Shard or Not to Shard? // // Defaults to false. SocketSharding bool `ini:"socket_sharding" json:"socketSharding" yaml:"SocketSharding" toml:"SocketSharding" env:"SOCKET_SHARDING"` // KeepAlive sets the TCP connection's keep-alive duration. // If set to greater than zero then a tcp listener featured keep alive // will be used instead of the simple tcp one. // // Defaults to 0. KeepAlive time.Duration `ini:"keepalive" json:"keepAlive" yaml:"KeepAlive" toml:"KeepAlive" env:"KEEP_ALIVE"` // Timeout wraps the application's router with an http timeout handler // if the value is greater than zero. // // The underline response writer supports the Pusher interface but does not support // the Hijacker or Flusher interfaces when Timeout handler is registered. // // Read more at: https://pkg.go.dev/net/http#TimeoutHandler. Timeout time.Duration `ini:"timeout" json:"timeout" yaml:"Timeout" toml:"Timeout"` // TimeoutMessage specifies the HTML body when a handler hits its life time based // on the Timeout configuration field. TimeoutMessage string `ini:"timeout_message" json:"timeoutMessage" yaml:"TimeoutMessage" toml:"TimeoutMessage"` // NonBlocking, if set to true then the server will start listening for incoming connections // without blocking the main goroutine. Use the Application.Wait method to block and wait for the server to be up and running. NonBlocking bool `ini:"non_blocking" json:"nonBlocking" yaml:"NonBlocking" toml:"NonBlocking"` // Tunneling can be optionally set to enable ngrok http(s) tunneling for this Iris app instance. // See the `WithTunneling` Configurator too. Tunneling TunnelingConfiguration `ini:"tunneling" json:"tunneling,omitempty" yaml:"Tunneling" toml:"Tunneling"` // IgnoreServerErrors will cause to ignore the matched "errors" // from the main application's `Run` function. // This is a slice of string, not a slice of error // users can register these errors using yaml or toml configuration file // like the rest of the configuration fields. // // See `WithoutServerError(...)` function too. // // Example: https://github.com/kataras/iris/tree/main/_examples/http-server/listen-addr/omit-server-errors // // Defaults to an empty slice. IgnoreServerErrors []string `ini:"ignore_server_errors" json:"ignoreServerErrors,omitempty" yaml:"IgnoreServerErrors" toml:"IgnoreServerErrors"` // DisableStartupLog if set to true then it turns off the write banner on server startup. // // Defaults to false. DisableStartupLog bool `ini:"disable_startup_log" json:"disableStartupLog,omitempty" yaml:"DisableStartupLog" toml:"DisableStartupLog"` // DisableInterruptHandler if set to true then it disables the automatic graceful server shutdown // when control/cmd+C pressed. // Turn this to true if you're planning to handle this by your own via a custom host.Task. // // Defaults to false. DisableInterruptHandler bool `ini:"disable_interrupt_handler" json:"disableInterruptHandler,omitempty" yaml:"DisableInterruptHandler" toml:"DisableInterruptHandler"` // DisablePathCorrection disables the correcting // and redirecting or executing directly the handler of // the requested path to the registered path // for example, if /home/ path is requested but no handler for this Route found, // then the Router checks if /home handler exists, if yes, // (permanent)redirects the client to the correct path /home. // // See `DisablePathCorrectionRedirection` to enable direct handler execution instead of redirection. // // Defaults to false. DisablePathCorrection bool `ini:"disable_path_correction" json:"disablePathCorrection,omitempty" yaml:"DisablePathCorrection" toml:"DisablePathCorrection"` // DisablePathCorrectionRedirection works whenever configuration.DisablePathCorrection is set to false // and if DisablePathCorrectionRedirection set to true then it will fire the handler of the matching route without // the trailing slash ("/") instead of send a redirection status. // // Defaults to false. DisablePathCorrectionRedirection bool `ini:"disable_path_correction_redirection" json:"disablePathCorrectionRedirection,omitempty" yaml:"DisablePathCorrectionRedirection" toml:"DisablePathCorrectionRedirection"` // EnablePathIntelligence if set to true, // the router will redirect HTTP "GET" not found pages to the most closest one path(if any). For example // you register a route at "/contact" path - // a client tries to reach it by "/cont", the path will be automatic fixed // and the client will be redirected to the "/contact" path // instead of getting a 404 not found response back. // // Defaults to false. EnablePathIntelligence bool `ini:"enable_path_intelligence" json:"enablePathIntelligence,omitempty" yaml:"EnablePathIntelligence" toml:"EnablePathIntelligence"` // EnablePathEscape when is true then its escapes the path and the named parameters (if any). // When do you need to Disable(false) it: // accepts parameters with slash '/' // Request: http://localhost:8080/details/Project%2FDelta // ctx.Param("project") returns the raw named parameter: Project%2FDelta // which you can escape it manually with net/url: // projectName, _ := url.QueryUnescape(c.Param("project"). // // Defaults to false. EnablePathEscape bool `ini:"enable_path_escape" json:"enablePathEscape,omitempty" yaml:"EnablePathEscape" toml:"EnablePathEscape"` // ForceLowercaseRouting if enabled, converts all registered routes paths to lowercase // and it does lowercase the request path too for matching. // // Defaults to false. ForceLowercaseRouting bool `ini:"force_lowercase_routing" json:"forceLowercaseRouting,omitempty" yaml:"ForceLowercaseRouting" toml:"ForceLowercaseRouting"` // EnableOptimizations enables dynamic request handler. // It gives the router the feature to add routes while in serve-time, // when `RefreshRouter` is called. // If this setting is set to true, the request handler will use a mutex for data(trie routing) protection, // hence the performance cost. // // Defaults to false. EnableDynamicHandler bool `ini:"enable_dynamic_handler" json:"enableDynamicHandler,omitempty" yaml:"EnableDynamicHandler" toml:"EnableDynamicHandler"` // FireMethodNotAllowed if it's true router checks for StatusMethodNotAllowed(405) and // fires the 405 error instead of 404 // Defaults to false. FireMethodNotAllowed bool `ini:"fire_method_not_allowed" json:"fireMethodNotAllowed,omitempty" yaml:"FireMethodNotAllowed" toml:"FireMethodNotAllowed"` // DisableAutoFireStatusCode if true then it turns off the http error status code // handler automatic execution on error code from a `Context.StatusCode` call. // By-default a custom http error handler will be fired when "Context.StatusCode(errorCode)" called. // // Defaults to false. DisableAutoFireStatusCode bool `ini:"disable_auto_fire_status_code" json:"disableAutoFireStatusCode,omitempty" yaml:"DisableAutoFireStatusCode" toml:"DisableAutoFireStatusCode"` // ResetOnFireErrorCode if true then any previously response body or headers through // response recorder will be ignored and the router // will fire the registered (or default) HTTP error handler instead. // See `core/router/handler#FireErrorCode` and `Context.EndRequest` for more details. // // Read more at: https://github.com/kataras/iris/issues/1531 // // Defaults to false. ResetOnFireErrorCode bool `ini:"reset_on_fire_error_code" json:"resetOnFireErrorCode,omitempty" yaml:"ResetOnFireErrorCode" toml:"ResetOnFireErrorCode"` // URLParamSeparator defines the character(s) separator for Context.URLParamSlice. // If empty or null then request url parameters with comma separated values will be retrieved as one. // // Defaults to comma ",". URLParamSeparator *string `ini:"url_param_separator" json:"urlParamSeparator,omitempty" yaml:"URLParamSeparator" toml:"URLParamSeparator"` // EnableOptimization when this field is true // then the application tries to optimize for the best performance where is possible. // // Defaults to false. // Deprecated. As of version 12.2.x this field does nothing. EnableOptimizations bool `ini:"enable_optimizations" json:"enableOptimizations,omitempty" yaml:"EnableOptimizations" toml:"EnableOptimizations"` // EnableProtoJSON when this field is true // enables the proto marshaler on given proto messages when calling the Context.JSON method. // // Defaults to false. EnableProtoJSON bool `ini:"enable_proto_json" json:"enableProtoJSON,omitempty" yaml:"EnableProtoJSON" toml:"EnableProtoJSON"` // EnableEasyJSON when this field is true // enables the fast easy json marshaler on compatible struct values when calling the Context.JSON method. // // Defaults to false. EnableEasyJSON bool `ini:"enable_easy_json" json:"enableEasyJSON,omitempty" yaml:"EnableEasyJSON" toml:"EnableEasyJSON"` // DisableBodyConsumptionOnUnmarshal manages the reading behavior of the context's body readers/binders. // If set to true then it // disables the body consumption by the `context.UnmarshalBody/ReadJSON/ReadXML`. // // By-default io.ReadAll` is used to read the body from the `context.Request.Body which is an `io.ReadCloser`, // if this field set to true then a new buffer will be created to read from and the request body. // The body will not be changed and existing data before the // context.UnmarshalBody/ReadJSON/ReadXML will be not consumed. // // See `Context.RecordRequestBody` method for the same feature, per-request. DisableBodyConsumptionOnUnmarshal bool `ini:"disable_body_consumption" json:"disableBodyConsumptionOnUnmarshal,omitempty" yaml:"DisableBodyConsumptionOnUnmarshal" toml:"DisableBodyConsumptionOnUnmarshal"` // FireEmptyFormError returns if set to tue true then the `context.ReadForm/ReadQuery/ReadBody` // will return an `iris.ErrEmptyForm` on empty request form data. FireEmptyFormError bool `ini:"fire_empty_form_error" json:"fireEmptyFormError,omitempty" yaml:"FireEmptyFormError" toml:"FireEmptyFormError"` // TimeFormat time format for any kind of datetime parsing // Defaults to "Mon, 02 Jan 2006 15:04:05 GMT". TimeFormat string `ini:"time_format" json:"timeFormat,omitempty" yaml:"TimeFormat" toml:"TimeFormat"` // Charset character encoding for various rendering // used for templates and the rest of the responses // Defaults to "utf-8". Charset string `ini:"charset" json:"charset,omitempty" yaml:"Charset" toml:"Charset"` // PostMaxMemory sets the maximum post data size // that a client can send to the server, this differs // from the overall request body size which can be modified // by the `context#SetMaxRequestBodySize` or `iris#LimitRequestBodySize`. // // Defaults to 32MB or 32 << 20 if you prefer. PostMaxMemory int64 `ini:"post_max_memory" json:"postMaxMemory" yaml:"PostMaxMemory" toml:"PostMaxMemory"` // +----------------------------------------------------+ // | Context's keys for values used on various featuers | // +----------------------------------------------------+ // Context values' keys for various features. // // LocaleContextKey is used by i18n to get the current request's locale, which contains a translate function too. // // Defaults to "iris.locale". LocaleContextKey string `ini:"locale_context_key" json:"localeContextKey,omitempty" yaml:"LocaleContextKey" toml:"LocaleContextKey"` // LanguageContextKey is the context key which a language can be modified by a middleware. // It has the highest priority over the rest and if it is empty then it is ignored, // if it set to a static string of "default" or to the default language's code // then the rest of the language extractors will not be called at all and // the default language will be set instead. // // Use with `Context.SetLanguage("el-GR")`. // // See `i18n.ExtractFunc` for a more organised way of the same feature. // Defaults to "iris.locale.language". LanguageContextKey string `ini:"language_context_key" json:"languageContextKey,omitempty" yaml:"LanguageContextKey" toml:"LanguageContextKey"` // LanguageInputContextKey is the context key of a language that is given by the end-user. // It's the real user input of the language string, matched or not. // // Defaults to "iris.locale.language.input". LanguageInputContextKey string `ini:"language_input_context_key" json:"languageInputContextKey,omitempty" yaml:"LanguageInputContextKey" toml:"LanguageInputContextKey"` // VersionContextKey is the context key which an API Version can be modified // via a middleware through `SetVersion` method, e.g. `versioning.SetVersion(ctx, ">=1.0.0 <2.0.0")`. // Defaults to "iris.api.version". VersionContextKey string `ini:"version_context_key" json:"versionContextKey" yaml:"VersionContextKey" toml:"VersionContextKey"` // VersionAliasesContextKey is the context key which the versioning feature // can look up for alternative values of a version and fallback to that. // Head over to the versioning package for more. // Defaults to "iris.api.version.aliases" VersionAliasesContextKey string `ini:"version_aliases_context_key" json:"versionAliasesContextKey" yaml:"VersionAliasesContextKey" toml:"VersionAliasesContextKey"` // ViewEngineContextKey is the context's values key // responsible to store and retrieve(view.Engine) the current view engine. // A middleware or a Party can modify its associated value to change // a view engine that `ctx.View` will render through. // If not an engine is registered by the end-developer // then its associated value is always nil, // meaning that the default value is nil. // See `Party.RegisterView` and `Context.ViewEngine` methods as well. // // Defaults to "iris.view.engine". ViewEngineContextKey string `ini:"view_engine_context_key" json:"viewEngineContextKey,omitempty" yaml:"ViewEngineContextKey" toml:"ViewEngineContextKey"` // ViewLayoutContextKey is the context's values key // responsible to store and retrieve(string) the current view layout. // A middleware can modify its associated value to change // the layout that `ctx.View` will use to render a template. // // Defaults to "iris.view.layout". ViewLayoutContextKey string `ini:"view_layout_context_key" json:"viewLayoutContextKey,omitempty" yaml:"ViewLayoutContextKey" toml:"ViewLayoutContextKey"` // ViewDataContextKey is the context's values key // responsible to store and retrieve(any) the current view binding data. // A middleware can modify its associated value to change // the template's data on-fly. // // Defaults to "iris.view.data". ViewDataContextKey string `ini:"view_data_context_key" json:"viewDataContextKey,omitempty" yaml:"ViewDataContextKey" toml:"ViewDataContextKey"` // FallbackViewContextKey is the context's values key // responsible to store the view fallback information. // // Defaults to "iris.view.fallback". FallbackViewContextKey string `ini:"fallback_view_context_key" json:"fallbackViewContextKey,omitempty" yaml:"FallbackViewContextKey" toml:"FallbackViewContextKey"` // RemoteAddrHeaders are the allowed request headers names // that can be valid to parse the client's IP based on. // By-default no "X-" header is consired safe to be used for retrieving the // client's IP address, because those headers can manually change by // the client. But sometimes are useful e.g. when behind a proxy // you want to enable the "X-Forwarded-For" or when cloudflare // you want to enable the "CF-Connecting-IP", indeed you // can allow the `ctx.RemoteAddr()` to use any header // that the client may sent. // // Defaults to an empty slice but an example usage is: // RemoteAddrHeaders { // "X-Real-Ip", // "X-Forwarded-For", // "CF-Connecting-IP", // "True-Client-Ip", // "X-Appengine-Remote-Addr", // } // // Look `context.RemoteAddr()` for more. RemoteAddrHeaders []string `ini:"remote_addr_headers" json:"remoteAddrHeaders,omitempty" yaml:"RemoteAddrHeaders" toml:"RemoteAddrHeaders"` // RemoteAddrHeadersForce forces the `Context.RemoteAddr()` method // to return the first entry of a request header as a fallback, // even if that IP is a part of the `RemoteAddrPrivateSubnets` list. // The default behavior, if a remote address is part of the `RemoteAddrPrivateSubnets`, // is to retrieve the IP from the `Request.RemoteAddr` field instead. RemoteAddrHeadersForce bool `ini:"remote_addr_headers_force" json:"remoteAddrHeadersForce,omitempty" yaml:"RemoteAddrHeadersForce" toml:"RemoteAddrHeadersForce"` // RemoteAddrPrivateSubnets defines the private sub-networks. // They are used to be compared against // IP Addresses fetched through `RemoteAddrHeaders` or `Context.Request.RemoteAddr`. // For details please navigate through: https://github.com/kataras/iris/issues/1453 // Defaults to: // { // Start: "10.0.0.0", // End: "10.255.255.255", // }, // { // Start: "100.64.0.0", // End: "100.127.255.255", // }, // { // Start: "172.16.0.0", // End: "172.31.255.255", // }, // { // Start: "192.0.0.0", // End: "192.0.0.255", // }, // { // Start: "192.168.0.0", // End: "192.168.255.255", // }, // { // Start: "198.18.0.0", // End: "198.19.255.255", // } // // Look `Context.RemoteAddr()` for more. RemoteAddrPrivateSubnets []netutil.IPRange `ini:"remote_addr_private_subnets" json:"remoteAddrPrivateSubnets" yaml:"RemoteAddrPrivateSubnets" toml:"RemoteAddrPrivateSubnets"` // SSLProxyHeaders defines the set of header key values // that would indicate a valid https Request (look `Context.IsSSL()`). // Example: `map[string]string{"X-Forwarded-Proto": "https"}`. // // Defaults to empty map. SSLProxyHeaders map[string]string `ini:"ssl_proxy_headers" json:"sslProxyHeaders" yaml:"SSLProxyHeaders" toml:"SSLProxyHeaders"` // HostProxyHeaders defines the set of headers that may hold a proxied hostname value for the clients. // Look `Context.Host()` for more. // Defaults to empty map. HostProxyHeaders map[string]bool `ini:"host_proxy_headers" json:"hostProxyHeaders" yaml:"HostProxyHeaders" toml:"HostProxyHeaders"` // Other are the custom, dynamic options, can be empty. // This field used only by you to set any app's options you want. // // Defaults to empty map. Other map[string]any `ini:"other" json:"other,omitempty" yaml:"Other" toml:"Other"` } var _ context.ConfigurationReadOnly = (*Configuration)(nil) // GetVHost returns the VHost config field. func (c *Configuration) GetVHost() string { vhost := c.VHost return vhost } // SetVHost sets the VHost config field. func (c *Configuration) SetVHost(s string) { c.VHost = s } // GetLogLevel returns the LogLevel field. func (c *Configuration) GetLogLevel() string { return c.LogLevel } // GetSocketSharding returns the SocketSharding field. func (c *Configuration) GetSocketSharding() bool { return c.SocketSharding } // GetKeepAlive returns the KeepAlive field. func (c *Configuration) GetKeepAlive() time.Duration { return c.KeepAlive } // GetTimeout returns the Timeout field. func (c *Configuration) GetTimeout() time.Duration { return c.Timeout } // GetNonBlocking returns the NonBlocking field. func (c *Configuration) GetNonBlocking() bool { return c.NonBlocking } // GetTimeoutMessage returns the TimeoutMessage field. func (c *Configuration) GetTimeoutMessage() string { return c.TimeoutMessage } // GetDisablePathCorrection returns the DisablePathCorrection field. func (c *Configuration) GetDisablePathCorrection() bool { return c.DisablePathCorrection } // GetDisablePathCorrectionRedirection returns the DisablePathCorrectionRedirection field. func (c *Configuration) GetDisablePathCorrectionRedirection() bool { return c.DisablePathCorrectionRedirection } // GetEnablePathIntelligence returns the EnablePathIntelligence field. func (c *Configuration) GetEnablePathIntelligence() bool { return c.EnablePathIntelligence } // GetEnablePathEscape returns the EnablePathEscape field. func (c *Configuration) GetEnablePathEscape() bool { return c.EnablePathEscape } // GetForceLowercaseRouting returns the ForceLowercaseRouting field. func (c *Configuration) GetForceLowercaseRouting() bool { return c.ForceLowercaseRouting } // GetEnableDynamicHandler returns the EnableDynamicHandler field. func (c *Configuration) GetEnableDynamicHandler() bool { return c.EnableDynamicHandler } // GetFireMethodNotAllowed returns the FireMethodNotAllowed field. func (c *Configuration) GetFireMethodNotAllowed() bool { return c.FireMethodNotAllowed } // GetEnableOptimizations returns the EnableOptimizations. func (c *Configuration) GetEnableOptimizations() bool { return c.EnableOptimizations } // GetEnableProtoJSON returns the EnableProtoJSON field. func (c *Configuration) GetEnableProtoJSON() bool { return c.EnableProtoJSON } // GetEnableEasyJSON returns the EnableEasyJSON field. func (c *Configuration) GetEnableEasyJSON() bool { return c.EnableEasyJSON } // GetDisableBodyConsumptionOnUnmarshal returns the DisableBodyConsumptionOnUnmarshal field. func (c *Configuration) GetDisableBodyConsumptionOnUnmarshal() bool { return c.DisableBodyConsumptionOnUnmarshal } // GetFireEmptyFormError returns the DisableBodyConsumptionOnUnmarshal field. func (c *Configuration) GetFireEmptyFormError() bool { return c.FireEmptyFormError } // GetDisableAutoFireStatusCode returns the DisableAutoFireStatusCode field. func (c *Configuration) GetDisableAutoFireStatusCode() bool { return c.DisableAutoFireStatusCode } // GetResetOnFireErrorCode returns ResetOnFireErrorCode field. func (c *Configuration) GetResetOnFireErrorCode() bool { return c.ResetOnFireErrorCode } // GetURLParamSeparator returns URLParamSeparator field. func (c *Configuration) GetURLParamSeparator() *string { return c.URLParamSeparator } // GetTimeFormat returns the TimeFormat field. func (c *Configuration) GetTimeFormat() string { return c.TimeFormat } // GetCharset returns the Charset field. func (c *Configuration) GetCharset() string { return c.Charset } // GetPostMaxMemory returns the PostMaxMemory field. func (c *Configuration) GetPostMaxMemory() int64 { return c.PostMaxMemory } // GetLocaleContextKey returns the LocaleContextKey field. func (c *Configuration) GetLocaleContextKey() string { return c.LocaleContextKey } // GetLanguageContextKey returns the LanguageContextKey field. func (c *Configuration) GetLanguageContextKey() string { return c.LanguageContextKey } // GetLanguageInputContextKey returns the LanguageInputContextKey field. func (c *Configuration) GetLanguageInputContextKey() string { return c.LanguageInputContextKey } // GetVersionContextKey returns the VersionContextKey field. func (c *Configuration) GetVersionContextKey() string { return c.VersionContextKey } // GetVersionAliasesContextKey returns the VersionAliasesContextKey field. func (c *Configuration) GetVersionAliasesContextKey() string { return c.VersionAliasesContextKey } // GetViewEngineContextKey returns the ViewEngineContextKey field. func (c *Configuration) GetViewEngineContextKey() string { return c.ViewEngineContextKey } // GetViewLayoutContextKey returns the ViewLayoutContextKey field. func (c *Configuration) GetViewLayoutContextKey() string { return c.ViewLayoutContextKey } // GetViewDataContextKey returns the ViewDataContextKey field. func (c *Configuration) GetViewDataContextKey() string { return c.ViewDataContextKey } // GetFallbackViewContextKey returns the FallbackViewContextKey field. func (c *Configuration) GetFallbackViewContextKey() string { return c.FallbackViewContextKey } // GetRemoteAddrHeaders returns the RemoteAddrHeaders field. func (c *Configuration) GetRemoteAddrHeaders() []string { return c.RemoteAddrHeaders } // GetRemoteAddrHeadersForce returns RemoteAddrHeadersForce field. func (c *Configuration) GetRemoteAddrHeadersForce() bool { return c.RemoteAddrHeadersForce } // GetSSLProxyHeaders returns the SSLProxyHeaders field. func (c *Configuration) GetSSLProxyHeaders() map[string]string { return c.SSLProxyHeaders } // GetRemoteAddrPrivateSubnets returns the RemoteAddrPrivateSubnets field. func (c *Configuration) GetRemoteAddrPrivateSubnets() []netutil.IPRange { return c.RemoteAddrPrivateSubnets } // GetHostProxyHeaders returns the HostProxyHeaders field. func (c *Configuration) GetHostProxyHeaders() map[string]bool { return c.HostProxyHeaders } // GetOther returns the Other field. func (c *Configuration) GetOther() map[string]any { return c.Other } // WithConfiguration sets the "c" values to the framework's configurations. // // Usage: // app.Listen(":8080", iris.WithConfiguration(iris.Configuration{/* fields here */ })) // or // iris.WithConfiguration(iris.YAML("./cfg/iris.yml")) // or // iris.WithConfiguration(iris.TOML("./cfg/iris.tml")) func WithConfiguration(c Configuration) Configurator { return func(app *Application) { main := app.config if main == nil { app.config = &c return } if v := c.LogLevel; v != "" { main.LogLevel = v } if v := c.SocketSharding; v { main.SocketSharding = v } if v := c.KeepAlive; v > 0 { main.KeepAlive = v } if v := c.Timeout; v > 0 { main.Timeout = v } if v := c.TimeoutMessage; v != "" { main.TimeoutMessage = v } if v := c.NonBlocking; v { main.NonBlocking = v } if len(c.Tunneling.Tunnels) > 0 { main.Tunneling = c.Tunneling } if v := c.IgnoreServerErrors; len(v) > 0 { main.IgnoreServerErrors = append(main.IgnoreServerErrors, v...) } if v := c.DisableStartupLog; v { main.DisableStartupLog = v } if v := c.DisableInterruptHandler; v { main.DisableInterruptHandler = v } if v := c.DisablePathCorrection; v { main.DisablePathCorrection = v } if v := c.DisablePathCorrectionRedirection; v { main.DisablePathCorrectionRedirection = v } if v := c.EnablePathIntelligence; v { main.EnablePathIntelligence = v } if v := c.EnablePathEscape; v { main.EnablePathEscape = v } if v := c.ForceLowercaseRouting; v { main.ForceLowercaseRouting = v } if v := c.EnableOptimizations; v { main.EnableOptimizations = v } if v := c.EnableProtoJSON; v { main.EnableProtoJSON = v } if v := c.EnableEasyJSON; v { main.EnableEasyJSON = v } if v := c.FireMethodNotAllowed; v { main.FireMethodNotAllowed = v } if v := c.DisableAutoFireStatusCode; v { main.DisableAutoFireStatusCode = v } if v := c.ResetOnFireErrorCode; v { main.ResetOnFireErrorCode = v } if v := c.URLParamSeparator; v != nil { main.URLParamSeparator = v } if v := c.DisableBodyConsumptionOnUnmarshal; v { main.DisableBodyConsumptionOnUnmarshal = v } if v := c.FireEmptyFormError; v { main.FireEmptyFormError = v } if v := c.TimeFormat; v != "" { main.TimeFormat = v } if v := c.Charset; v != "" { main.Charset = v } if v := c.PostMaxMemory; v > 0 { main.PostMaxMemory = v } if v := c.LocaleContextKey; v != "" { main.LocaleContextKey = v } if v := c.LanguageContextKey; v != "" { main.LanguageContextKey = v } if v := c.LanguageInputContextKey; v != "" { main.LanguageInputContextKey = v } if v := c.VersionContextKey; v != "" { main.VersionContextKey = v } if v := c.VersionAliasesContextKey; v != "" { main.VersionAliasesContextKey = v } if v := c.ViewEngineContextKey; v != "" { main.ViewEngineContextKey = v } if v := c.ViewLayoutContextKey; v != "" { main.ViewLayoutContextKey = v } if v := c.ViewDataContextKey; v != "" { main.ViewDataContextKey = v } if v := c.FallbackViewContextKey; v != "" { main.FallbackViewContextKey = v } if v := c.RemoteAddrHeaders; len(v) > 0 { main.RemoteAddrHeaders = v } if v := c.RemoteAddrHeadersForce; v { main.RemoteAddrHeadersForce = v } if v := c.RemoteAddrPrivateSubnets; len(v) > 0 { main.RemoteAddrPrivateSubnets = v } if v := c.SSLProxyHeaders; len(v) > 0 { if main.SSLProxyHeaders == nil { main.SSLProxyHeaders = make(map[string]string, len(v)) } for key, value := range v { main.SSLProxyHeaders[key] = value } } if v := c.HostProxyHeaders; len(v) > 0 { if main.HostProxyHeaders == nil { main.HostProxyHeaders = make(map[string]bool, len(v)) } for key, value := range v { main.HostProxyHeaders[key] = value } } if v := c.Other; len(v) > 0 { if main.Other == nil { main.Other = make(map[string]any, len(v)) } for key, value := range v { main.Other[key] = value } } } } // DefaultTimeoutMessage is the default timeout message which is rendered // on expired handlers when timeout handler is registered (see Timeout configuration field). var DefaultTimeoutMessage = `Timeout

      Timeout

      Looks like the server is taking too long to respond, this can be caused by either poor connectivity or an error with our servers. Please try again in a while.` func toStringPtr(s string) *string { return &s } // DefaultConfiguration returns the default configuration for an iris station, fills the main Configuration func DefaultConfiguration() Configuration { return Configuration{ LogLevel: "info", SocketSharding: false, KeepAlive: 0, Timeout: 0, TimeoutMessage: DefaultTimeoutMessage, NonBlocking: false, DisableStartupLog: false, DisableInterruptHandler: false, DisablePathCorrection: false, EnablePathEscape: false, ForceLowercaseRouting: false, FireMethodNotAllowed: false, DisableBodyConsumptionOnUnmarshal: false, FireEmptyFormError: false, DisableAutoFireStatusCode: false, ResetOnFireErrorCode: false, URLParamSeparator: toStringPtr(","), TimeFormat: "Mon, 02 Jan 2006 15:04:05 GMT", Charset: "utf-8", // PostMaxMemory is for post body max memory. // // The request body the size limit // can be set by the middleware `LimitRequestBodySize` // or `context#SetMaxRequestBodySize`. PostMaxMemory: 32 << 20, // 32MB LocaleContextKey: "iris.locale", LanguageContextKey: "iris.locale.language", LanguageInputContextKey: "iris.locale.language.input", VersionContextKey: "iris.api.version", VersionAliasesContextKey: "iris.api.version.aliases", ViewEngineContextKey: "iris.view.engine", ViewLayoutContextKey: "iris.view.layout", ViewDataContextKey: "iris.view.data", FallbackViewContextKey: "iris.view.fallback", RemoteAddrHeaders: nil, RemoteAddrHeadersForce: false, RemoteAddrPrivateSubnets: []netutil.IPRange{ { Start: "10.0.0.0", End: "10.255.255.255", }, { Start: "100.64.0.0", End: "100.127.255.255", }, { Start: "172.16.0.0", End: "172.31.255.255", }, { Start: "192.0.0.0", End: "192.0.0.255", }, { Start: "192.168.0.0", End: "192.168.255.255", }, { Start: "198.18.0.0", End: "198.19.255.255", }, }, SSLProxyHeaders: make(map[string]string), HostProxyHeaders: make(map[string]bool), EnableOptimizations: false, EnableProtoJSON: false, EnableEasyJSON: false, Other: make(map[string]any), } } ================================================ FILE: configuration_test.go ================================================ package iris import ( "os" "reflect" "testing" "time" "gopkg.in/yaml.v3" ) // $ go test -v -run TestConfiguration* func TestConfigurationStatic(t *testing.T) { def := DefaultConfiguration() app := New() afterNew := *app.config if !reflect.DeepEqual(def, afterNew) { t.Fatalf("Default configuration is not the same after New expected:\n %#v \ngot:\n %#v", def, afterNew) } afterNew.Charset = "changed" if reflect.DeepEqual(def, afterNew) { t.Fatalf("Configuration should be not equal, got: %#v", afterNew) } app = New().Configure(WithConfiguration(Configuration{DisableBodyConsumptionOnUnmarshal: true})) afterNew = *app.config if !app.config.DisableBodyConsumptionOnUnmarshal { t.Fatalf("Passing a Configuration field as Option fails, expected DisableBodyConsumptionOnUnmarshal to be true but was false") } app = New() // empty , means defaults so if !reflect.DeepEqual(def, *app.config) { t.Fatalf("Default configuration is not the same after New expected:\n %#v \ngot:\n %#v", def, *app.config) } } func TestConfigurationOptions(t *testing.T) { charset := "MYCHARSET" disableBodyConsumptionOnUnmarshal := true disableBanner := true app := New().Configure(WithCharset(charset), WithoutBodyConsumptionOnUnmarshal, WithoutBanner) if got := app.config.Charset; got != charset { t.Fatalf("Expected configuration Charset to be: %s but got: %s", charset, got) } if got := app.config.DisableBodyConsumptionOnUnmarshal; got != disableBodyConsumptionOnUnmarshal { t.Fatalf("Expected configuration DisableBodyConsumptionOnUnmarshal to be: %#v but got: %#v", disableBodyConsumptionOnUnmarshal, got) } if got := app.config.DisableStartupLog; got != disableBanner { t.Fatalf("Expected configuration DisableStartupLog to be: %#v but got: %#v", disableBanner, got) } // now check if other default values are set (should be set automatically) expected := DefaultConfiguration() expected.Charset = charset expected.DisableBodyConsumptionOnUnmarshal = disableBodyConsumptionOnUnmarshal expected.DisableStartupLog = disableBanner has := *app.config if !reflect.DeepEqual(has, expected) { t.Fatalf("Default configuration is not the same after New expected:\n %#v \ngot:\n %#v", expected, has) } } func TestConfigurationOptionsDeep(t *testing.T) { charset := "MYCHARSET" app := New().Configure(WithCharset(charset), WithoutBodyConsumptionOnUnmarshal, WithoutBanner) expected := DefaultConfiguration() expected.Charset = charset expected.DisableBodyConsumptionOnUnmarshal = true expected.DisableStartupLog = true has := *app.config if !reflect.DeepEqual(has, expected) { t.Fatalf("DEEP configuration is not the same after New expected:\n %#v \ngot:\n %#v", expected, has) } } func createGlobalConfiguration(t *testing.T) { filename := homeConfigurationFilename(".yml") c, err := parseYAML(filename) if err != nil { // this error will be occurred the first time that the configuration // file doesn't exist. // Create the YAML-ONLY global configuration file now using the default configuration 'c'. // This is useful when we run multiple iris servers that share the same // configuration, even with custom values at its "Other" field. out, err := yaml.Marshal(&c) if err == nil { err = os.WriteFile(filename, out, os.FileMode(0666)) } if err != nil { t.Fatalf("error while writing the global configuration field at: %s. Trace: %v\n", filename, err) } } } func TestConfigurationGlobal(t *testing.T) { t.Cleanup(func() { os.Remove(homeConfigurationFilename(".yml")) }) createGlobalConfiguration(t) testConfigurationGlobal(t, WithGlobalConfiguration) testConfigurationGlobal(t, WithConfiguration(YAML(globalConfigurationKeyword))) } func testConfigurationGlobal(t *testing.T, c Configurator) { app := New().Configure(c) if has, expected := *app.config, DefaultConfiguration(); !reflect.DeepEqual(has, expected) { t.Fatalf("global configuration (which should be defaulted) is not the same with the default one:\n %#v \ngot:\n %#v", has, expected) } } func TestConfigurationYAML(t *testing.T) { yamlFile, ferr := os.CreateTemp("", "configuration.yml") if ferr != nil { t.Fatal(ferr) } defer func() { yamlFile.Close() time.Sleep(50 * time.Millisecond) os.Remove(yamlFile.Name()) }() yamlConfigurationContents := ` DisablePathCorrection: false DisablePathCorrectionRedirection: true EnablePathIntelligence: true EnablePathEscape: false FireMethodNotAllowed: true EnableOptimizations: true DisableBodyConsumptionOnUnmarshal: true TimeFormat: "Mon, 02 Jan 2006 15:04:05 GMT" Charset: "utf-8" RemoteAddrHeaders: - X-Real-Ip - X-Forwarded-For - CF-Connecting-IP HostProxyHeaders: X-Host: true SSLProxyHeaders: X-Forwarded-Proto: https Other: MyServerName: "Iris: https://github.com/kataras/iris" ` yamlFile.WriteString(yamlConfigurationContents) filename := yamlFile.Name() app := New().Configure(WithConfiguration(YAML(filename))) c := app.config if expected := false; c.DisablePathCorrection != expected { t.Fatalf("error on TestConfigurationYAML: Expected DisablePathCorrection %v but got %v", expected, c.DisablePathCorrection) } if expected := true; c.DisablePathCorrectionRedirection != expected { t.Fatalf("error on TestConfigurationYAML: Expected DisablePathCorrectionRedirection %v but got %v", expected, c.DisablePathCorrectionRedirection) } if expected := true; c.EnablePathIntelligence != expected { t.Fatalf("error on TestConfigurationYAML: Expected EnablePathIntelligence %v but got %v", expected, c.EnablePathIntelligence) } if expected := false; c.EnablePathEscape != expected { t.Fatalf("error on TestConfigurationYAML: Expected EnablePathEscape %v but got %v", expected, c.EnablePathEscape) } if expected := true; c.EnableOptimizations != expected { t.Fatalf("error on TestConfigurationYAML: Expected EnableOptimizations %v but got %v", expected, c.EnablePathEscape) } if expected := true; c.FireMethodNotAllowed != expected { t.Fatalf("error on TestConfigurationYAML: Expected FireMethodNotAllowed %v but got %v", expected, c.FireMethodNotAllowed) } if expected := true; c.DisableBodyConsumptionOnUnmarshal != expected { t.Fatalf("error on TestConfigurationYAML: Expected DisableBodyConsumptionOnUnmarshal %v but got %v", expected, c.DisableBodyConsumptionOnUnmarshal) } if expected := "Mon, 02 Jan 2006 15:04:05 GMT"; c.TimeFormat != expected { t.Fatalf("error on TestConfigurationYAML: Expected TimeFormat %s but got %s", expected, c.TimeFormat) } if expected := "utf-8"; c.Charset != expected { t.Fatalf("error on TestConfigurationYAML: Expected Charset %s but got %s", expected, c.Charset) } if len(c.RemoteAddrHeaders) == 0 { t.Fatalf("error on TestConfigurationYAML: Expected RemoteAddrHeaders to be filled") } expectedRemoteAddrHeaders := []string{ "X-Real-Ip", "X-Forwarded-For", "CF-Connecting-IP", } if expected, got := len(c.RemoteAddrHeaders), len(expectedRemoteAddrHeaders); expected != got { t.Fatalf("error on TestConfigurationYAML: Expected RemoteAddrHeaders' len(%d) and got(%d), len is not the same", expected, got) } for i, v := range c.RemoteAddrHeaders { if expected, got := expectedRemoteAddrHeaders[i], v; expected != got { t.Fatalf("error on TestConfigurationYAML: Expected RemoteAddrHeaders[%d] = %s but got %s", i, expected, got) } } expectedHostProxyHeaders := map[string]bool{ "X-Host": true, } if expected, got := len(c.HostProxyHeaders), len(expectedHostProxyHeaders); expected != got { t.Fatalf("error on TestConfigurationYAML: Expected HostProxyHeaders' len(%d) and got(%d), len is not the same", expected, got) } for k, v := range c.HostProxyHeaders { if expected, got := expectedHostProxyHeaders[k], v; expected != got { t.Fatalf("error on TestConfigurationYAML: Expected HostProxyHeaders[%s] = %t but got %t", k, expected, got) } } expectedSSLProxyHeaders := map[string]string{ "X-Forwarded-Proto": "https", } if expected, got := len(c.SSLProxyHeaders), len(c.SSLProxyHeaders); expected != got { t.Fatalf("error on TestConfigurationYAML: Expected SSLProxyHeaders' len(%d) and got(%d), len is not the same", expected, got) } for k, v := range c.SSLProxyHeaders { if expected, got := expectedSSLProxyHeaders[k], v; expected != got { t.Fatalf("error on TestConfigurationYAML: Expected SSLProxyHeaders[%s] = %s but got %s", k, expected, got) } } if len(c.Other) == 0 { t.Fatalf("error on TestConfigurationYAML: Expected Other to be filled") } if expected, got := "Iris: https://github.com/kataras/iris", c.Other["MyServerName"]; expected != got { t.Fatalf("error on TestConfigurationYAML: Expected Other['MyServerName'] %s but got %s", expected, got) } } func TestConfigurationTOML(t *testing.T) { tomlFile, ferr := os.CreateTemp("", "configuration.toml") if ferr != nil { t.Fatal(ferr) } defer func() { tomlFile.Close() time.Sleep(50 * time.Millisecond) os.Remove(tomlFile.Name()) }() tomlConfigurationContents := ` DisablePathCorrectionRedirection = true EnablePathEscape = false FireMethodNotAllowed = true EnableOptimizations = true DisableBodyConsumptionOnUnmarshal = true TimeFormat = "Mon, 02 Jan 2006 15:04:05 GMT" Charset = "utf-8" RemoteAddrHeaders = ["X-Real-Ip", "X-Forwarded-For", "CF-Connecting-IP"] [Other] # Indentation (tabs and/or spaces) is allowed but not required MyServerName = "Iris: https://github.com/kataras/iris" ` tomlFile.WriteString(tomlConfigurationContents) filename := tomlFile.Name() app := New().Configure(WithConfiguration(TOML(filename))) c := app.config if expected := false; c.DisablePathCorrection != expected { t.Fatalf("error on TestConfigurationTOML: Expected DisablePathCorrection %v but got %v", expected, c.DisablePathCorrection) } if expected := true; c.DisablePathCorrectionRedirection != expected { t.Fatalf("error on TestConfigurationTOML: Expected DisablePathCorrectionRedirection %v but got %v", expected, c.DisablePathCorrectionRedirection) } if expected := false; c.EnablePathEscape != expected { t.Fatalf("error on TestConfigurationTOML: Expected EnablePathEscape %v but got %v", expected, c.EnablePathEscape) } if expected := true; c.EnableOptimizations != expected { t.Fatalf("error on TestConfigurationTOML: Expected EnableOptimizations %v but got %v", expected, c.EnablePathEscape) } if expected := true; c.FireMethodNotAllowed != expected { t.Fatalf("error on TestConfigurationTOML: Expected FireMethodNotAllowed %v but got %v", expected, c.FireMethodNotAllowed) } if expected := true; c.DisableBodyConsumptionOnUnmarshal != expected { t.Fatalf("error on TestConfigurationTOML: Expected DisableBodyConsumptionOnUnmarshal %v but got %v", expected, c.DisableBodyConsumptionOnUnmarshal) } if expected := "Mon, 02 Jan 2006 15:04:05 GMT"; c.TimeFormat != expected { t.Fatalf("error on TestConfigurationTOML: Expected TimeFormat %s but got %s", expected, c.TimeFormat) } if expected := "utf-8"; c.Charset != expected { t.Fatalf("error on TestConfigurationTOML: Expected Charset %s but got %s", expected, c.Charset) } if len(c.RemoteAddrHeaders) == 0 { t.Fatalf("error on TestConfigurationTOML: Expected RemoteAddrHeaders to be filled") } expectedRemoteAddrHeaders := []string{ "X-Real-Ip", "X-Forwarded-For", "CF-Connecting-IP", } if expected, got := len(c.RemoteAddrHeaders), len(expectedRemoteAddrHeaders); expected != got { t.Fatalf("error on TestConfigurationTOML: Expected RemoteAddrHeaders' len(%d) and got(%d), len is not the same", expected, got) } for i, got := range c.RemoteAddrHeaders { if expected := expectedRemoteAddrHeaders[i]; expected != got { t.Fatalf("error on TestConfigurationTOML: Expected RemoteAddrHeaders[%d] = %s but got %s", i, expected, got) } } if len(c.Other) == 0 { t.Fatalf("error on TestConfigurationTOML: Expected Other to be filled") } if expected, got := "Iris: https://github.com/kataras/iris", c.Other["MyServerName"]; expected != got { t.Fatalf("error on TestConfigurationTOML: Expected Other['MyServerName'] %s but got %s", expected, got) } } ================================================ FILE: context/accept_header.go ================================================ package context import "strings" func negotiationMatch(in []string, priorities []string) string { // e.g. // match json: // in: text/html, application/json // priorities: application/json // not match: // in: text/html, application/json // priorities: text/xml // match html: // in: text/html, application/json // priorities: */* // not match: // in: application/json // priorities: text/xml // match json: // in: text/html, application/* // priorities: application/json if len(priorities) == 0 { return "" } if len(in) == 0 { return priorities[0] } for _, accepted := range in { for _, p := range priorities { // wildcard is */* or text/* and etc. // so loop through each char. for i, n := 0, len(accepted); i < n; i++ { if accepted[i] != p[i] { break } if accepted[i] == '*' || p[i] == '*' { return p } if i == n-1 { return p } } } } return "" } func negotiateAcceptHeader(in []string, offers []string, bestOffer string) string { if bestOffer == "" { bestOffer = IDENTITY } bestQ := -1.0 specs := parseAccept(in) for _, offer := range offers { for _, spec := range specs { if spec.Q > bestQ && (spec.Value == "*" || spec.Value == offer) { bestQ = spec.Q bestOffer = offer } } } if bestQ == 0 { bestOffer = "" } return bestOffer } // acceptSpec describes an Accept* header. type acceptSpec struct { Value string Q float64 } // parseAccept parses Accept* headers. func parseAccept(in []string) (specs []acceptSpec) { loop: for _, s := range in { for { var spec acceptSpec spec.Value, s = expectTokenSlash(s) if spec.Value == "" { continue loop } spec.Q = 1.0 s = skipSpace(s) if strings.HasPrefix(s, ";") { s = skipSpace(s[1:]) if !strings.HasPrefix(s, "q=") { continue loop } spec.Q, s = expectQuality(s[2:]) if spec.Q < 0.0 { continue loop } } specs = append(specs, spec) s = skipSpace(s) if !strings.HasPrefix(s, ",") { continue loop } s = skipSpace(s[1:]) } } return } func skipSpace(s string) (rest string) { i := 0 for ; i < len(s); i++ { if octetTypes[s[i]]&isSpace == 0 { break } } return s[i:] } func expectTokenSlash(s string) (token, rest string) { i := 0 for ; i < len(s); i++ { b := s[i] if (octetTypes[b]&isToken == 0) && b != '/' { break } } return s[:i], s[i:] } func expectQuality(s string) (q float64, rest string) { switch { case s == "": return -1, "" case s[0] == '0': q = 0 case s[0] == '1': q = 1 default: return -1, "" } s = s[1:] if !strings.HasPrefix(s, ".") { return q, s } s = s[1:] i := 0 n := 0 d := 1 for ; i < len(s); i++ { b := s[i] if b < '0' || b > '9' { break } n = n*10 + int(b) - '0' d *= 10 } return q + float64(n)/float64(d), s[i:] } // Octet types from RFC 2616. var octetTypes [256]octetType type octetType byte const ( isToken octetType = 1 << iota isSpace ) func init() { // OCTET = // CHAR = // CTL = // CR = // LF = // SP = // HT = // <"> = // CRLF = CR LF // LWS = [CRLF] 1*( SP | HT ) // TEXT = // separators = "(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\" | <"> // | "/" | "[" | "]" | "?" | "=" | "{" | "}" | SP | HT // token = 1* // qdtext = > for c := 0; c < 256; c++ { var t octetType isCtl := c <= 31 || c == 127 isChar := 0 <= c && c <= 127 isSeparator := strings.ContainsRune(" \t\"(),/:;<=>?@[]\\{}", rune(c)) if strings.ContainsRune(" \t\r\n", rune(c)) { t |= isSpace } if isChar && !isCtl && !isSeparator { t |= isToken } octetTypes[c] = t } } ================================================ FILE: context/application.go ================================================ package context import ( stdContext "context" "io" "net/http" "sync" "github.com/kataras/golog" "github.com/tdewolff/minify/v2" ) // Application is the context's owner. // This interface contains the functions that can be used with safety inside a Handler // by `context.Application()`. type Application interface { // ConfigurationReadOnly returns all the available configuration values can be used on a request. ConfigurationReadOnly() ConfigurationReadOnly // Logger returns the golog logger instance(pointer) that is being used inside the "app". Logger() *golog.Logger // IsDebug reports whether the application is running // under debug/development mode. // It's just a shortcut of Logger().Level >= golog.DebugLevel. // The same method existss as Context.IsDebug() too. IsDebug() bool // I18nReadOnly returns the i18n's read-only features. I18nReadOnly() I18nReadOnly // Validate validates a value and returns nil if passed or // the failure reason if not. Validate(any) error // Minifier returns the minifier instance. // By default it can minifies: // - text/html // - text/css // - image/svg+xml // - application/text(javascript, ecmascript, json, xml). // Use that instance to add custom Minifiers before server ran. Minifier() *minify.M // View executes and write the result of a template file to the writer. // // Use context.View to render templates to the client instead. // Returns an error on failure, otherwise nil. View(writer io.Writer, filename string, layout string, bindingData any) error // GetContextPool returns the Iris sync.Pool which holds the contexts values. // Iris automatically releases the request context, so you don't have to use it. // It's only useful to manually release the context on cases that connection // is hijacked by a third-party middleware and the http handler return too fast. GetContextPool() *Pool // GetContextErrorHandler returns the handler which handles errors // on JSON write failures. GetContextErrorHandler() ErrorHandler // ServeHTTPC is the internal router, it's visible because it can be used for advanced use cases, // i.e: routing within a foreign context. // // It is ready to use after Build state. ServeHTTPC(ctx *Context) // ServeHTTP is the main router handler which calls the .Serve and acquires a new context from the pool. // // It is ready to use after Build state. ServeHTTP(w http.ResponseWriter, r *http.Request) // Shutdown gracefully terminates all the application's server hosts and any tunnels. // Returns an error on the first failure, otherwise nil. Shutdown(ctx stdContext.Context) error // GetRouteReadOnly returns the registered "read-only" route based on its name, otherwise nil. // One note: "routeName" should be case-sensitive. Used by the context to get the current route. // It returns an interface instead to reduce wrong usage and to keep the decoupled design between // the context and the routes. // // Look core/router/APIBuilder#GetRoute for more. GetRouteReadOnly(routeName string) RouteReadOnly // GetRoutesReadOnly returns the registered "read-only" routes. // // Look core/router/APIBuilder#GetRoutes for more. GetRoutesReadOnly() []RouteReadOnly // FireErrorCode handles the response's error response. // If `Configuration.ResetOnFireErrorCode()` is true // and the response writer was a recorder or a gzip writer one // then it will try to reset the headers and the body before calling the // registered (or default) error handler for that error code set by // `ctx.StatusCode` method. FireErrorCode(ctx *Context) // RouteExists reports whether a particular route exists // It will search from the current subdomain of context's host, if not inside the root domain. RouteExists(ctx *Context, method, path string) bool // FindClosestPaths returns a list of "n" paths close to "path" under the given "subdomain". // // Order may change. FindClosestPaths(subdomain, searchPath string, n int) []string // String returns the Application's Name. String() string } // Notes(@kataras): // Alternative places... // 1. in apps/store, but it would require an empty `import _ "....apps/store" // from end-developers, to avoid the import cycle and *iris.Application access. // 2. in root package level, that could be the best option, it has access to the *iris.Application // instead of the context.Application interface, but we try to keep the root package // as minimum as possible, however: if in the future, those Application instances // can be registered through network instead of same-process then we must think of that choice. // 3. this is the best possible place, as the root package and all subpackages // have access to this context package without import cycles and they already using it, // the only downside is that we don't have access to the *iris.Application instance // but this context.Application is designed that way that can execute all important methods // as the whole Iris code base is so well written. var ( // registerApps holds all the created iris Applications by this process. // It's slice instead of map because if IRIS_APP_NAME env var exists, // by-default all applications running on the same machine // will have the same name unless `Application.SetName` is called. registeredApps []Application onApplicationRegisteredListeners []func(Application) mu sync.RWMutex ) // RegisterApplication registers an application to the global shared storage. func RegisterApplication(app Application) { if app == nil { return } mu.Lock() registeredApps = append(registeredApps, app) mu.Unlock() mu.RLock() for _, listener := range onApplicationRegisteredListeners { listener(app) } mu.RUnlock() } // OnApplicationRegistered adds a function which fires when a new application // is registered. func OnApplicationRegistered(listeners ...func(app Application)) { mu.Lock() onApplicationRegisteredListeners = append(onApplicationRegisteredListeners, listeners...) mu.Unlock() } // GetApplications returns a slice of all the registered Applications. func GetApplications() []Application { mu.RLock() // a copy slice but the instances are pointers so be careful what modifications are done // the return value is read-only but it can be casted to *iris.Application. apps := make([]Application, 0, len(registeredApps)) copy(apps, registeredApps) mu.RUnlock() return apps } // LastApplication returns the last registered Application. // Handlers has access to the current Application, // use `Context.Application()` instead. func LastApplication() Application { mu.RLock() for i := len(registeredApps) - 1; i >= 0; i-- { if app := registeredApps[i]; app != nil { mu.RUnlock() return app } } mu.RUnlock() return nil } // GetApplication returns a registered Application // based on its name. If the "appName" is not unique // across Applications, then it will return the newest one. func GetApplication(appName string) (Application, bool) { mu.RLock() for i := len(registeredApps) - 1; i >= 0; i-- { if app := registeredApps[i]; app != nil && app.String() == appName { mu.RUnlock() return app, true } } mu.RUnlock() return nil, false } // MustGetApplication same as `GetApplication` but it // panics if "appName" is not a registered Application's name. func MustGetApplication(appName string) Application { app, ok := GetApplication(appName) if !ok || app == nil { panic(appName + " is not a registered Application") } return app } // DefaultLogger returns a Logger instance for an Iris module. // If the program contains at least one registered Iris Application // before this call then it will return a child of that Application's Logger // otherwise a fresh child of the `golog.Default` will be returned instead. // // It should be used when a module has no access to the Application or its Logger. func DefaultLogger(prefix string) (logger *golog.Logger) { if app := LastApplication(); app != nil { logger = app.Logger() } else { logger = golog.Default } logger = logger.Child(prefix) return } ================================================ FILE: context/compress.go ================================================ package context import ( "errors" "fmt" "io" "net/http" "sync" "github.com/andybalholm/brotli" "github.com/golang/snappy" "github.com/klauspost/compress/flate" "github.com/klauspost/compress/gzip" "github.com/klauspost/compress/s2" // snappy output but likely faster decompression. ) // The available builtin compression algorithms. const ( GZIP = "gzip" DEFLATE = "deflate" BROTLI = "br" SNAPPY = "snappy" S2 = "s2" ) // IDENTITY no transformation whatsoever. const IDENTITY = "identity" var ( // ErrResponseNotCompressed returned from AcquireCompressResponseWriter // when response's Content-Type header is missing due to golang/go/issues/31753 or // when accept-encoding is empty. The caller should fallback to the original response writer. ErrResponseNotCompressed = errors.New("compress: response will not be compressed") // ErrRequestNotCompressed returned from NewCompressReader // when request is not compressed. ErrRequestNotCompressed = errors.New("compress: request is not compressed") // ErrNotSupportedCompression returned from // AcquireCompressResponseWriter, NewCompressWriter and NewCompressReader // when the request's Accept-Encoding was not found in the server's supported // compression algorithms. Check that error with `errors.Is`. ErrNotSupportedCompression = errors.New("compress: unsupported compression") ) // AllEncodings is a slice of default content encodings. // See `AcquireCompressResponseWriter`. var AllEncodings = []string{GZIP, DEFLATE, BROTLI, SNAPPY} // GetEncoding extracts the best available encoding from the request. func GetEncoding(r *http.Request, offers []string) (string, error) { acceptEncoding := r.Header[AcceptEncodingHeaderKey] if len(acceptEncoding) == 0 { return "", ErrResponseNotCompressed } encoding := negotiateAcceptHeader(acceptEncoding, offers, IDENTITY) if encoding == "" { return "", fmt.Errorf("%w: %s", ErrNotSupportedCompression, encoding) } return encoding, nil } type ( noOpWriter struct{} noOpReadCloser struct { io.Reader } ) var ( _ io.ReadCloser = (*noOpReadCloser)(nil) _ io.Writer = (*noOpWriter)(nil) ) func (w *noOpWriter) Write(p []byte) (int, error) { return 0, nil } func (r *noOpReadCloser) Close() error { return nil } // CompressWriter is an interface which all compress writers should implement. type CompressWriter interface { io.WriteCloser // All known implementations contain `Flush` and `Reset` methods, // so we wanna declare them upfront. Flush() error Reset(io.Writer) } // NewCompressWriter returns a CompressWriter of "w" based on the given "encoding". func NewCompressWriter(w io.Writer, encoding string, level int) (cw CompressWriter, err error) { switch encoding { case GZIP: cw, err = gzip.NewWriterLevel(w, level) case DEFLATE: // -1 default level, same for gzip. cw, err = flate.NewWriter(w, level) case BROTLI: // 6 default level. if level == -1 { level = 6 } cw = brotli.NewWriterLevel(w, level) case SNAPPY: cw = snappy.NewBufferedWriter(w) case S2: cw = s2.NewWriter(w) default: // Throw if "identity" is given. As this is not acceptable on "Content-Encoding" header. // Only Accept-Encoding (client) can use that; it means, no transformation whatsoever. err = ErrNotSupportedCompression } return } // CompressReader is a structure which wraps a compressed reader. // It is used for determination across common request body and a compressed one. type CompressReader struct { io.ReadCloser // We need this to reset the body to its original state, if requested. Src io.ReadCloser // Encoding is the compression alogirthm is used to decompress and read the data. Encoding string } // NewCompressReader returns a new "compressReader" wrapper of "src". // It returns `ErrRequestNotCompressed` if client's request data are not compressed // or `ErrNotSupportedCompression` if server missing the decompression algorithm. // Note: on server-side the request body (src) will be closed automaticaly. func NewCompressReader(src io.Reader, encoding string) (*CompressReader, error) { if encoding == "" || src == nil { return nil, ErrRequestNotCompressed } var ( rc io.ReadCloser err error ) switch encoding { case GZIP: rc, err = gzip.NewReader(src) case DEFLATE: rc = flate.NewReader(src) case BROTLI: rc = &noOpReadCloser{brotli.NewReader(src)} case SNAPPY: rc = &noOpReadCloser{snappy.NewReader(src)} case S2: rc = &noOpReadCloser{s2.NewReader(src)} default: err = ErrNotSupportedCompression } if err != nil { return nil, err } srcReadCloser, ok := src.(io.ReadCloser) if !ok { srcReadCloser = &noOpReadCloser{src} } return &CompressReader{ ReadCloser: rc, Src: srcReadCloser, Encoding: encoding, }, nil } var compressWritersPool = sync.Pool{New: func() any { return &CompressResponseWriter{} }} // AddCompressHeaders just adds the headers "Vary" to "Accept-Encoding" // and "Content-Encoding" to the given encoding. func AddCompressHeaders(h http.Header, encoding string) { h.Set(VaryHeaderKey, AcceptEncodingHeaderKey) h.Set(ContentEncodingHeaderKey, encoding) } // CompressResponseWriter is a compressed data http.ResponseWriter. type CompressResponseWriter struct { CompressWriter ResponseWriter http.Hijacker Disabled bool Encoding string Level int } var _ ResponseWriter = (*CompressResponseWriter)(nil) // AcquireCompressResponseWriter returns a CompressResponseWriter from the pool. // It accepts an Iris response writer, a net/http request value and // the level of compression (use -1 for default compression level). // // It returns the best candidate among "gzip", "defate", "br", "snappy" and "s2" // based on the request's "Accept-Encoding" header value. func AcquireCompressResponseWriter(w ResponseWriter, r *http.Request, level int) (*CompressResponseWriter, error) { encoding, err := GetEncoding(r, AllEncodings) if err != nil { return nil, err } v := compressWritersPool.Get().(*CompressResponseWriter) if h, ok := w.(http.Hijacker); ok { v.Hijacker = h } else { v.Hijacker = nil } // The Naive() should be used to check for Pusher, // as examples explicitly says, so don't do it: // if p, ok := w.Naive().(http.Pusher); ok { // v.Pusher = p // } else { // v.Pusher = nil // } v.ResponseWriter = w v.Disabled = false if level == -1 && encoding == BROTLI { level = 6 } /* // Writer exists, encoding matching and it's valid because it has a non nil encWriter; // just reset to reduce allocations. if v.Encoding == encoding && v.Level == level && v.CompressWriter != nil { v.CompressWriter.Reset(w) return v, nil } */ v.Encoding = encoding v.Level = level encWriter, err := NewCompressWriter(w, encoding, level) if err != nil { return nil, err } v.CompressWriter = encWriter AddCompressHeaders(w.Header(), encoding) return v, nil } func releaseCompressResponseWriter(w *CompressResponseWriter) { compressWritersPool.Put(w) } // FlushResponse flushes any data, closes the underline compress writer // and writes the status code. // Called automatically before `EndResponse`. func (w *CompressResponseWriter) FlushResponse() { w.FlushHeaders() /* this should NEVER happen, see `context.CompressWriter` method. if rec, ok := w.ResponseWriter.(*ResponseRecorder); ok { // Usecase: record, then compression. w.CompressWriter.Close() // flushes and closes. rec.FlushResponse() return } */ // write the status, after header set and before any flushed content sent. w.ResponseWriter.FlushResponse() if w.IsHijacked() { // net/http docs: // It becomes the caller's responsibility to manage // and close the connection. return } w.CompressWriter.Close() // flushes and closes. } // FlushHeaders deletes the encoding headers if // the compressed writer was disabled otherwise // removes the content-length so next callers can re-calculate the correct length. func (w *CompressResponseWriter) FlushHeaders() { if w.Disabled { w.Header().Del(VaryHeaderKey) w.Header().Del(ContentEncodingHeaderKey) w.CompressWriter.Reset(&noOpWriter{}) } else { w.ResponseWriter.Header().Del(ContentLengthHeaderKey) } } // EndResponse reeases the writers. func (w *CompressResponseWriter) EndResponse() { w.ResponseWriter.EndResponse() releaseCompressResponseWriter(w) } func (w *CompressResponseWriter) Write(p []byte) (int, error) { if w.Disabled { // If disabled or the content-type is empty the response will not be compressed (golang/go/issues/31753). return w.ResponseWriter.Write(p) } if w.Header().Get(ContentTypeHeaderKey) == "" { w.Header().Set(ContentTypeHeaderKey, http.DetectContentType(p)) } return w.CompressWriter.Write(p) } // Flush sends any buffered data to the client. // Can be called manually. func (w *CompressResponseWriter) Flush() { // if w.Disabled { // w.Header().Del(VaryHeaderKey) // w.Header().Del(ContentEncodingHeaderKey) // } else { // w.encWriter.Flush() // } if !w.Disabled { w.CompressWriter.Flush() } w.ResponseWriter.Flush() } // WriteTo writes the "p" to "dest" Writer using the compression that this compress writer was made of. func (w *CompressResponseWriter) WriteTo(dest io.Writer, p []byte) (int, error) { if w.Disabled { return dest.Write(p) } cw, err := NewCompressWriter(dest, w.Encoding, w.Level) if err != nil { return 0, err } n, err := cw.Write(p) cw.Close() return n, err } // Reset implements the ResponseWriterReseter interface. func (w *CompressResponseWriter) Reset() bool { if w.Disabled { // If it's disabled then the underline one is responsible. rs, ok := w.ResponseWriter.(ResponseWriterReseter) return ok && rs.Reset() } w.CompressWriter.Reset(w.ResponseWriter) return true } ================================================ FILE: context/configuration.go ================================================ package context import ( "time" "github.com/kataras/iris/v12/core/netutil" ) // ConfigurationReadOnly can be implemented // by Configuration, it's being used inside the Context. // All methods that it contains should be "safe" to be called by the context // at "serve time". A configuration field may be missing when it's not // safe or its useless to be called from a request handler. type ConfigurationReadOnly interface { // GetVHost returns the non-exported vhost config field. GetVHost() string // GetLogLevel returns the LogLevel field. GetLogLevel() string // GetSocketSharding returns the SocketSharding field. GetSocketSharding() bool // GetKeepAlive returns the KeepAlive field. GetKeepAlive() time.Duration // GetTimeout returns the Timeout field. GetTimeout() time.Duration // GetTimeoutMessage returns the TimeoutMessage field. GetTimeoutMessage() string // GetNonBlocking returns the NonBlocking field. GetNonBlocking() bool // GetDisablePathCorrection returns the DisablePathCorrection field GetDisablePathCorrection() bool // GetDisablePathCorrectionRedirection returns the DisablePathCorrectionRedirection field. GetDisablePathCorrectionRedirection() bool // GetEnablePathIntelligence returns the EnablePathIntelligence field. GetEnablePathIntelligence() bool // GetEnablePathEscape returns the EnablePathEscape field. GetEnablePathEscape() bool // GetForceLowercaseRouting returns the ForceLowercaseRouting field. GetForceLowercaseRouting() bool // GetEnableOptimizations returns the EnableDynamicHandler field. GetEnableDynamicHandler() bool // GetFireMethodNotAllowed returns the FireMethodNotAllowed field. GetFireMethodNotAllowed() bool // GetDisableAutoFireStatusCode returns the DisableAutoFireStatusCode field. GetDisableAutoFireStatusCode() bool // GetResetOnFireErrorCode returns the ResetOnFireErrorCode field. GetResetOnFireErrorCode() bool // GetURLParamSeparator returns URLParamSeparator field. GetURLParamSeparator() *string // GetEnableOptimizations returns the EnableOptimizations field. GetEnableOptimizations() bool // GetEnableProtoJSON returns the EnableProtoJSON field. GetEnableProtoJSON() bool // GetEnableEasyJSON returns the EnableEasyJSON field. GetEnableEasyJSON() bool // GetDisableBodyConsumptionOnUnmarshal returns the DisableBodyConsumptionOnUnmarshal field. GetDisableBodyConsumptionOnUnmarshal() bool // GetFireEmptyFormError returns the FireEmptyFormError field. GetFireEmptyFormError() bool // GetTimeFormat returns the TimeFormat field. GetTimeFormat() string // GetCharset returns the Charset field. GetCharset() string // GetPostMaxMemory returns the PostMaxMemory field. GetPostMaxMemory() int64 // GetLocaleContextKey returns the LocaleContextKey field. GetLocaleContextKey() string // GetLanguageContextKey returns the LanguageContextKey field. GetLanguageContextKey() string // GetLanguageInputContextKey returns the LanguageInputContextKey field. GetLanguageInputContextKey() string // GetVersionContextKey returns the VersionContextKey field. GetVersionContextKey() string // GetVersionAliasesContextKey returns the VersionAliasesContextKey field. GetVersionAliasesContextKey() string // GetViewEngineContextKey returns the ViewEngineContextKey field. GetViewEngineContextKey() string // GetViewLayoutContextKey returns the ViewLayoutContextKey field. GetViewLayoutContextKey() string // GetViewDataContextKey returns the ViewDataContextKey field. GetViewDataContextKey() string // GetFallbackViewContextKey returns the FallbackViewContextKey field. GetFallbackViewContextKey() string // GetRemoteAddrHeaders returns RemoteAddrHeaders field. GetRemoteAddrHeaders() []string // GetRemoteAddrHeadersForce returns RemoteAddrHeadersForce field. GetRemoteAddrHeadersForce() bool // GetRemoteAddrPrivateSubnets returns the RemoteAddrPrivateSubnets field. GetRemoteAddrPrivateSubnets() []netutil.IPRange // GetSSLProxyHeaders returns the SSLProxyHeaders field. GetSSLProxyHeaders() map[string]string // GetHostProxyHeaders returns the HostProxyHeaders field. GetHostProxyHeaders() map[string]bool // GetOther returns the Other field. GetOther() map[string]any } ================================================ FILE: context/context.go ================================================ package context import ( "bytes" "context" "encoding/json" "encoding/xml" "errors" "fmt" "io" "mime" "mime/multipart" "net" "net/http" "net/url" "os" "path" "path/filepath" "reflect" "regexp" "sort" "strconv" "strings" "sync/atomic" "time" "unsafe" "github.com/kataras/iris/v12/core/memstore" "github.com/kataras/iris/v12/core/netutil" "github.com/Shopify/goreferrer" "github.com/fatih/structs" "github.com/gomarkdown/markdown" "github.com/gomarkdown/markdown/html" "github.com/iris-contrib/schema" "github.com/mailru/easyjson" "github.com/mailru/easyjson/jwriter" "github.com/microcosm-cc/bluemonday" "github.com/vmihailenco/msgpack/v5" "golang.org/x/net/publicsuffix" "golang.org/x/time/rate" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" "gopkg.in/yaml.v3" ) var ( // BuildRevision holds the vcs commit id information of the program's build. // Available at go version 1.18+ BuildRevision string // BuildTime holds the vcs commit time information of the program's build. // Available at go version 1.18+ BuildTime string ) type ( // BodyDecoder is an interface which any struct can implement in order to customize the decode action // from ReadJSON and ReadXML // // Trivial example of this could be: // type User struct { Username string } // // func (u *User) Decode(data []byte) error { // return json.Unmarshal(data, u) // } // // the 'Context.ReadJSON/ReadXML(&User{})' will call the User's // Decode option to decode the request body // // Note: This is totally optionally, the default decoders // for ReadJSON is the encoding/json and for ReadXML is the encoding/xml. // // Example: https://github.com/kataras/iris/blob/main/_examples/request-body/read-custom-per-type/main.go BodyDecoder interface { Decode(data []byte) error } // BodyDecoderWithContext same as BodyDecoder but it can accept a standard context, // which is binded to the HTTP request's context. BodyDecoderWithContext interface { DecodeContext(ctx context.Context, data []byte) error } // Unmarshaler is the interface implemented by types that can unmarshal any raw data. // TIP INFO: Any pointer to a value which implements the BodyDecoder can be override the unmarshaler. Unmarshaler interface { Unmarshal(data []byte, outPtr any) error } // UnmarshalerFunc a shortcut for the Unmarshaler interface // // See 'Unmarshaler' and 'BodyDecoder' for more. // // Example: https://github.com/kataras/iris/blob/main/_examples/request-body/read-custom-via-unmarshaler/main.go UnmarshalerFunc func(data []byte, outPtr any) error // DecodeFunc is a generic type of decoder function. // When the returned error is not nil the decode operation // is terminated and the error is received by the ReadJSONStream method, // otherwise it continues to read the next available object. // Look the `Context.ReadJSONStream` method. DecodeFunc func(outPtr any) error ) // Unmarshal parses the X-encoded data and stores the result in the value pointed to by v. // Unmarshal uses the inverse of the encodings that Marshal uses, allocating maps, // slices, and pointers as necessary. func (u UnmarshalerFunc) Unmarshal(data []byte, v any) error { return u(data, v) } // LimitRequestBodySize is a middleware which sets a request body size limit // for all next handlers in the chain. var LimitRequestBodySize = func(maxRequestBodySizeBytes int64) Handler { return func(ctx *Context) { ctx.SetMaxRequestBodySize(maxRequestBodySizeBytes) ctx.Next() } } // Map is just a type alias of the map[string]any type. type Map = map[string]any // Context is the midle-man server's "object" dealing with incoming requests. // // A New context is being acquired from a sync.Pool on each connection. // The Context is the most important thing on the iris's http flow. // // Developers send responses to the client's request through a Context. // Developers get request information from the client's request a Context. type Context struct { // the http.ResponseWriter wrapped by custom writer. writer ResponseWriter // the original http.Request request *http.Request // the current route registered to this request path. currentRoute RouteReadOnly // the local key-value storage params RequestParams // url named parameters. values memstore.Store // generic storage, middleware communication. query url.Values // GET url query temp cache, useful on many URLParamXXX calls. // the underline application app. app Application // the route's handlers handlers Handlers // the current position of the handler's chain currentHandlerIndex int // proceeded reports whether `Proceed` method // called before a `Next`. It is a flash field and it is set // to true on `Next` call when its called on the last handler in the chain. // Reports whether a `Next` is called, // even if the handler index remains the same (last handler). // // Also it's responsible to keep the old value of the last known handler index // before StopExecution. See ResumeExecution. proceeded int // if true, caller is responsible to release the context (put the context to the pool). manualRelease bool } // NewContext returns a new Context instance. func NewContext(app Application) *Context { return &Context{app: app} } /* Not required, unless requested. // SetApplication sets an Iris Application on-fly. // Do NOT use it after ServeHTTPC is fired. func (ctx *Context) SetApplication(app Application) { ctx.app = app } */ // Clone returns a copy of the context that // can be safely used outside the request's scope. // Note that if the request-response lifecycle terminated // or request canceled by the client (can be checked by `ctx.IsCanceled()`) // then the response writer is totally useless. // The http.Request pointer value is shared. func (ctx *Context) Clone() *Context { valuesCopy := make(memstore.Store, len(ctx.values)) copy(valuesCopy, ctx.values) paramsCopy := make(memstore.Store, len(ctx.params.Store)) copy(paramsCopy, ctx.params.Store) queryCopy := make(url.Values, len(ctx.query)) for k, v := range ctx.query { queryCopy[k] = v } req := ctx.request.Clone(ctx.request.Context()) return &Context{ app: ctx.app, values: valuesCopy, params: RequestParams{Store: paramsCopy}, query: queryCopy, writer: ctx.writer.Clone(), request: req, currentHandlerIndex: stopExecutionIndex, proceeded: ctx.proceeded, manualRelease: ctx.manualRelease, currentRoute: ctx.currentRoute, } } // BeginRequest is executing once for each request // it should prepare the (new or acquired from pool) context's fields for the new request. // Do NOT call it manually. Framework calls it automatically. // // Resets // 1. handlers to nil. // 2. values to empty. // 3. the defer function. // 4. response writer to the http.ResponseWriter. // 5. request to the *http.Request. func (ctx *Context) BeginRequest(w http.ResponseWriter, r *http.Request) { ctx.currentRoute = nil ctx.handlers = nil // will be filled by router.Serve/HTTP ctx.values = ctx.values[0:0] // >> >> by context.Values().Set ctx.params.Store = ctx.params.Store[0:0] ctx.query = nil ctx.request = r ctx.currentHandlerIndex = 0 ctx.proceeded = 0 ctx.manualRelease = false ctx.writer = AcquireResponseWriter() ctx.writer.BeginResponse(w) } // EndRequest is executing once after a response to the request was sent and this context is useless or released. // Do NOT call it manually. Framework calls it automatically. // // 1. executes the OnClose function (if any). // 2. flushes the response writer's result or fire any error handler. // 3. releases the response writer. func (ctx *Context) EndRequest() { if !ctx.app.ConfigurationReadOnly().GetDisableAutoFireStatusCode() && StatusCodeNotSuccessful(ctx.GetStatusCode()) { ctx.app.FireErrorCode(ctx) } ctx.writer.FlushResponse() ctx.writer.EndResponse() } // DisablePoolRelease disables the auto context pool Put call. // Do NOT use it, unless you know what you are doing. func (ctx *Context) DisablePoolRelease() { ctx.manualRelease = true } // IsCanceled reports whether the client canceled the request // or the underlying connection has gone. // Note that it will always return true // when called from a goroutine after the request-response lifecycle. func (ctx *Context) IsCanceled() bool { var err error if reqCtx := ctx.request.Context(); reqCtx != nil { err = reqCtx.Err() } else { err = ctx.GetErr() } return IsErrCanceled(err) } // IsErrCanceled reports whether the "err" is caused by a cancellation or timeout. func IsErrCanceled(err error) bool { if err == nil { return false } var netErr net.Error return (errors.As(err, &netErr) && netErr.Timeout()) || errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) || errors.Is(err, http.ErrHandlerTimeout) || err.Error() == "closed pool" } // OnConnectionClose registers the "cb" Handler // which will be fired on its on goroutine on a cloned Context // when the underlying connection has gone away. // // The code inside the given callback is running on its own routine, // as explained above, therefore the callback should NOT // try to access to handler's Context response writer. // // This mechanism can be used to cancel long operations on the server // if the client has disconnected before the response is ready. // // It depends on the Request's Context.Done() channel. // // Finally, it reports whether the protocol supports pipelines (HTTP/1.1 with pipelines disabled is not supported). // The "cb" will not fire for sure if the output value is false. // // Note that you can register only one callback per route. // // See `OnClose` too. func (ctx *Context) OnConnectionClose(cb Handler) bool { if cb == nil { return false } reqCtx := ctx.Request().Context() if reqCtx == nil { return false } notifyClose := reqCtx.Done() if notifyClose == nil { return false } go func() { <-notifyClose // Note(@kataras): No need to clone if not canceled, // EndRequest will be called on the end of the handler chain, // no matter the cancelation. // therefore the context will still be there. cb(ctx.Clone()) }() return true } // OnConnectionCloseErr same as `OnConnectionClose` but instead it // receives a function which returns an error. // If error is not nil, it will be logged as a debug message. func (ctx *Context) OnConnectionCloseErr(cb func() error) bool { if cb == nil { return false } reqCtx := ctx.Request().Context() if reqCtx == nil { return false } notifyClose := reqCtx.Done() if notifyClose == nil { return false } go func() { <-notifyClose if err := cb(); err != nil { // Can be ignored. ctx.app.Logger().Debugf("OnConnectionCloseErr: received error: %v", err) } }() return true } // OnClose registers a callback which // will be fired when the underlying connection has gone away(request canceled) // on its own goroutine or in the end of the request-response lifecylce // on the handler's routine itself (Context access). // // See `OnConnectionClose` too. func (ctx *Context) OnClose(cb Handler) { if cb == nil { return } // Note(@kataras): // - on normal request-response lifecycle // the `SetBeforeFlush` will be called first // and then `OnConnectionClose`, // - when request was canceled before handler finish its job // then the `OnConnectionClose` will be called first instead, // and when the handler function completed then `SetBeforeFlush` is fired. // These are synchronized, they cannot be executed the same exact time, // below we just make sure the "cb" is executed once // by simple boolean check or an atomic one. var executed uint32 callback := func(ctx *Context) { if atomic.CompareAndSwapUint32(&executed, 0, 1) { cb(ctx) } } ctx.OnConnectionClose(callback) onFlush := func() { callback(ctx) } ctx.writer.SetBeforeFlush(onFlush) } // OnCloseErr same as `OnClose` but instead it // receives a function which returns an error. // If error is not nil, it will be logged as a debug message. func (ctx *Context) OnCloseErr(cb func() error) { if cb == nil { return } var executed uint32 callback := func() error { if atomic.CompareAndSwapUint32(&executed, 0, 1) { return cb() } return nil } ctx.OnConnectionCloseErr(callback) onFlush := func() { if err := callback(); err != nil { // Can be ignored. ctx.app.Logger().Debugf("OnClose: SetBeforeFlush: received error: %v", err) } } ctx.writer.SetBeforeFlush(onFlush) } /* Note(@kataras): just leave end-developer decide. const goroutinesContextKey = "iris.goroutines" type goroutines struct { wg *sync.WaitGroup length int mu sync.RWMutex } var acquireGoroutines = func() any { return &goroutines{wg: new(sync.WaitGroup)} } func (ctx *Context) Go(fn func(cancelCtx context.Context)) (running int) { g := ctx.values.GetOrSet(goroutinesContextKey, acquireGoroutines).(*goroutines) if fn != nil { g.wg.Add(1) g.mu.Lock() g.length++ g.mu.Unlock() ctx.waitFunc = g.wg.Wait go func(reqCtx context.Context) { fn(reqCtx) g.wg.Done() g.mu.Lock() g.length-- g.mu.Unlock() }(ctx.request.Context()) } g.mu.RLock() running = g.length g.mu.RUnlock() return } */ // ResponseWriter returns an http.ResponseWriter compatible response writer, as expected. func (ctx *Context) ResponseWriter() ResponseWriter { return ctx.writer } // ResetResponseWriter sets a new ResponseWriter implementation // to this Context to use as its writer. // Note, to change the underline http.ResponseWriter use // ctx.ResponseWriter().SetWriter(http.ResponseWriter) instead. func (ctx *Context) ResetResponseWriter(newResponseWriter ResponseWriter) { if rec, ok := ctx.IsRecording(); ok { releaseResponseRecorder(rec) } ctx.writer = newResponseWriter } // Request returns the original *http.Request, as expected. func (ctx *Context) Request() *http.Request { return ctx.request } // ResetRequest sets the Context's Request, // It is useful to store the new request created by a std *http.Request#WithContext() into Iris' Context. // Use `ResetRequest` when for some reason you want to make a full // override of the *http.Request. // Note that: when you just want to change one of each fields you can use the Request() which returns a pointer to Request, // so the changes will have affect without a full override. // Usage: you use a native http handler which uses the standard "context" package // to get values instead of the Iris' Context#Values(): // r := ctx.Request() // stdCtx := context.WithValue(r.Context(), key, val) // ctx.ResetRequest(r.WithContext(stdCtx)). func (ctx *Context) ResetRequest(r *http.Request) { ctx.request = r } // SetCurrentRoute sets the route internally, // See `GetCurrentRoute()` method too. // It's being initialized by the Router. // See `Exec` or `SetHandlers/AddHandler` methods to simulate a request. func (ctx *Context) SetCurrentRoute(route RouteReadOnly) { ctx.currentRoute = route } // GetCurrentRoute returns the current "read-only" route that // was registered to this request's path. func (ctx *Context) GetCurrentRoute() RouteReadOnly { return ctx.currentRoute } // Do sets the "handlers" as the chain // and executes the first handler, // handlers should not be empty. // // It's used by the router, developers may use that // to replace and execute handlers immediately. func (ctx *Context) Do(handlers Handlers) { if len(handlers) == 0 { return } ctx.handlers = handlers handlers[0](ctx) } // AddHandler can add handler(s) // to the current request in serve-time, // these handlers are not persistenced to the router. // // Router is calling this function to add the route's handler. // If AddHandler called then the handlers will be inserted // to the end of the already-defined route's handler. func (ctx *Context) AddHandler(handlers ...Handler) { ctx.handlers = append(ctx.handlers, handlers...) } // SetHandlers replaces all handlers with the new. func (ctx *Context) SetHandlers(handlers Handlers) { ctx.handlers = handlers } // Handlers keeps tracking of the current handlers. func (ctx *Context) Handlers() Handlers { return ctx.handlers } // HandlerIndex sets the current index of the // current context's handlers chain. // If n < 0 or the current handlers length is 0 then it just returns the // current handler index without change the current index. // // Look Handlers(), Next() and StopExecution() too. func (ctx *Context) HandlerIndex(n int) (currentIndex int) { if n < 0 || n > len(ctx.handlers)-1 { return ctx.currentHandlerIndex } ctx.currentHandlerIndex = n return n } // Proceed is an alternative way to check if a particular handler // has been executed. // The given "h" Handler can report a failure with `StopXXX` methods // or ignore calling a `Next` (see `iris.ExecutionRules` too). // // This is useful only when you run a handler inside // another handler. It justs checks for before index and the after index. // // A usecase example is when you want to execute a middleware // inside controller's `BeginRequest` that calls the `ctx.Next` inside it. // The Controller looks the whole flow (BeginRequest, method handler, EndRequest) // as one handler, so `ctx.Next` will not be reflected to the method handler // if called from the `BeginRequest`. // // Although `BeginRequest` should NOT be used to call other handlers, // the `BeginRequest` has been introduced to be able to set // common data to all method handlers before their execution. // Controllers can accept middleware(s) from the MVC's Application's Router as normally. // // That said let's see an example of `ctx.Proceed`: // // var authMiddleware = basicauth.New(basicauth.Config{ // Users: map[string]string{ // "admin": "password", // }, // }) // // func (c *UsersController) BeginRequest(ctx iris.Context) { // if !ctx.Proceed(authMiddleware) { // ctx.StopExecution() // } // } // // This Get() will be executed in the same handler as `BeginRequest`, // internally controller checks for `ctx.StopExecution`. // So it will not be fired if BeginRequest called the `StopExecution`. // // func(c *UsersController) Get() []models.User { // return c.Service.GetAll() // } // // Alternative way is `!ctx.IsStopped()` if middleware make use of the `ctx.StopExecution()` on failure. func (ctx *Context) Proceed(h Handler) bool { _, ok := ctx.ProceedAndReportIfStopped(h) return ok } // ProceedAndReportIfStopped same as "Proceed" method // but the first output parameter reports whether the "h" // called "StopExecution" manually. func (ctx *Context) ProceedAndReportIfStopped(h Handler) (bool, bool) { ctx.proceeded = internalPauseExecutionIndex // Store the current index. beforeIdx := ctx.currentHandlerIndex h(ctx) // Retrieve the next one, if Next is called this is beforeIdx + 1 and so on. afterIdx := ctx.currentHandlerIndex // Restore prev index, no matter what. ctx.currentHandlerIndex = beforeIdx proceededByNext := ctx.proceeded == internalProceededHandlerIndex ctx.proceeded = beforeIdx // Stop called, return false but keep the handlers index. if afterIdx == stopExecutionIndex { return true, false } if proceededByNext { return false, true } // Next called or not. return false, afterIdx > beforeIdx } // HandlerName returns the current handler's name, helpful for debugging. func (ctx *Context) HandlerName() string { return HandlerName(ctx.handlers[ctx.currentHandlerIndex]) } // HandlerFileLine returns the current running handler's function source file and line information. // Useful mostly when debugging. func (ctx *Context) HandlerFileLine() (file string, line int) { return HandlerFileLine(ctx.handlers[ctx.currentHandlerIndex]) } // RouteName returns the route name that this handler is running on. // Note that it may return empty on not found handlers. func (ctx *Context) RouteName() string { if ctx.currentRoute == nil { return "" } return ctx.currentRoute.Name() } // Next calls the next handler from the handlers chain, // it should be used inside a middleware. func (ctx *Context) Next() { if ctx.IsStopped() { return } if ctx.proceeded <= internalPauseExecutionIndex /* pause and proceeded */ { ctx.proceeded = internalProceededHandlerIndex return } nextIndex, n := ctx.currentHandlerIndex+1, len(ctx.handlers) if nextIndex < n { ctx.currentHandlerIndex = nextIndex ctx.handlers[nextIndex](ctx) } } // NextOr checks if chain has a next handler, if so then it executes it // otherwise it sets a new chain assigned to this Context based on the given handler(s) // and executes its first handler. // // Returns true if next handler exists and executed, otherwise false. // // Note that if no next handler found and handlers are missing then // it sends a Status Not Found (404) to the client and it stops the execution. func (ctx *Context) NextOr(handlers ...Handler) bool { if next := ctx.NextHandler(); next != nil { ctx.Skip() // skip this handler from the chain. next(ctx) return true } if len(handlers) == 0 { ctx.NotFound() ctx.StopExecution() return false } ctx.Do(handlers) return false } // NextOrNotFound checks if chain has a next handler, if so then it executes it // otherwise it sends a Status Not Found (404) to the client and stops the execution. // // Returns true if next handler exists and executed, otherwise false. func (ctx *Context) NextOrNotFound() bool { return ctx.NextOr() } // NextHandler returns (it doesn't execute) the next handler from the handlers chain. // // Use .Skip() to skip this handler if needed to execute the next of this returning handler. func (ctx *Context) NextHandler() Handler { if ctx.IsStopped() { return nil } nextIndex := ctx.currentHandlerIndex + 1 // check if it has a next middleware if nextIndex < len(ctx.handlers) { return ctx.handlers[nextIndex] } return nil } // Skip skips/ignores the next handler from the handlers chain, // it should be used inside a middleware. func (ctx *Context) Skip() { ctx.HandlerIndex(ctx.currentHandlerIndex + 1) } const ( stopExecutionIndex = -1 internalPauseExecutionIndex = -2 internalProceededHandlerIndex = -3 ) // StopExecution stops the handlers chain of this request. // Meaning that any following `Next` calls are ignored, // as a result the next handlers in the chain will not be fire. // // See ResumeExecution too. func (ctx *Context) StopExecution() { if curIdx := ctx.currentHandlerIndex; curIdx != stopExecutionIndex { // Protect against multiple calls of StopExecution. // Resume should set the last proceeded handler index. // Store the current index. ctx.proceeded = curIdx // And stop. ctx.currentHandlerIndex = stopExecutionIndex } } // IsStopped reports whether the current position of the context's handlers is -1, // means that the StopExecution() was called at least once. func (ctx *Context) IsStopped() bool { return ctx.currentHandlerIndex == stopExecutionIndex } // ResumeExecution sets the current handler index to the last // index of the executed handler before StopExecution method was fired. // // Reports whether it's restored after a StopExecution call. func (ctx *Context) ResumeExecution() bool { if ctx.IsStopped() { ctx.currentHandlerIndex = ctx.proceeded return true } return false } // StopWithStatus stops the handlers chain and writes the "statusCode". // // If the status code is a failure one then // it will also fire the specified error code handler. func (ctx *Context) StopWithStatus(statusCode int) { ctx.StopExecution() ctx.StatusCode(statusCode) } // StopWithText stops the handlers chain and writes the "statusCode" // among with a fmt-style text of "format" and optional arguments. // // If the status code is a failure one then // it will also fire the specified error code handler. func (ctx *Context) StopWithText(statusCode int, text string) { ctx.StopWithStatus(statusCode) ctx.WriteString(text) } // StopWithError stops the handlers chain and writes the "statusCode" // among with the error "err". // It Calls the `SetErr` method so error handlers can access the given error. // // If the status code is a failure one then // it will also fire the specified error code handler. // // If the given "err" is private then the // status code's text is rendered instead (unless a registered error handler overrides it). func (ctx *Context) StopWithError(statusCode int, err error) { if err == nil { return } ctx.SetErr(err) if _, ok := err.(ErrPrivate); ok { // error is private, we SHOULD not render it, // leave the error handler alone to // render the code's text instead. ctx.StopWithStatus(statusCode) return } ctx.StopWithText(statusCode, err.Error()) } // StopWithPlainError like `StopWithError` but it does NOT // write anything to the response writer, it stores the error // so any error handler matching the given "statusCode" can handle it by its own. func (ctx *Context) StopWithPlainError(statusCode int, err error) { if err == nil { return } ctx.SetErr(err) ctx.StopWithStatus(statusCode) } // StopWithJSON stops the handlers chain, writes the status code // and sends a JSON response. // // If the status code is a failure one then // it will also fire the specified error code handler. func (ctx *Context) StopWithJSON(statusCode int, jsonObject any) error { ctx.StopWithStatus(statusCode) return ctx.writeJSON(jsonObject, &DefaultJSONOptions) // do not modify - see errors.DefaultContextErrorHandler. } // StopWithProblem stops the handlers chain, writes the status code // and sends an application/problem+json response. // See `iris.NewProblem` to build a "problem" value correctly. // // If the status code is a failure one then // it will also fire the specified error code handler. func (ctx *Context) StopWithProblem(statusCode int, problem Problem) error { ctx.StopWithStatus(statusCode) problem.Status(statusCode) return ctx.Problem(problem) } // +------------------------------------------------------------+ // | Current "user/request" storage | // | and share information between the handlers - Values(). | // | Save and get named path parameters - Params() | // +------------------------------------------------------------+ // Params returns the current url's named parameters key-value storage. // Named path parameters are being saved here. // This storage, as the whole context, is per-request lifetime. func (ctx *Context) Params() *RequestParams { return &ctx.params } // Values returns the current "user" storage. // Named path parameters and any optional data can be saved here. // This storage, as the whole context, is per-request lifetime. // // You can use this function to Set and Get local values // that can be used to share information between handlers and middleware. func (ctx *Context) Values() *memstore.Store { return &ctx.values } // +------------------------------------------------------------+ // | Path, Host, Subdomain, IP, Headers etc... | // +------------------------------------------------------------+ // Method returns the request.Method, the client's http method to the server. func (ctx *Context) Method() string { return ctx.request.Method } // Path returns the full request path, // escaped if EnablePathEscape config field is true. func (ctx *Context) Path() string { return ctx.RequestPath(ctx.app.ConfigurationReadOnly().GetEnablePathEscape()) } // DecodeQuery returns the uri parameter as url (string) // useful when you want to pass something to a database and be valid to retrieve it via context.Param // use it only for special cases, when the default behavior doesn't suits you. // // http://www.blooberry.com/indexdot/html/topics/urlencoding.htm // it uses just the url.QueryUnescape func DecodeQuery(path string) string { if path == "" { return "" } encodedPath, err := url.QueryUnescape(path) if err != nil { return path } return encodedPath } // DecodeURL returns the decoded uri // useful when you want to pass something to a database and be valid to retrieve it via context.Param // use it only for special cases, when the default behavior doesn't suits you. // // http://www.blooberry.com/indexdot/html/topics/urlencoding.htm // it uses just the url.Parse func DecodeURL(uri string) string { u, err := url.Parse(uri) if err != nil { return uri } return u.String() } // RequestPath returns the full request path, // based on the 'escape'. func (ctx *Context) RequestPath(escape bool) string { if escape { return ctx.request.URL.EscapedPath() // DecodeQuery(ctx.request.URL.EscapedPath()) } return ctx.request.URL.Path // RawPath returns empty, requesturi can be used instead also. } const sufscheme = "://" // GetScheme returns the full scheme of the request URL (https://, http:// or ws:// and e.t.c.). func GetScheme(r *http.Request) string { scheme := r.URL.Scheme if scheme == "" { if r.TLS != nil { scheme = netutil.SchemeHTTPS } else { scheme = netutil.SchemeHTTP } } return scheme + sufscheme } // Scheme returns the full scheme of the request (including :// suffix). func (ctx *Context) Scheme() string { return GetScheme(ctx.Request()) } // PathPrefixMap accepts a map of string and a handler. // The key of "m" is the key, which is the prefix, regular expressions are not valid. // The value of "m" is the handler that will be executed if HasPrefix(context.Path). // func (ctx *Context) PathPrefixMap(m map[string]context.Handler) bool { // path := ctx.Path() // for k, v := range m { // if strings.HasPrefix(path, k) { // v(ctx) // return true // } // } // return false // } no, it will not work because map is a random peek data structure. // GetHost returns the host part of the current URI. func GetHost(r *http.Request) string { // contains subdomain. if host := r.URL.Host; host != "" { return host } return r.Host } // Host returns the host:port part of the request URI, calls the `Request().Host`. // To get the subdomain part as well use the `Request().URL.Host` method instead. // To get the subdomain only use the `Subdomain` method instead. // This method makes use of the `Configuration.HostProxyHeaders` field too. func (ctx *Context) Host() string { for header, ok := range ctx.app.ConfigurationReadOnly().GetHostProxyHeaders() { if !ok { continue } if host := ctx.GetHeader(header); host != "" { return host } } return GetHost(ctx.request) } // GetDomain resolves and returns the server's domain. // To customize its behavior, developers can modify this package-level function at initialization. var GetDomain = func(hostport string) string { host := hostport if tmp, _, err := net.SplitHostPort(hostport); err == nil { host = tmp } switch host { // We could use the netutil.LoopbackRegex but leave it as it's for now, it's faster. case "localhost", "127.0.0.1", "0.0.0.0", "::1", "[::1]", "0:0:0:0:0:0:0:0", "0:0:0:0:0:0:0:1": // loopback. return "localhost" default: if net.ParseIP(host) != nil { // if it's an IP, see #1945. return host } if domain, err := publicsuffix.EffectiveTLDPlusOne(host); err == nil { host = domain } return host } } // Domain returns the root level domain. func (ctx *Context) Domain() string { return GetDomain(ctx.Host()) } // GetSubdomainFull returns the full subdomain level, e.g. // [test.user.]mydomain.com. func GetSubdomainFull(r *http.Request) string { host := GetHost(r) // host:port rootDomain := GetDomain(host) // mydomain.com rootDomainIdx := strings.Index(host, rootDomain) if rootDomainIdx == -1 { return "" } return host[0:rootDomainIdx] } // SubdomainFull returns the full subdomain level, e.g. // [test.user.]mydomain.com. // Note that HostProxyHeaders are being respected here. func (ctx *Context) SubdomainFull() string { host := ctx.Host() // host:port rootDomain := GetDomain(host) // mydomain.com rootDomainIdx := strings.Index(host, rootDomain) if rootDomainIdx == -1 { return "" } return host[0:rootDomainIdx] } // Subdomain returns the first subdomain of this request, // e.g. [user.]mydomain.com. // See `SubdomainFull` too. func (ctx *Context) Subdomain() (subdomain string) { host := ctx.Host() if index := strings.IndexByte(host, '.'); index > 0 { subdomain = host[0:index] } // listening on mydomain.com:80 // subdomain = mydomain, but it's wrong, it should return "" vhost := ctx.app.ConfigurationReadOnly().GetVHost() if strings.Contains(vhost, subdomain) { // then it's not subdomain return "" } return } // FindClosest returns a list of "n" paths close to // this request based on subdomain and request path. // // Order may change. // Example: https://github.com/kataras/iris/tree/main/_examples/routing/intelligence/manual func (ctx *Context) FindClosest(n int) []string { return ctx.app.FindClosestPaths(ctx.Subdomain(), ctx.Path(), n) } // IsWWW returns true if the current subdomain (if any) is www. func (ctx *Context) IsWWW() bool { host := ctx.Host() if index := strings.IndexByte(host, '.'); index > 0 { // if it has a subdomain and it's www then return true. if subdomain := host[0:index]; !strings.Contains(ctx.app.ConfigurationReadOnly().GetVHost(), subdomain) { return subdomain == "www" } } return false } // FullRequestURI returns the full URI, // including the scheme, the host and the relative requested path/resource. func (ctx *Context) FullRequestURI() string { return ctx.AbsoluteURI(ctx.Path()) } // RemoteAddr tries to parse and return the real client's request IP. // // Based on allowed headers names that can be modified from Configuration.RemoteAddrHeaders. // // If parse based on these headers fail then it will return the Request's `RemoteAddr` field // which is filled by the server before the HTTP handler, // unless the Configuration.RemoteAddrHeadersForce was set to true // which will force this method to return the first IP from RemoteAddrHeaders // even if it's part of a private network. // // Look `Configuration.RemoteAddrHeaders`, // // Configuration.RemoteAddrHeadersForce, // Configuration.WithRemoteAddrHeader(...), // Configuration.WithoutRemoteAddrHeader(...) and // Configuration.RemoteAddrPrivateSubnetsW for more. func (ctx *Context) RemoteAddr() string { if remoteHeaders := ctx.app.ConfigurationReadOnly().GetRemoteAddrHeaders(); len(remoteHeaders) > 0 { privateSubnets := ctx.app.ConfigurationReadOnly().GetRemoteAddrPrivateSubnets() for _, headerName := range remoteHeaders { ipAddresses := strings.Split(ctx.GetHeader(headerName), ",") if ip, ok := netutil.GetIPAddress(ipAddresses, privateSubnets); ok { return ip } } if ctx.app.ConfigurationReadOnly().GetRemoteAddrHeadersForce() { for _, headerName := range remoteHeaders { // return the first valid IP, // even if it's a part of a private network. ipAddresses := strings.Split(ctx.GetHeader(headerName), ",") for _, addr := range ipAddresses { if ip, _, err := net.SplitHostPort(addr); err == nil { return ip } } } } } addr := strings.TrimSpace(ctx.request.RemoteAddr) if addr != "" { // if addr has port use the net.SplitHostPort otherwise(error occurs) take as it is if ip, _, err := net.SplitHostPort(addr); err == nil { return ip } } return addr } // TrimHeaderValue returns the "v[0:first space or semicolon]". func TrimHeaderValue(v string) string { for i, char := range v { if char == ' ' || char == ';' { return v[:i] } } return v } // GetHeader returns the request header's value based on its name. func (ctx *Context) GetHeader(name string) string { return ctx.request.Header.Get(name) } // IsAjax returns true if this request is an 'ajax request'( XMLHttpRequest) // // There is no a 100% way of knowing that a request was made via Ajax. // You should never trust data coming from the client, they can be easily overcome by spoofing. // // Note that "X-Requested-With" Header can be modified by any client(because of "X-"), // so don't rely on IsAjax for really serious stuff, // try to find another way of detecting the type(i.e, content type), // there are many blogs that describe these problems and provide different kind of solutions, // it's always depending on the application you're building, // this is the reason why this `IsAjax` is simple enough for general purpose use. // // Read more at: https://developer.mozilla.org/en-US/docs/AJAX // and https://xhr.spec.whatwg.org/ func (ctx *Context) IsAjax() bool { return ctx.GetHeader("X-Requested-With") == "XMLHttpRequest" } var isMobileRegex = regexp.MustCompile("(?:hpw|i|web)os|alamofire|alcatel|amoi|android|avantgo|blackberry|blazer|cell|cfnetwork|darwin|dolfin|dolphin|fennec|htc|ip(?:hone|od|ad)|ipaq|j2me|kindle|midp|minimo|mobi|motorola|nec-|netfront|nokia|opera m(ob|in)i|palm|phone|pocket|portable|psp|silk-accelerated|skyfire|sony|ucbrowser|up.browser|up.link|windows ce|xda|zte|zune") // IsMobile checks if client is using a mobile device(phone or tablet) to communicate with this server. // If the return value is true that means that the http client using a mobile // device to communicate with the server, otherwise false. // // Keep note that this checks the "User-Agent" request header. func (ctx *Context) IsMobile() bool { s := strings.ToLower(ctx.GetHeader("User-Agent")) return isMobileRegex.MatchString(s) } var isScriptRegex = regexp.MustCompile("curl|wget|collectd|python|urllib|java|jakarta|httpclient|phpcrawl|libwww|perl|go-http|okhttp|lua-resty|winhttp|awesomium") // IsScript reports whether a client is a script. func (ctx *Context) IsScript() bool { s := strings.ToLower(ctx.GetHeader("User-Agent")) return isScriptRegex.MatchString(s) } // IsSSL reports whether the client is running under HTTPS SSL. // // See `IsHTTP2` too. func (ctx *Context) IsSSL() bool { ssl := strings.EqualFold(ctx.request.URL.Scheme, "https") || ctx.request.TLS != nil if !ssl { for k, v := range ctx.app.ConfigurationReadOnly().GetSSLProxyHeaders() { if ctx.GetHeader(k) == v { ssl = true break } } } return ssl } // IsHTTP2 reports whether the protocol version for incoming request was HTTP/2. // The client code always uses either HTTP/1.1 or HTTP/2. // // See `IsSSL` too. func (ctx *Context) IsHTTP2() bool { return ctx.request.ProtoMajor == 2 } // IsGRPC reports whether the request came from a gRPC client. func (ctx *Context) IsGRPC() bool { return ctx.IsHTTP2() && strings.Contains(ctx.GetContentTypeRequested(), ContentGRPCHeaderValue) } type ( // Referrer contains the extracted information from the `GetReferrer` // // The structure contains struct tags for JSON, form, XML, YAML and TOML. // Look the `GetReferrer() Referrer` and `goreferrer` external package. Referrer struct { // The raw refer(r)er URL. Raw string `json:"raw" form:"raw" xml:"Raw" yaml:"Raw" toml:"Raw"` Type ReferrerType `json:"type" form:"referrer_type" xml:"Type" yaml:"Type" toml:"Type"` Label string `json:"label" form:"referrer_form" xml:"Label" yaml:"Label" toml:"Label"` URL string `json:"url" form:"referrer_url" xml:"URL" yaml:"URL" toml:"URL"` Subdomain string `json:"subdomain" form:"referrer_subdomain" xml:"Subdomain" yaml:"Subdomain" toml:"Subdomain"` Domain string `json:"domain" form:"referrer_domain" xml:"Domain" yaml:"Domain" toml:"Domain"` Tld string `json:"tld" form:"referrer_tld" xml:"Tld" yaml:"Tld" toml:"Tld"` Path string `json:"path" form:"referrer_path" xml:"Path" yaml:"Path" toml:"Path"` Query string `json:"query" form:"referrer_query" xml:"Query" yaml:"Query" toml:"GoogleType"` GoogleType ReferrerGoogleSearchType `json:"googleType" form:"referrer_google_type" xml:"GoogleType" yaml:"GoogleType" toml:"GoogleType"` } // ReferrerType is the goreferrer enum for a referrer type (indirect, direct, email, search, social). ReferrerType = goreferrer.ReferrerType // ReferrerGoogleSearchType is the goreferrer enum for a google search type (organic, adwords). ReferrerGoogleSearchType = goreferrer.GoogleSearchType ) // String returns the raw ref url. func (ref Referrer) String() string { return ref.Raw } // Contains the available values of the goreferrer enums. const ( ReferrerInvalid ReferrerType = iota ReferrerIndirect ReferrerDirect ReferrerEmail ReferrerSearch ReferrerSocial ReferrerNotGoogleSearch ReferrerGoogleSearchType = iota ReferrerGoogleOrganicSearch ReferrerGoogleAdwords ) // unnecessary but good to know the default values upfront. var emptyReferrer = Referrer{Type: ReferrerInvalid, GoogleType: ReferrerNotGoogleSearch} // GetReferrer extracts and returns the information from the "Referer" (or "Referrer") header // and url query parameter as specified in https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy. func (ctx *Context) GetReferrer() Referrer { // the underline net/http follows the https://tools.ietf.org/html/rfc7231#section-5.5.2, // so there is nothing special left to do. // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy refURL := ctx.GetHeader("Referer") if refURL == "" { refURL = ctx.GetHeader("Referrer") if refURL == "" { refURL = ctx.URLParam("referer") if refURL == "" { refURL = ctx.URLParam("referrer") } } } if refURL == "" { return emptyReferrer } if ref := goreferrer.DefaultRules.Parse(refURL); ref.Type > goreferrer.Invalid { return Referrer{ Raw: refURL, Type: ReferrerType(ref.Type), Label: ref.Label, URL: ref.URL, Subdomain: ref.Subdomain, Domain: ref.Domain, Tld: ref.Tld, Path: ref.Path, Query: ref.Query, GoogleType: ReferrerGoogleSearchType(ref.GoogleType), } } return emptyReferrer } // SetLanguage force-sets the language for i18n, can be used inside a middleare. // It has the highest priority over the rest and if it is empty then it is ignored, // if it set to a static string of "default" or to the default language's code // then the rest of the language extractors will not be called at all and // the default language will be set instead. // // See `i18n.ExtractFunc` for a more organised way of the same feature. func (ctx *Context) SetLanguage(langCode string) { ctx.values.Set(ctx.app.ConfigurationReadOnly().GetLanguageContextKey(), langCode) } // GetLocale returns the current request's `Locale` found by i18n middleware. // It always fallbacks to the default one. // See `Tr` too. func (ctx *Context) GetLocale() Locale { // Cache the Locale itself for multiple calls of `Tr` method. contextKey := ctx.app.ConfigurationReadOnly().GetLocaleContextKey() if v := ctx.values.Get(contextKey); v != nil { if locale, ok := v.(Locale); ok { return locale } } if locale := ctx.app.I18nReadOnly().GetLocale(ctx); locale != nil { ctx.values.Set(contextKey, locale) return locale } return nil } // Tr returns a i18n localized message based on format with optional arguments. // See `GetLocale` too. // // Example: https://github.com/kataras/iris/tree/main/_examples/i18n func (ctx *Context) Tr(key string, args ...any) string { return ctx.app.I18nReadOnly().TrContext(ctx, key, args...) } // +------------------------------------------------------------+ // | Response Headers helpers | // +------------------------------------------------------------+ // Header adds a header to the response, if value is empty // it removes the header by its name. func (ctx *Context) Header(name string, value string) { if value == "" { ctx.writer.Header().Del(name) return } ctx.writer.Header().Add(name, value) } const contentTypeContextKey = "iris.content_type" func shouldAppendCharset(cType string) bool { if idx := strings.IndexRune(cType, '/'); idx > 1 && len(cType) > idx+1 { typ := cType[0:idx] if typ == "application" { switch cType[idx+1:] { case "json", "xml", "yaml", "problem+json", "problem+xml": return true default: return false } } } return true } func (ctx *Context) contentTypeOnce(cType string, charset string) { if charset == "" { charset = ctx.app.ConfigurationReadOnly().GetCharset() } if shouldAppendCharset(cType) { cType += "; charset=" + charset } ctx.values.Set(contentTypeContextKey, cType) ctx.writer.Header().Set(ContentTypeHeaderKey, cType) } // ContentType sets the response writer's // header "Content-Type" to the 'cType'. func (ctx *Context) ContentType(cType string) { if cType == "" { return } if _, wroteOnce := ctx.values.GetEntry(contentTypeContextKey); wroteOnce { return } // 1. if it's path or a filename or an extension, // then take the content type from that, // ^ No, it's not always a file,e .g. vnd.$type // if strings.Contains(cType, ".") { // ext := filepath.Ext(cType) // cType = mime.TypeByExtension(ext) // } // if doesn't contain a charset already then append it if shouldAppendCharset(cType) { if !strings.Contains(cType, "charset") { cType += "; charset=" + ctx.app.ConfigurationReadOnly().GetCharset() } } ctx.writer.Header().Set(ContentTypeHeaderKey, cType) } // GetContentType returns the response writer's // header value of "Content-Type". func (ctx *Context) GetContentType() string { return ctx.writer.Header().Get(ContentTypeHeaderKey) } // GetContentTypeRequested returns the request's // trim-ed(without the charset and priority values) // header value of "Content-Type". func (ctx *Context) GetContentTypeRequested() string { // could use mime.ParseMediaType too. return TrimHeaderValue(ctx.GetHeader(ContentTypeHeaderKey)) } // GetContentLength returns the request's // header value of "Content-Length". func (ctx *Context) GetContentLength() int64 { if v := ctx.GetHeader(ContentLengthHeaderKey); v != "" { n, _ := strconv.ParseInt(v, 10, 64) return n } return 0 } // StatusCode sets the status code header to the response. // Look .GetStatusCode & .FireStatusCode too. // // Note that you must set status code before write response body (except when recorder is used). func (ctx *Context) StatusCode(statusCode int) { ctx.writer.WriteHeader(statusCode) } // NotFound emits an error 404 to the client, using the specific custom error error handler. // Note that you may need to call ctx.StopExecution() if you don't want the next handlers // to be executed. Next handlers are being executed on iris because you can alt the // error code and change it to a more specific one, i.e // users := app.Party("/users") // users.Done(func(ctx iris.Context){ if ctx.GetStatusCode() == 400 { /* custom error code for /users */ }}) func (ctx *Context) NotFound() { ctx.StatusCode(http.StatusNotFound) } // GetStatusCode returns the current status code of the response. // Look StatusCode too. func (ctx *Context) GetStatusCode() int { return ctx.writer.StatusCode() } // +------------------------------------------------------------+ // | Various Request and Post Data | // +------------------------------------------------------------+ func (ctx *Context) getQuery() url.Values { if ctx.query == nil { ctx.query = ctx.request.URL.Query() } return ctx.query } // URLParamExists returns true if the url parameter exists, otherwise false. func (ctx *Context) URLParamExists(name string) bool { _, exists := ctx.getQuery()[name] return exists } // URLParamDefault returns the get parameter from a request, if not found then "def" is returned. func (ctx *Context) URLParamDefault(name string, def string) string { if v := ctx.getQuery().Get(name); v != "" { return v } return def } // URLParam returns the get parameter from a request, if any. func (ctx *Context) URLParam(name string) string { return ctx.URLParamDefault(name, "") } // URLParamSlice a shortcut of ctx.Request().URL.Query()[name]. // Like `URLParam` but it returns all values instead of a single string separated by commas. // Returns the values of a url query of the given "name" as string slice, e.g. // ?names=john&names=doe&names=kataras and ?names=john,doe,kataras will return [ john doe kataras]. // // Note that, this method skips any empty entries. // // See `URLParamsSorted` for sorted values. func (ctx *Context) URLParamSlice(name string) []string { values := ctx.getQuery()[name] n := len(values) if n == 0 { return values } var sep string if sepPtr := ctx.app.ConfigurationReadOnly().GetURLParamSeparator(); sepPtr != nil { sep = *sepPtr } normalizedValues := make([]string, 0, n) for _, v := range values { if v == "" { continue } if sep != "" { values := strings.Split(v, sep) normalizedValues = append(normalizedValues, values...) continue } normalizedValues = append(normalizedValues, v) } return normalizedValues } // URLParamTrim returns the url query parameter with trailing white spaces removed from a request. func (ctx *Context) URLParamTrim(name string) string { return strings.TrimSpace(ctx.URLParam(name)) } // URLParamEscape returns the escaped url query parameter from a request. func (ctx *Context) URLParamEscape(name string) string { return DecodeQuery(ctx.URLParam(name)) } // ErrNotFound is the type error which API users can make use of // to check if a `Context` action of a `Handler` is type of Not Found, // e.g. URL Query Parameters. // Example: // // n, err := context.URLParamInt("url_query_param_name") // // if errors.Is(err, context.ErrNotFound) { // // [handle error...] // } // // Another usage would be `err == context.ErrNotFound` // HOWEVER prefer use the new `errors.Is` as API details may change in the future. var ErrNotFound = errors.New("not found") // URLParamInt returns the url query parameter as int value from a request, // returns -1 and an error if parse failed or not found. func (ctx *Context) URLParamInt(name string) (int, error) { if v := ctx.URLParam(name); v != "" { n, err := strconv.Atoi(v) if err != nil { return -1, err } return n, nil } return -1, ErrNotFound } // URLParamIntDefault returns the url query parameter as int value from a request, // if not found or parse failed then "def" is returned. func (ctx *Context) URLParamIntDefault(name string, def int) int { v, err := ctx.URLParamInt(name) if err != nil { return def } return v } // URLParamInt32Default returns the url query parameter as int32 value from a request, // if not found or parse failed then "def" is returned. func (ctx *Context) URLParamInt32Default(name string, def int32) int32 { if v := ctx.URLParam(name); v != "" { n, err := strconv.ParseInt(v, 10, 32) if err != nil { return def } return int32(n) } return def } // URLParamInt64 returns the url query parameter as int64 value from a request, // returns -1 and an error if parse failed or not found. func (ctx *Context) URLParamInt64(name string) (int64, error) { if v := ctx.URLParam(name); v != "" { n, err := strconv.ParseInt(v, 10, 64) if err != nil { return -1, err } return n, nil } return -1, ErrNotFound } // URLParamInt64Default returns the url query parameter as int64 value from a request, // if not found or parse failed then "def" is returned. func (ctx *Context) URLParamInt64Default(name string, def int64) int64 { v, err := ctx.URLParamInt64(name) if err != nil { return def } return v } // URLParamUint64 returns the url query parameter as uint64 value from a request. // Returns 0 on parse errors or when the URL parameter does not exist in the Query. func (ctx *Context) URLParamUint64(name string) uint64 { if v := ctx.URLParam(name); v != "" { n, err := strconv.ParseUint(v, 10, 64) if err != nil { return 0 } return n } return 0 } // URLParamFloat64 returns the url query parameter as float64 value from a request, // returns an error and -1 if parse failed. func (ctx *Context) URLParamFloat64(name string) (float64, error) { if v := ctx.URLParam(name); v != "" { n, err := strconv.ParseFloat(v, 64) if err != nil { return -1, err } return n, nil } return -1, ErrNotFound } // URLParamFloat64Default returns the url query parameter as float64 value from a request, // if not found or parse failed then "def" is returned. func (ctx *Context) URLParamFloat64Default(name string, def float64) float64 { v, err := ctx.URLParamFloat64(name) if err != nil { return def } return v } // URLParamBool returns the url query parameter as boolean value from a request, // returns an error if parse failed. func (ctx *Context) URLParamBool(name string) (bool, error) { return strconv.ParseBool(ctx.URLParam(name)) } // URLParamBoolDefault returns the url query parameter as boolean value from a request, // if not found or parse failed then "def" is returned. func (ctx *Context) URLParamBoolDefault(name string, def bool) bool { v, err := ctx.URLParamBool(name) if err != nil { return def } return v } // URLParams returns a map of URL Query parameters. // If the value of a URL parameter is a slice, // then it is joined as one separated by comma. // It returns an empty map on empty URL query. // // See URLParamsSorted too. func (ctx *Context) URLParams() map[string]string { q := ctx.getQuery() values := make(map[string]string, len(q)) for k, v := range q { values[k] = strings.Join(v, ",") } return values } // URLParamsSorted returns a sorted (by key) slice // of key-value entries of the URL Query parameters. func (ctx *Context) URLParamsSorted() []memstore.StringEntry { q := ctx.getQuery() n := len(q) if n == 0 { return nil } keys := make([]string, 0, n) for key := range q { keys = append(keys, key) } sort.Strings(keys) entries := make([]memstore.StringEntry, 0, n) for _, key := range keys { value := q[key] entries = append(entries, memstore.StringEntry{ Key: key, Value: strings.Join(value, ","), }) } return entries } // ResetQuery clears the GET URL Query request, temporary, cache. // Any new URLParamXXX calls will receive the new parsed values. func (ctx *Context) ResetQuery() { ctx.query = nil } // No need anymore, net/http checks for the Form already. // func (ctx *Context) askParseForm() error { // if ctx.request.Form == nil { // if err := ctx.request.ParseForm(); err != nil { // return err // } // } // return nil // } // FormValueDefault returns a single parsed form value by its "name", // including both the URL field's query parameters and the POST or PUT form data. // // Returns the "def" if not found. func (ctx *Context) FormValueDefault(name string, def string) string { if form, has := ctx.form(); has { if v := form[name]; len(v) > 0 { return v[0] } } return def } // FormValueDefault retruns a single parsed form value. func FormValueDefault(r *http.Request, name string, def string, postMaxMemory int64, resetBody bool) string { if form, has := GetForm(r, postMaxMemory, resetBody); has { if v := form[name]; len(v) > 0 { return v[0] } } return def } // FormValue returns a single parsed form value by its "name", // including both the URL field's query parameters and the POST or PUT form data. func (ctx *Context) FormValue(name string) string { return ctx.FormValueDefault(name, "") } // FormValues returns the parsed form data, including both the URL // field's query parameters and the POST or PUT form data. // // The default form's memory maximum size is 32MB, it can be changed by the // `iris#WithPostMaxMemory` configurator at main configuration passed on `app.Run`'s second argument. // NOTE: A check for nil is necessary. func (ctx *Context) FormValues() map[string][]string { form, _ := ctx.form() return form } // Form contains the parsed form data, including both the URL // field's query parameters and the POST or PUT form data. func (ctx *Context) form() (form map[string][]string, found bool) { return GetForm(ctx.request, ctx.app.ConfigurationReadOnly().GetPostMaxMemory(), ctx.app.ConfigurationReadOnly().GetDisableBodyConsumptionOnUnmarshal()) } // GetForm returns the request form (url queries, post or multipart) values. func GetForm(r *http.Request, postMaxMemory int64, resetBody bool) (form map[string][]string, found bool) { /* net/http/request.go#1219 for k, v := range f.Value { r.Form[k] = append(r.Form[k], v...) // r.PostForm should also be populated. See Issue 9305. r.PostForm[k] = append(r.PostForm[k], v...) } */ if form := r.Form; len(form) > 0 { return form, true } if form := r.PostForm; len(form) > 0 { return form, true } if m := r.MultipartForm; m != nil { if len(m.Value) > 0 { return m.Value, true } } if resetBody { // on POST, PUT and PATCH it will read the form values from request body otherwise from URL queries. if m := r.Method; m == "POST" || m == "PUT" || m == "PATCH" { body, restoreBody, err := GetBody(r, resetBody) if err != nil { return nil, false } setBody(r, body) // so the ctx.request.Body works defer restoreBody() // so the next GetForm calls work. // r.Body = io.NopCloser(io.TeeReader(r.Body, buf)) } else { resetBody = false } } // ParseMultipartForm calls `request.ParseForm` automatically // therefore we don't need to call it here, although it doesn't hurt. // After one call to ParseMultipartForm or ParseForm, // subsequent calls have no effect, are idempotent. err := r.ParseMultipartForm(postMaxMemory) // if resetBody { // r.Body = io.NopCloser(bytes.NewBuffer(bodyCopy)) // } if err != nil && err != http.ErrNotMultipart { return nil, false } if form := r.Form; len(form) > 0 { return form, true } if form := r.PostForm; len(form) > 0 { return form, true } if m := r.MultipartForm; m != nil { if len(m.Value) > 0 { return m.Value, true } } return nil, false } // PostValues returns all the parsed form data from POST, PATCH, // or PUT body parameters based on a "name" as a string slice. // // The default form's memory maximum size is 32MB, it can be changed by the // `iris#WithPostMaxMemory` configurator at main configuration passed on `app.Run`'s second argument. // // In addition, it reports whether the form was empty // or when the "name" does not exist // or whether the available values are empty. // It strips any empty key-values from the slice before return. // // Look ErrEmptyForm, ErrNotFound and ErrEmptyFormField respectfully. // See `PostValueMany` method too. func (ctx *Context) PostValues(name string) ([]string, error) { _, ok := ctx.form() if !ok { if !ctx.app.ConfigurationReadOnly().GetFireEmptyFormError() { return nil, nil } return nil, ErrEmptyForm // empty form. } values, ok := ctx.request.PostForm[name] if !ok { return nil, ErrNotFound // field does not exist } if len(values) == 0 || // Fast check for its first empty value (see below). strings.TrimSpace(values[0]) == "" { return nil, fmt.Errorf("%w: %s", ErrEmptyFormField, name) } for _, value := range values { if value == "" { // if at least one empty value, then perform the strip from the beginning. result := make([]string, 0, len(values)) for _, value := range values { if strings.TrimSpace(value) != "" { result = append(result, value) // we store the value as it is, not space-trimmed. } } if len(result) == 0 { return nil, fmt.Errorf("%w: %s", ErrEmptyFormField, name) } return result, nil } } return values, nil } // PostValueMany is like `PostValues` method, it returns the post data of a given key. // In addition to `PostValues` though, the returned value is a single string // separated by commas on multiple values. // // See ErrEmptyForm, ErrNotFound and ErrEmptyFormField respectfully. func (ctx *Context) PostValueMany(name string) (string, error) { values, err := ctx.PostValues(name) if err != nil || len(values) == 0 { return "", err } return strings.Join(values, ","), nil } // PostValueDefault returns the last parsed form data from POST, PATCH, // or PUT body parameters based on a "name". // // If not found then "def" is returned instead. func (ctx *Context) PostValueDefault(name string, def string) string { values, err := ctx.PostValues(name) if err != nil || len(values) == 0 { return def // it returns "def" even if it's empty here. } return values[len(values)-1] } // PostValueString same as `PostValue` method but it reports // an error if the value with key equals to "name" does not exist. func (ctx *Context) PostValueString(name string) (string, error) { values, err := ctx.PostValues(name) if err != nil { return "", err } if len(values) == 0 { // just in case. return "", ErrEmptyForm } return values[len(values)-1], nil } // PostValue returns the last parsed form data from POST, PATCH, // or PUT body parameters based on a "name". // // See `PostValueMany` too. func (ctx *Context) PostValue(name string) string { return ctx.PostValueDefault(name, "") } // PostValueTrim returns the last parsed form data from POST, PATCH, // or PUT body parameters based on a "name", without trailing spaces. func (ctx *Context) PostValueTrim(name string) string { return strings.TrimSpace(ctx.PostValue(name)) } // PostValueUint returns the last parsed form data matches the given "name" key // from POST, PATCH, or PUT body request parameters as unassigned number. // // See ErrEmptyForm, ErrNotFound and ErrEmptyFormField respectfully. func (ctx *Context) PostValueUint(name string) (uint, error) { value, err := ctx.PostValueString(name) if err != nil { return 0, err } return strParseUint(value) } // PostValueUint returns the last parsed form data matches the given "name" key // from POST, PATCH, or PUT body request parameters as unassigned number. // // See ErrEmptyForm, ErrNotFound and ErrEmptyFormField respectfully. func (ctx *Context) PostValueUint8(name string) (uint8, error) { value, err := ctx.PostValueString(name) if err != nil { return 0, err } return strParseUint8(value) } // PostValueUint returns the last parsed form data matches the given "name" key // from POST, PATCH, or PUT body request parameters as unassigned number. // // See ErrEmptyForm, ErrNotFound and ErrEmptyFormField respectfully. func (ctx *Context) PostValueUint16(name string) (uint16, error) { value, err := ctx.PostValueString(name) if err != nil { return 0, err } return strParseUint16(value) } // PostValueUint returns the last parsed form data matches the given "name" key // from POST, PATCH, or PUT body request parameters as unassigned number. // // See ErrEmptyForm, ErrNotFound and ErrEmptyFormField respectfully. func (ctx *Context) PostValueUint32(name string) (uint32, error) { value, err := ctx.PostValueString(name) if err != nil { return 0, err } return strParseUint32(value) } // PostValueUint returns the last parsed form data matches the given "name" key // from POST, PATCH, or PUT body request parameters as unassigned number. // // See ErrEmptyForm, ErrNotFound and ErrEmptyFormField respectfully. func (ctx *Context) PostValueUint64(name string) (uint64, error) { value, err := ctx.PostValueString(name) if err != nil { return 0, err } return strParseUint64(value) } // PostValueInt returns the last parsed form data matches the given "name" key // from POST, PATCH, or PUT body request parameters as signed number. // // See ErrEmptyForm, ErrNotFound and ErrEmptyFormField respectfully. func (ctx *Context) PostValueInt(name string) (int, error) { value, err := ctx.PostValueString(name) if err != nil { return 0, err } return strParseInt(value) } // PostValueIntDefault same as PostValueInt but if errored it returns // the given "def" default value. func (ctx *Context) PostValueIntDefault(name string, def int) int { value, err := ctx.PostValueInt(name) if err != nil { return def } return value } // PostValueInt8 returns the last parsed form data matches the given "name" key // from POST, PATCH, or PUT body request parameters as int8. // // See ErrEmptyForm, ErrNotFound and ErrEmptyFormField respectfully. func (ctx *Context) PostValueInt8(name string) (int8, error) { value, err := ctx.PostValueString(name) if err != nil { return 0, err } return strParseInt8(value) } // PostValueInt8Default same as PostValueInt8 but if errored it returns // the given "def" default value. func (ctx *Context) PostValueInt8Default(name string, def int8) int8 { value, err := ctx.PostValueInt8(name) if err != nil { return def } return value } // PostValueInt16 returns the last parsed form data matches the given "name" key // from POST, PATCH, or PUT body request parameters as int16. // // See ErrEmptyForm, ErrNotFound and ErrEmptyFormField respectfully. func (ctx *Context) PostValueInt16(name string) (int16, error) { value, err := ctx.PostValueString(name) if err != nil { return 0, err } return strParseInt16(value) } // PostValueInt16Default same as PostValueInt16 but if errored it returns // the given "def" default value. func (ctx *Context) PostValueInt16Default(name string, def int16) int16 { value, err := ctx.PostValueInt16(name) if err != nil { return def } return value } // PostValueInt32 returns the last parsed form data matches the given "name" key // from POST, PATCH, or PUT body request parameters as int32. // // See ErrEmptyForm, ErrNotFound and ErrEmptyFormField respectfully. func (ctx *Context) PostValueInt32(name string) (int32, error) { value, err := ctx.PostValueString(name) if err != nil { return 0, err } return strParseInt32(value) } // PostValueInt32Default same as PostValueInt32 but if errored it returns // the given "def" default value. func (ctx *Context) PostValueInt32Default(name string, def int32) int32 { value, err := ctx.PostValueInt32(name) if err != nil { return def } return value } // PostValueInt64 returns the last parsed form data matches the given "name" key // from POST, PATCH, or PUT body request parameters as int64. // // See ErrEmptyForm, ErrNotFound and ErrEmptyFormField respectfully. func (ctx *Context) PostValueInt64(name string) (int64, error) { value, err := ctx.PostValueString(name) if err != nil { return 0, err } return strParseInt64(value) } // PostValueInt64Default same as PostValueInt64 but if errored it returns // the given "def" default value. func (ctx *Context) PostValueInt64Default(name string, def int64) int64 { value, err := ctx.PostValueInt64(name) if err != nil { return def } return value } // PostValueFloat32 returns the last parsed form data matches the given "name" key // from POST, PATCH, or PUT body request parameters as float32. // // See ErrEmptyForm, ErrNotFound and ErrEmptyFormField respectfully. func (ctx *Context) PostValueFloat32(name string) (float32, error) { value, err := ctx.PostValueString(name) if err != nil { return 0, err } return strParseFloat32(value) } // PostValueFloat32Default same as PostValueFloat32 but if errored it returns // the given "def" default value. func (ctx *Context) PostValueFloat32Default(name string, def float32) float32 { value, err := ctx.PostValueFloat32(name) if err != nil { return def } return value } // PostValueFloat64 returns the last parsed form data matches the given "name" key // from POST, PATCH, or PUT body request parameters as float64. // // See ErrEmptyForm, ErrNotFound and ErrEmptyFormField respectfully. func (ctx *Context) PostValueFloat64(name string) (float64, error) { value, err := ctx.PostValueString(name) if err != nil { return 0, err } return strParseFloat64(value) } // PostValueFloat64Default same as PostValueFloat64 but if errored it returns // the given "def" default value. func (ctx *Context) PostValueFloat64Default(name string, def float64) float64 { value, err := ctx.PostValueFloat64(name) if err != nil { return def } return value } // PostValueComplex64 returns the last parsed form data matches the given "name" key // from POST, PATCH, or PUT body request parameters as complex64. // // See ErrEmptyForm, ErrNotFound and ErrEmptyFormField respectfully. func (ctx *Context) PostValueComplex64(name string) (complex64, error) { value, err := ctx.PostValueString(name) if err != nil { return 0, err } return strParseComplex64(value) } // PostValueComplex64Default same as PostValueComplex64 but if errored it returns // the given "def" default value. func (ctx *Context) PostValueComplex64Default(name string, def complex64) complex64 { value, err := ctx.PostValueComplex64(name) if err != nil { return def } return value } // PostValueComplex128 returns the last parsed form data matches the given "name" key // from POST, PATCH, or PUT body request parameters as complex128. // // See ErrEmptyForm, ErrNotFound and ErrEmptyFormField respectfully. func (ctx *Context) PostValueComplex128(name string) (complex128, error) { value, err := ctx.PostValueString(name) if err != nil { return 0, err } return strParseComplex128(value) } // PostValueComplex128Default same as PostValueComplex128 but if errored it returns // the given "def" default value. func (ctx *Context) PostValueComplex128Default(name string, def complex128) complex128 { value, err := ctx.PostValueComplex128(name) if err != nil { return def } return value } // PostValueBool returns the last parsed form data matches the given "name" key // from POST, PATCH, or PUT body request parameters as bool. // // See ErrEmptyForm, ErrNotFound and ErrEmptyFormField respectfully. func (ctx *Context) PostValueBool(name string) (bool, error) { value, err := ctx.PostValueString(name) if err != nil { return false, err } return strParseBool(value) } // PostValueWeekday returns the last parsed form data matches the given "name" key // from POST, PATCH, or PUT body request parameters as time.Weekday. // // See ErrEmptyForm, ErrNotFound and ErrEmptyFormField respectfully. func (ctx *Context) PostValueWeekday(name string) (time.Weekday, error) { value, err := ctx.PostValueString(name) if err != nil { return 0, err } return strParseWeekday(value) } // PostValueTime returns the last parsed form data matches the given "name" key // from POST, PATCH, or PUT body request parameters as time.Time with the given "layout". // // See ErrEmptyForm, ErrNotFound and ErrEmptyFormField respectfully. func (ctx *Context) PostValueTime(layout, name string) (time.Time, error) { value, err := ctx.PostValueString(name) if err != nil { return time.Time{}, err } return strParseTime(layout, value) } // PostValueSimpleDate returns the last parsed form data matches the given "name" key // from POST, PATCH, or PUT body request parameters as time.Time with "2006/01/02" // or "2006-01-02" time layout. // // See ErrEmptyForm, ErrNotFound and ErrEmptyFormField respectfully. func (ctx *Context) PostValueSimpleDate(name string) (time.Time, error) { value, err := ctx.PostValueString(name) if err != nil { return time.Time{}, err } return strParseSimpleDate(value) } // FormFile returns the first uploaded file that received from the client. // // The default form's memory maximum size is 32MB, it can be changed by the // `iris#WithPostMaxMemory` configurator at main configuration passed on `app.Run`'s second argument. // // Example: https://github.com/kataras/iris/tree/main/_examples/file-server/upload-file func (ctx *Context) FormFile(key string) (multipart.File, *multipart.FileHeader, error) { // we don't have access to see if the request is body stream // and then the ParseMultipartForm can be useless // here but do it in order to apply the post limit, // the internal request.FormFile will not do it if that's filled // and it's not a stream body. if err := ctx.request.ParseMultipartForm(ctx.app.ConfigurationReadOnly().GetPostMaxMemory()); err != nil { return nil, nil, err } return ctx.request.FormFile(key) } // FormFiles same as FormFile but may return multiple file inputs based on a key, e.g. "files[]". func (ctx *Context) FormFiles(key string, before ...func(*Context, *multipart.FileHeader) bool) (files []multipart.File, headers []*multipart.FileHeader, err error) { err = ctx.request.ParseMultipartForm(ctx.app.ConfigurationReadOnly().GetPostMaxMemory()) if err != nil { return } if ctx.request.MultipartForm != nil { fhs := ctx.request.MultipartForm.File if n := len(fhs); n > 0 { files = make([]multipart.File, 0, n) headers = make([]*multipart.FileHeader, 0, n) innerLoop: for _, header := range fhs[key] { header.Filename = filepath.Base(header.Filename) for _, b := range before { if !b(ctx, header) { continue innerLoop } } file, fErr := header.Open() if fErr != nil { // exit on first error but return the succeed. return files, headers, fErr } files = append(files, file) headers = append(headers, header) } } return } return nil, nil, http.ErrMissingFile } var ( // ValidFileNameRegexp is used to validate the user input by using a regular expression. // See `Context.UploadFormFiles` method. ValidFilenameRegexp = regexp.MustCompile(`^[a-zA-Z0-9_\-\.]+$`) // ValidExtensionRegexp acts as an allowlist of valid extensions. It's optional. Defaults to nil (all file extensions are allowed to be uploaded). // See `Context.UploadFormFiles` method. ValidExtensionRegexp *regexp.Regexp ) // SafeFilename returns a safe filename based on the given name. // - Using filepath.Base and filepath.ToSlash: This ensures that only the base file name is used, without any directory components, // and converts all separators to slashes. This is a good practice to prevent directory traversal. // - Regular Expression for Filenames: The ValidFilenameRegexp ensures that filenames are restricted to a safe character set. // This helps prevent the use of special characters that could lead to path traversal or other types of injection attacks. // - Extension Validation: If you have a ValidExtensionRegexp, it would further ensure that the file has an expected and safe extension, which is another good practice. // - Canonical Path Check: By evaluating symlinks and ensuring that the destination path starts with the canonical destination directory, you’re adding. // // It returns the safe prefix directory (destination directory), the safe filename, a boolean indicating whether the filename is safe, and an error if any. func SafeFilename(prefixDir string, name string) (string, string, bool, error) { // Security fix for go < 1.17.5: // Reported by Kirill Efimov (snyk.io) through security reports. filename := filepath.Base(filepath.ToSlash(name)) // CWE-99. // Sanitize the user input by using a regular expression // and an allowlist of valid extensions isValidFilename := ValidFilenameRegexp.MatchString(filename) if !isValidFilename { // Reject the input as it is invalid or unsafe. return prefixDir, name, false, nil } if ValidExtensionRegexp != nil && !ValidExtensionRegexp.MatchString(filename) { // Reject the input as it is invalid or unsafe. return prefixDir, name, false, nil } var destPath string if prefixDir != "" { // Join the sanitized input with the destination directory. destPath = filepath.Join(prefixDir, filename) // Get the canonical path of the destination directory. canonicalDestDir, err := filepath.EvalSymlinks(prefixDir) // the prefix dir should exists. if err != nil { return prefixDir, name, false, fmt.Errorf("dest directory: %s: eval symlinks: %w", prefixDir, err) } // Check if the destination path is within the destination directory. if !strings.HasPrefix(destPath, canonicalDestDir) { // Reject the input as it is a path traversal attempt. return prefixDir, name, false, nil } } return destPath, filename, true, nil } // UploadFormFiles uploads any received file(s) from the client // to the system physical location "destDirectory". // // The second optional argument "before" gives caller the chance to // modify or cancel the *miltipart.FileHeader before saving to the disk, // it can be used to change a file's name based on the current request, // all FileHeader's options can be changed. You can ignore it if // you don't need to use this capability before saving a file to the disk. // // Note that it doesn't check if request body streamed. // // Returns the copied length as int64 and // a not nil error if at least one new file // can't be created due to the operating system's permissions or // http.ErrMissingFile if no file received. // // If you want to receive & accept files and manage them manually you can use the `context#FormFile` // instead and create a copy function that suits your needs or use the `SaveFormFile` method, // the below is for generic usage. // // The default form's memory maximum size is 32MB, it can be changed by // the `WithPostMaxMemory` configurator or by `SetMaxRequestBodySize` or // by the `LimitRequestBodySize` middleware (depends the use case). // // See `FormFile` and `FormFiles` to a more controlled way to receive a file. // // Example: https://github.com/kataras/iris/tree/main/_examples/file-server/upload-files func (ctx *Context) UploadFormFiles(destDirectory string, before ...func(*Context, *multipart.FileHeader) bool) (uploaded []*multipart.FileHeader, n int64, err error) { err = ctx.request.ParseMultipartForm(ctx.app.ConfigurationReadOnly().GetPostMaxMemory()) if err != nil { return nil, 0, err } if ctx.request.MultipartForm != nil { if fhs := ctx.request.MultipartForm.File; fhs != nil { for _, files := range fhs { innerLoop: for _, file := range files { for _, b := range before { if !b(ctx, file) { continue innerLoop } } destPath, filename, ok, err := SafeFilename(destDirectory, file.Filename) if err != nil { return nil, 0, err } if !ok { continue } file.Filename = filename n0, err0 := ctx.SaveFormFile(file, destPath) if err0 != nil { return nil, 0, err0 } n += n0 uploaded = append(uploaded, file) } } return uploaded, n, nil } } return nil, 0, http.ErrMissingFile } // SaveFormFile saves a result of `FormFile` to the "dest" disk full path (directory + filename). // See `FormFile` and `UploadFormFiles` too. func (ctx *Context) SaveFormFile(fh *multipart.FileHeader, dest string) (int64, error) { src, err := fh.Open() if err != nil { return 0, err } defer src.Close() out, err := os.Create(dest) if err != nil { return 0, err } defer out.Close() return io.Copy(out, src) } // AbsoluteURI parses the "s" and returns its absolute URI form. func (ctx *Context) AbsoluteURI(s string) string { if s == "" { return "" } userInfo := "" if s[0] == '@' { endUserInfoIdx := strings.IndexByte(s, '/') if endUserInfoIdx > 0 && len(s) > endUserInfoIdx { userInfo = s[1:endUserInfoIdx] + "@" s = s[endUserInfoIdx:] } } if s[0] == '/' { scheme := ctx.request.URL.Scheme if scheme == "" { if ctx.request.TLS != nil { scheme = "https:" } else { scheme = "http:" } } host := ctx.Host() return scheme + "//" + userInfo + host + path.Clean(s) } if u, err := url.Parse(s); err == nil { r := ctx.request if u.Scheme == "" && u.Host == "" { oldpath := r.URL.Path if oldpath == "" { oldpath = "/" } if s == "" || s[0] != '/' { olddir, _ := path.Split(oldpath) s = olddir + s } var query string if i := strings.Index(s, "?"); i != -1 { s, query = s[:i], s[i:] } // clean up but preserve trailing slash trailing := strings.HasSuffix(s, "/") s = path.Clean(s) if trailing && !strings.HasSuffix(s, "/") { s += "/" } s += query } } return s } // Redirect sends a redirect response to the client // of an absolute or relative target URL. // It accepts 2 input arguments, a string and an optional integer. // The first parameter is the target url to redirect. // The second one is the HTTP status code should be sent // among redirection response, // If the second parameter is missing, then it defaults to 302 (StatusFound). // It can be set to 301 (Permant redirect), StatusTemporaryRedirect(307) // or 303 (StatusSeeOther) if POST method. func (ctx *Context) Redirect(urlToRedirect string, statusHeader ...int) { ctx.StopExecution() // get the previous status code given by the end-developer. status := ctx.GetStatusCode() if status < 300 { // the previous is not a RCF-valid redirect status. status = 0 } if len(statusHeader) > 0 { // check if status code is passed via receivers. if s := statusHeader[0]; s > 0 { status = s } } if status == 0 { // if status remains zero then default it. // a 'temporary-redirect-like' which works better than for our purpose status = http.StatusFound } http.Redirect(ctx.writer, ctx.request, urlToRedirect, status) } // +------------------------------------------------------------+ // | Body Readers | // +------------------------------------------------------------+ // SetMaxRequestBodySize sets a limit to the request body size // should be called before reading the request body from the client. func (ctx *Context) SetMaxRequestBodySize(limitOverBytes int64) { ctx.request.Body = http.MaxBytesReader(ctx.writer, ctx.request.Body, limitOverBytes) } var emptyFunc = func() {} // GetBody reads and returns the request body. func GetBody(r *http.Request, resetBody bool) ([]byte, func(), error) { data, err := io.ReadAll(r.Body) if err != nil { return nil, nil, err } if resetBody { // * remember, Request.Body has no Bytes(), we have to consume them first // and after re-set them to the body, this is the only solution. return data, func() { setBody(r, data) }, nil } return data, emptyFunc, nil } func setBody(r *http.Request, data []byte) { r.Body = io.NopCloser(bytes.NewBuffer(data)) } const disableRequestBodyConsumptionContextKey = "iris.request.body.record" // RecordRequestBody same as the Application's DisableBodyConsumptionOnUnmarshal // configuration field but acts only for the current request. // It makes the request body readable more than once. func (ctx *Context) RecordRequestBody(b bool) { ctx.values.Set(disableRequestBodyConsumptionContextKey, b) } // IsRecordingBody reports whether the request body can be readen multiple times. func (ctx *Context) IsRecordingBody() bool { if ctx.app.ConfigurationReadOnly().GetDisableBodyConsumptionOnUnmarshal() { return true } value, _ := ctx.values.GetBool(disableRequestBodyConsumptionContextKey) return value } // GetBody reads and returns the request body. // The default behavior for the http request reader is to consume the data readen // but you can change that behavior by passing the `WithoutBodyConsumptionOnUnmarshal` Iris option // or by calling the `RecordRequestBody` method. // // However, whenever you can use the `ctx.Request().Body` instead. func (ctx *Context) GetBody() ([]byte, error) { body, release, err := GetBody(ctx.request, ctx.IsRecordingBody()) if err != nil { return nil, err } release() return body, nil } // Validator is the validator for request body on Context methods such as // ReadJSON, ReadMsgPack, ReadXML, ReadYAML, ReadForm, ReadQuery, ReadBody and e.t.c. type Validator interface { Struct(any) error // If community asks for more than a struct validation on JSON, XML, MsgPack, Form, Query and e.t.c // then we should add more methods here, alternative approach would be to have a // `Validator:Validate(any) error` and a map[reflect.Kind]Validator instead. } // UnmarshalBody reads the request's body and binds it to a value or pointer of any type // Examples of usage: context.ReadJSON, context.ReadXML. // // Example: https://github.com/kataras/iris/blob/main/_examples/request-body/read-custom-via-unmarshaler/main.go func (ctx *Context) UnmarshalBody(outPtr any, unmarshaler Unmarshaler) error { if ctx.request.Body == nil { return fmt.Errorf("unmarshal: empty body: %w", ErrNotFound) } rawData, err := ctx.GetBody() if err != nil { return err } if decoderWithCtx, ok := outPtr.(BodyDecoderWithContext); ok { return decoderWithCtx.DecodeContext(ctx.request.Context(), rawData) } // check if the v contains its own decode // in this case the v should be a pointer also, // but this is up to the user's custom Decode implementation* // // See 'BodyDecoder' for more. if decoder, isDecoder := outPtr.(BodyDecoder); isDecoder { return decoder.Decode(rawData) } // // check if v is already a pointer, if yes then pass as it's // if reflect.TypeOf(v).Kind() == reflect.Ptr { // return unmarshaler.Unmarshal(rawData, v) // } <- no need for that, ReadJSON is documented enough to receive a pointer, // we don't need to reduce the performance here by using the reflect.TypeOf method. // f the v doesn't contains a self-body decoder use the custom unmarshaler to bind the body. err = unmarshaler.Unmarshal(rawData, outPtr) if err != nil { return err } return ctx.app.Validate(outPtr) } // JSONReader holds the JSON decode options of the `Context.ReadJSON, ReadBody` methods. type JSONReader struct { // Note(@kataras): struct instead of optional funcs to keep consistently with the encoder options. // DisallowUnknownFields causes the json decoder to return an error when the destination // is a struct and the input contains object keys which do not match any // non-ignored, exported fields in the destination. DisallowUnknownFields bool // If set to true then a bit faster json decoder is used instead, // note that if this is true then it overrides // the Application's EnableOptimizations configuration field. Optimize bool // This field only applies to the ReadJSONStream. // The Optimize field has no effect when this is true. // If set to true the request body stream MUST start with a `[` // and end with `]` literals, example: // [ // {"username":"john"}, // {"username": "makis"}, // {"username": "george"} // ] // Defaults to false: decodes a json object one by one, example: // {"username":"john"} // {"username": "makis"} // {"username": "george"} ArrayStream bool } var ReadJSON = func(ctx *Context, outPtr any, opts ...JSONReader) error { var body io.Reader if ctx.IsRecordingBody() { data, err := io.ReadAll(ctx.request.Body) if err != nil { return err } setBody(ctx.request, data) body = bytes.NewReader(data) } else { body = ctx.request.Body } decoder := json.NewDecoder(body) // decoder := gojson.NewDecoder(ctx.Request().Body) if len(opts) > 0 { options := opts[0] if options.DisallowUnknownFields { decoder.DisallowUnknownFields() } } if err := decoder.Decode(&outPtr); err != nil { return err } return ctx.app.Validate(outPtr) } // ReadJSON reads JSON from request's body and binds it to a value of any json-valid type. // // Example: https://github.com/kataras/iris/blob/main/_examples/request-body/read-json/main.go func (ctx *Context) ReadJSON(outPtr any, opts ...JSONReader) error { return ReadJSON(ctx, outPtr, opts...) } // ReadJSONStream is an alternative of ReadJSON which can reduce the memory load // by reading only one json object every time. // It buffers just the content required for a single json object instead of the entire string, // and discards that once it reaches an end of value that can be decoded into the provided struct // inside the onDecode's DecodeFunc. // // It accepts a function which accepts the json Decode function and returns an error. // The second variadic argument is optional and can be used to customize the decoder even further. // // Example: https://github.com/kataras/iris/blob/main/_examples/request-body/read-json-stream/main.go func (ctx *Context) ReadJSONStream(onDecode func(DecodeFunc) error, opts ...JSONReader) error { decoder := json.NewDecoder(ctx.request.Body) if len(opts) > 0 && opts[0].ArrayStream { _, err := decoder.Token() // read open bracket. if err != nil { return err } for decoder.More() { // hile the array contains values. if err = onDecode(decoder.Decode); err != nil { return err } } _, err = decoder.Token() // read closing bracket. return err } // while the array contains values for decoder.More() { if err := onDecode(decoder.Decode); err != nil { return err } } return nil } // ReadXML reads XML from request's body and binds it to a value of any xml-valid type. // // Example: https://github.com/kataras/iris/blob/main/_examples/request-body/read-xml/main.go func (ctx *Context) ReadXML(outPtr any) error { return ctx.UnmarshalBody(outPtr, UnmarshalerFunc(xml.Unmarshal)) } // ReadYAML reads YAML from request's body and binds it to the "outPtr" value. // // Example: https://github.com/kataras/iris/blob/main/_examples/request-body/read-yaml/main.go func (ctx *Context) ReadYAML(outPtr any) error { return ctx.UnmarshalBody(outPtr, UnmarshalerFunc(yaml.Unmarshal)) } var ( // IsErrEmptyJSON reports whether the given "err" is caused by a // Client.ReadJSON call when the request body was empty or // didn't start with { or [. IsErrEmptyJSON = func(err error) bool { if err == nil { return false } if errors.Is(err, io.EOF) { return true } if v, ok := err.(*json.SyntaxError); ok { // standard go json encoder error. return v.Offset == 0 && v.Error() == "unexpected end of JSON input" } errMsg := err.Error() // 3rd party pacakges: return strings.Contains(errMsg, "readObjectStart: expect {") || strings.Contains(errMsg, "readArrayStart: expect [") } // IsErrPath can be used at `context#ReadForm` and `context#ReadQuery`. // It reports whether the incoming error // can be ignored when server allows unknown post values to be sent by the client. // // A shortcut for the `schema#IsErrPath`. IsErrPath = schema.IsErrPath // IsErrPathCRSFToken reports whether the given "err" is caused // by unknown key error on "csrf.token". See `context#ReadForm` for more. IsErrPathCRSFToken = func(err error) bool { if err == nil || CSRFTokenFormKey == "" { return false } if m, ok := err.(schema.MultiError); ok { if csrfErr, hasCSRFToken := m[CSRFTokenFormKey]; hasCSRFToken { _, is := csrfErr.(schema.UnknownKeyError) return is } } return false } // ErrEmptyForm is returned by // - `context#ReadForm` // - `context#ReadQuery` // - `context#ReadBody` // when the request data (form, query and body respectfully) is empty. ErrEmptyForm = errors.New("empty form") // ErrEmptyFormField reports whether a specific field exists but it's empty. // Usage: errors.Is(err, ErrEmptyFormField) // See postValue method. It's only returned on parsed post value methods. ErrEmptyFormField = errors.New("empty form field") // ConnectionCloseErrorSubstr if at least one of the given // substrings are found in a net.OpError:os.SyscallError error type // on `IsErrConnectionReset` then the function will report true. ConnectionCloseErrorSubstr = []string{ "broken pipe", "connection reset by peer", } // IsErrConnectionClosed reports whether the given "err" // is caused because of a broken connection. IsErrConnectionClosed = func(err error) bool { if err == nil { return false } if opErr, ok := err.(*net.OpError); ok { if syscallErr, ok := opErr.Err.(*os.SyscallError); ok { errStr := strings.ToLower(syscallErr.Error()) for _, s := range ConnectionCloseErrorSubstr { if strings.Contains(errStr, s) { return true } } } } return false } ) // CSRFTokenFormKey the CSRF token key of the form data. // // See ReadForm method for more. const CSRFTokenFormKey = "csrf.token" // ReadForm binds the request body of a form to the "formObject". // It supports any kind of type, including custom structs. // It will return nothing if request data are empty. // The struct field tag is "form". // If tag is missing it tries to bind using the field's name. // To ignore a specific field from binding use tag value "-". // Note that it will return nil error on empty form data if `Configuration.FireEmptyFormError` // is false (as defaulted) in this case the caller should check the pointer to // see if something was actually binded. // // If a client sent an unknown field, this method will return an error, // in order to ignore that error use the `err != nil && !iris.IsErrPath(err)`. // // As of 15 Aug 2022, ReadForm does not return an error over unknown CSRF token form key, // to change this behavior globally, set the `context.CSRFTokenFormKey` to an empty value. // // Example: https://github.com/kataras/iris/blob/main/_examples/request-body/read-form/main.go func (ctx *Context) ReadForm(formObject any) error { values := ctx.FormValues() if len(values) == 0 { if ctx.app.ConfigurationReadOnly().GetFireEmptyFormError() { return ErrEmptyForm } return nil } err := schema.DecodeForm(values, formObject) if err != nil && !IsErrPathCRSFToken(err) { return err } return ctx.app.Validate(formObject) } type ( // MultipartRelated is the result of the context.ReadMultipartRelated method. MultipartRelated struct { // ContentIDs keeps an ordered list of all the // content-ids of the multipart related request. ContentIDs []string // Contents keeps each part's information by Content-ID. // Contents holds each of the multipart/related part's data. Contents map[string]MultipartRelatedContent } // MultipartRelatedContent holds a multipart/related part's id, header and body. MultipartRelatedContent struct { // ID holds the Content-ID. ID string // Headers holds the part's request headers. Headers map[string][]string // Body holds the part's body. Body []byte } ) // ReadMultipartRelated returns a structure which contain // information about each part (id, headers, body). // // Read more at: https://www.ietf.org/rfc/rfc2387.txt. // // Example request (2387/5.2 Text/X-Okie): // Content-Type: Multipart/Related; boundary=example-2; // start="<950118.AEBH@XIson.com>" // type="Text/x-Okie" // // --example-2 // Content-Type: Text/x-Okie; charset=iso-8859-1; // declaration="<950118.AEB0@XIson.com>" // Content-ID: <950118.AEBH@XIson.com> // Content-Description: Document // // {doc} // This picture was taken by an automatic camera mounted ... // {image file=cid:950118.AECB@XIson.com} // {para} // Now this is an enlargement of the area ... // {image file=cid:950118:AFDH@XIson.com} // {/doc} // --example-2 // Content-Type: image/jpeg // Content-ID: <950118.AFDH@XIson.com> // Content-Transfer-Encoding: BASE64 // Content-Description: Picture A // // [encoded jpeg image] // --example-2 // Content-Type: image/jpeg // Content-ID: <950118.AECB@XIson.com> // Content-Transfer-Encoding: BASE64 // Content-Description: Picture B // // [encoded jpeg image] // --example-2-- func (ctx *Context) ReadMultipartRelated() (MultipartRelated, error) { contentType, params, err := mime.ParseMediaType(ctx.GetHeader(ContentTypeHeaderKey)) if err != nil { return MultipartRelated{}, err } if !strings.HasPrefix(contentType, ContentMultipartRelatedHeaderValue) { return MultipartRelated{}, ErrEmptyForm } var ( contentIDs []string contents = make(map[string]MultipartRelatedContent) ) if ctx.IsRecordingBody() { // * remember, Request.Body has no Bytes(), we have to consume them first // and after re-set them to the body, this is the only solution. body, restoreBody, err := GetBody(ctx.request, true) if err != nil { return MultipartRelated{}, fmt.Errorf("multipart related: body copy because of iris.Configuration.DisableBodyConsumptionOnUnmarshal: %w", err) } setBody(ctx.request, body) // so the ctx.request.Body works defer restoreBody() // so the next ctx.GetBody calls work. } multipartReader := multipart.NewReader(ctx.request.Body, params["boundary"]) for { part, err := multipartReader.NextPart() if err != nil { if err == io.EOF { break } return MultipartRelated{}, fmt.Errorf("multipart related: next part: %w", err) } defer part.Close() b, err := io.ReadAll(part) if err != nil { return MultipartRelated{}, fmt.Errorf("multipart related: next part: read: %w", err) } contentID := part.Header.Get("Content-ID") contentIDs = append(contentIDs, contentID) contents[contentID] = MultipartRelatedContent{ // replace if same Content-ID appears, which it shouldn't. ID: contentID, Headers: http.Header(part.Header), Body: b, } } if len(contents) != len(contentIDs) { contentIDs = distinctStrings(contentIDs) } result := MultipartRelated{ ContentIDs: contentIDs, Contents: contents, } return result, nil } func distinctStrings(values []string) []string { seen := make(map[string]struct{}, len(values)) result := make([]string, 0, len(values)) for _, val := range values { if _, ok := seen[val]; !ok { seen[val] = struct{}{} result = append(result, val) } } return result } // ReadQuery binds URL Query to "ptr". The struct field tag is "url". // If tag is missing it tries to bind using the field's name. // To ignore a specific field from binding use tag value "-". // // Example: https://github.com/kataras/iris/blob/main/_examples/request-body/read-query/main.go func (ctx *Context) ReadQuery(ptr any) error { values := ctx.getQuery() if len(values) == 0 { if ctx.app.ConfigurationReadOnly().GetFireEmptyFormError() { return ErrEmptyForm } return nil } err := schema.DecodeQuery(values, ptr) if err != nil { return err } return ctx.app.Validate(ptr) } // ReadHeaders binds request headers to "ptr". The struct field tag is "header". // // Example: https://github.com/kataras/iris/blob/main/_examples/request-body/read-headers/main.go func (ctx *Context) ReadHeaders(ptr any) error { err := schema.DecodeHeaders(ctx.request.Header, ptr) if err != nil { return err } return ctx.app.Validate(ptr) } // ReadParams binds URI Dynamic Path Parameters to "ptr". The struct field tag is "param". // // Example: https://github.com/kataras/iris/blob/main/_examples/request-body/read-params/main.go func (ctx *Context) ReadParams(ptr any) error { n := ctx.params.Len() if n == 0 { return nil } values := make(map[string][]string, n) ctx.params.Visit(func(key string, value string) { // []string on path parameter, e.g. // /.../{tail:path} // Tail []string `param:"tail"` values[key] = strings.Split(value, "/") }) err := schema.DecodeParams(values, ptr) if err != nil { return err } return ctx.app.Validate(ptr) } // ReadURL is a shortcut of ReadParams and ReadQuery. // It binds dynamic path parameters and URL query parameters // to the "ptr" pointer struct value. // The struct fields may contain "url" or "param" binding tags. // If a validator exists then it validates the result too. func (ctx *Context) ReadURL(ptr any) error { values := make(map[string][]string, ctx.params.Len()) ctx.params.Visit(func(key string, value string) { values[key] = strings.Split(value, "/") }) for k, v := range ctx.getQuery() { values[k] = append(values[k], v...) } // Decode using all available binding tags (url, header, param). err := schema.Decode(values, ptr) if err != nil { return err } return ctx.app.Validate(ptr) } // ReadProtobuf binds the body to the "ptr" of a proto Message and returns any error. // Look `ReadJSONProtobuf` too. func (ctx *Context) ReadProtobuf(ptr proto.Message) error { rawData, err := ctx.GetBody() if err != nil { return err } return proto.Unmarshal(rawData, ptr) } // ProtoUnmarshalOptions is a type alias for protojson.UnmarshalOptions. type ProtoUnmarshalOptions = protojson.UnmarshalOptions var defaultProtobufUnmarshalOptions ProtoUnmarshalOptions // ReadJSONProtobuf reads a JSON body request into the given "ptr" proto.Message. // Look `ReadProtobuf` too. func (ctx *Context) ReadJSONProtobuf(ptr proto.Message, opts ...ProtoUnmarshalOptions) error { rawData, err := ctx.GetBody() if err != nil { return err } opt := defaultProtobufUnmarshalOptions if len(opts) > 0 { opt = opts[0] } return opt.Unmarshal(rawData, ptr) } // ReadMsgPack binds the request body of msgpack format to the "ptr" and returns any error. func (ctx *Context) ReadMsgPack(ptr any) error { rawData, err := ctx.GetBody() if err != nil { return err } err = msgpack.Unmarshal(rawData, ptr) if err != nil { return err } return ctx.app.Validate(ptr) } // ReadBody binds the request body to the "ptr" depending on the HTTP Method and the Request's Content-Type. // If a GET method request then it reads from a form (or URL Query), otherwise // it tries to match (depending on the request content-type) the data format e.g. // JSON, Protobuf, MsgPack, XML, YAML, MultipartForm and binds the result to the "ptr". // As a special case if the "ptr" was a pointer to string or []byte // then it will bind it to the request body as it is. func (ctx *Context) ReadBody(ptr any) error { // If the ptr is string or byte, read the body as it's. switch v := ptr.(type) { case *string: b, err := ctx.GetBody() if err != nil { return err } *v = string(b) case *[]byte: b, err := ctx.GetBody() if err != nil { return err } copy(*v, b) } if ctx.Method() == http.MethodGet { if ctx.Request().URL.RawQuery != "" { // try read from query. return ctx.ReadQuery(ptr) } // otherwise use the ReadForm, // it's actually the same except // ReadQuery will not fire errors on: // 1. unknown or empty url query parameters // 2. empty query or form (if FireEmptyFormError is enabled). return ctx.ReadForm(ptr) } switch ctx.GetContentTypeRequested() { case ContentXMLHeaderValue, ContentXMLUnreadableHeaderValue: return ctx.ReadXML(ptr) // "%v reflect.Indirect(reflect.ValueOf(ptr)).Interface()) case ContentYAMLHeaderValue, ContentYAMLTextHeaderValue: return ctx.ReadYAML(ptr) case ContentFormHeaderValue, ContentFormMultipartHeaderValue: return ctx.ReadForm(ptr) case ContentMultipartRelatedHeaderValue: return fmt.Errorf("context: read body: cannot bind multipart/related: use ReadMultipartRelated instead") case ContentJSONHeaderValue: return ctx.ReadJSON(ptr) case ContentProtobufHeaderValue: msg, ok := ptr.(proto.Message) if !ok { return ErrContentNotSupported } return ctx.ReadProtobuf(msg) case ContentMsgPackHeaderValue, ContentMsgPack2HeaderValue: return ctx.ReadMsgPack(ptr) default: if ctx.Request().URL.RawQuery != "" { // try read from query. return ctx.ReadQuery(ptr) } // otherwise default to JSON. return ctx.ReadJSON(ptr) } } // +------------------------------------------------------------+ // | Body (raw) Writers | // +------------------------------------------------------------+ // Write writes the data to the connection as part of an HTTP reply. // // If WriteHeader has not yet been called, Write calls // WriteHeader(http.StatusOK) before writing the data. If the Header // does not contain a Content-Type line, Write adds a Content-Type set // to the result of passing the initial 512 bytes of written data to // DetectContentType. // // Depending on the HTTP protocol version and the client, calling // Write or WriteHeader may prevent future reads on the // Request.Body. For HTTP/1.x requests, handlers should read any // needed request body data before writing the response. Once the // headers have been flushed (due to either an explicit Flusher.Flush // call or writing enough data to trigger a flush), the request body // may be unavailable. For HTTP/2 requests, the Go HTTP server permits // handlers to continue to read the request body while concurrently // writing the response. However, such behavior may not be supported // by all HTTP/2 clients. Handlers should read before writing if // possible to maximize compatibility. // // It reports any write errors back to the caller, Application.SetContentErrorHandler does NOT apply here // as this is a lower-level method which must be remain as it is. func (ctx *Context) Write(rawBody []byte) (int, error) { return ctx.writer.Write(rawBody) } // Writef formats according to a format specifier and writes to the response. // // Returns the number of bytes written and any write error encountered. func (ctx *Context) Writef(format string, a ...any) (n int, err error) { /* if len(a) == 0 { return ctx.WriteString(format) } ^ No, let it complain about arguments, because go test will do even if the app is running. Users should use WriteString instead of (format, args) when format may contain go-sprintf reserved chars (e.g. %).*/ return fmt.Fprintf(ctx.writer, format, a...) } // WriteString writes a simple string to the response. // // Returns the number of bytes written and any write error encountered. func (ctx *Context) WriteString(body string) (n int, err error) { return io.WriteString(ctx.writer, body) } const ( // ContentTypeHeaderKey is the header key of "Content-Type". ContentTypeHeaderKey = "Content-Type" // LastModifiedHeaderKey is the header key of "Last-Modified". LastModifiedHeaderKey = "Last-Modified" // IfModifiedSinceHeaderKey is the header key of "If-Modified-Since". IfModifiedSinceHeaderKey = "If-Modified-Since" // CacheControlHeaderKey is the header key of "Cache-Control". CacheControlHeaderKey = "Cache-Control" // ETagHeaderKey is the header key of "ETag". ETagHeaderKey = "ETag" // ContentDispositionHeaderKey is the header key of "Content-Disposition". ContentDispositionHeaderKey = "Content-Disposition" // ContentLengthHeaderKey is the header key of "Content-Length" ContentLengthHeaderKey = "Content-Length" // ContentEncodingHeaderKey is the header key of "Content-Encoding". ContentEncodingHeaderKey = "Content-Encoding" // GzipHeaderValue is the header value of "gzip". GzipHeaderValue = "gzip" // AcceptEncodingHeaderKey is the header key of "Accept-Encoding". AcceptEncodingHeaderKey = "Accept-Encoding" // VaryHeaderKey is the header key of "Vary". VaryHeaderKey = "Vary" ) var unixEpochTime = time.Unix(0, 0) // IsZeroTime reports whether t is obviously unspecified (either zero or Unix()=0). func IsZeroTime(t time.Time) bool { return t.IsZero() || t.Equal(unixEpochTime) } // ParseTime parses a time header (such as the Date: header), // trying each forth formats (or three if Application's configuration's TimeFormat is defaulted) // that are allowed by HTTP/1.1: // Application's configuration's TimeFormat or/and http.TimeFormat, // time.RFC850, and time.ANSIC. // // Look `context#FormatTime` for the opossite operation (Time to string). var ParseTime = func(ctx *Context, text string) (t time.Time, err error) { t, err = time.Parse(ctx.Application().ConfigurationReadOnly().GetTimeFormat(), text) if err != nil { return http.ParseTime(text) } return } // FormatTime returns a textual representation of the time value formatted // according to the Application's configuration's TimeFormat field // which defines the format. // // Look `context#ParseTime` for the opossite operation (string to Time). var FormatTime = func(ctx *Context, t time.Time) string { return t.Format(ctx.Application().ConfigurationReadOnly().GetTimeFormat()) } // SetLastModified sets the "Last-Modified" based on the "modtime" input. // If "modtime" is zero then it does nothing. // // It's mostly internally on core/router and context packages. func (ctx *Context) SetLastModified(modtime time.Time) { if !IsZeroTime(modtime) { ctx.Header(LastModifiedHeaderKey, FormatTime(ctx, modtime.UTC())) // or modtime.UTC()? } } // ErrPreconditionFailed may be returned from `Context` methods // that has to perform one or more client side preconditions before the actual check, e.g. `CheckIfModifiedSince`. // Usage: // ok, err := context.CheckIfModifiedSince(modTime) // // if err != nil { // if errors.Is(err, context.ErrPreconditionFailed) { // [handle missing client conditions,such as not valid request method...] // }else { // [the error is probably a time parse error...] // } // } var ErrPreconditionFailed = errors.New("precondition failed") // CheckIfModifiedSince checks if the response is modified since the "modtime". // Note that it has nothing to do with server-side caching. // It does those checks by checking if the "If-Modified-Since" request header // sent by client or a previous server response header // (e.g with WriteWithExpiration or HandleDir or Favicon etc.) // is a valid one and it's before the "modtime". // // A check for !modtime && err == nil is necessary to make sure that // it's not modified since, because it may return false but without even // had the chance to check the client-side (request) header due to some errors, // like the HTTP Method is not "GET" or "HEAD" or if the "modtime" is zero // or if parsing time from the header failed. See `ErrPreconditionFailed` too. // // It's mostly used internally, e.g. `context#WriteWithExpiration`. func (ctx *Context) CheckIfModifiedSince(modtime time.Time) (bool, error) { if method := ctx.Method(); method != http.MethodGet && method != http.MethodHead { return false, fmt.Errorf("method: %w", ErrPreconditionFailed) } ims := ctx.GetHeader(IfModifiedSinceHeaderKey) if ims == "" || IsZeroTime(modtime) { return false, fmt.Errorf("zero time: %w", ErrPreconditionFailed) } t, err := ParseTime(ctx, ims) if err != nil { return false, err } // sub-second precision, so // use mtime < t+1s instead of mtime <= t to check for unmodified. if modtime.UTC().Before(t.Add(1 * time.Second)) { return false, nil } return true, nil } // WriteNotModified sends a 304 "Not Modified" status code to the client, // it makes sure that the content type, the content length headers // and any "ETag" are removed before the response sent. // // It's mostly used internally on core/router/fs.go and context methods. func (ctx *Context) WriteNotModified() { // RFC 7232 section 4.1: // a sender SHOULD NOT generate representation metadata other than the // above listed fields unless said metadata exists for the purpose of // guiding cache updates (e.g.," Last-Modified" might be useful if the // response does not have an ETag field). h := ctx.ResponseWriter().Header() delete(h, ContentTypeHeaderKey) delete(h, ContentLengthHeaderKey) if h.Get(ETagHeaderKey) != "" { delete(h, LastModifiedHeaderKey) } ctx.StatusCode(http.StatusNotModified) } // WriteWithExpiration works like `Write` but it will check if a resource is modified, // based on the "modtime" input argument, // otherwise sends a 304 status code in order to let the client-side render the cached content. func (ctx *Context) WriteWithExpiration(body []byte, modtime time.Time) (int, error) { if modified, err := ctx.CheckIfModifiedSince(modtime); !modified && err == nil { ctx.WriteNotModified() return 0, nil } ctx.SetLastModified(modtime) return ctx.writer.Write(body) } // StreamWriter registers the given stream writer for populating // response body. // // Access to context's and/or its' members is forbidden from writer. // // This function may be used in the following cases: // // - if response body is too big (more than iris.LimitRequestBodySize(if set)). // - if response body is streamed from slow external sources. // - if response body must be streamed to the client in chunks. // (aka `http server push`). func (ctx *Context) StreamWriter(writer func(w io.Writer) error) error { cancelCtx := ctx.Request().Context() notifyClosed := cancelCtx.Done() for { select { // response writer forced to close, exit. case <-notifyClosed: return cancelCtx.Err() default: if err := writer(ctx.writer); err != nil { return err } ctx.writer.Flush() } } } // +------------------------------------------------------------+ // | Body Writers with compression | // +------------------------------------------------------------+ // ClientSupportsEncoding reports whether the // client expects one of the given "encodings" compression. // // Note, this method just reports back the first valid encoding it sees, // meaning that request accept-encoding offers don't matter here. // See `CompressWriter` too. func (ctx *Context) ClientSupportsEncoding(encodings ...string) bool { if len(encodings) == 0 { return false } if h := ctx.GetHeader(AcceptEncodingHeaderKey); h != "" { for _, v := range strings.Split(h, ",") { for _, encoding := range encodings { if strings.Contains(v, encoding) { return true } } } } return false } // CompressWriter enables or disables the compress response writer. // if the client expects a valid compression algorithm then this // will change the response writer to a compress writer instead. // All future write and rich write methods will respect this option. // Usage: // // app.Use(func(ctx iris.Context){ // err := ctx.CompressWriter(true) // ctx.Next() // }) // // The recommendation is to compress data as much as possible and therefore to use this field, // but some types of resources, such as jpeg images, are already compressed. // Sometimes, using additional compression doesn't reduce payload size and // can even make the payload longer. func (ctx *Context) CompressWriter(enable bool) error { switch w := ctx.writer.(type) { case *CompressResponseWriter: if enable { return nil } w.Disabled = true case *ResponseRecorder: if enable { // If it's a recorder which already wraps the compress, exit. if _, ok := w.ResponseWriter.(*CompressResponseWriter); ok { return nil } // Keep the Recorder as ctx.writer. // Wrap the existing net/http response writer // with the compressed writer and // replace the recorder's response writer // reference with that compressed one. // Fixes an issue when Record is called before CompressWriter. cw, err := AcquireCompressResponseWriter(w.ResponseWriter, ctx.request, -1) if err != nil { return err } w.ResponseWriter = cw } else { cw, ok := w.ResponseWriter.(*CompressResponseWriter) if ok { cw.Disabled = true } } default: if !enable { return nil } cw, err := AcquireCompressResponseWriter(w, ctx.request, -1) if err != nil { return err } ctx.writer = cw } return nil } // CompressReader accepts a boolean, which, if set to true // it wraps the request body reader with a reader which decompresses request data before read. // If the "enable" input argument is false then the request body will reset to the default one. // // Useful when incoming request data are compressed. // All future calls of `ctx.GetBody/ReadXXX/UnmarshalBody` methods will respect this option. // // Usage: // // app.Use(func(ctx iris.Context){ // err := ctx.CompressReader(true) // ctx.Next() // }) // // More: // // if cr, ok := ctx.Request().Body.(*CompressReader); ok { // cr.Src // the original request body // cr.Encoding // the compression algorithm. // } // // It returns `ErrRequestNotCompressed` if client's request data are not compressed // (or empty) // or `ErrNotSupportedCompression` if server missing the decompression algorithm. func (ctx *Context) CompressReader(enable bool) error { cr, ok := ctx.request.Body.(*CompressReader) if enable { if ok { // already called. return nil } encoding := ctx.GetHeader(ContentEncodingHeaderKey) if encoding == IDENTITY { // no transformation whatsoever, return nil error and // don't wrap the body reader. return nil } r, err := NewCompressReader(ctx.request.Body, encoding) if err != nil { return err } ctx.request.Body = r } else if ok { ctx.request.Body = cr.Src } return nil } // +------------------------------------------------------------+ // | Rich Body Content Writers/Renderers | // +------------------------------------------------------------+ // ViewEngine registers a view engine for the current chain of handlers. // It overrides any previously registered engines, including the application's root ones. // Note that, because performance is everything, // the "engine" MUST be already ready-to-use, // meaning that its `Load` method should be called once before this method call. // // To register a view engine per-group of groups too see `Party.RegisterView` instead. func (ctx *Context) ViewEngine(engine ViewEngine) { ctx.values.Set(ctx.app.ConfigurationReadOnly().GetViewEngineContextKey(), engine) } // ViewLayout sets the "layout" option if and when .View // is being called afterwards, in the same request. // Useful when need to set or/and change a layout based on the previous handlers in the chain. // // Note that the 'layoutTmplFile' argument can be set to iris.NoLayout // to disable the layout for a specific view render action, // it disables the engine's configuration's layout property. // // Look .ViewData and .View too. // // Example: https://github.com/kataras/iris/tree/main/_examples/view/context-view-data/ func (ctx *Context) ViewLayout(layoutTmplFile string) { ctx.values.Set(ctx.app.ConfigurationReadOnly().GetViewLayoutContextKey(), layoutTmplFile) } // ViewData saves one or more key-value pair in order to be passed if and when .View // is being called afterwards, in the same request. // Useful when need to set or/and change template data from previous hanadlers in the chain. // // If .View's "binding" argument is not nil and it's not a type of map // then these data are being ignored, binding has the priority, so the main route's handler can still decide. // If binding is a map or iris.Map then these data are being added to the view data // and passed to the template. // // After .View, the data are not destroyed, in order to be re-used if needed (again, in the same request as everything else), // to clear the view data, developers can call: // ctx.Set(ctx.Application().ConfigurationReadOnly().GetViewDataContextKey(), nil) // // If 'key' is empty then the value is added as it's (struct or map) and developer is unable to add other value. // // Look .ViewLayout and .View too. // // Example: https://github.com/kataras/iris/tree/main/_examples/view/context-view-data/ func (ctx *Context) ViewData(key string, value any) { viewDataContextKey := ctx.app.ConfigurationReadOnly().GetViewDataContextKey() if key == "" { ctx.values.Set(viewDataContextKey, value) return } v := ctx.values.Get(viewDataContextKey) if v == nil { ctx.values.Set(viewDataContextKey, Map{key: value}) return } if data, ok := v.(Map); ok { data[key] = value } } // GetViewData returns the values registered by `context#ViewData`. // The return value is `map[string]any`, this means that // if a custom struct registered to ViewData then this function // will try to parse it to map, if failed then the return value is nil // A check for nil is always a good practise if different // kind of values or no data are registered via `ViewData`. // // Similarly to `viewData := ctx.Values().Get("iris.view.data")` or // `viewData := ctx.Values().Get(ctx.Application().ConfigurationReadOnly().GetViewDataContextKey())`. func (ctx *Context) GetViewData() map[string]any { if v := ctx.values.Get(ctx.app.ConfigurationReadOnly().GetViewDataContextKey()); v != nil { // if pure map[string]any if viewData, ok := v.(Map); ok { return viewData } // if struct, convert it to map[string]any if structs.IsStruct(v) { return structs.Map(v) } } // if no values found, then return nil return nil } // FallbackViewProvider is an interface which can be registered to the `Party.FallbackView` // or `Context.FallbackView` methods to handle fallback views. // See FallbackView, FallbackViewLayout and FallbackViewFunc. type FallbackViewProvider interface { FallbackView(ctx *Context, err ErrViewNotExist) error } /* Notes(@kataras): If ever requested, this fallback logic (of ctx, error) can go to all necessary methods. I've designed with a bit more complexity here instead of a simple filename fallback in order to give the freedom to the developer to do whatever he/she wants with that template/layout not exists error, e.g. have a list of fallbacks views to loop through until succeed or fire a different error than the default. We also provide some helpers for common fallback actions (FallbackView, FallbackViewLayout). This naming was chosen in order to be easy to follow up with the previous view-relative context features. Also note that here we catch a specific error, we want the developer to be aware of the rest template errors (e.g. when a template having parsing issues). */ // FallbackViewFunc is a function that can be registered // to handle view fallbacks. It accepts the Context and // a special error which contains information about the previous template error. // It implements the FallbackViewProvider interface. // // See `Context.View` method. type FallbackViewFunc func(ctx *Context, err ErrViewNotExist) error // FallbackView completes the FallbackViewProvider interface. func (fn FallbackViewFunc) FallbackView(ctx *Context, err ErrViewNotExist) error { return fn(ctx, err) } var ( _ FallbackViewProvider = FallbackView("") _ FallbackViewProvider = FallbackViewLayout("") ) // FallbackView is a helper to register a single template filename as a fallback // when the provided tempate filename was not found. type FallbackView string // FallbackView completes the FallbackViewProvider interface. func (f FallbackView) FallbackView(ctx *Context, err ErrViewNotExist) error { if err.IsLayout { // Not responsible to render layouts. return err } // ctx.StatusCode(200) // Let's keep the previous status code here, developer can change it anyways. return ctx.View(string(f), err.Data) } // FallbackViewLayout is a helper to register a single template filename as a fallback // layout when the provided layout filename was not found. type FallbackViewLayout string // FallbackView completes the FallbackViewProvider interface. func (f FallbackViewLayout) FallbackView(ctx *Context, err ErrViewNotExist) error { if !err.IsLayout { // Responsible to render layouts only. return err } ctx.ViewLayout(string(f)) return ctx.View(err.Name, err.Data) } const fallbackViewOnce = "iris.fallback.view.once" func (ctx *Context) fireFallbackViewOnce(err ErrViewNotExist) error { // Note(@kataras): this is our way to keep the same View method for // both fallback and normal views, remember, we export the whole // Context functionality to the end-developer through the fallback view provider. if ctx.values.Get(fallbackViewOnce) != nil { return err } v := ctx.values.Get(ctx.app.ConfigurationReadOnly().GetFallbackViewContextKey()) if v == nil { return err } providers, ok := v.([]FallbackViewProvider) if !ok { return err } ctx.values.Set(fallbackViewOnce, struct{}{}) var pErr error for _, provider := range providers { pErr = provider.FallbackView(ctx, err) if pErr != nil { if vErr, ok := pErr.(ErrViewNotExist); ok { // This fallback view does not exist or it's not responsible to handle, // try the next. pErr = vErr continue } } // If OK then we found the correct fallback. // If the error was a parse error and not a template not found // then exit and report the pErr error. break } return pErr } // FallbackView registers one or more fallback views for a template or a template layout. // When View cannot find the given filename to execute then this "provider" // is responsible to handle the error or render a different view. // // Usage: // // FallbackView(iris.FallbackView("fallback.html")) // FallbackView(iris.FallbackViewLayout("layouts/fallback.html")) // OR // FallbackView(iris.FallbackViewFunc(ctx iris.Context, err iris.ErrViewNotExist) error { // err.Name is the previous template name. // err.IsLayout reports whether the failure came from the layout template. // err.Data is the template data provided to the previous View call. // [...custom logic e.g. ctx.View("fallback", err.Data)] // }) func (ctx *Context) FallbackView(providers ...FallbackViewProvider) { key := ctx.app.ConfigurationReadOnly().GetFallbackViewContextKey() if key == "" { return } v := ctx.values.Get(key) if v == nil { ctx.values.Set(key, providers) return } // Can register more than one. storedProviders, ok := v.([]FallbackViewProvider) if !ok { return } storedProviders = append(storedProviders, providers...) ctx.values.Set(key, storedProviders) } // View renders a template based on the registered view engine(s). // First argument accepts the filename, relative to the view engine's Directory and Extension, // i.e: if directory is "./templates" and want to render the "./templates/users/index.html" // then you pass the "users/index.html" as the filename argument. // // The second optional argument can receive a single "view model". // If "optionalViewModel" exists, even if it's nil, overrides any previous `ViewData` calls. // If second argument is missing then binds the data through previous `ViewData` calls (e.g. middleware). // // Look .ViewData and .ViewLayout too. // // Examples: https://github.com/kataras/iris/tree/main/_examples/view func (ctx *Context) View(filename string, optionalViewModel ...any) error { ctx.ContentType(ContentHTMLHeaderValue) err := ctx.renderView(filename, optionalViewModel...) if err != nil { if errNotExists, ok := err.(ErrViewNotExist); ok { err = ctx.fireFallbackViewOnce(errNotExists) } } if err != nil { if ctx.IsDebug() { // send the error back to the client, when debug mode. ctx.StopWithError(http.StatusInternalServerError, err) } else { ctx.SetErrPrivate(err) ctx.StopWithStatus(http.StatusInternalServerError) } } return err } func (ctx *Context) renderView(filename string, optionalViewModel ...any) error { cfg := ctx.app.ConfigurationReadOnly() layout := ctx.values.GetString(cfg.GetViewLayoutContextKey()) var bindingData any if len(optionalViewModel) > 0 /* Don't do it: can break a lot of servers: && optionalViewModel[0] != nil */ { // a nil can override the existing data or model sent by `ViewData`. bindingData = optionalViewModel[0] } else { bindingData = ctx.values.Get(cfg.GetViewDataContextKey()) } if key := cfg.GetViewEngineContextKey(); key != "" { if engineV := ctx.values.Get(key); engineV != nil { if engine, ok := engineV.(ViewEngine); ok { return engine.ExecuteWriter(ctx, filename, layout, bindingData) } } } return ctx.app.View(ctx, filename, layout, bindingData) } const ( // ContentBinaryHeaderValue header value for binary data. ContentBinaryHeaderValue = "application/octet-stream" // ContentWebassemblyHeaderValue header value for web assembly files. ContentWebassemblyHeaderValue = "application/wasm" // ContentHTMLHeaderValue is the string of text/html response header's content type value. ContentHTMLHeaderValue = "text/html" // ContentJSONHeaderValue header value for JSON data. ContentJSONHeaderValue = "application/json" // ContentJSONProblemHeaderValue header value for JSON API problem error. // Read more at: https://tools.ietf.org/html/rfc7807 ContentJSONProblemHeaderValue = "application/problem+json" // ContentXMLProblemHeaderValue header value for XML API problem error. // Read more at: https://tools.ietf.org/html/rfc7807 ContentXMLProblemHeaderValue = "application/problem+xml" // ContentJavascriptHeaderValue header value for JSONP & Javascript data. ContentJavascriptHeaderValue = "text/javascript" // ContentTextHeaderValue header value for Text data. ContentTextHeaderValue = "text/plain" // ContentXMLHeaderValue header value for XML data. ContentXMLHeaderValue = "text/xml" // ContentXMLUnreadableHeaderValue obselete header value for XML. ContentXMLUnreadableHeaderValue = "application/xml" // ContentMarkdownHeaderValue custom key/content type, the real is the text/html. ContentMarkdownHeaderValue = "text/markdown" // ContentYAMLHeaderValue header value for YAML data. ContentYAMLHeaderValue = "application/x-yaml" // ContentYAMLTextHeaderValue header value for YAML plain text. ContentYAMLTextHeaderValue = "text/yaml" // ContentProtobufHeaderValue header value for Protobuf messages data. ContentProtobufHeaderValue = "application/x-protobuf" // ContentMsgPackHeaderValue header value for MsgPack data. ContentMsgPackHeaderValue = "application/msgpack" // ContentMsgPack2HeaderValue alternative header value for MsgPack data. ContentMsgPack2HeaderValue = "application/x-msgpack" // ContentFormHeaderValue header value for post form data. ContentFormHeaderValue = "application/x-www-form-urlencoded" // ContentFormMultipartHeaderValue header value for post multipart form data. ContentFormMultipartHeaderValue = "multipart/form-data" // ContentMultipartRelatedHeaderValue header value for multipart related data. ContentMultipartRelatedHeaderValue = "multipart/related" // ContentGRPCHeaderValue Content-Type header value for gRPC. ContentGRPCHeaderValue = "application/grpc" ) // Binary writes out the raw bytes as binary data. func (ctx *Context) Binary(data []byte) (int, error) { ctx.ContentType(ContentBinaryHeaderValue) return ctx.Write(data) } // Text writes out a string as plain text. func (ctx *Context) Text(text string) (int, error) { ctx.ContentType(ContentTextHeaderValue) return ctx.WriteString(text) } // HTML writes out a string as text/html. func (ctx *Context) HTML(text string) (int, error) { ctx.ContentType(ContentHTMLHeaderValue) return ctx.WriteString(text) } // ProtoMarshalOptions is a type alias for protojson.MarshalOptions. type ProtoMarshalOptions = protojson.MarshalOptions // JSON contains the options for the JSON (Context's) Renderer. type JSON struct { // content-specific UnescapeHTML bool `yaml:"UnescapeHTML"` Indent string `yaml:"Indent"` Prefix string `yaml:"Prefix"` ASCII bool `yaml:"ASCII"` // if true writes with unicode to ASCII content. Secure bool `yaml:"Secure"` // if true then it prepends a "while(1);" when Go slice (to JSON Array) value. // proto.Message specific marshal options. Proto ProtoMarshalOptions `yaml:"ProtoMarshalOptions"` // If true and json writing failed then the error handler is skipped // and it just returns to the caller. // // See StopWithJSON and x/errors package. OmitErrorHandler bool `yaml:"OmitErrorHandler"` } // DefaultJSONOptions is the optional settings that are being used // inside `Context.JSON`. var DefaultJSONOptions = JSON{} // IsDefault reports whether this JSON options structure holds the default values. func (j *JSON) IsDefault() bool { return j.UnescapeHTML == DefaultJSONOptions.UnescapeHTML && j.Indent == DefaultJSONOptions.Indent && j.Prefix == DefaultJSONOptions.Prefix && j.ASCII == DefaultJSONOptions.ASCII && j.Secure == DefaultJSONOptions.Secure && j.Proto == DefaultJSONOptions.Proto } // JSONP contains the options for the JSONP (Context's) Renderer. type JSONP struct { // content-specific Indent string Callback string OmitErrorHandler bool // See JSON.OmitErrorHandler. } // XML contains the options for the XML (Context's) Renderer. type XML struct { // content-specific Indent string Prefix string OmitErrorHandler bool // See JSON.OmitErrorHandler. } // Markdown contains the options for the Markdown (Context's) Renderer. type Markdown struct { // content-specific Sanitize bool OmitErrorHandler bool // See JSON.OmitErrorHandler. // // Library-specific. // E.g. Flags: html.CommonFlags | html.HrefTargetBlank RenderOptions html.RendererOptions } var ( newLineB = []byte("\n") // the html codes for unescaping. ltHex = []byte("\\u003c") lt = []byte("<") gtHex = []byte("\\u003e") gt = []byte(">") andHex = []byte("\\u0026") and = []byte("&") // secure JSON. jsonArrayPrefix = []byte("[") jsonArraySuffix = []byte("]") secureJSONPrefix = []byte("while(1);") ) func (ctx *Context) handleSpecialJSONResponseValue(v any, options *JSON) (bool, int, error) { if ctx.app.ConfigurationReadOnly().GetEnableProtoJSON() { if m, ok := v.(proto.Message); ok { protoJSON := ProtoMarshalOptions{} if options != nil { protoJSON = options.Proto } result, err := protoJSON.Marshal(m) if err != nil { return true, 0, err } n, err := ctx.writer.Write(result) return true, n, err } } if ctx.app.ConfigurationReadOnly().GetEnableEasyJSON() { if easyObject, ok := v.(easyjson.Marshaler); ok { noEscapeHTML := false if options != nil { noEscapeHTML = !options.UnescapeHTML } jw := jwriter.Writer{NoEscapeHTML: noEscapeHTML} easyObject.MarshalEasyJSON(&jw) n, err := jw.DumpTo(ctx.writer) return true, n, err } } return false, 0, nil } // WriteJSON marshals the given interface object and writes the JSON response to the 'writer'. var WriteJSON = func(ctx *Context, v any, options *JSON) error { if !options.Secure && !options.ASCII && options.Prefix == "" { // jsoniterConfig := jsoniter.Config{ // EscapeHTML: !options.UnescapeHTML, // IndentionStep: 4, // }.Froze() // enc := jsoniterConfig.NewEncoder(ctx.writer) // err = enc.Encode(v) // // enc := gojson.NewEncoder(ctx.writer) // enc.SetEscapeHTML(!options.UnescapeHTML) // enc.SetIndent(options.Prefix, options.Indent) // err = enc.EncodeContext(ctx, v) enc := json.NewEncoder(ctx.writer) enc.SetEscapeHTML(!options.UnescapeHTML) enc.SetIndent(options.Prefix, options.Indent) return enc.Encode(v) } var ( result []byte err error ) if indent := options.Indent; indent != "" { result, err = json.MarshalIndent(v, "", indent) result = append(result, newLineB...) } else { result, err = json.Marshal(v) } if err != nil { return err } prependSecure := false if options.Secure { if bytes.HasPrefix(result, jsonArrayPrefix) { if options.Indent == "" { prependSecure = bytes.HasSuffix(result, jsonArraySuffix) } else { prependSecure = bytes.HasSuffix(bytes.TrimRightFunc(result, func(r rune) bool { return r == '\n' || r == '\r' }), jsonArraySuffix) } } } if options.UnescapeHTML { result = bytes.ReplaceAll(result, ltHex, lt) result = bytes.ReplaceAll(result, gtHex, gt) result = bytes.ReplaceAll(result, andHex, and) } if prependSecure { result = append(secureJSONPrefix, result...) } if options.ASCII { if len(result) > 0 { buf := new(bytes.Buffer) for _, s := range string(result) { char := string(s) if s >= 128 { char = fmt.Sprintf("\\u%04x", int64(s)) } buf.WriteString(char) } result = buf.Bytes() } } if prefix := options.Prefix; prefix != "" { result = append(stringToBytes(prefix), result...) } _, err = ctx.Write(result) return err } // See https://golang.org/src/strings/builder.go#L45 // func bytesToString(b []byte) string { // return unsafe.String(unsafe.SliceData(b), len(b)) // } func stringToBytes(s string) []byte { return unsafe.Slice(unsafe.StringData(s), len(s)) } type ( // ErrorHandler describes a context error handler which applies on // JSON, JSONP, Protobuf, MsgPack, XML, YAML and Markdown write errors. // // It does NOT modify or handle the result of Context.GetErr at all, // use a custom middleware instead if you want to handle the handler-provided custom errors (Context.SetErr) // // An ErrorHandler can be registered once via Application.SetErrorHandler method to override the default behavior. // The default behavior is to simply send status internal code error // without a body back to the client. // // See Application.SetContextErrorHandler for more. ErrorHandler interface { HandleContextError(ctx *Context, err error) } // ErrorHandlerFunc a function shortcut for ErrorHandler interface. ErrorHandlerFunc func(ctx *Context, err error) ) // HandleContextError completes the ErrorHandler interface. func (h ErrorHandlerFunc) HandleContextError(ctx *Context, err error) { h(ctx, err) } func (ctx *Context) handleContextError(err error) { if err == nil { return } if errHandler := ctx.app.GetContextErrorHandler(); errHandler != nil { errHandler.HandleContextError(ctx, err) } else { if ctx.IsDebug() { ctx.app.Logger().Error(err) } ctx.StatusCode(http.StatusInternalServerError) } // keep the error non nil so the caller has control over further actions. } // Render writes the response headers and calls the given renderer "r" to render data. // This method can be used while migrating from other frameworks. func (ctx *Context) Render(statusCode int, r interface { // Render should push data with custom content type to the client. Render(http.ResponseWriter) error // WriteContentType writes custom content type to the response. WriteContentType(w http.ResponseWriter) }) { ctx.StatusCode(statusCode) if statusCode >= 100 && statusCode <= 199 || statusCode == http.StatusNoContent || statusCode == http.StatusNotModified { r.WriteContentType(ctx.writer) return } if err := r.Render(ctx.writer); err != nil { ctx.StopWithError(http.StatusInternalServerError, err) } } // Component is the interface which all components must implement. // A component is a struct which can be rendered to a writer. // It's being used by the `Context.RenderComponent` method. // An example of compatible Component is a templ.Component. type Component interface { Render(context.Context, io.Writer) error } // RenderComponent renders a component to the client. // It sets the "Content-Type" header to "text/html; charset=utf-8". // It reports any component render errors back to the caller. // Look the Application.SetContextErrorHandler to override the // default status code 500 with a custom error response. func (ctx *Context) RenderComponent(component Component) error { ctx.ContentType("text/html; charset=utf-8") err := component.Render(ctx.Request().Context(), ctx.ResponseWriter()) if err != nil { ctx.handleContextError(err) } return err } // JSON marshals the given "v" value to JSON and writes the response to the client. // Look the Configuration.EnableProtoJSON and EnableEasyJSON too. // // It reports any JSON parser or write errors back to the caller. // Look the Application.SetContextErrorHandler to override the // default status code 500 with a custom error response. // // Customize the behavior of every `Context.JSON“ can be achieved // by modifying the package-level `WriteJSON` function on program initilization. func (ctx *Context) JSON(v any, opts ...JSON) (err error) { var options *JSON if len(opts) > 0 { options = &opts[0] } else { // If no options are given safely read the already-initialized value. options = &DefaultJSONOptions } if err = ctx.writeJSON(v, options); err != nil { // if no options are given or OmitErrorHandler is false // then call the error handler (which may lead to a cycle if the error handler fails to write JSON too). if !options.OmitErrorHandler { ctx.handleContextError(err) } } return } func (ctx *Context) writeJSON(v any, options *JSON) error { ctx.ContentType(ContentJSONHeaderValue) // After content type given and before everything else, try handle proto or easyjson, no matter the performance mode. if handled, _, err := ctx.handleSpecialJSONResponseValue(v, options); handled { return err } return WriteJSON(ctx, v, options) } var finishCallbackB = []byte(");") // WriteJSONP marshals the given interface object and writes the JSONP response to the writer. var WriteJSONP = func(ctx *Context, v any, options *JSONP) (err error) { if callback := options.Callback; callback != "" { _, err = ctx.Write(stringToBytes(callback + "(")) if err != nil { return err } defer func() { if err == nil { ctx.Write(finishCallbackB) } }() } err = WriteJSON(ctx, v, &JSON{ Indent: options.Indent, OmitErrorHandler: options.OmitErrorHandler, }) return err } // DefaultJSONPOptions is the optional settings that are being used // inside `ctx.JSONP`. var DefaultJSONPOptions = JSONP{} // JSONP marshals the given "v" value to JSON and sends the response to the client. // // It reports any JSON parser or write errors back to the caller. // Look the Application.SetContextErrorHandler to override the // default status code 500 with a custom error response. func (ctx *Context) JSONP(v any, opts ...JSONP) (err error) { var options *JSONP if len(opts) > 0 { options = &opts[0] } else { options = &DefaultJSONPOptions } ctx.ContentType(ContentJavascriptHeaderValue) if err = WriteJSONP(ctx, v, options); err != nil { if !options.OmitErrorHandler { ctx.handleContextError(err) } } return } type xmlMapEntry struct { XMLName xml.Name Value any `xml:",chardata"` } // XMLMap wraps a map[string]any to compatible xml marshaler, // in order to be able to render maps as XML on the `Context.XML` method. // // Example: `Context.XML(XMLMap("Root", map[string]any{...})`. func XMLMap(elementName string, v Map) xml.Marshaler { return xmlMap{ entries: v, elementName: elementName, } } type xmlMap struct { entries Map elementName string } // MarshalXML marshals a map to XML. func (m xmlMap) MarshalXML(e *xml.Encoder, start xml.StartElement) error { if len(m.entries) == 0 { return nil } start.Name = xml.Name{Local: m.elementName} err := e.EncodeToken(start) if err != nil { return err } for k, v := range m.entries { err = e.Encode(xmlMapEntry{XMLName: xml.Name{Local: k}, Value: v}) if err != nil { return err } } return e.EncodeToken(start.End()) } // WriteXML marshals the given interface object and writes the XML response to the writer. var WriteXML = func(ctx *Context, v any, options *XML) error { if prefix := options.Prefix; prefix != "" { _, err := ctx.Write(stringToBytes(prefix)) if err != nil { return err } } encoder := xml.NewEncoder(ctx.writer) encoder.Indent("", options.Indent) if err := encoder.Encode(v); err != nil { return err } return encoder.Flush() } // DefaultXMLOptions is the optional settings that are being used // from `ctx.XML`. var DefaultXMLOptions = XML{} // XML marshals the given interface object and writes the XML response to the client. // To render maps as XML see the `XMLMap` package-level function. // // It reports any XML parser or write errors back to the caller. // Look the Application.SetContextErrorHandler to override the // default status code 500 with a custom error response. func (ctx *Context) XML(v any, opts ...XML) (err error) { var options *XML if len(opts) > 0 { options = &opts[0] } else { options = &DefaultXMLOptions } ctx.ContentType(ContentXMLHeaderValue) if err = WriteXML(ctx, v, options); err != nil { if !options.OmitErrorHandler { ctx.handleContextError(err) } } return } // Problem writes a JSON or XML problem response. // Order of Problem fields are not always rendered the same. // // Behaves exactly like the `Context.JSON` method // but with default ProblemOptions.JSON indent of " " and // a response content type of "application/problem+json" instead. // // Use the options.RenderXML and XML fields to change this behavior and // send a response of content type "application/problem+xml" instead. // // Read more at: https://github.com/kataras/iris/blob/main/_examples/routing/http-errors. func (ctx *Context) Problem(v any, opts ...ProblemOptions) error { options := DefaultProblemOptions if len(opts) > 0 { options = opts[0] // Currently apply only if custom options passsed, otherwise, // with the current settings, it's not required. // This may change in the future though. options.Apply(ctx) } if p, ok := v.(Problem); ok { // if !p.Validate() { // ctx.StatusCode(http.StatusInternalServerError) // return ErrNotValidProblem // } p.updateURIsToAbs(ctx) code, _ := p.getStatus() if code == 0 { // get the current status code and set it to the problem. code = ctx.GetStatusCode() ctx.StatusCode(code) } else { // send the problem's status code ctx.StatusCode(code) } if options.RenderXML { ctx.contentTypeOnce(ContentXMLProblemHeaderValue, "") // Problem is an xml Marshaler already, don't use `XMLMap`. return ctx.XML(v, options.XML) } } ctx.contentTypeOnce(ContentJSONProblemHeaderValue, "") return ctx.writeJSON(v, &options.JSON) } var sanitizer = bluemonday.UGCPolicy() // WriteMarkdown parses the markdown to html and writes these contents to the writer. var WriteMarkdown = func(ctx *Context, markdownB []byte, options *Markdown) error { out := markdown.NormalizeNewlines(markdownB) renderer := html.NewRenderer(options.RenderOptions) doc := markdown.Parse(out, nil) out = markdown.Render(doc, renderer) if options.Sanitize { out = sanitizer.SanitizeBytes(out) } _, err := ctx.Write(out) return err } // DefaultMarkdownOptions is the optional settings that are being used // from `WriteMarkdown` and `ctx.Markdown`. var DefaultMarkdownOptions = Markdown{} // Markdown parses the markdown to html and renders its result to the client. // // It reports any Markdown parser or write errors back to the caller. // Look the Application.SetContextErrorHandler to override the // default status code 500 with a custom error response. func (ctx *Context) Markdown(markdownB []byte, opts ...Markdown) (err error) { var options *Markdown if len(opts) > 0 { options = &opts[0] } else { options = &DefaultMarkdownOptions } ctx.ContentType(ContentHTMLHeaderValue) if err = WriteMarkdown(ctx, markdownB, options); err != nil { if !options.OmitErrorHandler { ctx.handleContextError(err) } } return } // WriteYAML sends YAML response to the client. var WriteYAML = func(ctx *Context, v any, indentSpace int) error { encoder := yaml.NewEncoder(ctx.writer) encoder.SetIndent(indentSpace) if err := encoder.Encode(v); err != nil { return err } return encoder.Close() } // YAML marshals the given "v" value using the yaml marshaler and writes the result to the client. // // It reports any YAML parser or write errors back to the caller. // Look the Application.SetContextErrorHandler to override the // default status code 500 with a custom error response. func (ctx *Context) YAML(v any) error { ctx.ContentType(ContentYAMLHeaderValue) err := WriteYAML(ctx, v, 0) if err != nil { ctx.handleContextError(err) return err } return nil } // TextYAML calls the Context.YAML method but with the text/yaml content type instead. func (ctx *Context) TextYAML(v any) error { ctx.ContentType(ContentYAMLTextHeaderValue) err := WriteYAML(ctx, v, 4) if err != nil { ctx.handleContextError(err) return err } return nil } // Protobuf marshals the given "v" value of proto Message and writes its result to the client. // // It reports any protobuf parser or write errors back to the caller. // Look the Application.SetContextErrorHandler to override the // default status code 500 with a custom error response. func (ctx *Context) Protobuf(v proto.Message) (int, error) { out, err := proto.Marshal(v) if err != nil { ctx.handleContextError(err) return 0, err } ctx.ContentType(ContentProtobufHeaderValue) n, err := ctx.Write(out) if err != nil { ctx.handleContextError(err) } return n, err } // MsgPack marshals the given "v" value of msgpack format and writes its result to the client. // // It reports any message pack or write errors back to the caller. // Look the Application.SetContextErrorHandler to override the // default status code 500 with a custom error response. func (ctx *Context) MsgPack(v any) (int, error) { out, err := msgpack.Marshal(v) if err != nil { ctx.handleContextError(err) } ctx.ContentType(ContentMsgPackHeaderValue) n, err := ctx.Write(out) if err != nil { ctx.handleContextError(err) } return n, err } // +-----------------------------------------------------------------------+ // | Content Νegotiation | // | https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation | | // +-----------------------------------------------------------------------+ // ErrContentNotSupported returns from the `Negotiate` method // when server responds with 406. var ErrContentNotSupported = errors.New("unsupported content") // ContentSelector is the interface which structs can implement // to manually choose a content based on the negotiated mime (content type). // It can be passed to the `Context.Negotiate` method. // // See the `N` struct too. type ContentSelector interface { SelectContent(mime string) any } // ContentNegotiator is the interface which structs can implement // to override the `Context.Negotiate` default implementation and // manually respond to the client based on a manuall call of `Context.Negotiation().Build()` // to get the final negotiated mime and charset. // It can be passed to the `Context.Negotiate` method. type ContentNegotiator interface { // mime and charset can be retrieved by: // mime, charset := Context.Negotiation().Build() // Pass this method to `Context.Negotiate` method // to write custom content. // Overriding the existing behavior of Context.Negotiate for selecting values based on // content types, although it can accept any custom mime type with []byte. // Content type is already set. // Use it with caution, 99.9% you don't need this but it's here for extreme cases. Negotiate(ctx *Context) (int, error) } // N is a struct which can be passed on the `Context.Negotiate` method. // It contains fields which should be filled based on the `Context.Negotiation()` // server side values. If no matched mime then its "Other" field will be sent, // which should be a string or []byte. // It completes the `ContentSelector` interface. type N struct { Text, HTML string Markdown []byte Binary []byte JSON any Problem Problem JSONP any XML any YAML any Protobuf any MsgPack any Other []byte // custom content types. } var _ ContentSelector = N{} // SelectContent returns a content based on the matched negotiated "mime". func (n N) SelectContent(mime string) any { switch mime { case ContentTextHeaderValue: return n.Text case ContentHTMLHeaderValue: return n.HTML case ContentMarkdownHeaderValue: return n.Markdown case ContentBinaryHeaderValue: return n.Binary case ContentJSONHeaderValue: return n.JSON case ContentJSONProblemHeaderValue: return n.Problem case ContentJavascriptHeaderValue: return n.JSONP case ContentXMLHeaderValue, ContentXMLUnreadableHeaderValue: return n.XML case ContentYAMLHeaderValue: return n.YAML case ContentProtobufHeaderValue: return n.Protobuf case ContentMsgPackHeaderValue, ContentMsgPack2HeaderValue: return n.MsgPack default: return n.Other } } const negotiationContextKey = "iris.negotiation_builder" // Negotiation creates once and returns the negotiation builder // to build server-side available prioritized content // for specific content type(s), charset(s) and encoding algorithm(s). // // See `Negotiate` method too. func (ctx *Context) Negotiation() *NegotiationBuilder { if n := ctx.values.Get(negotiationContextKey); n != nil { return n.(*NegotiationBuilder) } acceptBuilder := NegotiationAcceptBuilder{} acceptBuilder.accept = parseHeader(ctx.GetHeader("Accept")) acceptBuilder.charset = parseHeader(ctx.GetHeader("Accept-Charset")) n := &NegotiationBuilder{Accept: acceptBuilder} ctx.values.Set(negotiationContextKey, n) return n } func parseHeader(headerValue string) []string { in := strings.Split(headerValue, ",") out := make([]string, 0, len(in)) for _, value := range in { // remove any spaces and quality values such as ;q=0.8. v := strings.TrimSpace(strings.Split(value, ";")[0]) if v != "" { out = append(out, v) } } return out } // Negotiate used for serving different representations of a resource at the same URI. // // The "v" can be a single `N` struct value. // The "v" can be any value completes the `ContentSelector` interface. // The "v" can be any value completes the `ContentNegotiator` interface. // The "v" can be any value of struct(JSON, JSONP, XML, YAML, Protobuf, MsgPack) or // string(TEXT, HTML) or []byte(Markdown, Binary) or []byte with any matched mime type. // // If the "v" is nil, the `Context.Negotitation()` builder's // content will be used instead, otherwise "v" overrides builder's content // (server mime types are still retrieved by its registered, supported, mime list) // // Set mime type priorities by `Negotiation().JSON().XML().HTML()...`. // Set charset priorities by `Negotiation().Charset(...)`. // Set encoding algorithm priorities by `Negotiation().Encoding(...)`. // Modify the accepted by // `Negotiation().Accept./Override()/.XML().JSON().Charset(...).Encoding(...)...`. // // It returns `ErrContentNotSupported` when not matched mime type(s). // // Resources: // https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Charset // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding // // Supports the above without quality values. // // Read more at: https://github.com/kataras/iris/tree/main/_examples/response-writer/content-negotiation func (ctx *Context) Negotiate(v any) (int, error) { contentType, charset, encoding, content := ctx.Negotiation().Build() if v == nil { v = content } if contentType == "" { // If the server cannot serve any matching set, // it SHOULD send back a 406 (Not Acceptable) error code. ctx.StatusCode(http.StatusNotAcceptable) return -1, ErrContentNotSupported } if charset == "" { charset = ctx.app.ConfigurationReadOnly().GetCharset() } if encoding != "" { ctx.CompressWriter(true) } ctx.contentTypeOnce(contentType, charset) if n, ok := v.(ContentNegotiator); ok { return n.Negotiate(ctx) } if s, ok := v.(ContentSelector); ok { v = s.SelectContent(contentType) } // switch v := value.(type) { // case []byte: // if contentType == ContentMarkdownHeaderValue { // return ctx.Markdown(v) // } // return ctx.Write(v) // case string: // return ctx.WriteString(v) // default: // make it switch by content-type only, but we lose custom mime types capability that way: // ^ solved with []byte on default case and // ^ N.Other and // ^ ContentSelector and ContentNegotiator interfaces. switch contentType { case ContentTextHeaderValue, ContentHTMLHeaderValue: return ctx.WriteString(v.(string)) case ContentMarkdownHeaderValue: err := ctx.Markdown(v.([]byte)) if err != nil { return 0, err } return ctx.writer.Written(), nil case ContentJSONHeaderValue: err := ctx.JSON(v) if err != nil { return 0, err } return ctx.writer.Written(), nil case ContentJSONProblemHeaderValue, ContentXMLProblemHeaderValue: err := ctx.Problem(v) if err != nil { return 0, err } return ctx.writer.Written(), nil case ContentJavascriptHeaderValue: err := ctx.JSONP(v) if err != nil { return 0, err } return ctx.writer.Written(), nil case ContentXMLHeaderValue, ContentXMLUnreadableHeaderValue: err := ctx.XML(v) if err != nil { return 0, err } return ctx.writer.Written(), nil case ContentYAMLHeaderValue: err := ctx.YAML(v) if err != nil { return 0, err } return ctx.writer.Written(), nil case ContentYAMLTextHeaderValue: err := ctx.TextYAML(v) if err != nil { return 0, err } return ctx.writer.Written(), nil case ContentProtobufHeaderValue: msg, ok := v.(proto.Message) if !ok { return -1, ErrContentNotSupported } return ctx.Protobuf(msg) case ContentMsgPackHeaderValue, ContentMsgPack2HeaderValue: return ctx.MsgPack(v) default: // maybe "Other" or v is []byte or string but not a built-in framework mime, // for custom content types, // panic if not correct usage. switch vv := v.(type) { case []byte: return ctx.Write(vv) case string: return ctx.WriteString(vv) default: ctx.StatusCode(http.StatusNotAcceptable) return -1, ErrContentNotSupported } } } // NegotiationBuilder returns from the `Context.Negotitation` // and can be used inside chain of handlers to build server-side // mime type(s), charset(s) and encoding algorithm(s) // that should match with the client's // Accept, Accept-Charset and Accept-Encoding headers (by-default). // To modify the client's accept use its "Accept" field // which it's the `NegotitationAcceptBuilder`. // // See the `Negotiate` method too. type NegotiationBuilder struct { Accept NegotiationAcceptBuilder mime []string // we need order. contents map[string]any // map to the "mime" and content should be rendered if that mime requested. charset []string encoding []string } // MIME registers a mime type and optionally the value that should be rendered // through `Context.Negotiate` when this mime type is accepted by client. // // Returns itself for recursive calls. func (n *NegotiationBuilder) MIME(mime string, content any) *NegotiationBuilder { mimes := parseHeader(mime) // if contains more than one sep by commas ",". if content == nil { n.mime = append(n.mime, mimes...) return n } if n.contents == nil { n.contents = make(map[string]any) } for _, m := range mimes { n.mime = append(n.mime, m) n.contents[m] = content } return n } // Text registers the "text/plain" content type and, optionally, // a value that `Context.Negotiate` will render // when a client accepts the "text/plain" content type. // // Returns itself for recursive calls. func (n *NegotiationBuilder) Text(v ...string) *NegotiationBuilder { var content any if len(v) > 0 { content = v[0] } return n.MIME(ContentTextHeaderValue, content) } // HTML registers the "text/html" content type and, optionally, // a value that `Context.Negotiate` will render // when a client accepts the "text/html" content type. // // Returns itself for recursive calls. func (n *NegotiationBuilder) HTML(v ...string) *NegotiationBuilder { var content any if len(v) > 0 { content = v[0] } return n.MIME(ContentHTMLHeaderValue, content) } // Markdown registers the "text/markdown" content type and, optionally, // a value that `Context.Negotiate` will render // when a client accepts the "text/markdown" content type. // // Returns itself for recursive calls. func (n *NegotiationBuilder) Markdown(v ...[]byte) *NegotiationBuilder { var content any if len(v) > 0 { content = v } return n.MIME(ContentMarkdownHeaderValue, content) } // Binary registers the "application/octet-stream" content type and, optionally, // a value that `Context.Negotiate` will render // when a client accepts the "application/octet-stream" content type. // // Returns itself for recursive calls. func (n *NegotiationBuilder) Binary(v ...[]byte) *NegotiationBuilder { var content any if len(v) > 0 { content = v[0] } return n.MIME(ContentBinaryHeaderValue, content) } // JSON registers the "application/json" content type and, optionally, // a value that `Context.Negotiate` will render // when a client accepts the "application/json" content type. // // Returns itself for recursive calls. func (n *NegotiationBuilder) JSON(v ...any) *NegotiationBuilder { var content any if len(v) > 0 { content = v[0] } return n.MIME(ContentJSONHeaderValue, content) } // Problem registers the "application/problem+json" or "application/problem+xml" content type and, optionally, // a value that `Context.Negotiate` will render // when a client accepts the "application/problem+json" or the "application/problem+xml" content type. // // Returns itself for recursive calls. func (n *NegotiationBuilder) Problem(v ...any) *NegotiationBuilder { var content any if len(v) > 0 { content = v[0] } return n.MIME(ContentJSONProblemHeaderValue+","+ContentXMLProblemHeaderValue, content) } // JSONP registers the "text/javascript" content type and, optionally, // a value that `Context.Negotiate` will render // when a client accepts the "javascript/javascript" content type. // // Returns itself for recursive calls. func (n *NegotiationBuilder) JSONP(v ...any) *NegotiationBuilder { var content any if len(v) > 0 { content = v[0] } return n.MIME(ContentJavascriptHeaderValue, content) } // XML registers the "text/xml" and "application/xml" content types and, optionally, // a value that `Context.Negotiate` will render // when a client accepts one of the "text/xml" or "application/xml" content types. // // Returns itself for recursive calls. func (n *NegotiationBuilder) XML(v ...any) *NegotiationBuilder { var content any if len(v) > 0 { content = v[0] } return n.MIME(ContentXMLHeaderValue+","+ContentXMLUnreadableHeaderValue, content) } // YAML registers the "application/x-yaml" content type and, optionally, // a value that `Context.Negotiate` will render // when a client accepts the "application/x-yaml" content type. // // Returns itself for recursive calls. func (n *NegotiationBuilder) YAML(v ...any) *NegotiationBuilder { var content any if len(v) > 0 { content = v[0] } return n.MIME(ContentYAMLHeaderValue, content) } // TextYAML registers the "text/yaml" content type and, optionally, // a value that `Context.Negotiate` will render // when a client accepts the "application/x-yaml" content type. // // Returns itself for recursive calls. func (n *NegotiationBuilder) TextYAML(v ...any) *NegotiationBuilder { var content any if len(v) > 0 { content = v[0] } return n.MIME(ContentYAMLTextHeaderValue, content) } // Protobuf registers the "application/x-protobuf" content type and, optionally, // a value that `Context.Negotiate` will render // when a client accepts the "application/x-protobuf" content type. // // Returns itself for recursive calls. func (n *NegotiationBuilder) Protobuf(v ...any) *NegotiationBuilder { var content any if len(v) > 0 { content = v[0] } return n.MIME(ContentProtobufHeaderValue, content) } // MsgPack registers the "application/x-msgpack" and "application/msgpack" content types and, optionally, // a value that `Context.Negotiate` will render // when a client accepts one of the "application/x-msgpack" or "application/msgpack" content types. // // Returns itself for recursive calls. func (n *NegotiationBuilder) MsgPack(v ...any) *NegotiationBuilder { var content any if len(v) > 0 { content = v[0] } return n.MIME(ContentMsgPackHeaderValue+","+ContentMsgPack2HeaderValue, content) } // Any registers a wildcard that can match any client's accept content type. // // Returns itself for recursive calls. func (n *NegotiationBuilder) Any(v ...any) *NegotiationBuilder { var content any if len(v) > 0 { content = v[0] } return n.MIME("*", content) } // Charset overrides the application's config's charset (which defaults to "utf-8") // that a client should match for // (through Accept-Charset header or custom through `NegotitationBuilder.Accept.Override().Charset(...)` call). // Do not set it if you don't know what you're doing. // // Returns itself for recursive calls. func (n *NegotiationBuilder) Charset(charset ...string) *NegotiationBuilder { n.charset = append(n.charset, charset...) return n } // Encoding registers one or more encoding algorithms by name, i.e gzip, deflate, br, snappy, s2. // that a client should match for (through Accept-Encoding header). // // Returns itself for recursive calls. func (n *NegotiationBuilder) Encoding(encoding ...string) *NegotiationBuilder { n.encoding = append(n.encoding, encoding...) return n } // EncodingGzip registers the "gzip" encoding algorithm // that a client should match for (through Accept-Encoding header or call of Accept.Encoding(enc)). // // It will make resources to served by "gzip" if Accept-Encoding contains the "gzip" as well. // // Returns itself for recursive calls. func (n *NegotiationBuilder) EncodingGzip() *NegotiationBuilder { return n.Encoding(GzipHeaderValue) } // Build calculates the client's and server's mime type(s), charset(s) and encoding // and returns the final content type, charset and encoding that server should render // to the client. It does not clear the fields, use the `Clear` method if neeeded. // // The returned "content" can be nil if the matched "contentType" does not provide any value, // in that case the `Context.Negotiate(v)` must be called with a non-nil value. func (n *NegotiationBuilder) Build() (contentType, charset, encoding string, content any) { contentType = negotiationMatch(n.Accept.accept, n.mime) charset = negotiationMatch(n.Accept.charset, n.charset) encoding = negotiationMatch(n.Accept.encoding, n.encoding) if n.contents != nil { if data, ok := n.contents[contentType]; ok { content = data } } return } // Clear clears the prioritized mime type(s), charset(s) and any contents // relative to those mime type(s). // The "Accept" field is stay as it is, use its `Override` method // to clear out the client's accepted mime type(s) and charset(s). func (n *NegotiationBuilder) Clear() *NegotiationBuilder { n.mime = n.mime[0:0] n.contents = nil n.charset = n.charset[0:0] return n } // NegotiationAcceptBuilder builds the accepted mime types and charset // // and "Accept-Charset" headers respectfully. // The default values are set by the client side, server can append or override those. // The end result will be challenged with runtime preffered set of content types and charsets. // // See the `Negotiate` method too. type NegotiationAcceptBuilder struct { // initialized with "Accept" request header values. accept []string // initialized with "Accept-Charset" request header. and if was empty then the // application's default (which defaults to utf-8). charset []string // initialized with "Accept-Encoding" request header values. encoding []string // To support override in request life cycle. // We need slice when data is the same format // for one or more mime types, // i.e text/xml and obselete application/xml. lastAccept []string lastCharset []string lastEncoding []string } // Override clears the default values for accept and accept charset. // Returns itself. func (n *NegotiationAcceptBuilder) Override() *NegotiationAcceptBuilder { // when called first. n.accept = n.accept[0:0] n.charset = n.charset[0:0] n.encoding = n.encoding[0:0] // when called after. if len(n.lastAccept) > 0 { n.accept = append(n.accept, n.lastAccept...) n.lastAccept = n.lastAccept[0:0] } if len(n.lastCharset) > 0 { n.charset = append(n.charset, n.lastCharset...) n.lastCharset = n.lastCharset[0:0] } if len(n.lastEncoding) > 0 { n.encoding = append(n.encoding, n.lastEncoding...) n.lastEncoding = n.lastEncoding[0:0] } return n } // MIME adds accepted client's mime type(s). // Returns itself. func (n *NegotiationAcceptBuilder) MIME(mimeType ...string) *NegotiationAcceptBuilder { n.lastAccept = mimeType n.accept = append(n.accept, mimeType...) return n } // Text adds the "text/plain" as accepted client content type. // Returns itself. func (n *NegotiationAcceptBuilder) Text() *NegotiationAcceptBuilder { return n.MIME(ContentTextHeaderValue) } // HTML adds the "text/html" as accepted client content type. // Returns itself. func (n *NegotiationAcceptBuilder) HTML() *NegotiationAcceptBuilder { return n.MIME(ContentHTMLHeaderValue) } // Markdown adds the "text/markdown" as accepted client content type. // Returns itself. func (n *NegotiationAcceptBuilder) Markdown() *NegotiationAcceptBuilder { return n.MIME(ContentMarkdownHeaderValue) } // Binary adds the "application/octet-stream" as accepted client content type. // Returns itself. func (n *NegotiationAcceptBuilder) Binary() *NegotiationAcceptBuilder { return n.MIME(ContentBinaryHeaderValue) } // JSON adds the "application/json" as accepted client content type. // Returns itself. func (n *NegotiationAcceptBuilder) JSON() *NegotiationAcceptBuilder { return n.MIME(ContentJSONHeaderValue) } // Problem adds the "application/problem+json" and "application/problem-xml" // as accepted client content types. // Returns itself. func (n *NegotiationAcceptBuilder) Problem() *NegotiationAcceptBuilder { return n.MIME(ContentJSONProblemHeaderValue, ContentXMLProblemHeaderValue) } // JSONP adds the "text/javascript" as accepted client content type. // Returns itself. func (n *NegotiationAcceptBuilder) JSONP() *NegotiationAcceptBuilder { return n.MIME(ContentJavascriptHeaderValue) } // XML adds the "text/xml" and "application/xml" as accepted client content types. // Returns itself. func (n *NegotiationAcceptBuilder) XML() *NegotiationAcceptBuilder { return n.MIME(ContentXMLHeaderValue, ContentXMLUnreadableHeaderValue) } // YAML adds the "application/x-yaml" as accepted client content type. // Returns itself. func (n *NegotiationAcceptBuilder) YAML() *NegotiationAcceptBuilder { return n.MIME(ContentYAMLHeaderValue) } // TextYAML adds the "text/yaml" as accepted client content type. // Returns itself. func (n *NegotiationAcceptBuilder) TextYAML() *NegotiationAcceptBuilder { return n.MIME(ContentYAMLTextHeaderValue) } // Protobuf adds the "application/x-protobuf" as accepted client content type. // Returns itself. func (n *NegotiationAcceptBuilder) Protobuf() *NegotiationAcceptBuilder { return n.MIME(ContentYAMLHeaderValue) } // MsgPack adds the "application/msgpack" and "application/x-msgpack" as accepted client content types. // Returns itself. func (n *NegotiationAcceptBuilder) MsgPack() *NegotiationAcceptBuilder { return n.MIME(ContentYAMLHeaderValue) } // Charset adds one or more client accepted charsets. // Returns itself. func (n *NegotiationAcceptBuilder) Charset(charset ...string) *NegotiationAcceptBuilder { n.lastCharset = charset n.charset = append(n.charset, charset...) return n } // Encoding adds one or more client accepted encoding algorithms. // Returns itself. func (n *NegotiationAcceptBuilder) Encoding(encoding ...string) *NegotiationAcceptBuilder { n.lastEncoding = encoding n.encoding = append(n.encoding, encoding...) return n } // EncodingGzip adds the "gzip" as accepted encoding. // Returns itself. func (n *NegotiationAcceptBuilder) EncodingGzip() *NegotiationAcceptBuilder { return n.Encoding(GzipHeaderValue) } // +------------------------------------------------------------+ // | Serve files | // +------------------------------------------------------------+ // ServeContent replies to the request using the content in the // provided ReadSeeker. The main benefit of ServeContent over io.Copy // is that it handles Range requests properly, sets the MIME type, and // handles If-Match, If-Unmodified-Since, If-None-Match, If-Modified-Since, // and If-Range requests. // // If the response's Content-Type header is not set, ServeContent // first tries to deduce the type from name's file extension. // // The name is otherwise unused; in particular it can be empty and is // never sent in the response. // // If modtime is not the zero time or Unix epoch, ServeContent // includes it in a Last-Modified header in the response. If the // request includes an If-Modified-Since header, ServeContent uses // modtime to decide whether the content needs to be sent at all. // // The content's Seek method must work: ServeContent uses // a seek to the end of the content to determine its size. // // If the caller has set w's ETag header formatted per RFC 7232, section 2.3, // ServeContent uses it to handle requests using If-Match, If-None-Match, or If-Range. // // Note that *os.File implements the io.ReadSeeker interface. // Note that compression can be registered // through `ctx.CompressWriter(true)` or `app.Use(iris.Compression)`. func (ctx *Context) ServeContent(content io.ReadSeeker, filename string, modtime time.Time) { ctx.ServeContentWithRate(content, filename, modtime, 0, 0) } // rateReadSeeker is a io.ReadSeeker that is rate limited by // the given token bucket. Each token in the bucket // represents one byte. See "golang.org/x/time/rate" package. type rateReadSeeker struct { io.ReadSeeker ctx context.Context limiter *rate.Limiter } func (rs *rateReadSeeker) Read(buf []byte) (int, error) { n, err := rs.ReadSeeker.Read(buf) if n <= 0 { return n, err } err = rs.limiter.WaitN(rs.ctx, n) return n, err } // ServeContentWithRate same as `ServeContent` but it can throttle the speed of reading // and though writing the "content" to the client. func (ctx *Context) ServeContentWithRate(content io.ReadSeeker, filename string, modtime time.Time, limit float64, burst int) { if limit > 0 { content = &rateReadSeeker{ ReadSeeker: content, ctx: ctx.request.Context(), limiter: rate.NewLimiter(rate.Limit(limit), burst), } } http.ServeContent(ctx.writer, ctx.request, filename, modtime, content) } // ServeFile replies to the request with the contents of the named // file or directory. // // If the provided file or directory name is a relative path, it is // interpreted relative to the current directory and may ascend to // parent directories. If the provided name is constructed from user // input, it should be sanitized before calling `ServeFile`. // // Use it when you want to serve assets like css and javascript files. // If client should confirm and save the file use the `SendFile` instead. // Note that compression can be registered // through `ctx.CompressWriter(true)` or `app.Use(iris.Compression)`. func (ctx *Context) ServeFile(filename string) error { return ctx.ServeFileWithRate(filename, 0, 0) } // ServeFileWithRate same as `ServeFile` but it can throttle the speed of reading // and though writing the file to the client. func (ctx *Context) ServeFileWithRate(filename string, limit float64, burst int) error { f, err := os.Open(filename) if err != nil { ctx.StatusCode(http.StatusNotFound) return err } defer f.Close() st, err := f.Stat() if err != nil { code := http.StatusInternalServerError if os.IsNotExist(err) { code = http.StatusNotFound } if os.IsPermission(err) { code = http.StatusForbidden } ctx.StatusCode(code) return err } if st.IsDir() { return ctx.ServeFile(path.Join(filename, "index.html")) } ctx.ServeContentWithRate(f, st.Name(), st.ModTime(), limit, burst) return nil } // SendFile sends a file as an attachment, that is downloaded and saved locally from client. // Note that compression can be registered // through `ctx.CompressWriter(true)` or `app.Use(iris.Compression)`. // Use `ServeFile` if a file should be served as a page asset instead. func (ctx *Context) SendFile(src string, destName string) error { return ctx.SendFileWithRate(src, destName, 0, 0) } // SendFileWithRate same as `SendFile` but it can throttle the speed of reading // and though writing the file to the client. func (ctx *Context) SendFileWithRate(src, destName string, limit float64, burst int) error { if destName == "" { destName = filepath.Base(src) } ctx.writer.Header().Set(ContentDispositionHeaderKey, MakeDisposition(destName)) return ctx.ServeFileWithRate(src, limit, burst) } // MakeDisposition generates an HTTP Content-Disposition field-value. // Similar solution followed by: Spring(Java), Symfony(PHP) and Ruby on Rails frameworks too. // // Fixes CVE-2020-5398. Reported by motoyasu-saburi. func MakeDisposition(filename string) string { return `attachment; filename*=UTF-8''` + url.QueryEscape(filename) } /* // Found at: https://stackoverflow.com/questions/53069040/checking-a-string-contains-only-ascii-characters // A faster (better, more idiomatic) version, which avoids unnecessary rune conversions. func isASCII(s string) bool { for i := 0; i < len(s); i++ { if s[i] > unicode.MaxASCII { return false } } return true } func isRFC5987AttrChar(c rune) bool { return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '!' || c == '#' || c == '$' || c == '&' || c == '+' || c == '-' || c == '.' || c == '^' || c == '_' || c == '`' || c == '|' || c == '~' } */ // +------------------------------------------------------------+ // | Cookies | // +------------------------------------------------------------+ // Set of Cookie actions for `CookieOption`. const ( OpCookieGet uint8 = iota OpCookieSet OpCookieDel ) // CookieOption is the type of function that is accepted on // context's methods like `SetCookieKV`, `RemoveCookie` and `SetCookie` // as their (last) variadic input argument to amend the to-be-sent cookie. // // The "op" is the operation code, 0 is GET, 1 is SET and 2 is REMOVE. type CookieOption func(ctx *Context, c *http.Cookie, op uint8) // CookieIncluded reports whether the "cookie.Name" is in the list of "cookieNames". // Notes: // If "cookieNames" slice is empty then it returns true, // If "cookie.Name" is empty then it returns false. func CookieIncluded(cookie *http.Cookie, cookieNames []string) bool { if cookie.Name == "" { return false } if len(cookieNames) > 0 { for _, name := range cookieNames { if cookie.Name == name { return true } } return false } return true } // var cookieNameSanitizer = strings.NewReplacer("\n", "-", "\r", "-") // // func sanitizeCookieName(n string) string { // return cookieNameSanitizer.Replace(n) // } // CookieOverride is a CookieOption which overrides the cookie explicitly to the given "cookie". // // Usage: // ctx.RemoveCookie("the_cookie_name", iris.CookieOverride(&http.Cookie{Domain: "example.com"})) func CookieOverride(cookie *http.Cookie) CookieOption { // The "Cookie" word method name is reserved as it's used as an alias. return func(_ *Context, c *http.Cookie, op uint8) { if op == OpCookieGet { return } *cookie = *c } } // CookieDomain is a CookieOption which sets the cookie's Domain field. // If empty then the current domain is used. // // Usage: // ctx.RemoveCookie("the_cookie_name", iris.CookieDomain("example.com")) func CookieDomain(domain string) CookieOption { return func(_ *Context, c *http.Cookie, op uint8) { if op == OpCookieGet { return } c.Domain = domain } } // CookieAllowReclaim accepts the Context itself. // If set it will add the cookie to (on `CookieSet`, `CookieSetKV`, `CookieUpsert`) // or remove the cookie from (on `CookieRemove`) the Request object too. func CookieAllowReclaim(cookieNames ...string) CookieOption { return func(ctx *Context, c *http.Cookie, op uint8) { if op == OpCookieGet { return } if !CookieIncluded(c, cookieNames) { return } switch op { case OpCookieSet: // perform upsert on request cookies or is it too much and not worth the cost? ctx.Request().AddCookie(c) case OpCookieDel: cookies := ctx.Request().Cookies() ctx.Request().Header.Del("Cookie") for i, v := range cookies { if v.Name != c.Name { ctx.Request().AddCookie(cookies[i]) } } } } } // CookieAllowSubdomains set to the Cookie Options // in order to allow subdomains to have access to the cookies. // It sets the cookie's Domain field (if was empty) and // it also sets the cookie's SameSite to lax mode too. func CookieAllowSubdomains(cookieNames ...string) CookieOption { return func(ctx *Context, c *http.Cookie, _ uint8) { if c.Domain != "" { return // already set. } if !CookieIncluded(c, cookieNames) { return } c.Domain = ctx.Domain() c.SameSite = http.SameSiteLaxMode // allow subdomain sharing. } } // CookieSameSite sets a same-site rule for cookies to set. // SameSite allows a server to define a cookie attribute making it impossible for // the browser to send this cookie along with cross-site requests. The main // goal is to mitigate the risk of cross-origin information leakage, and provide // some protection against cross-site request forgery attacks. // // See https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00 for details. func CookieSameSite(sameSite http.SameSite) CookieOption { return func(_ *Context, c *http.Cookie, op uint8) { if op == OpCookieSet { c.SameSite = sameSite } } } // CookieSecure sets the cookie's Secure option if the current request's // connection is using TLS. See `CookieHTTPOnly` too. func CookieSecure(ctx *Context, c *http.Cookie, op uint8) { if op == OpCookieSet { if ctx.Request().TLS != nil { c.Secure = true } } } // CookieHTTPOnly is a `CookieOption`. // Use it to set the cookie's HttpOnly field to false or true. // HttpOnly field defaults to true for `RemoveCookie` and `SetCookieKV`. // See `CookieSecure` too. func CookieHTTPOnly(httpOnly bool) CookieOption { return func(_ *Context, c *http.Cookie, op uint8) { if op == OpCookieSet { c.HttpOnly = httpOnly } } } // CookiePath is a `CookieOption`. // Use it to change the cookie's Path field. func CookiePath(path string) CookieOption { return func(_ *Context, c *http.Cookie, op uint8) { if op > OpCookieGet { // on set and remove. c.Path = path } } } // CookieCleanPath is a `CookieOption`. // Use it to clear the cookie's Path field, exactly the same as `CookiePath("")`. func CookieCleanPath(_ *Context, c *http.Cookie, op uint8) { if op > OpCookieGet { c.Path = "" } } // CookieExpires is a `CookieOption`. // Use it to change the cookie's Expires and MaxAge fields by passing the lifetime of the cookie. func CookieExpires(durFromNow time.Duration) CookieOption { return func(_ *Context, c *http.Cookie, op uint8) { if op == OpCookieSet { c.Expires = time.Now().Add(durFromNow) c.MaxAge = int(durFromNow.Seconds()) } } } // SecureCookie should encodes and decodes // authenticated and optionally encrypted cookie values. // See `CookieEncoding` package-level function. type SecureCookie interface { // Encode should encode the cookie value. // Should accept the cookie's name as its first argument // and as second argument the cookie value ptr. // Should return an encoded value or an empty one if encode operation failed. // Should return an error if encode operation failed. // // Note: Errors are not printed, so you have to know what you're doing, // and remember: if you use AES it only supports key sizes of 16, 24 or 32 bytes. // You either need to provide exactly that amount or you derive the key from what you type in. // // See `Decode` too. Encode(cookieName string, cookieValue any) (string, error) // Decode should decode the cookie value. // Should accept the cookie's name as its first argument, // as second argument the encoded cookie value and as third argument the decoded value ptr. // Should return a decoded value or an empty one if decode operation failed. // Should return an error if decode operation failed. // // Note: Errors are not printed, so you have to know what you're doing, // and remember: if you use AES it only supports key sizes of 16, 24 or 32 bytes. // You either need to provide exactly that amount or you derive the key from what you type in. // // See `Encode` too. Decode(cookieName string, cookieValue string, cookieValuePtr any) error } // CookieEncoding accepts a value which implements `Encode` and `Decode` methods. // It calls its `Encode` on `Context.SetCookie, UpsertCookie, and SetCookieKV` methods. // And on `Context.GetCookie` method it calls its `Decode`. // If "cookieNames" slice is not empty then only cookies // with that `Name` will be encoded on set and decoded on get, that way you can encrypt // specific cookie names (like the session id) and let the rest of the cookies "insecure". // // Example: https://github.com/kataras/iris/tree/main/_examples/cookies/securecookie func CookieEncoding(encoding SecureCookie, cookieNames ...string) CookieOption { if encoding == nil { return func(_ *Context, _ *http.Cookie, _ uint8) {} } return func(ctx *Context, c *http.Cookie, op uint8) { if op == OpCookieDel { return } if !CookieIncluded(c, cookieNames) { return } switch op { case OpCookieSet: // Should encode, it's a write to the client operation. newVal, err := encoding.Encode(c.Name, c.Value) if err != nil { ctx.Application().Logger().Error(err) c.Value = "" } else { c.Value = newVal } return case OpCookieGet: // Should decode, it's a read from the client operation. if err := encoding.Decode(c.Name, c.Value, &c.Value); err != nil { c.Value = "" } } } } const cookieOptionsContextKey = "iris.cookie.options" // AddCookieOptions adds cookie options for `SetCookie`, // `SetCookieKV, UpsertCookie` and `RemoveCookie` methods // for the current request. It can be called from a middleware before // cookies sent or received from the next Handler in the chain. // // Available builtin Cookie options are: // - CookieOverride // - CookieDomain // - CookieAllowReclaim // - CookieAllowSubdomains // - CookieSecure // - CookieHTTPOnly // - CookieSameSite // - CookiePath // - CookieCleanPath // - CookieExpires // - CookieEncoding // // Example at: https://github.com/kataras/iris/tree/main/_examples/cookies/securecookie func (ctx *Context) AddCookieOptions(options ...CookieOption) { if len(options) == 0 { return } if v := ctx.values.Get(cookieOptionsContextKey); v != nil { if opts, ok := v.([]CookieOption); ok { options = append(opts, options...) } } ctx.values.Set(cookieOptionsContextKey, options) } func (ctx *Context) applyCookieOptions(c *http.Cookie, op uint8, override []CookieOption) { if v := ctx.values.Get(cookieOptionsContextKey); v != nil { if options, ok := v.([]CookieOption); ok { for _, opt := range options { opt(ctx, c, op) } } } // The function's ones should be called last, so they can override // the stored ones (i.e by a prior middleware). for _, opt := range override { opt(ctx, c, op) } } // ClearCookieOptions clears any previously registered cookie options. // See `AddCookieOptions` too. func (ctx *Context) ClearCookieOptions() { ctx.values.Remove(cookieOptionsContextKey) } // SetCookie adds a cookie. // Use of the "options" is not required, they can be used to amend the "cookie". // // Example: https://github.com/kataras/iris/tree/main/_examples/cookies/basic func (ctx *Context) SetCookie(cookie *http.Cookie, options ...CookieOption) { ctx.applyCookieOptions(cookie, OpCookieSet, options) http.SetCookie(ctx.writer, cookie) } const setCookieHeaderKey = "Set-Cookie" // UpsertCookie adds a cookie to the response like `SetCookie` does // but it will also perform a replacement of the cookie // if already set by a previous `SetCookie` call. // It reports whether the cookie is new (true) or an existing one was updated (false). func (ctx *Context) UpsertCookie(cookie *http.Cookie, options ...CookieOption) bool { ctx.applyCookieOptions(cookie, OpCookieSet, options) header := ctx.ResponseWriter().Header() if cookies := header[setCookieHeaderKey]; len(cookies) > 0 { s := cookie.Name + "=" // name=?value existingUpdated := false for i, c := range cookies { if strings.HasPrefix(c, s) { if existingUpdated { // fixes #1877 // remove any duplicated. cookies[i] = "" header[setCookieHeaderKey] = cookies continue } // We need to update the Set-Cookie (to update the expiration or any other cookie's properties). // Probably the cookie is set and then updated in the first session creation // (e.g. UpdateExpiration, see https://github.com/kataras/iris/issues/1485). cookies[i] = cookie.String() header[setCookieHeaderKey] = cookies existingUpdated = true } } if existingUpdated { return false // existing one updated. } } header.Add(setCookieHeaderKey, cookie.String()) return true } // SetCookieKVExpiration is 365 days by-default // you can change it or simple, use the SetCookie for more control. // // See `CookieExpires` and `AddCookieOptions` for more. var SetCookieKVExpiration = 8760 * time.Hour // SetCookieKV adds a cookie, requires the name(string) and the value(string). // // By default it expires after 365 days and it is added to the root URL path, // use the `CookieExpires` and `CookiePath` to modify them. // Alternatively: ctx.SetCookie(&http.Cookie{...}) or ctx.AddCookieOptions(...) // // If you want to set custom the path: // ctx.SetCookieKV(name, value, iris.CookiePath("/custom/path/cookie/will/be/stored")) // // If you want to be visible only to current request path: // (note that client should be responsible for that if server sent an empty cookie's path, all browsers are compatible) // ctx.SetCookieKV(name, value, iris.CookieCleanPath/iris.CookiePath("")) // More: // // iris.CookieExpires(time.Duration) // iris.CookieHTTPOnly(false) // // Examples: https://github.com/kataras/iris/tree/main/_examples/cookies/basic func (ctx *Context) SetCookieKV(name, value string, options ...CookieOption) { c := &http.Cookie{} c.Path = "/" c.Name = name c.Value = url.QueryEscape(value) c.HttpOnly = true // MaxAge=0 means no 'Max-Age' attribute specified. // MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0' // MaxAge>0 means Max-Age attribute present and given in seconds c.Expires = time.Now().Add(SetCookieKVExpiration) c.MaxAge = int(time.Until(c.Expires).Seconds()) ctx.SetCookie(c, options...) } // GetCookie returns cookie's value by its name // returns empty string if nothing was found. // // If you want more than the value then: // cookie, err := ctx.GetRequestCookie("name") // // Example: https://github.com/kataras/iris/tree/main/_examples/cookies/basic func (ctx *Context) GetCookie(name string, options ...CookieOption) string { c, err := ctx.GetRequestCookie(name, options...) if err != nil { return "" } value, _ := url.QueryUnescape(c.Value) return value } // GetRequestCookie returns the request cookie including any context's cookie options (stored or given by this method). func (ctx *Context) GetRequestCookie(name string, options ...CookieOption) (*http.Cookie, error) { c, err := ctx.request.Cookie(name) if err != nil { return nil, err } ctx.applyCookieOptions(c, OpCookieGet, options) return c, nil } var ( // CookieExpireDelete may be set on Cookie.Expire for expiring the given cookie. CookieExpireDelete = memstore.ExpireDelete // CookieExpireUnlimited indicates that does expires after 24 years. CookieExpireUnlimited = time.Now().AddDate(24, 10, 10) ) // RemoveCookie deletes a cookie by its name and path = "/". // Tip: change the cookie's path to the current one by: RemoveCookie("the_cookie_name", iris.CookieCleanPath) // // If you intend to remove a cookie with a specific domain and value, please ensure to pass these values explicitly: // // ctx.RemoveCookie("the_cookie_name", iris.CookieDomain("example.com"), iris.CookiePath("/")) // // OR use a Cookie value instead: // // ctx.RemoveCookie("the_cookie_name", iris.CookieOverride(&http.Cookie{Domain: "example.com", Path: "/"})) // // Example: https://github.com/kataras/iris/tree/main/_examples/cookies/basic func (ctx *Context) RemoveCookie(name string, options ...CookieOption) { c := &http.Cookie{Path: "/"} // Send the cookie back to the client ctx.applyCookieOptions(c, OpCookieDel, options) c.Name = name c.Value = "" c.HttpOnly = true // Set the cookie expiration date to a past time c.Expires = CookieExpireDelete c.MaxAge = -1 // RFC says 1 second, but let's do it -1 to make sure is working. http.SetCookie(ctx.writer, c) } // VisitAllCookies takes a visitor function which is called // on each (request's) cookies' name and value. func (ctx *Context) VisitAllCookies(visitor func(name string, value string)) { for _, cookie := range ctx.request.Cookies() { visitor(cookie.Name, cookie.Value) } } var maxAgeExp = regexp.MustCompile(`maxage=(\d+)`) // MaxAge returns the "cache-control" request header's value // seconds as int64 // if header not found or parse failed then it returns -1. func (ctx *Context) MaxAge() int64 { header := ctx.GetHeader(CacheControlHeaderKey) if header == "" { return -1 } m := maxAgeExp.FindStringSubmatch(header) if len(m) == 2 { if v, err := strconv.Atoi(m[1]); err == nil { return int64(v) } } return -1 } // +------------------------------------------------------------+ // | Advanced: Response Recorder | // +------------------------------------------------------------+ // Record transforms the context's basic and direct responseWriter to a *ResponseRecorder // which can be used to reset the body, reset headers, get the body, // get & set the status code at any time and more. func (ctx *Context) Record() { switch w := ctx.writer.(type) { case *ResponseRecorder: default: recorder := AcquireResponseRecorder() recorder.BeginRecord(w) ctx.ResetResponseWriter(recorder) } } // Recorder returns the context's ResponseRecorder // if not recording then it starts recording and returns the new context's ResponseRecorder func (ctx *Context) Recorder() *ResponseRecorder { ctx.Record() return ctx.writer.(*ResponseRecorder) } // IsRecording returns the response recorder and a true value // when the response writer is recording the status code, body, headers and so on, // else returns nil and false. func (ctx *Context) IsRecording() (*ResponseRecorder, bool) { // NOTE: // two return values in order to minimize the if statement: // if (Recording) then writer = Recorder() // instead we do: recorder,ok = Recording() rr, ok := ctx.writer.(*ResponseRecorder) return rr, ok } // Exec calls the framewrok's ServeHTTPC // based on this context but with a changed method and path // like it was requested by the user, but it is not. // // Offline means that the route is registered to the iris and have all features that a normal route has // BUT it isn't available by browsing, its handlers executed only when other handler's context call them // it can validate paths, has sessions, path parameters and all. // // You can find the Route by app.GetRoute("theRouteName") // you can set a route name as: myRoute := app.Get("/mypath", handler)("theRouteName") // that will set a name to the route and returns its RouteInfo instance for further usage. // // It doesn't changes the global state, if a route was "offline" it remains offline. // // app.None(...) and app.GetRoutes().Offline(route)/.Online(route, method) // // Example: https://github.com/kataras/iris/tree/main/_examples/routing/route-state // // User can get the response by simple using rec := ctx.Recorder(); rec.Body()/rec.StatusCode()/rec.Header(). // // context's Values and the Session are kept in order to be able to communicate via the result route. // // It's for extreme use cases, 99% of the times will never be useful for you. func (ctx *Context) Exec(method string, path string) { if path == "" { return } if method == "" { method = "GET" } // backup the handlers backupHandlers := ctx.handlers[0:] backupPos := ctx.currentHandlerIndex req := ctx.request // backup the request path information backupPath := req.URL.Path backupMethod := req.Method // don't backupValues := ctx.values.ReadOnly() // set the request to be align with the 'againstRequestPath' req.RequestURI = path req.URL.Path = path req.Method = method // [values stays] // reset handlers ctx.handlers = ctx.handlers[0:0] ctx.currentHandlerIndex = 0 // execute the route from the (internal) context router // this way we keep the sessions and the values ctx.app.ServeHTTPC(ctx) // set the request back to its previous state req.RequestURI = backupPath req.URL.Path = backupPath req.Method = backupMethod // set back the old handlers and the last known index ctx.handlers = backupHandlers ctx.currentHandlerIndex = backupPos } // RouteExists reports whether a particular route exists // It will search from the current subdomain of context's host, if not inside the root domain. func (ctx *Context) RouteExists(method, path string) bool { return ctx.app.RouteExists(ctx, method, path) } const ( reflectValueContextKey = "iris.context.reflect_value" // ControllerContextKey returns the context key from which // the `Context.Controller` method returns the store's value. ControllerContextKey = "iris.controller.reflect_value" ) // ReflectValue caches and returns a []reflect.Value{reflect.ValueOf(ctx)}. // It's just a helper to maintain variable inside the context itself. func (ctx *Context) ReflectValue() []reflect.Value { if v := ctx.values.Get(reflectValueContextKey); v != nil { return v.([]reflect.Value) } v := []reflect.Value{reflect.ValueOf(ctx)} ctx.values.Set(reflectValueContextKey, v) return v } var emptyValue reflect.Value // Controller returns a reflect Value of the custom Controller from which this handler executed. // It will return a Kind() == reflect.Invalid if the handler was not executed from within a controller. func (ctx *Context) Controller() reflect.Value { if v := ctx.values.Get(ControllerContextKey); v != nil { return v.(reflect.Value) } return emptyValue } // DependenciesContextKey is the context key for the context's value // to keep the serve-time static dependencies raw values. const DependenciesContextKey = "iris.dependencies" // DependenciesMap is the type which context serve-time // struct dependencies are stored with. type DependenciesMap map[reflect.Type]reflect.Value // RegisterDependency registers a struct or slice // or pointer to struct dependency at request-time // for the next handler in the chain. One value per type. // Note that it's highly recommended to register // your dependencies before server ran // through Party.ConfigureContainer or mvc.Application.Register // in sake of minimum performance cost. // // See `UnregisterDependency` too. func (ctx *Context) RegisterDependency(v any) { if v == nil { return } val, ok := v.(reflect.Value) if !ok { val = reflect.ValueOf(v) } cv := ctx.values.Get(DependenciesContextKey) if cv != nil { m, ok := cv.(DependenciesMap) if !ok { return } m[val.Type()] = val return } ctx.values.Set(DependenciesContextKey, DependenciesMap{ val.Type(): val, }) } // UnregisterDependency removes a dependency based on its type. // Reports whether a dependency with that type was found and removed successfully. func (ctx *Context) UnregisterDependency(typ reflect.Type) bool { cv := ctx.values.Get(DependenciesContextKey) if cv != nil { m, ok := cv.(DependenciesMap) if ok { delete(m, typ) return true } } return false } // Application returns the iris app instance which belongs to this context. // Worth to notice that this function returns an interface // of the Application, which contains methods that are safe // to be executed at serve-time. The full app's fields // and methods are not available here for the developer's safety. func (ctx *Context) Application() Application { return ctx.app } // IsDebug reports whether the application runs with debug log level. // It is a shortcut of Application.IsDebug(). func (ctx *Context) IsDebug() bool { return ctx.app.IsDebug() } // SetErr is just a helper that sets an error value // as a context value, it does nothing more. // Also, by-default this error's value is written to the client // on failures when no registered error handler is available (see `Party.On(Any)ErrorCode`). // See `GetErr` to retrieve it back. // // To remove an error simply pass nil. // // Note that, if you want to stop the chain // with an error see the `StopWithError/StopWithPlainError` instead. func (ctx *Context) SetErr(err error) { if err == nil { ctx.values.Remove(errorContextKey) return } ctx.values.Set(errorContextKey, err) } // GetErr is a helper which retrieves // the error value stored by `SetErr`. // // Note that, if an error was stored by `SetErrPrivate` // then it returns the underline/original error instead // of the internal error wrapper. func (ctx *Context) GetErr() error { _, err := ctx.GetErrPublic() return err } // ErrPrivate if provided then the error saved in context // should NOT be visible to the client no matter what. type ErrPrivate interface { error IrisPrivateError() } // An internal wrapper for the `SetErrPrivate` method. type privateError struct{ error } func (e privateError) IrisPrivateError() {} // PrivateError accepts an error and returns a wrapped private one. func PrivateError(err error) ErrPrivate { if err == nil { return nil } errPrivate, ok := err.(ErrPrivate) if !ok { errPrivate = privateError{err} } return errPrivate } const errorContextKey = "iris.context.error" // SetErrPrivate sets an error that it's only accessible through `GetErr` // and it should never be sent to the client. // // Same as ctx.SetErr with an error that completes the `ErrPrivate` interface. // See `GetErrPublic` too. func (ctx *Context) SetErrPrivate(err error) { ctx.SetErr(PrivateError(err)) } // GetErrPublic reports whether the stored error // can be displayed to the client without risking // to expose security server implementation to the client. // // If the error is not nil, it is always the original one. func (ctx *Context) GetErrPublic() (bool, error) { if v := ctx.values.Get(errorContextKey); v != nil { switch err := v.(type) { case privateError: // If it's an error set by SetErrPrivate then unwrap it. return false, err.error case ErrPrivate: return false, err case error: return true, err } } return false, nil } // ErrPanicRecovery may be returned from `Context` actions of a `Handler` // which recovers from a manual panic. type ErrPanicRecovery struct { ErrPrivate Cause any Callers []string // file:line callers. Stack []byte // the full debug stack. RegisteredHandlers []string // file:line of all registered handlers. CurrentHandlerFileLine string // the handler panic came from. CurrentHandlerName string // the handler name panic came from. Request string // the http dumped request. } // Error implements the Go standard error type. func (e *ErrPanicRecovery) Error() string { if e.Cause != nil { if err, ok := e.Cause.(error); ok { return err.Error() } } return fmt.Sprintf("%v\n%s\nRequest:\n%s", e.Cause, strings.Join(e.Callers, "\n"), e.Request) } // Is completes the internal errors.Is interface. func (e *ErrPanicRecovery) Is(err error) bool { _, ok := IsErrPanicRecovery(err) return ok } func (e *ErrPanicRecovery) LogMessage() string { logMessage := fmt.Sprintf("Recovered from a route's Handler('%s')\n", e.CurrentHandlerName) logMessage += fmt.Sprint(e.Request) logMessage += fmt.Sprintf("%s\n", e.Cause) logMessage += fmt.Sprintf("%s\n", strings.Join(e.Callers, "\n")) return logMessage } // IsErrPanicRecovery reports whether the given "err" is a type of ErrPanicRecovery. func IsErrPanicRecovery(err error) (*ErrPanicRecovery, bool) { if err == nil { return nil, false } v, ok := err.(*ErrPanicRecovery) return v, ok } // IsRecovered reports whether this handler has been recovered // by the Iris recover middleware. func (ctx *Context) IsRecovered() (*ErrPanicRecovery, bool) { if ctx.GetStatusCode() == http.StatusInternalServerError { // Panic error from recovery middleware is private. return IsErrPanicRecovery(ctx.GetErr()) } return nil, false } const ( funcsContextPrefixKey = "iris.funcs." funcLogoutContextKey = "auth.logout_func" ) // SetFunc registers a custom function to this Request. // It's a helper to pass dynamic functions across handlers of the same chain. // For a more complete solution please use Dependency Injection instead. // This is just an easy to way to pass a function to the // next handler like ctx.Values().Set/Get does. // Sometimes is faster and easier to pass the object as a request value // and cast it when you want to use one of its methods instead of using // these `SetFunc` and `CallFunc` methods. // This implementation is suitable for functions that may change inside the // handler chain and the object holding the method is not predictable. // // The "name" argument is the custom name of the function, // you will use its name to call it later on, e.g. "auth.myfunc". // // The second, "fn" argument is the raw function/method you want // to pass through the next handler(s) of the chain. // // The last variadic input argument is optionally, if set // then its arguments are passing into the function's input arguments, // they should be always be the first ones to be accepted by the "fn" inputs, // e.g. an object, a receiver or a static service. // // See its `CallFunc` to call the "fn" on the next handler. // // Example at: // https://github.com/kataras/iris/tree/main/_examples/routing/writing-a-middleware/share-funcs func (ctx *Context) SetFunc(name string, fn any, persistenceArgs ...any) { f := newFunc(name, fn, persistenceArgs...) ctx.values.Set(funcsContextPrefixKey+name, f) } // GetFunc returns the context function declaration which holds // some information about the function registered under the given "name" by // the `SetFunc` method. func (ctx *Context) GetFunc(name string) (*Func, bool) { fn := ctx.values.Get(funcsContextPrefixKey + name) if fn == nil { return nil, false } return fn.(*Func), true } // CallFunc calls the function registered by the `SetFunc`. // The input arguments MUST match the expected ones. // // If the registered function was just a handler // or a handler which returns an error // or a simple function // or a simple function which returns an error // then this operation will perform without any serious cost, // otherwise reflection will be used instead, which may slow down the overall performance. // // Retruns ErrNotFound if the function was not registered. // // For a more complete solution without limiations navigate through // the Iris Dependency Injection feature instead. func (ctx *Context) CallFunc(name string, args ...any) ([]reflect.Value, error) { fn, ok := ctx.GetFunc(name) if !ok || fn == nil { return nil, ErrNotFound } return fn.call(ctx, args...) } // SetLogoutFunc registers a custom logout function that will be // available to use inside the next handler(s). The function // may be registered multiple times but the last one is the valid. // So a logout function may start with basic authentication // and other middleware in the chain may change it to a custom sessions logout one. // This method uses the `SetFunc` method under the hoods. // // See `Logout` method too. func (ctx *Context) SetLogoutFunc(fn any, persistenceArgs ...any) { ctx.SetFunc(funcLogoutContextKey, fn, persistenceArgs...) } // Logout calls the registered logout function. // Returns ErrNotFound if a logout function was not specified // by a prior call of `SetLogoutFunc`. func (ctx *Context) Logout(args ...any) error { _, err := ctx.CallFunc(funcLogoutContextKey, args...) return err } const userContextKey = "iris.user" // SetUser sets a value as a User for this request. // It's used by auth middlewares as a common // method to provide user information to the // next handlers in the chain. // // The "i" input argument can be: // - A value which completes the User interface // - A map[string]any. // - A value which does not complete the whole User interface // - A value which does not complete the User interface at all // (only its `User().GetRaw` method is available). // // Look the `User` method to retrieve it. func (ctx *Context) SetUser(i any) error { if i == nil { ctx.values.Remove(userContextKey) return nil } u, ok := i.(User) if !ok { if m, ok := i.(Map); ok { // it's a map, convert it to a User. u = UserMap(m) } else { // It's a structure, wrap it and let // runtime decide the features. p := newUserPartial(i) if p == nil { return ErrNotSupported } u = p } } ctx.values.Set(userContextKey, u) return nil } // User returns the registered User of this request. // To get the original value (even if a value set by SetUser does not implement the User interface) // use its GetRaw method. // / // See `SetUser` too. func (ctx *Context) User() User { if v := ctx.values.Get(userContextKey); v != nil { if u, ok := v.(User); ok { return u } } return nil } // Ensure Iris Context implements the standard Context package, build-time. var _ context.Context = (*Context)(nil) // Deadline returns the time when work done on behalf of this context // should be canceled. Deadline returns ok==false when no deadline is // set. Successive calls to Deadline return the same results. // // Shortcut of Request().Context().Deadline(). func (ctx *Context) Deadline() (deadline time.Time, ok bool) { return ctx.request.Context().Deadline() } // Done returns a channel that's closed when work done on behalf of this // context should be canceled. Done may return nil if this context can // never be canceled. Successive calls to Done return the same value. // The close of the Done channel may happen asynchronously, // after the cancel function returns. // // WithCancel arranges for Done to be closed when cancel is called; // WithDeadline arranges for Done to be closed when the deadline // expires; WithTimeout arranges for Done to be closed when the timeout // elapses. // // Done is provided for use in select statements: // // // Stream generates values with DoSomething and sends them to out // // until DoSomething returns an error or ctx.Done is closed. // func Stream(ctx context.Context, out chan<- Value) error { // for { // v, err := DoSomething(ctx) // if err != nil { // return err // } // select { // case <-ctx.Done(): // return ctx.Err() // case out <- v: // } // } // } // // See https://blog.golang.org/pipelines for more examples of how to use // a Done channel for cancellation. // // Shortcut of Request().Context().Done(). func (ctx *Context) Done() <-chan struct{} { return ctx.request.Context().Done() } // If Done is not yet closed, Err returns nil. // If Done is closed, Err returns a non-nil error explaining why: // Canceled if the context was canceled // or DeadlineExceeded if the context's deadline passed. // After Err returns a non-nil error, successive calls to Err return the same error. // // Shortcut of Request().Context().Err(). func (ctx *Context) Err() error { return ctx.request.Context().Err() } // Value returns the value associated with this context for key, or nil // if no value is associated with key. Successive calls to Value with // the same key returns the same result. // // Shortcut of Request().Context().Value(key any) any. func (ctx *Context) Value(key any) any { if keyStr, ok := key.(string); ok { // check if the key is a type of string, which can be retrieved by the mem store. if entry, exists := ctx.values.GetEntry(keyStr); exists { return entry.ValueRaw } } // otherwise return the chained value. return ctx.request.Context().Value(key) } const idContextKey = "iris.context.id" // SetID sets an ID, any value, to the Request Context. // If possible the "id" should implement a `String() string` method // so it can be rendered on `Context.String` method. // // See `GetID` and `middleware/requestid` too. func (ctx *Context) SetID(id any) { ctx.values.Set(idContextKey, id) } // GetID returns the Request Context's ID. // It returns nil if not given by a prior `SetID` call. // See `middleware/requestid` too. func (ctx *Context) GetID() any { return ctx.values.Get(idContextKey) } // String returns the string representation of this request. // // It returns the Context's ID given by a `SetID`call, // followed by the client's IP and the method:uri. func (ctx *Context) String() string { id := ctx.GetID() if id != nil { if stringer, ok := id.(fmt.Stringer); ok { id = stringer.String() } } return fmt.Sprintf("[%v] %s ▶ %s:%s", id, ctx.RemoteAddr(), ctx.Method(), ctx.Request().RequestURI) } ================================================ FILE: context/context_func.go ================================================ package context import ( "errors" "reflect" "sync" ) // ErrInvalidArgs fires when the `Context.CallFunc` // is called with invalid number of arguments. var ErrInvalidArgs = errors.New("invalid arguments") // Func represents a function registered by the Context. // See its `buildMeta` and `call` internal methods. type Func struct { RegisterName string // the name of which this function is registered, for information only. Raw any // the Raw function, can be used for custom casting. PersistenceArgs []any // the persistence input arguments given on registration. once sync.Once // guards build once, on first call. // Available after the first call. Meta *FuncMeta } func newFunc(name string, fn any, persistenceArgs ...any) *Func { return &Func{ RegisterName: name, Raw: fn, PersistenceArgs: persistenceArgs, } } // FuncMeta holds the necessary information about a registered // context function. Built once by the Func. type FuncMeta struct { Handler Handler // when it's just a handler. HandlerWithErr func(*Context) error // when it's just a handler which returns an error. RawFunc func() // when it's just a func. RawFuncWithErr func() error // when it's just a func which returns an error. RawFuncArgs func(...any) RawFuncArgsWithErr func(...any) error Value reflect.Value Type reflect.Type ExpectedArgumentsLength int PersistenceInputs []reflect.Value AcceptsContext bool // the Context, if exists should be always first argument. ReturnsError bool // when the function's last output argument is error. } func (f *Func) buildMeta() { switch fn := f.Raw.(type) { case Handler: f.Meta = &FuncMeta{Handler: fn} return // case func(*Context): // f.Meta = &FuncMeta{Handler: fn} // return case func(*Context) error: f.Meta = &FuncMeta{HandlerWithErr: fn} return case func(): f.Meta = &FuncMeta{RawFunc: fn} return case func() error: f.Meta = &FuncMeta{RawFuncWithErr: fn} return case func(...any): f.Meta = &FuncMeta{RawFuncArgs: fn} return case func(...any) error: f.Meta = &FuncMeta{RawFuncArgsWithErr: fn} return } fn := f.Raw meta := FuncMeta{} if val, ok := fn.(reflect.Value); ok { meta.Value = val } else { meta.Value = reflect.ValueOf(fn) } meta.Type = meta.Value.Type() if meta.Type.Kind() != reflect.Func { return } meta.ExpectedArgumentsLength = meta.Type.NumIn() skipInputs := len(meta.PersistenceInputs) if meta.ExpectedArgumentsLength > skipInputs { meta.AcceptsContext = isContext(meta.Type.In(skipInputs)) } if numOut := meta.Type.NumOut(); numOut > 0 { // error should be the last output. if isError(meta.Type.Out(numOut - 1)) { meta.ReturnsError = true } } persistenceArgs := f.PersistenceArgs if len(persistenceArgs) > 0 { inputs := make([]reflect.Value, 0, len(persistenceArgs)) for _, arg := range persistenceArgs { if in, ok := arg.(reflect.Value); ok { inputs = append(inputs, in) } else { inputs = append(inputs, reflect.ValueOf(in)) } } meta.PersistenceInputs = inputs } f.Meta = &meta } func (f *Func) call(ctx *Context, args ...any) ([]reflect.Value, error) { f.once.Do(f.buildMeta) meta := f.Meta if meta.Handler != nil { meta.Handler(ctx) return nil, nil } if meta.HandlerWithErr != nil { return nil, meta.HandlerWithErr(ctx) } if meta.RawFunc != nil { meta.RawFunc() return nil, nil } if meta.RawFuncWithErr != nil { return nil, meta.RawFuncWithErr() } if meta.RawFuncArgs != nil { meta.RawFuncArgs(args...) return nil, nil } if meta.RawFuncArgsWithErr != nil { return nil, meta.RawFuncArgsWithErr(args...) } inputs := make([]reflect.Value, 0, f.Meta.ExpectedArgumentsLength) inputs = append(inputs, f.Meta.PersistenceInputs...) if f.Meta.AcceptsContext { inputs = append(inputs, reflect.ValueOf(ctx)) } for _, arg := range args { if in, ok := arg.(reflect.Value); ok { inputs = append(inputs, in) } else { inputs = append(inputs, reflect.ValueOf(arg)) } } // keep it here, the inptus may contain the context. if f.Meta.ExpectedArgumentsLength != len(inputs) { return nil, ErrInvalidArgs } outputs := f.Meta.Value.Call(inputs) return outputs, getError(outputs) } var contextType = reflect.TypeOf((*Context)(nil)) // isContext returns true if the "typ" is a type of Context. func isContext(typ reflect.Type) bool { return typ == contextType } var errTyp = reflect.TypeOf((*error)(nil)).Elem() // isError returns true if "typ" is type of `error`. func isError(typ reflect.Type) bool { return typ.Implements(errTyp) } func getError(outputs []reflect.Value) error { if n := len(outputs); n > 0 { lastOut := outputs[n-1] if isError(lastOut.Type()) { if lastOut.IsNil() { return nil } return lastOut.Interface().(error) } } return nil } ================================================ FILE: context/context_go118.go ================================================ //go:build go1.18 // +build go1.18 package context import "runtime/debug" func init() { if info, ok := debug.ReadBuildInfo(); ok { for _, setting := range info.Settings { if BuildRevision != "" && BuildTime != "" { break } if setting.Key == "vcs.revision" { BuildRevision = setting.Value } if setting.Key == "vcs.time" { BuildTime = setting.Value } } } } ================================================ FILE: context/context_user.go ================================================ package context import ( "encoding/json" "errors" "strings" "time" "unicode" ) // ErrNotSupported is fired when a specific method is not implemented // or not supported entirely. // Can be used by User implementations when // an authentication system does not implement a specific, but required, // method of the User interface. var ErrNotSupported = errors.New("not supported") // User is a generic view of an authorized client. // See `Context.User` and `SetUser` methods for more. // // The informational methods starts with a "Get" prefix // in order to allow the implementation to contain exported // fields such as `Username` so they can be JSON encoded when necessary. // // The caller is free to cast this with the implementation directly // when special features are offered by the authorization system. // // To make optional some of the fields you can just embed the User interface // and implement whatever methods you want to support. // // There are three builtin implementations of the User interface: // - SimpleUser // - UserMap (a wrapper by SetUser) // - UserPartial (a wrapper by SetUser) type User interface { // GetRaw should return the raw instance of the user, if supported. GetRaw() (any, error) // GetAuthorization should return the authorization method, // e.g. Basic Authentication. GetAuthorization() (string, error) // GetAuthorizedAt should return the exact time the // client has been authorized for the "first" time. GetAuthorizedAt() (time.Time, error) // GetID should return the ID of the User. GetID() (string, error) // GetUsername should return the name of the User. GetUsername() (string, error) // GetPassword should return the encoded or raw password // (depends on the implementation) of the User. GetPassword() (string, error) // GetEmail should return the e-mail of the User. GetEmail() (string, error) // GetRoles should optionally return the specific user's roles. // Returns `ErrNotSupported` if this method is not // implemented by the User implementation. GetRoles() ([]string, error) // GetToken should optionally return a token used // to authorize this User. GetToken() ([]byte, error) // GetField should optionally return a dynamic field // based on its key. Useful for custom user fields. // Keep in mind that these fields are encoded as a separate JSON key. GetField(key string) (any, error) } /* Notes: We could use a structure of User wrapper and separate interfaces for each of the methods so they return ErrNotSupported if the implementation is missing it, so the `Features` field and HasUserFeature can be omitted and add a Raw() any to return the underline User implementation too. The advandages of the above idea is that we don't have to add new methods for each of the builtin features and we can keep the (assumed) struct small. But we dont as it has many disadvantages, unless is requested. ^ UPDATE: this is done through UserPartial. The disadvantage of the current implementation is that the developer MUST complete the whole interface in order to be a valid User and if we add new methods in the future their implementation will break (unless they have a static interface implementation check as we have on SimpleUser). We kind of by-pass this disadvantage by providing a SimpleUser which can be embedded (as pointer) to the end-developer's custom implementations. */ // SimpleUser is a simple implementation of the User interface. type SimpleUser struct { Authorization string `json:"authorization,omitempty" db:"authorization"` AuthorizedAt time.Time `json:"authorized_at,omitempty" db:"authorized_at"` ID string `json:"id,omitempty" db:"id"` Username string `json:"username,omitempty" db:"username"` Password string `json:"password,omitempty" db:"password"` Email string `json:"email,omitempty" db:"email"` Roles []string `json:"roles,omitempty" db:"roles"` Token json.RawMessage `json:"token,omitempty" db:"token"` Fields Map `json:"fields,omitempty" db:"fields"` } var _ User = (*SimpleUser)(nil) // GetRaw returns itself. func (u *SimpleUser) GetRaw() (any, error) { return u, nil } // GetAuthorization returns the authorization method, // e.g. Basic Authentication. func (u *SimpleUser) GetAuthorization() (string, error) { return u.Authorization, nil } // GetAuthorizedAt returns the exact time the // client has been authorized for the "first" time. func (u *SimpleUser) GetAuthorizedAt() (time.Time, error) { return u.AuthorizedAt, nil } // GetID returns the ID of the User. func (u *SimpleUser) GetID() (string, error) { return u.ID, nil } // GetUsername returns the name of the User. func (u *SimpleUser) GetUsername() (string, error) { return u.Username, nil } // GetPassword returns the raw password of the User. func (u *SimpleUser) GetPassword() (string, error) { return u.Password, nil } // GetEmail returns the e-mail of (string,error) User. func (u *SimpleUser) GetEmail() (string, error) { return u.Email, nil } // GetRoles returns the specific user's roles. // Returns with `ErrNotSupported` if the Roles field is not initialized. func (u *SimpleUser) GetRoles() ([]string, error) { if u.Roles == nil { return nil, ErrNotSupported } return u.Roles, nil } // GetToken returns the token associated with this User. // It may return empty if the User is not featured with a Token. // // The implementation can change that behavior. // Returns with `ErrNotSupported` if the Token field is empty. func (u *SimpleUser) GetToken() ([]byte, error) { if len(u.Token) == 0 { return nil, ErrNotSupported } return u.Token, nil } // GetField optionally returns a dynamic field from the `Fields` field // based on its key. func (u *SimpleUser) GetField(key string) (any, error) { if u.Fields == nil { return nil, ErrNotSupported } return u.Fields[key], nil } // UserMap can be used to convert a common map[string]any to a User. // Usage: // // user := map[string]any{ // "username": "kataras", // "age" : 27, // } // // ctx.SetUser(user) // OR // user := UserStruct{....} // ctx.SetUser(user) // [...] // username, err := ctx.User().GetUsername() // field,err := ctx.User().GetField("age") // age := field.(int) // OR cast it: // user := ctx.User().(UserMap) // username := user["username"].(string) // age := user["age"].(int) type UserMap Map var _ User = UserMap{} // GetRaw returns the underline map. func (u UserMap) GetRaw() (any, error) { return Map(u), nil } // GetAuthorization returns the authorization or Authorization value of the map. func (u UserMap) GetAuthorization() (string, error) { return u.str("authorization") } // GetAuthorizedAt returns the authorized_at or Authorized_At value of the map. func (u UserMap) GetAuthorizedAt() (time.Time, error) { return u.time("authorized_at") } // GetID returns the id or Id or ID value of the map. func (u UserMap) GetID() (string, error) { return u.str("id") } // GetUsername returns the username or Username value of the map. func (u UserMap) GetUsername() (string, error) { return u.str("username") } // GetPassword returns the password or Password value of the map. func (u UserMap) GetPassword() (string, error) { return u.str("password") } // GetEmail returns the email or Email value of the map. func (u UserMap) GetEmail() (string, error) { return u.str("email") } // GetRoles returns the roles or Roles value of the map. func (u UserMap) GetRoles() ([]string, error) { return u.strSlice("roles") } // GetToken returns the roles or Roles value of the map. func (u UserMap) GetToken() ([]byte, error) { return u.bytes("token") } // GetField returns the raw map's value based on its "key". // It's not kind of useful here as you can just use the map. func (u UserMap) GetField(key string) (any, error) { return u[key], nil } func (u UserMap) val(key string) any { isTitle := unicode.IsTitle(rune(key[0])) // if starts with uppercase. if isTitle { key = strings.ToLower(key) } return u[key] } func (u UserMap) bytes(key string) ([]byte, error) { if v := u.val(key); v != nil { switch s := v.(type) { case []byte: return s, nil case string: return []byte(s), nil } } return nil, ErrNotSupported } func (u UserMap) str(key string) (string, error) { if v := u.val(key); v != nil { if s, ok := v.(string); ok { return s, nil } // exists or not we don't care, if it's invalid type we don't fill it. } return "", ErrNotSupported } func (u UserMap) strSlice(key string) ([]string, error) { if v := u.val(key); v != nil { if s, ok := v.([]string); ok { return s, nil } } return nil, ErrNotSupported } func (u UserMap) time(key string) (time.Time, error) { if v := u.val(key); v != nil { if t, ok := v.(time.Time); ok { return t, nil } } return time.Time{}, ErrNotSupported } type ( userGetAuthorization interface { GetAuthorization() string } userGetAuthorizedAt interface { GetAuthorizedAt() time.Time } userGetID interface { GetID() string } // UserGetUsername interface which // requires a single method to complete // a User on Context.SetUser. UserGetUsername interface { GetUsername() string } // UserGetPassword interface which // requires a single method to complete // a User on Context.SetUser. UserGetPassword interface { GetPassword() string } userGetEmail interface { GetEmail() string } userGetRoles interface { GetRoles() []string } userGetToken interface { GetToken() []byte } userGetField interface { GetField(string) any } // UserPartial is a User. // It's a helper which wraps a struct value that // may or may not complete the whole User interface. // See Context.SetUser. UserPartial struct { Raw any `json:"raw"` userGetAuthorization `json:",omitempty"` userGetAuthorizedAt `json:",omitempty"` userGetID `json:",omitempty"` UserGetUsername `json:",omitempty"` UserGetPassword `json:",omitempty"` userGetEmail `json:",omitempty"` userGetRoles `json:",omitempty"` userGetToken `json:",omitempty"` userGetField `json:",omitempty"` } ) var _ User = (*UserPartial)(nil) func newUserPartial(i any) *UserPartial { if i == nil { return nil } p := &UserPartial{Raw: i} if u, ok := i.(userGetAuthorization); ok { p.userGetAuthorization = u } if u, ok := i.(userGetAuthorizedAt); ok { p.userGetAuthorizedAt = u } if u, ok := i.(userGetID); ok { p.userGetID = u } if u, ok := i.(UserGetUsername); ok { p.UserGetUsername = u } if u, ok := i.(UserGetPassword); ok { p.UserGetPassword = u } if u, ok := i.(userGetEmail); ok { p.userGetEmail = u } if u, ok := i.(userGetRoles); ok { p.userGetRoles = u } if u, ok := i.(userGetToken); ok { p.userGetToken = u } if u, ok := i.(userGetField); ok { p.userGetField = u } // if !containsAtLeastOneMethod { // return nil // } return p } // GetRaw returns the original raw instance of the user. func (u *UserPartial) GetRaw() (any, error) { if u == nil { return nil, ErrNotSupported } return u.Raw, nil } // GetAuthorization should return the authorization method, // e.g. Basic Authentication. func (u *UserPartial) GetAuthorization() (string, error) { if v := u.userGetAuthorization; v != nil { return v.GetAuthorization(), nil } return "", ErrNotSupported } // GetAuthorizedAt should return the exact time the // client has been authorized for the "first" time. func (u *UserPartial) GetAuthorizedAt() (time.Time, error) { if v := u.userGetAuthorizedAt; v != nil { return v.GetAuthorizedAt(), nil } return time.Time{}, ErrNotSupported } // GetID should return the ID of the User. func (u *UserPartial) GetID() (string, error) { if v := u.userGetID; v != nil { return v.GetID(), nil } return "", ErrNotSupported } // GetUsername should return the name of the User. func (u *UserPartial) GetUsername() (string, error) { if v := u.UserGetUsername; v != nil { return v.GetUsername(), nil } return "", ErrNotSupported } // GetPassword should return the encoded or raw password // (depends on the implementation) of the User. func (u *UserPartial) GetPassword() (string, error) { if v := u.UserGetPassword; v != nil { return v.GetPassword(), nil } return "", ErrNotSupported } // GetEmail should return the e-mail of the User. func (u *UserPartial) GetEmail() (string, error) { if v := u.userGetEmail; v != nil { return v.GetEmail(), nil } return "", ErrNotSupported } // GetRoles should optionally return the specific user's roles. // Returns `ErrNotSupported` if this method is not // implemented by the User implementation. func (u *UserPartial) GetRoles() ([]string, error) { if v := u.userGetRoles; v != nil { return v.GetRoles(), nil } return nil, ErrNotSupported } // GetToken should optionally return a token used // to authorize this User. func (u *UserPartial) GetToken() ([]byte, error) { if v := u.userGetToken; v != nil { return v.GetToken(), nil } return nil, ErrNotSupported } // GetField should optionally return a dynamic field // based on its key. Useful for custom user fields. // Keep in mind that these fields are encoded as a separate JSON key. func (u *UserPartial) GetField(key string) (any, error) { if v := u.userGetField; v != nil { return v.GetField(key), nil } return nil, ErrNotSupported } ================================================ FILE: context/counter.go ================================================ package context import ( "math" "sync/atomic" ) // Counter is the shared counter instances between Iris applications of the same process. var Counter = NewGlobalCounter() // it's not used anywhere, currently but it's here. // NewGlobalCounter returns a fresh instance of a global counter. // End developers can use it as a helper for their applications. func NewGlobalCounter() *GlobalCounter { return &GlobalCounter{Max: math.MaxUint64} } // GlobalCounter is a counter which // atomically increments until Max. type GlobalCounter struct { value uint64 Max uint64 } // Increment increments the Value. // The value cannot exceed the Max one. // It uses Compare and Swap with the atomic package. // // Returns the new number value. func (c *GlobalCounter) Increment() (newValue uint64) { for { prev := atomic.LoadUint64(&c.value) newValue = prev + 1 if newValue >= c.Max { newValue = 0 } if atomic.CompareAndSwapUint64(&c.value, prev, newValue) { break } } return } // Get returns the current counter without incrementing. func (c *GlobalCounter) Get() uint64 { return atomic.LoadUint64(&c.value) } ================================================ FILE: context/fs.go ================================================ package context import ( "embed" "fmt" "io/fs" "net/http" "os" "path" "path/filepath" "strings" ) // ResolveFS accepts a single input argument of any type // and tries to cast it to fs.FS. // // It affects the view engine's filesystem resolver. // // This package-level variable can be modified on initialization. var ResolveFS = func(fsOrDir any) fs.FS { if fsOrDir == nil { return noOpFS{} } switch v := fsOrDir.(type) { case string: if v == "" { return noOpFS{} } return os.DirFS(v) case fs.FS: return v case http.FileSystem: // handles go-bindata. return &httpFS{v} default: panic(fmt.Errorf(`unexpected "fsOrDir" argument type of %T (string or fs.FS or embed.FS or http.FileSystem)`, v)) } } type noOpFS struct{} func (fileSystem noOpFS) Open(name string) (fs.File, error) { return nil, nil } // IsNoOpFS reports whether the given "fileSystem" is a no operation fs. func IsNoOpFS(fileSystem fs.FS) bool { _, ok := fileSystem.(noOpFS) return ok } type httpFS struct { fs http.FileSystem } func (f *httpFS) Open(name string) (fs.File, error) { if name == "." { name = "/" } return f.fs.Open(filepath.ToSlash(name)) } func (f *httpFS) ReadDir(name string) ([]fs.DirEntry, error) { name = filepath.ToSlash(name) if name == "." { name = "/" } file, err := f.fs.Open(name) if err != nil { return nil, err } defer file.Close() infos, err := file.Readdir(-1) if err != nil { return nil, err } entries := make([]fs.DirEntry, 0, len(infos)) for _, info := range infos { if info.IsDir() { // http file's does not return the whole tree, so read it. sub, err := f.ReadDir(info.Name()) if err != nil { return nil, err } entries = append(entries, sub...) continue } entry := fs.FileInfoToDirEntry(info) entries = append(entries, entry) } return entries, nil } // ResolveHTTPFS accepts a single input argument of any type // and tries to cast it to http.FileSystem. // // It affects the Application's API Builder's `HandleDir` method. // // This package-level variable can be modified on initialization. var ResolveHTTPFS = func(fsOrDir any) http.FileSystem { var fileSystem http.FileSystem switch v := fsOrDir.(type) { case string: fileSystem = http.Dir(v) case http.FileSystem: fileSystem = v case embed.FS: direEtries, err := v.ReadDir(".") if err != nil { panic(err) } if len(direEtries) == 0 { panic("HandleDir: no directories found under the embedded file system") } subfs, err := fs.Sub(v, direEtries[0].Name()) if err != nil { panic(err) } fileSystem = http.FS(subfs) case fs.FS: fileSystem = http.FS(v) default: panic(fmt.Sprintf(`unexpected "fsOrDir" argument type of %T (string or http.FileSystem or embed.FS or fs.FS)`, v)) } return fileSystem } // FindNames accepts a "http.FileSystem" and a root name and returns // the list containing its file names. func FindNames(fileSystem http.FileSystem, name string) ([]string, error) { if strings.Contains(name, "..") { return nil, fmt.Errorf("invalid root name") } f, err := fileSystem.Open(name) // it's the root dir. if err != nil { return nil, err } defer f.Close() fi, err := f.Stat() if err != nil { return nil, err } if !fi.IsDir() { return []string{name}, nil } fileinfos, err := f.Readdir(-1) if err != nil { return nil, err } files := make([]string, 0) for _, info := range fileinfos { // Note: // go-bindata has absolute names with os.Separator, // http.Dir the basename. baseFilename := toBaseName(info.Name()) fullname := path.Join(name, baseFilename) if fullname == name { // prevent looping through itself. continue } rfiles, err := FindNames(fileSystem, fullname) if err != nil { return nil, err } files = append(files, rfiles...) } return files, nil } // Instead of path.Base(filepath.ToSlash(s)) // let's do something like that, it is faster // (used to list directories on serve-time too): func toBaseName(s string) string { n := len(s) - 1 for i := n; i >= 0; i-- { if c := s[i]; c == '/' || c == '\\' { if i == n { // "s" ends with a slash, remove it and retry. return toBaseName(s[:n]) } return s[i+1:] // return the rest, trimming the slash. } } return s } ================================================ FILE: context/handler.go ================================================ package context import ( "os" "path/filepath" "reflect" "regexp" "runtime" "strings" "sync" ) var ( // PackageName is the Iris Go module package name. PackageName = strings.TrimSuffix(reflect.TypeOf(Context{}).PkgPath(), "/context") // WorkingDir is the (initial) current directory. WorkingDir, _ = os.Getwd() ) var ( handlerNames = make(map[*NameExpr]string) handlerNamesMu sync.RWMutex ignoreMainHandlerNames = [...]string{ "iris.cache", "iris.basicauth", "iris.hCaptcha", "iris.reCAPTCHA", "iris.profiling", "iris.recover", "iris.accesslog", "iris.grpc", "iris.requestid", "iris.rewrite", "iris.cors", "iris.jwt", "iris.logger", "iris.rate", "iris.methodoverride", "iris.errors.recover", } ) // SetHandlerName sets a handler name that could be // fetched through `HandlerName`. The "original" should be // the Go's original regexp-featured (can be retrieved through a `HandlerName` call) function name. // The "replacement" should be the custom, human-text of that function name. // // If the name starts with "iris" then it replaces that string with the // full Iris module package name, // e.g. iris/middleware/logger.(*requestLoggerMiddleware).ServeHTTP-fm to // github.com/kataras/iris/v12/middleware/logger.(*requestLoggerMiddleware).ServeHTTP-fm // for convenient between Iris versions. func SetHandlerName(original string, replacement string) { if strings.HasPrefix(original, "iris") { original = PackageName + strings.TrimPrefix(original, "iris") } handlerNamesMu.Lock() // If regexp syntax is wrong // then its `MatchString` will compare through literal. Fixes an issue // when a handler name is declared as it's and cause regex parsing expression error, // e.g. `iris/cache/client.(*Handler).ServeHTTP-fm` regex, _ := regexp.Compile(original) handlerNames[&NameExpr{ literal: original, normalizedLiteral: normalizeExpression(original), regex: regex, }] = replacement handlerNamesMu.Unlock() } // NameExpr regex or literal comparison through `MatchString`. type NameExpr struct { regex *regexp.Regexp literal string normalizedLiteral string } // MatchString reports whether "s" is literal of "literal" // or it matches the regex expression at "regex". func (expr *NameExpr) MatchString(s string) bool { if expr.literal == s { // if matches as string, as it's. return true } if expr.regex != nil { return expr.regex.MatchString(s) } return false } // MatchFilename reports whether "filename" contains the "literal". func (expr *NameExpr) MatchFilename(filename string) bool { if filename == "" { return false } return strings.Contains(filename, expr.normalizedLiteral) } // The regular expression to match the versioning and the domain part var trimFileModuleNameRegex = regexp.MustCompile(`^[\w.]+/(kataras|iris-contrib)/|/v\d+|\.\*`) func normalizeExpression(str string) string { // Replace all occurrences of the regular expression with the replacement string. return strings.ToLower(trimFileModuleNameRegex.ReplaceAllString(str, "")) } // A Handler responds to an HTTP request. // It writes reply headers and data to the Context.ResponseWriter() and then return. // Returning signals that the request is finished; // it is not valid to use the Context after or concurrently with the completion of the Handler call. // // Depending on the HTTP client software, HTTP protocol version, // and any intermediaries between the client and the iris server, // it may not be possible to read from the Context.Request().Body after writing to the Context.ResponseWriter(). // Cautious handlers should read the Context.Request().Body first, and then reply. // // Except for reading the body, handlers should not modify the provided Context. // // If Handler panics, the server (the caller of Handler) assumes that the effect of the panic was isolated to the active request. // It recovers the panic, logs a stack trace to the server error log, and hangs up the connection. type Handler = func(*Context) // Handlers is just a type of slice of []Handler. // // See `Handler` for more. type Handlers = []Handler func valueOf(v any) reflect.Value { if val, ok := v.(reflect.Value); ok { return val } return reflect.ValueOf(v) } // HandlerName returns the handler's function name. // See `Context.HandlerName` method to get function name of the current running handler in the chain. // See `SetHandlerName` too. func HandlerName(h any) string { pc := valueOf(h).Pointer() fn := runtime.FuncForPC(pc) name := fn.Name() filename, _ := fn.FileLine(fn.Entry()) filenameLower := strings.ToLower(filename) handlerNamesMu.RLock() for expr, newName := range handlerNames { if expr.MatchString(name) || expr.MatchFilename(filenameLower) { name = newName break } } handlerNamesMu.RUnlock() return trimHandlerName(name) } // HandlersNames returns a slice of "handlers" names // separated by commas. Can be used for debugging // or to determinate if end-developer // called the same exactly Use/UseRouter/Done... API methods // so framework can give a warning. func HandlersNames(handlers ...any) string { if len(handlers) == 1 { if hs, ok := handlers[0].(Handlers); ok { asInterfaces := make([]any, 0, len(hs)) for _, h := range hs { asInterfaces = append(asInterfaces, h) } return HandlersNames(asInterfaces...) } } names := make([]string, 0, len(handlers)) for _, h := range handlers { names = append(names, HandlerName(h)) } return strings.Join(names, ",") } // HandlerFileLine returns the handler's file and line information. // See `context.HandlerFileLine` to get the file, line of the current running handler in the chain. func HandlerFileLine(h any) (file string, line int) { pc := valueOf(h).Pointer() return runtime.FuncForPC(pc).FileLine(pc) } // HandlerFileLineRel same as `HandlerFileLine` but it returns the path // corresponding to its relative based on the package-level "WorkingDir" variable. func HandlerFileLineRel(h any) (file string, line int) { file, line = HandlerFileLine(h) if relFile, err := filepath.Rel(WorkingDir, file); err == nil { if !strings.HasPrefix(relFile, "..") { // Only if it's relative to this path, not parent. file = "./" + relFile } } return } // MainHandlerName tries to find the main handler that end-developer // registered on the provided chain of handlers and returns its function name. func MainHandlerName(handlers ...any) (name string, index int) { if len(handlers) == 0 { return } if hs, ok := handlers[0].(Handlers); ok { tmp := make([]any, 0, len(hs)) for _, h := range hs { tmp = append(tmp, h) } return MainHandlerName(tmp...) } for i := 0; i < len(handlers); i++ { name = HandlerName(handlers[i]) if name == "" { continue } index = i if !ingoreMainHandlerName(name) { break } } return } func trimHandlerName(name string) string { // trim the path for Iris' internal middlewares, e.g. // irs/mvc.GRPC.Apply.func1 if internalName := PackageName; strings.HasPrefix(name, internalName) { name = strings.Replace(name, internalName, "iris", 1) } if internalName := "github.com/iris-contrib"; strings.HasPrefix(name, internalName) { name = strings.Replace(name, internalName, "iris-contrib", 1) } name = strings.TrimSuffix(name, "GRPC.Apply.func1") return name } var ignoreHandlerNames = [...]string{ "iris/macro/handler.MakeHandler", "iris/hero.makeHandler.func2", "iris/core/router.ExecutionOptions.buildHandler", "iris/core/router.(*APIBuilder).Favicon", "iris/core/router.StripPrefix", "iris/core/router.PrefixDir", "iris/core/router.PrefixFS", "iris/context.glob..func2.1", } // IgnoreHandlerName compares a static slice of Iris builtin // internal methods that should be ignored from trace. // Some internal methods are kept out of this list for actual debugging. func IgnoreHandlerName(name string) bool { for _, ignore := range ignoreHandlerNames { if name == ignore { return true } } return false } // ingoreMainHandlerName reports whether a main handler of "name" should // be ignored and continue to match the next. // The ignored main handler names are literals and respects the `ignoreNameHandlers` too. func ingoreMainHandlerName(name string) bool { if IgnoreHandlerName(name) { // If ignored at all, it can't be the main. return true } for _, ignore := range ignoreMainHandlerNames { if name == ignore { return true } } return false } // Filter is just a type of func(Context) bool which reports whether an action must be performed // based on the incoming request. // // See `NewConditionalHandler` for more. type Filter func(*Context) bool // NewConditionalHandler returns a single Handler which can be registered // as a middleware. // Filter is just a type of Handler which returns a boolean. // Handlers here should act like middleware, they should contain `ctx.Next` to proceed // to the next handler of the chain. Those "handlers" are registered to the per-request context. // // It checks the "filter" and if passed then // it, correctly, executes the "handlers". // // If passed, this function makes sure that the Context's information // about its per-request handler chain based on the new "handlers" is always updated. // // If not passed, then simply the Next handler(if any) is executed and "handlers" are ignored. // // Example can be found at: _examples/routing/conditional-chain. func NewConditionalHandler(filter Filter, handlers ...Handler) Handler { return func(ctx *Context) { if filter(ctx) { // Note that we don't want just to fire the incoming handlers, we must make sure // that it won't break any further handler chain // information that may be required for the next handlers. // // The below code makes sure that this conditional handler does not break // the ability that iris provides to its end-devs // to check and modify the per-request handlers chain at runtime. currIdx := ctx.HandlerIndex(-1) currHandlers := ctx.Handlers() if currIdx == len(currHandlers)-1 { // if this is the last handler of the chain // just add to the last the new handlers and call Next to fire those. ctx.AddHandler(handlers...) ctx.Next() return } // otherwise insert the new handlers in the middle of the current executed chain and the next chain. newHandlers := append(currHandlers[:currIdx+1], append(handlers, currHandlers[currIdx+1:]...)...) ctx.SetHandlers(newHandlers) ctx.Next() return } // if not pass, then just execute the next. ctx.Next() } } // JoinHandlers returns a copy of "h1" and "h2" Handlers slice joined as one slice of Handlers. func JoinHandlers(h1 Handlers, h2 Handlers) Handlers { if len(h1) == 0 { return h2 } if len(h2) == 0 { return h1 } nowLen := len(h1) totalLen := nowLen + len(h2) // create a new slice of Handlers in order to merge the "h1" and "h2" newHandlers := make(Handlers, totalLen) // copy the already Handlers to the just created copy(newHandlers, h1) // start from there we finish, and store the new Handlers too copy(newHandlers[nowLen:], h2) return newHandlers } // UpsertHandlers like `JoinHandlers` but it does // NOT copies the handlers entries and it does remove duplicates. func UpsertHandlers(h1 Handlers, h2 Handlers) Handlers { reg: for _, handler := range h2 { name := HandlerName(handler) for i, registeredHandler := range h1 { registeredName := HandlerName(registeredHandler) if name == registeredName { h1[i] = handler // replace this handler with the new one. continue reg // break and continue to the next handler. } } h1 = append(h1, handler) // or just insert it. } return h1 } // CopyHandlers returns a copy of "handlers" Handlers slice. func CopyHandlers(handlers Handlers) Handlers { handlersCp := make(Handlers, 0, len(handlers)) for _, handler := range handlers { if handler == nil { continue } handlersCp = append(handlersCp, handler) } return handlersCp } // HandlerExists reports whether a handler exists in the "handlers" slice. func HandlerExists(handlers Handlers, handlerNameOrFunc any) bool { if handlerNameOrFunc == nil { return false } var matchHandler func(any) bool switch v := handlerNameOrFunc.(type) { case string: matchHandler = func(handler any) bool { return HandlerName(handler) == v } case Handler: handlerName := HandlerName(v) matchHandler = func(handler any) bool { return HandlerName(handler) == handlerName } default: matchHandler = func(handler any) bool { return reflect.TypeOf(handler) == reflect.TypeOf(v) } } for _, handler := range handlers { if matchHandler(handler) { return true } } return false } ================================================ FILE: context/i18n.go ================================================ package context import "golang.org/x/text/language" // I18nReadOnly is the interface which contains the read-only i18n features. // Read the "i18n" package fo details. type I18nReadOnly interface { Tags() []language.Tag GetLocale(ctx *Context) Locale Tr(lang string, key string, args ...any) string TrContext(ctx *Context, key string, args ...any) string } // Locale is the interface which returns from a `Localizer.GetLocale` method. // It serves the translations based on "key" or format. See `GetMessage`. type Locale interface { // Index returns the current locale index from the languages list. Index() int // Tag returns the full language Tag attached to this Locale, // it should be unique across different Locales. Tag() *language.Tag // Language should return the exact languagecode of this `Locale` //that the user provided on `New` function. // // Same as `Tag().String()` but it's static. Language() string // GetMessage should return translated text based on the given "key". GetMessage(key string, args ...any) string } ================================================ FILE: context/pool.go ================================================ package context import ( "net/http" "sync" ) // Pool is the context pool, it's used inside router and the framework by itself. type Pool struct { pool *sync.Pool } // New creates and returns a new context pool. func New(newFunc func() any) *Pool { return &Pool{pool: &sync.Pool{New: newFunc}} } // Acquire returns a Context from pool. // See Release. func (c *Pool) Acquire(w http.ResponseWriter, r *http.Request) *Context { ctx := c.pool.Get().(*Context) ctx.BeginRequest(w, r) return ctx } // Release puts a Context back to its pull, this function releases its resources. // See Acquire. func (c *Pool) Release(ctx *Context) { if !ctx.manualRelease { ctx.EndRequest() c.pool.Put(ctx) } } // ReleaseLight will just release the object back to the pool, but the // clean method is caller's responsibility now, currently this is only used // on `SPABuilder` and `websocket.Handler`. // // ReleaseLight does a force-put, it does NOT respect the context.DisablePoolRelease. func (c *Pool) ReleaseLight(ctx *Context) { c.pool.Put(ctx) } ================================================ FILE: context/problem.go ================================================ package context import ( "encoding/xml" "fmt" "math" "net/http" "strconv" "strings" "time" ) // Problem Details for HTTP APIs. // Pass a Problem value to `context.Problem` to // write an "application/problem+json" response. // // Read more at: https://github.com/kataras/iris/blob/main/_examples/routing/http-errors. type Problem map[string]any // NewProblem retruns a new Problem. // Head over to the `Problem` type godoc for more. func NewProblem() Problem { p := make(Problem) return p } func (p Problem) keyExists(key string) bool { if p == nil { return false } _, found := p[key] return found } // DefaultProblemStatusCode is being sent to the client // when Problem's status is not a valid one. var DefaultProblemStatusCode = http.StatusBadRequest func (p Problem) getStatus() (int, bool) { statusField, found := p["status"] if !found { return DefaultProblemStatusCode, false } status, ok := statusField.(int) if !ok { return DefaultProblemStatusCode, false } if !StatusCodeNotSuccessful(status) { return DefaultProblemStatusCode, false } return status, true } func isEmptyTypeURI(uri string) bool { return uri == "" || uri == "about:blank" } func (p Problem) getURI(key string) string { f, found := p[key] if found { if typ, ok := f.(string); ok { if !isEmptyTypeURI(typ) { return typ } } } return "" } // Updates "type" field to absolute URI, recursively. func (p Problem) updateURIsToAbs(ctx *Context) { if p == nil { return } if uriRef := p.getURI("type"); uriRef != "" && !strings.HasPrefix(uriRef, "http") { p.Type(ctx.AbsoluteURI(uriRef)) } if uriRef := p.getURI("instance"); uriRef != "" { p.Instance(ctx.AbsoluteURI(uriRef)) } if cause, ok := p["cause"]; ok { if causeP, ok := cause.(Problem); ok { causeP.updateURIsToAbs(ctx) } } } const ( problemTempKeyPrefix = "@temp_" ) // TempKey sets a temporary key-value pair, which is being removed // on the its first get. func (p Problem) TempKey(key string, value any) Problem { return p.Key(problemTempKeyPrefix+key, value) } // GetTempKey returns the temp value based on "key" and removes it. func (p Problem) GetTempKey(key string) any { key = problemTempKeyPrefix + key v, ok := p[key] if ok { delete(p, key) return v } return nil } // Key sets a custom key-value pair. func (p Problem) Key(key string, value any) Problem { p[key] = value return p } // Type URI SHOULD resolve to HTML [W3C.REC-html5-20141028] // documentation that explains how to resolve the problem. // Example: "https://example.net/validation-error" // // Empty URI or "about:blank", when used as a problem type, // indicates that the problem has no additional semantics beyond that of the HTTP status code. // When "about:blank" is used and "title" was not set-ed, // the title is being automatically set the same as the recommended HTTP status phrase for that code // (e.g., "Not Found" for 404, and so on) on `Status` call. // // Relative paths are also valid when writing this Problem to an Iris Context. func (p Problem) Type(uri string) Problem { return p.Key("type", uri) } // Title sets the problem's title field. // Example: "Your request parameters didn't validate." // It is set to status Code text if missing, // (e.g., "Not Found" for 404, and so on). func (p Problem) Title(title string) Problem { return p.Key("title", title) } // Status sets HTTP error code for problem's status field. // Example: 404 // // It is required. func (p Problem) Status(statusCode int) Problem { shouldOverrideTitle := !p.keyExists("title") // if !shouldOverrideTitle { // typ, found := p["type"] // shouldOverrideTitle = !found || isEmptyTypeURI(typ.(string)) // } if shouldOverrideTitle { // Set title by code. p.Title(http.StatusText(statusCode)) } return p.Key("status", statusCode) } // Detail sets the problem's detail field. // Example: "Optional details about the error...". func (p Problem) Detail(detail string) Problem { return p.Key("detail", detail) } // DetailErr calls `Detail(err.Error())`. func (p Problem) DetailErr(err error) Problem { if err == nil { return p } return p.Key("detail", err.Error()) } // Instance sets the problem's instance field. // A URI reference that identifies the specific // occurrence of the problem. It may or may not yield further // information if dereferenced. func (p Problem) Instance(instanceURI string) Problem { return p.Key("instance", instanceURI) } // Cause sets the problem's cause field. // Any chain of problems. func (p Problem) Cause(cause Problem) Problem { if !cause.Validate() { return p } return p.Key("cause", cause) } // Validate reports whether this Problem value is a valid problem one. func (p Problem) Validate() bool { // A nil problem is not a valid one. if p == nil { return false } return p.keyExists("type") && p.keyExists("title") && p.keyExists("status") } // Error method completes the go error. // Returns the "[Status] Title" string form of this Problem. // If Problem is not a valid one, it returns "invalid problem". func (p Problem) Error() string { if !p.Validate() { return "invalid problem" } return fmt.Sprintf("[%d] %s", p["status"], p["title"]) } // MarshalXML makes this Problem XML-compatible content to render. func (p Problem) MarshalXML(e *xml.Encoder, start xml.StartElement) error { if len(p) == 0 { return nil } err := e.EncodeToken(start) if err != nil { return err } // toTitle := cases.Title(language.English) // toTitle.String(k) for k, v := range p { // convert keys like "type" to "Type", "productName" to "ProductName" and e.t.c. when xml. err = e.Encode(xmlMapEntry{XMLName: xml.Name{Local: strings.Title(k)}, Value: v}) if err != nil { return err } } return e.EncodeToken(start.End()) } // DefaultProblemOptions the default options for `Context.Problem` method. var DefaultProblemOptions = ProblemOptions{ JSON: JSON{Indent: " "}, XML: XML{Indent: " "}, } // ProblemOptions the optional settings when server replies with a Problem. // See `Context.Problem` method and `Problem` type for more details. type ProblemOptions struct { // JSON are the optional JSON renderer options. JSON JSON // RenderXML set to true if want to render as XML doc. // See `XML` option field too. RenderXML bool // XML are the optional XML renderer options. // Affect only when `RenderXML` field is set to true. XML XML // RetryAfter sets the Retry-After response header. // https://tools.ietf.org/html/rfc7231#section-7.1.3 // The value can be one of those: // time.Time // time.Duration for seconds // int64, int, float64 for seconds // string for duration string or for datetime string. // // Examples: // time.Now().Add(5 * time.Minute), // 300 * time.Second, // "5m", // 300 RetryAfter any // A function that, if specified, can dynamically set // retry-after based on the request. Useful for ProblemOptions reusability. // Should return time.Time, time.Duration, int64, int, float64 or string. // // Overrides the RetryAfter field. RetryAfterFunc func(*Context) any } func parseDurationToSeconds(dur time.Duration) int64 { return int64(math.Round(dur.Seconds())) } func (o *ProblemOptions) parseRetryAfter(value any, timeLayout string) string { // https://tools.ietf.org/html/rfc7231#section-7.1.3 // Retry-After = HTTP-date / delay-seconds switch v := value.(type) { case int64: return strconv.FormatInt(v, 10) case int: return o.parseRetryAfter(int64(v), timeLayout) case float64: return o.parseRetryAfter(int64(math.Round(v)), timeLayout) case time.Time: return v.Format(timeLayout) case time.Duration: return o.parseRetryAfter(parseDurationToSeconds(v), timeLayout) case string: dur, err := time.ParseDuration(v) if err != nil { t, err := time.Parse(timeLayout, v) if err != nil { return "" } return o.parseRetryAfter(t, timeLayout) } return o.parseRetryAfter(parseDurationToSeconds(dur), timeLayout) } return "" } // Apply accepts a Context and applies specific response-time options. func (o *ProblemOptions) Apply(ctx *Context) { retryAfterHeaderValue := "" timeLayout := ctx.Application().ConfigurationReadOnly().GetTimeFormat() if o.RetryAfterFunc != nil { retryAfterHeaderValue = o.parseRetryAfter(o.RetryAfterFunc(ctx), timeLayout) } else if o.RetryAfter != nil { retryAfterHeaderValue = o.parseRetryAfter(o.RetryAfter, timeLayout) } if retryAfterHeaderValue != "" { ctx.Header("Retry-After", retryAfterHeaderValue) } } ================================================ FILE: context/request_params.go ================================================ package context import ( "fmt" "reflect" "strconv" "strings" "time" "github.com/kataras/iris/v12/core/memstore" ) // RequestParams is a key string - value string storage which // context's request dynamic path params are being kept. // Empty if the route is static. type RequestParams struct { memstore.Store } // Set inserts a parameter value. // See `Get` too. func (r *RequestParams) Set(key, value string) { if ln := len(r.Store); cap(r.Store) > ln { r.Store = r.Store[:ln+1] p := &r.Store[ln] p.Key = key p.ValueRaw = value return } r.Store = append(r.Store, memstore.Entry{ Key: key, ValueRaw: value, }) } // Get returns a path parameter's value based on its route's dynamic path key. func (r *RequestParams) Get(key string) string { for i := range r.Store { if kv := r.Store[i]; kv.Key == key { if v, ok := kv.ValueRaw.(string); ok { return v // it should always be string here on :string parameter. } if v, ok := kv.ValueRaw.(fmt.Stringer); ok { return v.String() } return fmt.Sprintf("%v", kv.ValueRaw) } } return "" } // GetEntryAt will return the parameter's internal store's `Entry` based on the index. // If not found it will return an emptry `Entry`. func (r *RequestParams) GetEntryAt(index int) memstore.Entry { entry, _ := r.Store.GetEntryAt(index) return entry } // GetEntry will return the parameter's internal store's `Entry` based on its name/key. // If not found it will return an emptry `Entry`. func (r *RequestParams) GetEntry(key string) memstore.Entry { entry, _ := r.Store.GetEntry(key) return entry } // Visit accepts a visitor which will be filled // by the key-value params. func (r *RequestParams) Visit(visitor func(key string, value string)) { r.Store.Visit(func(k string, v any) { visitor(k, fmt.Sprintf("%v", v)) // always string here. }) } // GetTrim returns a path parameter's value without trailing spaces based on its route's dynamic path key. func (r *RequestParams) GetTrim(key string) string { return strings.TrimSpace(r.Get(key)) } // GetEscape returns a path parameter's double-url-query-escaped value based on its route's dynamic path key. func (r *RequestParams) GetEscape(key string) string { return DecodeQuery(DecodeQuery(r.Get(key))) } // GetDecoded returns a path parameter's double-url-query-escaped value based on its route's dynamic path key. // same as `GetEscape`. func (r *RequestParams) GetDecoded(key string) string { return r.GetEscape(key) } // TrimParamFilePart is a middleware which replaces all route dynamic path parameters // with values that do not contain any part after the last dot (.) character. // // Example Code: // // package main // // import ( // "github.com/kataras/iris/v12" // ) // // func main() { // app := iris.New() // app.Get("/{uid:string regexp(^[0-9]{1,20}.html$)}", iris.TrimParamFilePart, handler) // // TrimParamFilePart can be registered as a middleware to a Party (group of routes) as well. // app.Listen(":8080") // } // // func handler(ctx iris.Context) { // // // // The above line is useless now that we've registered the TrimParamFilePart middleware: // // uid := ctx.Params().GetTrimFileUint64("uid") // // // // uid := ctx.Params().GetUint64Default("uid", 0) // ctx.Writef("Param value: %d\n", uid) // } func TrimParamFilePart(ctx *Context) { // See #2024. params := ctx.Params() for i, param := range params.Store { if value, ok := param.ValueRaw.(string); ok { if idx := strings.LastIndexByte(value, '.'); idx > 1 /* at least .h */ { value = value[0:idx] param.ValueRaw = value } } params.Store[i] = param } ctx.Next() } // GetTrimFile returns a parameter value but without the last ".ANYTHING_HERE" part. func (r *RequestParams) GetTrimFile(key string) string { value := r.Get(key) if idx := strings.LastIndexByte(value, '.'); idx > 1 /* at least .h */ { return value[0:idx] } return value } // GetTrimFileInt same as GetTrimFile but it returns the value as int. func (r *RequestParams) GetTrimFileInt(key string) int { value := r.Get(key) if idx := strings.LastIndexByte(value, '.'); idx > 1 /* at least .h */ { value = value[0:idx] } v, _ := strconv.Atoi(value) return v } // GetTrimFileUint64 same as GetTrimFile but it returns the value as uint64. func (r *RequestParams) GetTrimFileUint64(key string) uint64 { value := r.Get(key) if idx := strings.LastIndexByte(value, '.'); idx > 1 /* at least .h */ { value = value[0:idx] } v, err := strconv.ParseUint(value, 10, strconv.IntSize) if err != nil { return 0 } return v } // GetTrimFileUint64 same as GetTrimFile but it returns the value as uint. func (r *RequestParams) GetTrimFileUint(key string) uint { return uint(r.GetTrimFileUint64(key)) } func (r *RequestParams) getRightTrimmed(key string, cutset string) string { return strings.TrimRight(strings.ToLower(r.Get(key)), cutset) } // GetTrimHTML returns a parameter value but without the last ".html" part. func (r *RequestParams) GetTrimHTML(key string) string { return r.getRightTrimmed(key, ".html") } // GetTrimJSON returns a parameter value but without the last ".json" part. func (r *RequestParams) GetTrimJSON(key string) string { return r.getRightTrimmed(key, ".json") } // GetTrimXML returns a parameter value but without the last ".xml" part. func (r *RequestParams) GetTrimXML(key string) string { return r.getRightTrimmed(key, ".xml") } // GetIntUnslashed same as Get but it removes the first slash if found. // Usage: Get an id from a wildcard path. // // Returns -1 and false if not path parameter with that "key" found. func (r *RequestParams) GetIntUnslashed(key string) (int, bool) { v := r.Get(key) if v != "" { if len(v) > 1 { if v[0] == '/' { v = v[1:] } } vInt, err := strconv.Atoi(v) if err != nil { return -1, false } return vInt, true } return -1, false } // ParamResolvers is the global param resolution for a parameter type for a specific go std or custom type. // // Key is the specific type, which should be unique. // The value is a function which accepts the parameter index // and it should return the value as the parameter type evaluator expects it. // // i.e [reflect.TypeOf("string")] = func(paramIndex int) any { // return func(ctx *Context) { // return ctx.Params().GetEntryAt(paramIndex).ValueRaw.() // } // } // // Read https://github.com/kataras/iris/tree/main/_examples/routing/macros for more details. // Checks for total available request parameters length // and parameter index based on the hero/mvc function added // in order to support the MVC.HandleMany("GET", "/path/{ps}/{pssecond} /path/{ps}") // when on the second requested path, the 'pssecond' should be empty. var ParamResolvers = map[reflect.Type]func(paramIndex int) any{ reflect.TypeOf(""): func(paramIndex int) any { return func(ctx *Context) string { if ctx.Params().Len() <= paramIndex { return "" } return ctx.Params().GetEntryAt(paramIndex).ValueRaw.(string) } }, reflect.TypeOf(int(1)): func(paramIndex int) any { return func(ctx *Context) int { if ctx.Params().Len() <= paramIndex { return 0 } // v, _ := ctx.Params().GetEntryAt(paramIndex).IntDefault(0) // return v return ctx.Params().GetEntryAt(paramIndex).ValueRaw.(int) } }, reflect.TypeOf(int8(1)): func(paramIndex int) any { return func(ctx *Context) int8 { if ctx.Params().Len() <= paramIndex { return 0 } return ctx.Params().GetEntryAt(paramIndex).ValueRaw.(int8) } }, reflect.TypeOf(int16(1)): func(paramIndex int) any { return func(ctx *Context) int16 { if ctx.Params().Len() <= paramIndex { return 0 } return ctx.Params().GetEntryAt(paramIndex).ValueRaw.(int16) } }, reflect.TypeOf(int32(1)): func(paramIndex int) any { return func(ctx *Context) int32 { if ctx.Params().Len() <= paramIndex { return 0 } return ctx.Params().GetEntryAt(paramIndex).ValueRaw.(int32) } }, reflect.TypeOf(int64(1)): func(paramIndex int) any { return func(ctx *Context) int64 { if ctx.Params().Len() <= paramIndex { return 0 } return ctx.Params().GetEntryAt(paramIndex).ValueRaw.(int64) } }, reflect.TypeOf(uint(1)): func(paramIndex int) any { return func(ctx *Context) uint { if ctx.Params().Len() <= paramIndex { return 0 } return ctx.Params().GetEntryAt(paramIndex).ValueRaw.(uint) } }, reflect.TypeOf(uint8(1)): func(paramIndex int) any { return func(ctx *Context) uint8 { if ctx.Params().Len() <= paramIndex { return 0 } return ctx.Params().GetEntryAt(paramIndex).ValueRaw.(uint8) } }, reflect.TypeOf(uint16(1)): func(paramIndex int) any { return func(ctx *Context) uint16 { if ctx.Params().Len() <= paramIndex { return 0 } return ctx.Params().GetEntryAt(paramIndex).ValueRaw.(uint16) } }, reflect.TypeOf(uint32(1)): func(paramIndex int) any { return func(ctx *Context) uint32 { if ctx.Params().Len() <= paramIndex { return 0 } return ctx.Params().GetEntryAt(paramIndex).ValueRaw.(uint32) } }, reflect.TypeOf(uint64(1)): func(paramIndex int) any { return func(ctx *Context) uint64 { if ctx.Params().Len() <= paramIndex { return 0 } return ctx.Params().GetEntryAt(paramIndex).ValueRaw.(uint64) } }, reflect.TypeOf(true): func(paramIndex int) any { return func(ctx *Context) bool { if ctx.Params().Len() <= paramIndex { return false } return ctx.Params().GetEntryAt(paramIndex).ValueRaw.(bool) } }, reflect.TypeOf(time.Time{}): func(paramIndex int) any { return func(ctx *Context) time.Time { if ctx.Params().Len() <= paramIndex { return unixEpochTime } v, ok := ctx.Params().GetEntryAt(paramIndex).ValueRaw.(time.Time) if !ok { return unixEpochTime } return v } }, reflect.TypeOf(time.Weekday(0)): func(paramIndex int) any { return func(ctx *Context) time.Weekday { if ctx.Params().Len() <= paramIndex { return time.Sunday } v, ok := ctx.Params().GetEntryAt(paramIndex).ValueRaw.(time.Weekday) if !ok { return time.Sunday } return v } }, } // ParamResolverByTypeAndIndex will return a function that can be used to bind path parameter's exact value by its Go std type // and the parameter's index based on the registered path. // Usage: nameResolver := ParamResolverByKindAndKey(reflect.TypeOf(""), 0) // Inside a Handler: nameResolver.Call(ctx)[0] // // it will return the reflect.Value Of the exact type of the parameter(based on the path parameters and macros). // // It is only useful for dynamic binding of the parameter, it is used on "hero" package and it should be modified // only when Macros are modified in such way that the default selections for the available go std types are not enough. // // Returns empty value and false if "k" does not match any valid parameter resolver. func ParamResolverByTypeAndIndex(typ reflect.Type, paramIndex int) (reflect.Value, bool) { /* NO: // This could work but its result is not exact type, so direct binding is not possible. resolver := m.ParamResolver fn := func(ctx *context.Context) any { entry, _ := ctx.Params().GetEntry(paramName) return resolver(entry) } // // This works but it is slower on serve-time. paramNameValue := []reflect.Value{reflect.ValueOf(paramName)} var fnSignature func(*context.Context) string return reflect.MakeFunc(reflect.ValueOf(&fnSignature).Elem().Type(), func(in []reflect.Value) []reflect.Value { return in[0].MethodByName("Params").Call(emptyIn)[0].MethodByName("Get").Call(paramNameValue) // return []reflect.Value{reflect.ValueOf(in[0].Interface().(*context.Context).Params().Get(paramName))} }) // */ r, ok := ParamResolvers[typ] if !ok || r == nil { return reflect.Value{}, false } return reflect.ValueOf(r(paramIndex)), true } ================================================ FILE: context/response_recorder.go ================================================ package context import ( "bytes" "errors" "fmt" "io" "net/http" "net/textproto" "strconv" "sync" ) // Recorder the middleware to enable response writer recording ( ResponseWriter -> ResponseRecorder) var Recorder = func(ctx *Context) { ctx.Record() ctx.Next() } var rrpool = sync.Pool{New: func() any { return &ResponseRecorder{} }} // AcquireResponseRecorder returns a new *AcquireResponseRecorder from the pool. // Releasing is done automatically when request and response is done. func AcquireResponseRecorder() *ResponseRecorder { return rrpool.Get().(*ResponseRecorder) } func releaseResponseRecorder(w *ResponseRecorder) { rrpool.Put(w) } // A ResponseRecorder is used mostly for testing // in order to record and modify, if necessary, the body and status code and headers. // // See `context.Recorder“ method too. type ResponseRecorder struct { ResponseWriter // keep track of the body written. chunks []byte // the saved headers headers http.Header result *http.Response } var _ ResponseWriter = (*ResponseRecorder)(nil) // Naive returns the simple, underline and original http.ResponseWriter // that backends this response writer. func (w *ResponseRecorder) Naive() http.ResponseWriter { return w.ResponseWriter.Naive() } // BeginRecord accepts its parent ResponseWriter and // prepares itself, the response recorder, to record and send response to the client. func (w *ResponseRecorder) BeginRecord(underline ResponseWriter) { w.ResponseWriter = underline w.headers = underline.Header().Clone() w.result = nil w.ResetBody() } // EndResponse is auto-called when the whole client's request is done, // releases the response recorder and its underline ResponseWriter. func (w *ResponseRecorder) EndResponse() { w.ResponseWriter.EndResponse() releaseResponseRecorder(w) } // Write Adds the contents to the body reply, it writes the contents temporarily // to a value in order to be flushed at the end of the request, // this method give us the opportunity to reset the body if needed. // // If WriteHeader has not yet been called, Write calls // WriteHeader(http.StatusOK) before writing the data. If the Header // does not contain a Content-Type line, Write adds a Content-Type set // to the result of passing the initial 512 bytes of written data to // DetectContentType. // // Depending on the HTTP protocol version and the client, calling // Write or WriteHeader may prevent future reads on the // Request.Body. For HTTP/1.x requests, handlers should read any // needed request body data before writing the response. Once the // headers have been flushed (due to either an explicit Flusher.Flush // call or writing enough data to trigger a flush), the request body // may be unavailable. For HTTP/2 requests, the Go HTTP server permits // handlers to continue to read the request body while concurrently // writing the response. However, such behavior may not be supported // by all HTTP/2 clients. Handlers should read before writing if // possible to maximize compatibility. func (w *ResponseRecorder) Write(contents []byte) (int, error) { w.chunks = append(w.chunks, contents...) // Remember that we should not return all the written length within `Write`: // see https://github.com/kataras/iris/pull/931 return len(contents), nil } // Header returns the temporary header map that, on flush response, // will be sent by the underline's ResponseWriter's WriteHeader method. func (w *ResponseRecorder) Header() http.Header { return w.headers } // SetBody overrides the body and sets it to a slice of bytes value. func (w *ResponseRecorder) SetBody(b []byte) { w.chunks = b } // SetBodyString overrides the body and sets it to a string value. func (w *ResponseRecorder) SetBodyString(s string) { w.SetBody([]byte(s)) } // Body returns the body tracked from the writer so far, // do not use this for edit. func (w *ResponseRecorder) Body() []byte { return w.chunks } // ResetBody resets the response body. func (w *ResponseRecorder) ResetBody() { w.chunks = w.chunks[0:0] } // ResetHeaders sets the headers to the underline's response writer's headers, may empty. func (w *ResponseRecorder) ResetHeaders() { w.headers = w.ResponseWriter.Header().Clone() } // ClearHeaders clears all headers, both temp and underline's response writer. func (w *ResponseRecorder) ClearHeaders() { w.headers = http.Header{} h := w.ResponseWriter.Header() for k := range h { delete(h, k) } } // Reset clears headers, sets the status code to 200 // and clears the cached body. // // - Use ResetBody() and ResetHeaders() instead to keep compression after reseting. // // - Use Reset() & ResponseRecorder.ResponseWriter.(*context.CompressResponseWriter).Disabled = true // to set a new body without compression when the previous handler was iris.Compression. // // Implements the `ResponseWriterReseter`. func (w *ResponseRecorder) Reset() bool { w.ClearHeaders() w.WriteHeader(defaultStatusCode) w.ResetBody() return true } // FlushResponse the full body, headers and status code to the underline response writer // called automatically at the end of each request. func (w *ResponseRecorder) FlushResponse() { // copy the headers to the underline response writer if w.headers != nil { h := w.ResponseWriter.Header() // note: we don't reset the current underline's headers. for k, v := range w.headers { h[k] = v } } cw, mustWriteToClose := w.ResponseWriter.(*CompressResponseWriter) if mustWriteToClose { // see #1569#issuecomment-664003098 cw.FlushHeaders() } else { // NOTE: before the ResponseWriter.Write in order to: // set the given status code even if the body is empty. w.ResponseWriter.FlushResponse() } if len(w.chunks) > 0 { // ignore error w.ResponseWriter.Write(w.chunks) } if mustWriteToClose { cw.ResponseWriter.FlushResponse() cw.CompressWriter.Close() } } // Clone returns a clone of this response writer // it copies the header, status code, headers and the beforeFlush finally returns a new ResponseRecorder func (w *ResponseRecorder) Clone() ResponseWriter { wc := &ResponseRecorder{} // copy headers. wc.headers = w.headers.Clone() // copy body. chunksCopy := make([]byte, len(w.chunks)) copy(chunksCopy, w.chunks) wc.chunks = chunksCopy if resW, ok := w.ResponseWriter.(*responseWriter); ok { wc.ResponseWriter = &responseWriter{ ResponseWriter: resW.ResponseWriter, statusCode: resW.statusCode, written: resW.written, beforeFlush: resW.beforeFlush, } // clone it } else { // else just copy, may pointer, developer can change its behavior wc.ResponseWriter = w.ResponseWriter } return wc } // CopyTo writes a response writer (temp: status code, headers and body) to another response writer func (w *ResponseRecorder) CopyTo(res ResponseWriter) { if to, ok := res.(*ResponseRecorder); ok { // set the status code, to is first ( probably an error? (context.StatusCodeNotSuccessful, defaults to >=400). if statusCode := w.ResponseWriter.StatusCode(); statusCode == defaultStatusCode { to.WriteHeader(statusCode) } if beforeFlush := w.ResponseWriter.GetBeforeFlush(); beforeFlush != nil { // if to had a before flush, lets combine them if to.GetBeforeFlush() != nil { nextBeforeFlush := beforeFlush prevBeforeFlush := to.GetBeforeFlush() to.SetBeforeFlush(func() { prevBeforeFlush() nextBeforeFlush() }) } else { to.SetBeforeFlush(w.ResponseWriter.GetBeforeFlush()) } } // if "to" is *responseWriter and it never written before (if -1), // set the "w"'s written length. if resW, ok := to.ResponseWriter.(*responseWriter); ok { if resW.Written() != StatusCodeWritten { resW.written = w.ResponseWriter.Written() } } // append the headers for k, values := range w.headers { for _, v := range values { if to.headers.Get(v) == "" { to.headers.Add(k, v) } } } // append the body if len(w.chunks) > 0 { // ignore error to.Write(w.chunks) } } } // Flush sends any buffered data to the client. func (w *ResponseRecorder) Flush() { // This fixes response recorder when chunked + Flush is used. if w.headers.Get("Transfer-Encoding") == "chunked" { if w.Written() == NoWritten { if len(w.headers) > 0 { h := w.ResponseWriter.Header() // note: we don't reset the current underline's headers. for k, v := range w.headers { h[k] = v } } } if len(w.chunks) > 0 { w.ResponseWriter.Write(w.chunks) } } w.ResponseWriter.Flush() w.ResetBody() } // ErrPushNotSupported is returned by the Push method to // indicate that HTTP/2 Push support is not available. var ErrPushNotSupported = errors.New("push feature is not supported by this ResponseWriter") // Push initiates an HTTP/2 server push. This constructs a synthetic // request using the given target and options, serializes that request // into a PUSH_PROMISE frame, then dispatches that request using the // server's request handler. If opts is nil, default options are used. // // The target must either be an absolute path (like "/path") or an absolute // URL that contains a valid host and the same scheme as the parent request. // If the target is a path, it will inherit the scheme and host of the // parent request. // // The HTTP/2 spec disallows recursive pushes and cross-authority pushes. // Push may or may not detect these invalid pushes; however, invalid // pushes will be detected and canceled by conforming clients. // // Handlers that wish to push URL X should call Push before sending any // data that may trigger a request for URL X. This avoids a race where the // client issues requests for X before receiving the PUSH_PROMISE for X. // // Push returns ErrPushNotSupported if the client has disabled push or if push // is not supported on the underlying connection. func (w *ResponseRecorder) Push(target string, opts *http.PushOptions) (err error) { w.FlushResponse() if pusher, ok := w.ResponseWriter.Naive().(http.Pusher); ok { err = pusher.Push(target, opts) if err != nil && err.Error() == http.ErrNotSupported.ErrorString { return ErrPushNotSupported } } // NOTE: we have to reset them even if the push failed. w.ResetBody() w.ResetHeaders() return ErrPushNotSupported } // Result returns the response generated by the handler. // It does set all provided headers. // // Result must only be called after the handler has finished running. func (w *ResponseRecorder) Result() *http.Response { // a modified copy of net/http/httptest if w.result != nil { return w.result } headers := w.headers.Clone() // for k, v := range w.ResponseWriter.Header() { // headers[k] = v // } /* dateFound := false for k := range headers { if strings.ToLower(k) == "date" { dateFound = true break } } if !dateFound { headers["Date"] = []string{time.Now().Format(http.TimeFormat)} } */ res := &http.Response{ Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1, StatusCode: w.StatusCode(), Header: headers, } if res.StatusCode == 0 { res.StatusCode = 200 } res.Status = fmt.Sprintf("%03d %s", res.StatusCode, http.StatusText(res.StatusCode)) if w.chunks != nil { res.Body = io.NopCloser(bytes.NewReader(w.chunks)) } else { res.Body = http.NoBody } res.ContentLength = parseContentLength(res.Header.Get("Content-Length")) w.result = res return res } // copy of net/http/httptest func parseContentLength(cl string) int64 { cl = textproto.TrimString(cl) if cl == "" { return -1 } n, err := strconv.ParseUint(cl, 10, 63) if err != nil { return -1 } return int64(n) } ================================================ FILE: context/response_writer.go ================================================ package context import ( "bufio" "errors" "io" "net" "net/http" "sync" ) // ResponseWriter interface is used by the context to serve an HTTP handler to // construct an HTTP response. // // Note: Only this ResponseWriter is an interface in order to be able // for developers to change the response writer of the Context via `context.ResetResponseWriter`. // The rest of the response writers implementations (ResponseRecorder & CompressResponseWriter) // are coupled to the internal ResponseWriter implementation(*responseWriter). // // A ResponseWriter may not be used after the Handler // has returned. type ResponseWriter interface { http.ResponseWriter // Naive returns the simple, underline and original http.ResponseWriter // that backends this response writer. Naive() http.ResponseWriter // SetWriter sets the underline http.ResponseWriter // that this responseWriter should write on. SetWriter(underline http.ResponseWriter) // BeginResponse receives an http.ResponseWriter // and initialize or reset the response writer's field's values. BeginResponse(http.ResponseWriter) // EndResponse is the last function which is called right before the server sent the final response. // // Here is the place which we can make the last checks or do a cleanup. EndResponse() // IsHijacked reports whether this response writer's connection is hijacked. IsHijacked() bool // StatusCode returns the status code header value. StatusCode() int // Written should returns the total length of bytes that were being written to the client. // In addition iris provides some variables to help low-level actions: // NoWritten, means that nothing were written yet and the response writer is still live. // StatusCodeWritten, means that status code was written but no other bytes are written to the client, response writer may closed. // > 0 means that the reply was written and it's the total number of bytes were written. Written() int // SetWritten sets manually a value for written, it can be // NoWritten(-1) or StatusCodeWritten(0), > 0 means body length which is useless here. SetWritten(int) // SetBeforeFlush registers the unique callback which called exactly before the response is flushed to the client. SetBeforeFlush(cb func()) // GetBeforeFlush returns (not execute) the before flush callback, or nil if not set by SetBeforeFlush. GetBeforeFlush() func() // FlushResponse should be called only once before EndResponse. // it tries to send the status code if not sent already // and calls the before flush callback, if any. // // FlushResponse can be called before EndResponse, but it should // be the last call of this response writer. FlushResponse() // clone returns a clone of this response writer // it copies the header, status code, headers and the beforeFlush finally returns a new ResponseRecorder. Clone() ResponseWriter // CopyTo writes a response writer (temp: status code, headers and body) to another response writer CopyTo(ResponseWriter) // Flusher indicates if `Flush` is supported by the client. // // The default HTTP/1.x and HTTP/2 ResponseWriter implementations // support Flusher, but ResponseWriter wrappers may not. Handlers // should always test for this ability at runtime. // // Note that even for ResponseWriters that support Flush, // if the client is connected through an HTTP proxy, // the buffered data may not reach the client until the response // completes. Flusher() (http.Flusher, bool) // Flush sends any buffered data to the client. Flush() // required by compress writer. } // ResponseWriterBodyReseter can be implemented by // response writers that supports response body overriding // (e.g. recorder and compressed). type ResponseWriterBodyReseter interface { // ResetBody should reset the body and reports back if it could reset successfully. ResetBody() } // ResponseWriterDisabler can be implemented // by response writers that can be disabled and restored to their previous state // (e.g. compressed). type ResponseWriterDisabler interface { // Disable should disable this type of response writer and fallback to the default one. Disable() } // ResponseWriterReseter can be implemented // by response writers that can clear the whole response // so a new handler can write into this from the beginning. // E.g. recorder, compressed (full) and common writer (status code and headers). type ResponseWriterReseter interface { // Reset should reset the whole response and reports // whether it could reset successfully. Reset() bool } // ResponseWriterWriteTo can be implemented // by response writers that needs a special // encoding before writing to their buffers. // E.g. a custom recorder that wraps a custom compressed one. // // Not used by the framework itself. type ResponseWriterWriteTo interface { WriteTo(dest io.Writer, p []byte) } // +------------------------------------------------------------+ // | Response Writer Implementation | // +------------------------------------------------------------+ var rpool = sync.Pool{New: func() any { return &responseWriter{} }} // AcquireResponseWriter returns a new *ResponseWriter from the pool. // Releasing is done automatically when request and response is done. func AcquireResponseWriter() ResponseWriter { return rpool.Get().(*responseWriter) } func releaseResponseWriter(w ResponseWriter) { rpool.Put(w) } // ResponseWriter is the basic response writer, // it writes directly to the underline http.ResponseWriter type responseWriter struct { http.ResponseWriter statusCode int // the saved status code which will be used from the cache service // statusCodeSent bool // reply header has been (logically) written | no needed any more as we have a variable to catch total len of written bytes written int // the total size of bytes were written // yes only one callback, we need simplicity here because on FireStatusCode the beforeFlush events should NOT be cleared // but the response is cleared. // Sometimes is useful to keep the event, // so we keep one func only and let the user decide when he/she wants to override it with an empty func before the FireStatusCode (context's behavior) beforeFlush func() } var _ ResponseWriter = (*responseWriter)(nil) const ( defaultStatusCode = http.StatusOK // NoWritten !=-1 => when nothing written before NoWritten = -1 // StatusCodeWritten != 0 => when only status code written StatusCodeWritten = 0 ) // Naive returns the simple, underline and original http.ResponseWriter // that backends this response writer. func (w *responseWriter) Naive() http.ResponseWriter { return w.ResponseWriter } // BeginResponse receives an http.ResponseWriter // and initialize or reset the response writer's field's values. func (w *responseWriter) BeginResponse(underline http.ResponseWriter) { w.beforeFlush = nil w.written = NoWritten w.statusCode = defaultStatusCode w.SetWriter(underline) } // SetWriter sets the underline http.ResponseWriter // that this responseWriter should write on. func (w *responseWriter) SetWriter(underline http.ResponseWriter) { w.ResponseWriter = underline } // EndResponse is the last function which is called right before the server sent the final response. // // Here is the place which we can make the last checks or do a cleanup. func (w *responseWriter) EndResponse() { releaseResponseWriter(w) } // Reset clears headers, sets the status code to 200 // and clears the cached body. // // Implements the `ResponseWriterReseter`. func (w *responseWriter) Reset() bool { if w.written > 0 { return false // if already written we can't reset this type of response writer. } h := w.Header() for k := range h { h[k] = nil } w.written = NoWritten w.statusCode = defaultStatusCode return true } // SetWritten sets manually a value for written, it can be // NoWritten(-1) or StatusCodeWritten(0), > 0 means body length which is useless here. func (w *responseWriter) SetWritten(n int) { if n >= NoWritten && n <= StatusCodeWritten { w.written = n } } // Written should returns the total length of bytes that were being written to the client. // In addition iris provides some variables to help low-level actions: // NoWritten, means that nothing were written yet and the response writer is still live. // StatusCodeWritten, means that status code were written but no other bytes are written to the client, response writer may closed. // > 0 means that the reply was written and it's the total number of bytes were written. func (w *responseWriter) Written() int { return w.written } // WriteHeader sends an HTTP response header with status code. // If WriteHeader is not called explicitly, the first call to Write // will trigger an implicit WriteHeader(http.StatusOK). // Thus explicit calls to WriteHeader are mainly used to // send error codes. func (w *responseWriter) WriteHeader(statusCode int) { w.statusCode = statusCode } func (w *responseWriter) tryWriteHeader() { if w.written == NoWritten { // before write, once. w.written = StatusCodeWritten w.ResponseWriter.WriteHeader(w.statusCode) } } // IsHijacked reports whether this response writer's connection is hijacked. func (w *responseWriter) IsHijacked() bool { // Note: // A zero-byte `ResponseWriter.Write` on a hijacked connection will // return `http.ErrHijacked` without any other side effects. _, err := w.ResponseWriter.Write(nil) return err == http.ErrHijacked } // Write writes to the client // If WriteHeader has not yet been called, Write calls // WriteHeader(http.StatusOK) before writing the data. If the Header // does not contain a Content-Type line, Write adds a Content-Type set // to the result of passing the initial 512 bytes of written data to // DetectContentType. // // Depending on the HTTP protocol version and the client, calling // Write or WriteHeader may prevent future reads on the // Request.Body. For HTTP/1.x requests, handlers should read any // needed request body data before writing the response. Once the // headers have been flushed (due to either an explicit Flusher.Flush // call or writing enough data to trigger a flush), the request body // may be unavailable. For HTTP/2 requests, the Go HTTP server permits // handlers to continue to read the request body while concurrently // writing the response. However, such behavior may not be supported // by all HTTP/2 clients. Handlers should read before writing if // possible to maximize compatibility. func (w *responseWriter) Write(contents []byte) (int, error) { w.tryWriteHeader() n, err := w.ResponseWriter.Write(contents) w.written += n return n, err } // StatusCode returns the status code header value func (w *responseWriter) StatusCode() int { return w.statusCode } func (w *responseWriter) GetBeforeFlush() func() { return w.beforeFlush } // SetBeforeFlush registers the unique callback which called exactly before the response is flushed to the client func (w *responseWriter) SetBeforeFlush(cb func()) { w.beforeFlush = cb } func (w *responseWriter) FlushResponse() { if w.beforeFlush != nil { w.beforeFlush() } w.tryWriteHeader() } // Clone returns a clone of this response writer // it copies the header, status code, headers and the beforeFlush finally returns a new ResponseRecorder. func (w *responseWriter) Clone() ResponseWriter { wc := &responseWriter{} wc.ResponseWriter = w.ResponseWriter wc.statusCode = w.statusCode wc.beforeFlush = w.beforeFlush wc.written = w.written return wc } // CopyTo writes a response writer (temp: status code, headers and body) to another response writer. func (w *responseWriter) CopyTo(to ResponseWriter) { // set the status code, failure status code are first class if w.statusCode >= 400 { to.WriteHeader(w.statusCode) } // append the headers for k, values := range w.Header() { for _, v := range values { if to.Header().Get(v) == "" { to.Header().Add(k, v) } } } // the body is not copied, this writer doesn't support recording } // ErrHijackNotSupported is returned by the Hijack method to // indicate that Hijack feature is not available. var ErrHijackNotSupported = errors.New("hijack is not supported by this ResponseWriter") // Hijack lets the caller take over the connection. // After a call to Hijack(), the HTTP server library // will not do anything else with the connection. // // It becomes the caller's responsibility to manage // and close the connection. // // The returned net.Conn may have read or write deadlines // already set, depending on the configuration of the // Server. It is the caller's responsibility to set // or clear those deadlines as needed. func (w *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { if h, isHijacker := w.ResponseWriter.(http.Hijacker); isHijacker { w.written = StatusCodeWritten return h.Hijack() } return nil, nil, ErrHijackNotSupported } // Flusher indicates if `Flush` is supported by the client. // // The default HTTP/1.x and HTTP/2 ResponseWriter implementations // support Flusher, but ResponseWriter wrappers may not. Handlers // should always test for this ability at runtime. // // Note that even for ResponseWriters that support Flush, // if the client is connected through an HTTP proxy, // the buffered data may not reach the client until the response // completes. func (w *responseWriter) Flusher() (http.Flusher, bool) { flusher, canFlush := w.ResponseWriter.(http.Flusher) return flusher, canFlush } // Flush sends any buffered data to the client. func (w *responseWriter) Flush() { if flusher, ok := w.Flusher(); ok { // Flow: WriteHeader -> Flush -> Write -> Write -> Write.... w.tryWriteHeader() flusher.Flush() } } ================================================ FILE: context/route.go ================================================ package context import ( "io" "time" "github.com/kataras/iris/v12/macro" ) // RouteReadOnly allows decoupled access to the current route // inside the context. type RouteReadOnly interface { // Name returns the route's name. Name() string // StatusErrorCode returns 0 for common resource routes // or the error code that an http error handler registered on. StatusErrorCode() int // Method returns the route's method. Method() string // Subdomains returns the route's subdomain. Subdomain() string // Path returns the route's original registered path. Path() string // String returns the form of METHOD, SUBDOMAIN, TMPL PATH. String() string // IsOnline returns true if the route is marked as "online" (state). IsOnline() bool // IsStatic reports whether this route is a static route. // Does not contain dynamic path parameters, // is online and registered on GET HTTP Method. IsStatic() bool // StaticPath returns the static part of the original, registered route path. // if /user/{id} it will return /user // if /user/{id}/friend/{friendid:uint64} it will return /user too // if /assets/{filepath:path} it will return /assets. StaticPath() string // ResolvePath returns the formatted path's %v replaced with the args. ResolvePath(args ...string) string // Trace should writes debug route info to the "w". // Should be called after Build. Trace(w io.Writer, stoppedIndex int) // Tmpl returns the path template, // it contains the parsed template // for the route's path. // May contain zero named parameters. // // Available after the build state, i.e a request handler or Iris Configurator. Tmpl() macro.Template // MainHandlerName returns the first registered handler for the route. MainHandlerName() string // MainHandlerIndex returns the first registered handler's index for the route. MainHandlerIndex() int // Property returns a specific property based on its "key" // of this route's Party owner. Property(key string) (any, bool) // Sitemap properties: https://www.sitemaps.org/protocol.html // GetLastMod returns the date of last modification of the file served by this route. GetLastMod() time.Time // GetChangeFreq returns the the page frequently is likely to change. GetChangeFreq() string // GetPriority returns the priority of this route's URL relative to other URLs on your site. GetPriority() float32 } ================================================ FILE: context/status.go ================================================ package context import "net/http" // ClientErrorCodes holds the 4xx Client errors. var ( ClientErrorCodes = []int{ http.StatusBadRequest, http.StatusUnauthorized, http.StatusPaymentRequired, http.StatusForbidden, http.StatusNotFound, http.StatusMethodNotAllowed, http.StatusNotAcceptable, http.StatusProxyAuthRequired, http.StatusRequestTimeout, http.StatusConflict, http.StatusGone, http.StatusLengthRequired, http.StatusPreconditionFailed, http.StatusRequestEntityTooLarge, http.StatusRequestURITooLong, http.StatusUnsupportedMediaType, http.StatusRequestedRangeNotSatisfiable, http.StatusExpectationFailed, http.StatusTeapot, http.StatusMisdirectedRequest, http.StatusUnprocessableEntity, http.StatusLocked, http.StatusFailedDependency, http.StatusTooEarly, http.StatusUpgradeRequired, http.StatusPreconditionRequired, http.StatusTooManyRequests, http.StatusRequestHeaderFieldsTooLarge, http.StatusUnavailableForLegalReasons, // Unofficial. StatusPageExpired, StatusBlockedByWindowsParentalControls, StatusInvalidToken, StatusTokenRequired, } // ServerErrorCodes holds the 5xx Server errors. ServerErrorCodes = []int{ http.StatusInternalServerError, http.StatusNotImplemented, http.StatusBadGateway, http.StatusServiceUnavailable, http.StatusGatewayTimeout, http.StatusHTTPVersionNotSupported, http.StatusVariantAlsoNegotiates, http.StatusInsufficientStorage, http.StatusLoopDetected, http.StatusNotExtended, http.StatusNetworkAuthenticationRequired, // Unofficial. StatusBandwidthLimitExceeded, StatusInvalidSSLCertificate, StatusSiteOverloaded, StatusSiteFrozen, StatusNetworkReadTimeout, } // ClientAndServerErrorCodes is the static list of all client and server error codes. ClientAndServerErrorCodes = append(ClientErrorCodes, ServerErrorCodes...) ) // Unofficial status error codes. const ( // 4xx StatusPageExpired = 419 StatusBlockedByWindowsParentalControls = 450 StatusInvalidToken = 498 StatusTokenRequired = 499 // 5xx StatusBandwidthLimitExceeded = 509 StatusInvalidSSLCertificate = 526 StatusSiteOverloaded = 529 StatusSiteFrozen = 530 StatusNetworkReadTimeout = 598 ) var unofficialStatusText = map[int]string{ StatusPageExpired: "Page Expired", StatusBlockedByWindowsParentalControls: "Blocked by Windows Parental Controls", StatusInvalidToken: "Invalid Token", StatusTokenRequired: "Token Required", StatusBandwidthLimitExceeded: "Bandwidth Limit Exceeded", StatusInvalidSSLCertificate: "Invalid SSL Certificate", StatusSiteOverloaded: "Site is overloaded", StatusSiteFrozen: "Site is frozen", StatusNetworkReadTimeout: "Network read timeout error", } // StatusText returns a text for the HTTP status code. It returns the empty // string if the code is unknown. func StatusText(code int) string { text := http.StatusText(code) if text == "" { text = unofficialStatusText[code] } return text } // StatusCodeNotSuccessful defines if a specific "statusCode" is not // a valid status code for a successful response. // By default if the status code is lower than 400 then it is not a failure one, // otherwise it is considered as an error code. // // Read more at `iris/Configuration#DisableAutoFireStatusCode` and // `iris/core/router/Party#OnAnyErrorCode` for relative information. // // Modify this variable when your Iris server or/and client // not follows the RFC: https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html var StatusCodeNotSuccessful = func(statusCode int) bool { return statusCode >= 400 } ================================================ FILE: context/strconv.go ================================================ package context import ( "fmt" "strconv" "time" ) func strParseUint(value string) (uint, error) { result, err := strconv.ParseUint(value, 10, strconv.IntSize) if err != nil { return 0, err } return uint(result), nil } func strParseUint8(value string) (uint8, error) { result, err := strconv.ParseUint(value, 10, 8) if err != nil { return 0, err } return uint8(result), nil } func strParseUint16(value string) (uint16, error) { result, err := strconv.ParseUint(value, 10, 16) if err != nil { return 0, err } return uint16(result), nil } func strParseUint32(value string) (uint32, error) { result, err := strconv.ParseUint(value, 10, 32) if err != nil { return 0, err } return uint32(result), nil } func strParseUint64(value string) (uint64, error) { result, err := strconv.ParseUint(value, 10, 64) if err != nil { return 0, err } return result, nil } func strParseInt(value string) (int, error) { result, err := strconv.Atoi(value) if err != nil { return 0, err } return result, nil } func strParseInt8(value string) (int8, error) { result, err := strconv.ParseInt(value, 10, 8) if err != nil { return 0, err } return int8(result), nil } func strParseInt16(value string) (int16, error) { result, err := strconv.ParseInt(value, 10, 16) if err != nil { return 0, err } return int16(result), nil } func strParseInt32(value string) (int32, error) { result, err := strconv.ParseInt(value, 10, 32) if err != nil { return 0, err } return int32(result), nil } func strParseInt64(value string) (int64, error) { result, err := strconv.ParseInt(value, 10, 64) if err != nil { return 0, err } return result, nil } func strParseFloat32(value string) (float32, error) { result, err := strconv.ParseFloat(value, 32) if err != nil { return 0, err } return float32(result), nil } func strParseFloat64(value string) (float64, error) { result, err := strconv.ParseFloat(value, 64) if err != nil { return 0, err } return result, nil } func strParseComplex64(value string) (complex64, error) { result, err := strconv.ParseComplex(value, 64) if err != nil { return 0, err } return complex64(result), nil } func strParseComplex128(value string) (complex128, error) { result, err := strconv.ParseComplex(value, 128) if err != nil { return 0, err } return result, nil } func strParseBool(value string) (bool, error) { result, err := strconv.ParseBool(value) if err != nil { return false, err } return result, nil } var dayNames = map[string]time.Weekday{ // longDayNames. "Sunday": time.Sunday, "Monday": time.Monday, "Tuesday": time.Tuesday, "Wednesday": time.Wednesday, "Thursday": time.Thursday, "Friday": time.Friday, "Saturday": time.Saturday, // longDayNames: lowercase. "sunday": time.Sunday, "monday": time.Monday, "tuesday": time.Tuesday, "wednesday": time.Wednesday, "thursday": time.Thursday, "friday": time.Friday, "saturday": time.Saturday, // shortDayNames "Sun": time.Sunday, "Mon": time.Monday, "Tue": time.Tuesday, "Wed": time.Wednesday, "Thu": time.Thursday, "Fri": time.Friday, "Sat": time.Saturday, // shortDayNames: lowercase. "sun": time.Sunday, "mon": time.Monday, "tue": time.Tuesday, "wed": time.Wednesday, "thu": time.Thursday, "fri": time.Friday, "sat": time.Saturday, } func strParseWeekday(value string) (time.Weekday, error) { result, ok := dayNames[value] if !ok { return 0, ErrNotFound } return result, nil } func strParseTime(layout, value string) (time.Time, error) { return time.Parse(layout, value) } const ( simpleDateLayout1 = "2006/01/02" simpleDateLayout2 = "2006-01-02" ) func strParseSimpleDate(value string) (time.Time, error) { t1, err := strParseTime(simpleDateLayout1, value) if err != nil { t2, err2 := strParseTime(simpleDateLayout2, value) if err2 != nil { return time.Time{}, fmt.Errorf("%s, %w", err.Error(), err2) } return t2, nil } return t1, nil } ================================================ FILE: context/view.go ================================================ package context import ( "fmt" "io" ) // ErrViewNotExist it's an error. // It reports whether a template was not found in the parsed templates tree. type ErrViewNotExist struct { Name string IsLayout bool Data any } // Error completes the `error` interface. func (e ErrViewNotExist) Error() string { title := "template" if e.IsLayout { title = "layout" } return fmt.Sprintf("%s '%s' does not exist", title, e.Name) } // ViewEngine is the interface which all view engines should be implemented in order to be registered inside iris. type ViewEngine interface { // Name returns the name of the engine. Name() string // Load should load the templates from the given FileSystem. Load() error // ExecuteWriter should execute a template by its filename with an optional layout and bindingData. ExecuteWriter(w io.Writer, filename string, layout string, bindingData any) error // Ext should return the final file extension (including the dot) // which this view engine is responsible to render. // If the filename extension on ExecuteWriter is empty then this is appended. Ext() string } // ViewEngineFuncer is an addition of a view engine, // if a view engine implements that interface // then iris can add some closed-relative iris functions // like {{ url }}, {{ urlpath }} and {{ tr }}. type ViewEngineFuncer interface { // AddFunc should adds a function to the template's function map. AddFunc(funcName string, funcBody any) } ================================================ FILE: context_wrapper.go ================================================ package iris import ( "sync" "time" "github.com/kataras/iris/v12/context" ) type ( // ContextSetter is an interface which can be implemented by a struct // to set the iris.Context to the struct. // The receiver must be a pointer of the struct. ContextSetter interface { // SetContext sets the iris.Context to the struct. SetContext(Context) } // ContextSetterPtr is a pointer of T which implements the `ContextSetter` interface. // The T must be a struct. ContextSetterPtr[T any] interface { *T ContextSetter } // emptyContextSetter is an empty struct which implements the `ContextSetter` interface. emptyContextSetter struct{} ) // SetContext method implements `ContextSetter` interface. func (*emptyContextSetter) SetContext(Context) {} // ContextPool is a pool of T. It's used to acquire and release custom context. // Use of custom implementation or `NewContextPool`. // // See `NewContextWrapper` and `NewContextPool` for more. type ( ContextPool[T any] interface { // Acquire must return a new T from a pool. Acquire(ctx Context) T // Release must put the T back to the pool. Release(T) } // syncContextPool is a sync pool implementation of T. // It's used to acquire and release T. // The contextPtr is acquired from the sync pool and released back to the sync pool after the handler's execution. // The contextPtr is passed to the handler as an argument. // ThecontextPtr is not shared between requests. // The contextPtr must implement the `ContextSetter` interface. // The T must be a struct. // The contextPtr must be a pointer of T. syncContextPool[T any, contextPtr ContextSetterPtr[T]] struct { pool *sync.Pool } ) // Ensure that syncContextPool implements ContextPool. var _ ContextPool[*emptyContextSetter] = (*syncContextPool[emptyContextSetter, *emptyContextSetter])(nil) // NewContextPool returns a new ContextPool default implementation which // uses sync.Pool to implement its Acquire and Release methods. // The contextPtr is acquired from the sync pool and released back to the sync pool after the handler's execution. // The contextPtr is passed to the handler as an argument. // ThecontextPtr is not shared between requests. // The contextPtr must implement the `ContextSetter` interface. // The T must be a struct. // The contextPtr must be a pointer of T. // // Example: // w := iris.NewContextWrapper(iris.NewContextPool[myCustomContext, *myCustomContext]()) func NewContextPool[T any, contextPtr ContextSetterPtr[T]]() ContextPool[contextPtr] { return &syncContextPool[T, contextPtr]{ pool: &sync.Pool{ New: func() any { var t contextPtr = new(T) return t }, }, } } // Acquire returns a new T from the sync pool. func (p *syncContextPool[T, contextPtr]) Acquire(ctx Context) contextPtr { // var t contextPtr // if v := p.pool.Get(); v == nil { // t = new(T) // } else { // t = v.(contextPtr) // } t := p.pool.Get().(contextPtr) t.SetContext(ctx) return t } // Release puts the T back to the sync pool. func (p *syncContextPool[T, contextPtr]) Release(t contextPtr) { p.pool.Put(t) } // ContextWrapper is a wrapper for handlers which expect a T instead of iris.Context. // // See the `NewContextWrapper` function for more. type ContextWrapper[T any] struct { pool ContextPool[T] } // NewContextWrapper returns a new ContextWrapper. // If pool is nil, a default pool is used. // The default pool's AcquireFunc returns a zero value of T. // The default pool's ReleaseFunc does nothing. // The default pool is used when the pool is nil. // Use the `iris.NewContextPool[T, *T]()` to pass a simple context pool. // Then, use the `Handler` method to wrap custom handlers to iris ones. // // Example: https://github.com/kataras/iris/tree/main/_examples/routing/custom-context func NewContextWrapper[T any](pool ContextPool[T]) *ContextWrapper[T] { if pool == nil { panic("pool cannot be nil") } return &ContextWrapper[T]{ pool: pool, } } // Pool returns the pool, useful when manually Acquire and Release of custom context is required. func (w *ContextWrapper[T]) Pool() ContextPool[T] { return w.pool } // Handler wraps the handler with the pool's Acquire and Release methods. // It returns a new handler which expects a T instead of iris.Context. // The T is the type of the pool. // The T is acquired from the pool and released back to the pool after the handler's execution. // The T is passed to the handler as an argument. // The T is not shared between requests. func (w *ContextWrapper[T]) Handler(handler func(T)) Handler { if handler == nil { return nil } return func(ctx Context) { newT := w.pool.Acquire(ctx) handler(newT) w.pool.Release(newT) } } // Handlers wraps the handlers with the pool's Acquire and Release methods. func (w *ContextWrapper[T]) Handlers(handlers ...func(T)) context.Handlers { newHandlers := make(context.Handlers, len(handlers)) for i, handler := range handlers { newHandlers[i] = w.Handler(handler) } return newHandlers } // HandlerReturnError same as `Handler` but it converts a handler which returns an error. func (w *ContextWrapper[T]) HandlerReturnError(handler func(T) error) func(Context) error { if handler == nil { return nil } return func(ctx Context) error { newT := w.pool.Acquire(ctx) err := handler(newT) w.pool.Release(newT) return err } } // HandlerReturnDuration same as `Handler` but it converts a handler which returns a time.Duration. func (w *ContextWrapper[T]) HandlerReturnDuration(handler func(T) time.Duration) func(Context) time.Duration { if handler == nil { return nil } return func(ctx Context) time.Duration { newT := w.pool.Acquire(ctx) duration := handler(newT) w.pool.Release(newT) return duration } } // Filter same as `Handler` but it converts a handler to Filter. func (w *ContextWrapper[T]) Filter(handler func(T) bool) Filter { if handler == nil { return nil } return func(ctx Context) bool { newT := w.pool.Acquire(ctx) shouldContinue := handler(newT) w.pool.Release(newT) return shouldContinue } } // FallbackViewFunc same as `Handler` but it converts a handler to FallbackViewFunc. func (w *ContextWrapper[T]) FallbackViewFunc(handler func(ctx T, err ErrViewNotExist) error) FallbackViewFunc { if handler == nil { return nil } return func(ctx Context, err ErrViewNotExist) error { newT := w.pool.Acquire(ctx) returningErr := handler(newT, err) w.pool.Release(newT) return returningErr } } ================================================ FILE: core/errgroup/errgroup.go ================================================ package errgroup import ( "errors" "fmt" "sort" "strings" ) // Check reports whether the "err" is not nil. // If it is a group then it returns true if that or its children contains any error. func Check(err error) error { if isNotNil(err) { return err } return nil } // Walk loops through each of the errors of "err". // If "err" is *Group then it fires the "visitor" for each of its errors, including children. // if "err" is *Error then it fires the "visitor" with its type and wrapped error. // Otherwise it fires the "visitor" once with typ of nil and err as "err". func Walk(err error, visitor func(typ any, err error)) error { if err == nil { return nil } if group, ok := err.(*Group); ok { list := group.getAllErrors() for _, entry := range list { if e, ok := entry.(*Error); ok { visitor(e.Type, e.Err) // e.Unwrap() <-no. } else { visitor(nil, err) } } } else if e, ok := err.(*Error); ok { visitor(e.Type, e.Err) } else { visitor(nil, err) } return err } /* func Errors(err error, conv bool) []error { if err == nil { return nil } if group, ok := err.(*Group); ok { list := group.getAllErrors() if conv { for i, entry := range list { if _, ok := entry.(*Error); !ok { list[i] = &Error{Err: entry, Type: group.Type} } } } return list } return []error{err} } func Type(err error) any { if err == nil { return nil } if e, ok := err.(*Error); ok && e.Err != nil { return e.Type } return nil } func Fill(parent *Group, errors []*Error) { for _, err := range errors { if err.Type == parent.Type { parent.Add(err) continue } parent.Group(err.Type).Err(err) } return } */ // Error implements the error interface. // It is a special error type which keep the "Type" of the // Group that it's created through Group's `Err` and `Errf` methods. type Error struct { Err error `json:"error" xml:"Error" yaml:"Error" toml:"Error" sql:"error"` Type any `json:"type" xml:"Type" yaml:"Type" toml:"Type" sql:"type"` } // Error returns the error message of the "Err". func (e *Error) Error() string { return e.Err.Error() } // Unwrap calls and returns the result of the "Err" Unwrap method or nil. func (e *Error) Unwrap() error { return errors.Unwrap(e.Err) } // Is reports whether the "err" is an *Error. func (e *Error) Is(err error) bool { if err == nil { return false } ok := errors.Is(e.Err, err) if !ok { te, ok := err.(*Error) if !ok { return false } return errors.Is(e.Err, te.Err) } return ok } // As reports whether the "target" can be used as &Error{target.Type: ?}. func (e *Error) As(target any) bool { if target == nil { return target == e } ok := errors.As(e.Err, target) if !ok { te, ok := target.(*Error) if !ok { return false } if te.Type != nil { if te.Type != e.Type { return false } } return errors.As(te.Err, &e) } return ok } // Group is an error container of a specific Type and can have child containers per type too. type Group struct { parent *Group // a list of children groups, used to get or create new group through Group method. children map[any]*Group depth int Type any Errors []error // []*Error // if true then this Group's Error method will return the messages of the errors made by this Group's Group method. // Defaults to true. IncludeChildren bool // it clones. // IncludeTypeText bool index int // group index. } // New returns a new empty Group. func New(typ any) *Group { return &Group{ Type: typ, IncludeChildren: true, } } const delim = "\n" func (g *Group) Error() (s string) { if len(g.Errors) > 0 { msgs := make([]string, len(g.Errors)) for i, err := range g.Errors { msgs[i] = err.Error() } s = strings.Join(msgs, delim) } if g.IncludeChildren && len(g.children) > 0 { // return with order of definition. groups := g.getAllChildren() sortGroups(groups) for _, ge := range groups { for _, childErr := range ge.Errors { s += childErr.Error() + delim } } if s != "" { return s[:len(s)-1] } } return } func (g *Group) getAllErrors() []error { list := g.Errors if len(g.children) > 0 { // return with order of definition. groups := g.getAllChildren() sortGroups(groups) for _, ge := range groups { list = append(list, ge.Errors...) } } return list } func (g *Group) getAllChildren() []*Group { if len(g.children) == 0 { return nil } var groups []*Group for _, child := range g.children { groups = append(groups, append([]*Group{child}, child.getAllChildren()...)...) } return groups } // Unwrap implements the dynamic std errors interface and it returns the parent Group. func (g *Group) Unwrap() error { if g == nil { return nil } return g.parent } // Group creates a new group of "typ" type, if does not exist, and returns it. func (g *Group) Group(typ any) *Group { if g.children == nil { g.children = make(map[any]*Group) } else { for _, child := range g.children { if child.Type == typ { return child } } } child := &Group{ Type: typ, parent: g, depth: g.depth + 1, IncludeChildren: g.IncludeChildren, index: g.index + 1 + len(g.children), } g.children[typ] = child return child } // Add adds an error to the group. func (g *Group) Add(err error) { if err == nil { return } g.Errors = append(g.Errors, err) } // Addf adds an error to the group like `fmt.Errorf` and returns it. func (g *Group) Addf(format string, args ...any) error { err := fmt.Errorf(format, args...) g.Add(err) return err } // Err adds an error to the group, it transforms it to an Error type if necessary and returns it. func (g *Group) Err(err error) error { if err == nil { return nil } e, ok := err.(*Error) if !ok { if ge, ok := err.(*Group); ok { if g.children == nil { g.children = make(map[any]*Group) } g.children[ge.Type] = ge return ge } e = &Error{err, 0} } e.Type = g.Type g.Add(e) return e } // Errf adds an error like `fmt.Errorf` and returns it. func (g *Group) Errf(format string, args ...any) error { return g.Err(fmt.Errorf(format, args...)) } func sortGroups(groups []*Group) { sort.Slice(groups, func(i, j int) bool { return groups[i].index < groups[j].index }) } func isNotNil(err error) bool { if g, ok := err.(*Group); ok { if len(g.Errors) > 0 { return true } for _, child := range g.children { if isNotNil(child) { return true } } return false } return err != nil } ================================================ FILE: core/errgroup/errgroup_test.go ================================================ package errgroup import ( "errors" "fmt" "strings" "testing" ) func TestErrorError(t *testing.T) { testErr := errors.New("error") err := &Error{Err: testErr} if expected, got := testErr.Error(), err.Error(); expected != got { t.Fatalf("expected %s but got %s", expected, got) } } func TestErrorUnwrap(t *testing.T) { wrapped := errors.New("unwrap") err := &Error{Err: fmt.Errorf("this wraps:%w", wrapped)} if expected, got := wrapped, errors.Unwrap(err); expected != got { t.Fatalf("expected %#+v but got %#+v", expected, got) } } func TestErrorIs(t *testing.T) { testErr := errors.New("is") err := &Error{Err: fmt.Errorf("this is: %w", testErr)} if expected, got := true, errors.Is(err, testErr); expected != got { t.Fatalf("expected %v but got %v", expected, got) } } // errorString is a trivial implementation of error. type errorString struct { s string } func (e *errorString) Error() string { return e.s } func TestErrorAs(t *testing.T) { testErr := &errorString{"as"} err := &Error{Err: testErr} if expected, got := true, errors.As(err, &testErr); expected != got { t.Fatalf("[testErr as err] expected %v but got %v", expected, got) } if expected, got := false, errors.As(testErr, &err); expected != got /* errorString does not implemeny As, so the std/default functionality will be applied */ { t.Fatalf("[err as testErr] expected %v but got %v", expected, got) } } func TestGroupError(t *testing.T) { g := New(0) tests := []string{"error 1", "error 2", "error 3"} for _, tt := range tests { g.Add(errors.New(tt)) } if expected, got := strings.Join(tests, "\n"), g.Error(); expected != got { t.Fatalf("expected '%s' but got '%s'", expected, got) } } func TestGroup(t *testing.T) { const ( apiErrorsType = iota + 1 childAPIErrorsType childAPIErrors2Type = "string type 1" childAPIErrors2Type1 = "string type 2" apiErrorsText = "apiErrors error 1" childAPIErrorsText = "apiErrors:child error 1" childAPIErrors2Text = "apiErrors:child2 error 1" childAPIErrors2Text1 = "apiErrors:child2_1 error 1" ) g := New(nil) apiErrorsGroup := g.Group(apiErrorsType) apiErrorsGroup.Errf(apiErrorsText) childAPIErrorsGroup := apiErrorsGroup.Group(childAPIErrorsType) childAPIErrorsGroup.Addf(childAPIErrorsText) childAPIErrorsGroup2 := apiErrorsGroup.Group(childAPIErrors2Type) childAPIErrorsGroup2.Addf(childAPIErrors2Text) childAPIErrorsGroup2Group1 := childAPIErrorsGroup2.Group(childAPIErrors2Type1) childAPIErrorsGroup2Group1.Addf(childAPIErrors2Text1) if apiErrorsGroup.Type != apiErrorsType { t.Fatal("invalid type") } if childAPIErrorsGroup.Type != childAPIErrorsType { t.Fatal("invalid type") } if childAPIErrorsGroup2.Type != childAPIErrors2Type { t.Fatal("invalid type") } if childAPIErrorsGroup2Group1.Type != childAPIErrors2Type1 { t.Fatal("invalid type") } if expected, got := 2, len(apiErrorsGroup.children); expected != got { t.Fatalf("expected %d but got %d", expected, got) } if expected, got := 0, len(childAPIErrorsGroup.children); expected != got { t.Fatalf("expected %d but got %d", expected, got) } if expected, got := 1, len(childAPIErrorsGroup2.children); expected != got { t.Fatalf("expected %d but got %d", expected, got) } if expected, got := 0, len(childAPIErrorsGroup2Group1.children); expected != got { t.Fatalf("expected %d but got %d", expected, got) } if expected, got := 1, apiErrorsGroup.index; expected != got { t.Fatalf("expected index %d but got %d", expected, got) } if expected, got := 2, childAPIErrorsGroup.index; expected != got { t.Fatalf("expected index %d but got %d", expected, got) } if expected, got := 3, childAPIErrorsGroup2.index; expected != got { t.Fatalf("expected index %d but got %d", expected, got) } if expected, got := 4, childAPIErrorsGroup2Group1.index; expected != got { t.Fatalf("expected index %d but got %d", expected, got) } t.Run("Error", func(t *testing.T) { if expected, got := strings.Join([]string{apiErrorsText, childAPIErrorsText, childAPIErrors2Text, childAPIErrors2Text1}, delim), g.Error(); expected != got { t.Fatalf("expected '%s' but got '%s'", expected, got) } }) t.Run("Walk", func(t *testing.T) { expectedEntries := 4 _ = Walk(g, func(typ any, err error) { g.IncludeChildren = false childAPIErrorsGroup.IncludeChildren = false childAPIErrorsGroup2.IncludeChildren = false childAPIErrorsGroup2Group1.IncludeChildren = false expectedEntries-- var expected string switch typ { case apiErrorsType: expected = apiErrorsText case childAPIErrorsType: expected = childAPIErrorsText case childAPIErrors2Type: expected = childAPIErrors2Text case childAPIErrors2Type1: expected = childAPIErrors2Text1 } if got := err.Error(); expected != got { t.Fatalf("[%v] expected '%s' but got '%s'", typ, expected, got) } }) if expectedEntries != 0 { t.Fatalf("not valid number of errors [...%d]", expectedEntries) } g.IncludeChildren = true childAPIErrorsGroup.IncludeChildren = true childAPIErrorsGroup2.IncludeChildren = true childAPIErrorsGroup2Group1.IncludeChildren = true }) } ================================================ FILE: core/handlerconv/from_std.go ================================================ package handlerconv import ( "fmt" "net/http" "github.com/kataras/iris/v12/context" ) // FromStd converts native http.Handler & http.HandlerFunc to context.Handler. // // Supported form types: // // .FromStd(h http.Handler) // .FromStd(func(w http.ResponseWriter, r *http.Request)) // .FromStd(func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc)) func FromStd(handler any) context.Handler { switch h := handler.(type) { case context.Handler: return h // case func(*context.Context): // return h case http.Handler: // handlerFunc.ServeHTTP(w,r) return func(ctx *context.Context) { h.ServeHTTP(ctx.ResponseWriter(), ctx.Request()) } case func(http.ResponseWriter, *http.Request): // handlerFunc(w,r) return FromStd(http.HandlerFunc(h)) case func(http.ResponseWriter, *http.Request, http.HandlerFunc): // handlerFunc(w,r, http.HandlerFunc) // return FromStdWithNext(h) case func(http.Handler) http.Handler: panic(fmt.Errorf(` Passed handler cannot be converted directly: - http.Handler(http.Handler) --------------------------------------------------------------------- Please use the Application.WrapRouter method instead, example code: app := iris.New() // ... app.WrapRouter(func(w http.ResponseWriter, r *http.Request, router http.HandlerFunc) { httpThirdPartyHandler(router).ServeHTTP(w, r) })`)) default: // No valid handler passed panic(fmt.Errorf(` Passed argument is not a func(iris.Context) neither one of these types: - http.Handler - func(w http.ResponseWriter, r *http.Request) - func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) --------------------------------------------------------------------- It seems to be a %T points to: %v`, handler, handler)) } } // FromStdWithNext receives a standar handler - middleware form - and returns a // compatible context.Handler wrapper. func FromStdWithNext(h func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc)) context.Handler { return func(ctx *context.Context) { next := func(w http.ResponseWriter, r *http.Request) { ctx.ResetRequest(r) ctx.Next() } h(ctx.ResponseWriter(), ctx.Request(), next) } } ================================================ FILE: core/handlerconv/from_std_test.go ================================================ // black-box testing package handlerconv_test import ( stdContext "context" "net/http" "testing" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/core/handlerconv" "github.com/kataras/iris/v12/httptest" ) func TestFromStd(t *testing.T) { expected := "ok" std := func(w http.ResponseWriter, r *http.Request) { _, err := w.Write([]byte(expected)) if err != nil { t.Fatal(err) } } h := handlerconv.FromStd(http.HandlerFunc(std)) hFunc := handlerconv.FromStd(std) app := iris.New() app.Get("/handler", h) app.Get("/func", hFunc) e := httptest.New(t, app) e.GET("/handler"). Expect().Status(iris.StatusOK).Body().IsEqual(expected) e.GET("/func"). Expect().Status(iris.StatusOK).Body().IsEqual(expected) } func TestFromStdWithNext(t *testing.T) { basicauth := "secret" passed := "ok" type contextKey string stdWNext := func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { if username, password, ok := r.BasicAuth(); ok && username == basicauth && password == basicauth { ctx := stdContext.WithValue(r.Context(), contextKey("key"), "ok") next.ServeHTTP(w, r.WithContext(ctx)) return } w.WriteHeader(iris.StatusForbidden) } h := handlerconv.FromStdWithNext(stdWNext) next := func(ctx *context.Context) { ctx.WriteString(ctx.Request().Context().Value(contextKey("key")).(string)) } app := iris.New() app.Get("/handlerwithnext", h, next) e := httptest.New(t, app) e.GET("/handlerwithnext"). Expect().Status(iris.StatusForbidden) e.GET("/handlerwithnext").WithBasicAuth(basicauth, basicauth). Expect().Status(iris.StatusOK).Body().IsEqual(passed) } ================================================ FILE: core/host/interrupt.go ================================================ package host import ( "os" "os/signal" "sync" "syscall" ) // RegisterOnInterrupt registers a global function to call when CTRL+C pressed or a unix kill command received. func RegisterOnInterrupt(cb func()) { // var cb func() // switch v := callbackFuncOrFuncReturnsError.(type) { // case func(): // cb = v // case func() error: // cb = func() { v() } // default: // panic(fmt.Errorf("unknown type of RegisterOnInterrupt callback: expected func() or func() error but got: %T", v)) // } Interrupt.Register(cb) } // Interrupt watches the os.Signals for interruption signals // and fires the callbacks when those happens. // A call of its `FireNow` manually will fire and reset the registered interrupt handlers. var Interrupt TnterruptListener = new(interruptListener) type TnterruptListener interface { Register(cb func()) FireNow() } type interruptListener struct { mu sync.Mutex once sync.Once // onInterrupt contains a list of the functions that should be called when CTRL+C/CMD+C or // a unix kill command received. onInterrupt []func() } // Register registers a global function to call when CTRL+C/CMD+C pressed or a unix kill command received. func (i *interruptListener) Register(cb func()) { if cb == nil { return } i.listenOnce() i.mu.Lock() i.onInterrupt = append(i.onInterrupt, cb) i.mu.Unlock() } // FireNow can be called more than one times from a Consumer in order to // execute all interrupt handlers manually. func (i *interruptListener) FireNow() { i.mu.Lock() for _, f := range i.onInterrupt { f() } i.onInterrupt = i.onInterrupt[0:0] i.mu.Unlock() } // listenOnce fires a goroutine which calls the interrupt handlers when CTRL+C/CMD+C and e.t.c. // If `FireNow` called before then it does nothing when interrupt signal received, // so it's safe to be used side by side with `FireNow`. // // Btw this `listenOnce` is called automatically on first register, it's useless for outsiders. func (i *interruptListener) listenOnce() { i.once.Do(func() { go i.notifyAndFire() }) } func (i *interruptListener) notifyAndFire() { ch := make(chan os.Signal, 1) signal.Notify(ch, // kill -SIGINT XXXX or Ctrl+c os.Interrupt, syscall.SIGINT, // register that too, it should be ok // os.Kill is equivalent with the syscall.SIGKILL // os.Kill, // syscall.SIGKILL, // register that too, it should be ok // kill -SIGTERM XXXX syscall.SIGTERM, ) <-ch i.FireNow() } ================================================ FILE: core/host/proxy.go ================================================ package host import ( "crypto/tls" "net/http" "net/http/httputil" "net/url" "path" "strings" "time" "github.com/kataras/iris/v12/core/netutil" ) // ProxyHandler returns a new ReverseProxy that rewrites // URLs to the scheme, host, and base path provided in target. If the // target's path is "/base" and the incoming request was for "/dir", // the target request will be for /base/dir. // // Relative to httputil.NewSingleHostReverseProxy with some additions. // // Look `ProxyHandlerRemote` too. func ProxyHandler(target *url.URL, config *tls.Config) *httputil.ReverseProxy { if config == nil { config = &tls.Config{MinVersion: tls.VersionTLS13} } director := func(req *http.Request) { modifyProxiedRequest(req, target) req.Host = target.Host req.URL.Path = path.Join(target.Path, req.URL.Path) } // TODO: when go 1.20 released: /* rewrite := func(r *httputil.ProxyRequest) { r.SetURL(target) // Forward request to outboundURL. r.SetXForwarded() // Set X-Forwarded-* headers. // r.Out.Header.Set("X-Additional-Header", "header set by the proxy") // To preserve the inbound request's Host header (the default behavior of NewSingleHostReverseProxy): // r.Out.Host = r.In.Host } */ p := &httputil.ReverseProxy{Director: director /*, Rewrite: rewrite */} if netutil.IsLoopbackHost(target.Host) { transport := &http.Transport{ TLSClientConfig: config, // lint:ignore } p.Transport = transport } return p } // mergeQuery return a query string that combines targetQuery and reqQuery // and remove the duplicated query parameters of them. func mergeQuery(targetQuery, reqQuery string) string { var paramSlice []string if targetQuery != "" { paramSlice = strings.Split(targetQuery, "&") } if reqQuery != "" { paramSlice = append(paramSlice, strings.Split(reqQuery, "&")...) } var mergedSlice []string queryMap := make(map[string]bool) for _, param := range paramSlice { size := len(queryMap) queryMap[param] = true if size != len(queryMap) { mergedSlice = append(mergedSlice, param) } } return strings.Join(mergedSlice, "&") } func modifyProxiedRequest(req *http.Request, target *url.URL) { req.URL.Scheme = target.Scheme req.URL.Host = target.Host req.URL.RawQuery = mergeQuery(target.RawQuery, req.URL.RawQuery) if _, ok := req.Header["User-Agent"]; !ok { // explicitly disable User-Agent so it's not set to default value req.Header.Set("User-Agent", "") } } // ProxyHandlerRemote returns a new ReverseProxy that rewrites // URLs to the scheme, host, and path provided in target. // Case 1: req.Host == target.Host // behavior same as ProxyHandler // Case 2: req.Host != target.Host // the target request will be forwarded to the target's url // insecureSkipVerify indicates enable ssl certificate verification or not. // // Look `ProxyHandler` too. func ProxyHandlerRemote(target *url.URL, config *tls.Config) *httputil.ReverseProxy { if config == nil { config = &tls.Config{MinVersion: tls.VersionTLS13} } director := func(req *http.Request) { modifyProxiedRequest(req, target) if req.Host != target.Host { req.URL.Path = target.Path } else { req.URL.Path = path.Join(target.Path, req.URL.Path) } req.Host = target.Host } p := &httputil.ReverseProxy{Director: director} if netutil.IsLoopbackHost(target.Host) { config.InsecureSkipVerify = true } transport := &http.Transport{ TLSClientConfig: config, // lint:ignore } p.Transport = transport return p } // NewProxy returns a new host (server supervisor) which // proxies all requests to the target. // It uses the httputil.NewSingleHostReverseProxy. // // Usage: // target, _ := url.Parse("https://mydomain.com") // proxy := NewProxy("mydomain.com:80", target) // proxy.ListenAndServe() // use of `proxy.Shutdown` to close the proxy server. func NewProxy(hostAddr string, target *url.URL, config *tls.Config) *Supervisor { proxyHandler := ProxyHandler(target, config) proxy := New(&http.Server{ Addr: hostAddr, Handler: proxyHandler, }) return proxy } // NewProxyRemote returns a new host (server supervisor) which // proxies all requests to the target. // It uses the httputil.NewSingleHostReverseProxy. // // Usage: // target, _ := url.Parse("https://anotherdomain.com/abc") // proxy := NewProxyRemote("mydomain.com", target, false) // proxy.ListenAndServe() // use of `proxy.Shutdown` to close the proxy server. func NewProxyRemote(hostAddr string, target *url.URL, config *tls.Config) *Supervisor { proxyHandler := ProxyHandlerRemote(target, config) proxy := New(&http.Server{ Addr: hostAddr, Handler: proxyHandler, }) return proxy } // NewRedirection returns a new host (server supervisor) which // redirects all requests to the target. // Usage: // target, _ := url.Parse("https://mydomain.com") // r := NewRedirection(":80", target, 307) // r.ListenAndServe() // use of `r.Shutdown` to close this server. func NewRedirection(hostAddr string, target *url.URL, redirectStatus int) *Supervisor { redirectSrv := &http.Server{ ReadTimeout: 30 * time.Second, WriteTimeout: 60 * time.Second, Addr: hostAddr, Handler: RedirectHandler(target, redirectStatus), } return New(redirectSrv) } // RedirectHandler returns a simple redirect handler. // See `NewProxy` or `ProxyHandler` for more features. func RedirectHandler(target *url.URL, redirectStatus int) http.Handler { targetURI := target.String() if redirectStatus <= 300 { // here we should use StatusPermanentRedirect but // that may result on unexpected behavior // for end-developers who might change their minds // after a while, so keep status temporary. // Note thatwe could also use StatusFound // as we do on the `Context#Redirect`. // It will also help us to prevent any post data issues. redirectStatus = http.StatusTemporaryRedirect } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { redirectTo := path.Join(targetURI, r.URL.Path) if len(r.URL.RawQuery) > 0 { redirectTo += "?" + r.URL.RawQuery } http.Redirect(w, r, redirectTo, redirectStatus) }) } ================================================ FILE: core/host/proxy_test.go ================================================ // black-box testing package host_test import ( stdContext "context" "crypto/tls" "net" "net/url" "testing" "time" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/core/host" "github.com/kataras/iris/v12/httptest" ) func TestProxy(t *testing.T) { expectedIndex := "ok /" expectedAbout := "ok /about" unexpectedRoute := "unexpected" // proxySrv := iris.New() u, err := url.Parse("https://localhost:4444") if err != nil { t.Fatalf("%v while parsing url", err) } config := &tls.Config{ InsecureSkipVerify: true, MinVersion: tls.VersionTLS13, } proxy := host.NewProxy("", u, config) proxy.Configure(host.NonBlocking()) addr := &net.TCPAddr{ IP: net.IPv4(127, 0, 0, 1), Port: 0, } listener, err := net.ListenTCP("tcp", addr) if err != nil { t.Fatalf("%v while creating listener", err) } // non-blocking (see above). proxy.Serve(listener) ctx, cancelFunc := stdContext.WithTimeout(stdContext.Background(), 15*time.Second) defer cancelFunc() // Wait for up to 15 seconds or until the proxy is ready to serve or a serve failure. proxy.Wait(ctx) t.Log(listener.Addr().String()) app := iris.New() app.Get("/", func(ctx *context.Context) { ctx.WriteString(expectedIndex) }) app.Get("/about", func(ctx *context.Context) { ctx.WriteString(expectedAbout) }) app.OnErrorCode(iris.StatusNotFound, func(ctx *context.Context) { ctx.WriteString(unexpectedRoute) }) l, err := net.Listen("tcp", "localhost:4444") if err != nil { t.Fatalf("%v while creating tcp4 listener for new tls local test listener", err) } // main server go app.Run(iris.Listener(httptest.NewLocalTLSListener(l)), iris.WithoutStartupLog) // nolint:errcheck e := httptest.NewInsecure(t, httptest.URL("http://"+listener.Addr().String())) e.GET("/").Expect().Status(iris.StatusOK).Body().IsEqual(expectedIndex) e.GET("/about").Expect().Status(iris.StatusOK).Body().IsEqual(expectedAbout) e.GET("/notfound").Expect().Status(iris.StatusNotFound).Body().IsEqual(unexpectedRoute) } ================================================ FILE: core/host/supervisor.go ================================================ package host import ( "context" "crypto/tls" "errors" "fmt" "net" "net/http" "net/url" "os" "strings" "sync" "sync/atomic" "time" "github.com/kataras/iris/v12/core/netutil" "golang.org/x/crypto/acme/autocert" ) // Configurator provides an easy way to modify // the Supervisor. // // Look the `Configure` func for more. type Configurator func(su *Supervisor) // NonBlocking sets the server to non-blocking mode. Use its `Wait` method to wait for server to be up and running. func NonBlocking() Configurator { return func(su *Supervisor) { su.nonBlocking = true } } // Supervisor is the wrapper and the manager for a compatible server // and it's relative actions, called Tasks. // // Interfaces are separated to return relative functionality to them. type Supervisor struct { Server *http.Server // FriendlyAddr can be set to customize the "Now Listening on: {FriendlyAddr}". FriendlyAddr string // e.g mydomain.com instead of :443 when AutoTLS is used, see `WriteStartupLogOnServe` task. disableHTTP1ToHTTP2Redirection bool closedByInterruptHandler uint32 // non-zero means that the end-developer interrupted it by-purpose. manuallyTLS bool // we need that in order to determinate what to output on the console before the server begin. autoTLS bool mu sync.RWMutex onServe []func(TaskHost) // IgnoreErrors should contains the errors that should be ignored // on both serve functions return statements and error handlers. // // Note that this will match the string value instead of the equality of the type's variables. // // Defaults to empty. IgnoredErrors []string onErr []func(error) // Fallback should return a http.Server, which may already running // to handle the HTTP/1.1 clients when TLS/AutoTLS. // On manual TLS the accepted "challengeHandler" just returns the passed handler, // otherwise it binds to the acme challenge wrapper. // Example: // Fallback = func(h func(fallback http.Handler) http.Handler) *http.Server { // s := &http.Server{ // Handler: h(myServerHandler), // ...otherOptions // } // go s.ListenAndServe() // return s // } Fallback func(challegeHandler func(fallback http.Handler) http.Handler) *http.Server // See `iris.Configuration.SocketSharding`. SocketSharding bool // If more than zero then tcp keep alive listener is attached instead of the simple TCP listener. // See `iris.Configuration.KeepAlive` KeepAlive time.Duration address string nonBlocking bool waiter *Waiter } // New returns a new host supervisor // based on a native net/http "srv". // // It contains all native net/http's Server methods. // Plus you can add tasks on specific events. // It has its own flow, which means that you can prevent // to return and exit and restore the flow too. func New(srv *http.Server) *Supervisor { su := &Supervisor{ Server: srv, } su.waiter = NewWaiter(7, su.getAddress) return su } // Configure accepts one or more `Configurator`. // With this function you can use simple functions // that are spread across your app to modify // the supervisor, these Configurators can be // used on any Supervisor instance. // // Look `Configurator` too. // // Returns itself. func (su *Supervisor) Configure(configurators ...Configurator) *Supervisor { for _, conf := range configurators { conf(su) } return su } // NoRedirect should be called before `ListenAndServeTLS` when // secondary http1 to http2 server is not required. This method will disable // the automatic registration of secondary http.Server // which would redirect "http://" requests to their "https://" equivalent. func (su *Supervisor) NoRedirect() { su.disableHTTP1ToHTTP2Redirection = true } func (su *Supervisor) newListener() (net.Listener, error) { var ( l net.Listener err error ) if su.KeepAlive > 0 { l, err = netutil.TCPKeepAlive(su.Server.Addr, su.SocketSharding, su.KeepAlive) } else { l, err = netutil.TCP(su.Server.Addr, su.SocketSharding) } if err != nil { return nil, err } // here we can check for sure, without the need of the supervisor's `manuallyTLS` field. if netutil.IsTLS(su.Server) { // means tls tlsl := tls.NewListener(l, su.Server.TLSConfig) return tlsl, nil } return l, nil } // RegisterOnError registers a function to call when errors occurred by the underline http server. func (su *Supervisor) RegisterOnError(cb func(error)) { su.mu.Lock() su.onErr = append(su.onErr, cb) su.mu.Unlock() } func (su *Supervisor) validateErr(err error) error { if err == nil { return nil } if errors.Is(err, http.ErrServerClosed) && atomic.LoadUint32(&su.closedByInterruptHandler) > 0 { return nil } su.mu.Lock() defer su.mu.Unlock() for _, e := range su.IgnoredErrors { if err.Error() == e { return nil } } return err } func (su *Supervisor) notifyErr(err error) { if err == nil { return } su.mu.Lock() for _, f := range su.onErr { go f(err) } su.mu.Unlock() } func (su *Supervisor) getAddress() string { su.mu.RLock() addr := su.address su.mu.RUnlock() return addr } func (su *Supervisor) setAddress(addr string) { su.mu.Lock() su.address = addr su.mu.Unlock() } // RegisterOnServe registers a function to call on // Serve/ListenAndServe/ListenAndServeTLS/ListenAndServeAutoTLS. func (su *Supervisor) RegisterOnServe(cb func(TaskHost)) { su.mu.Lock() su.onServe = append(su.onServe, cb) su.mu.Unlock() } func (su *Supervisor) notifyServe(host TaskHost) { su.mu.Lock() for _, f := range su.onServe { go f(host) } su.mu.Unlock() } func (su *Supervisor) supervise(blockFunc func() error) error { host := createTaskHost(su) su.notifyServe(host) atomic.StoreUint32(&su.closedByInterruptHandler, 0) if su.nonBlocking { go func() { err := blockFunc() if err != nil { su.waiter.Fail(err) } err = su.validateErr(err) su.notifyErr(err) }() return nil } err := blockFunc() err = su.validateErr(err) su.notifyErr(err) return err } // Wait blocks until server is up and running or a serve failure. func (su *Supervisor) Wait(ctx context.Context) error { return su.waiter.Wait(ctx) } // Serve accepts incoming connections on the Listener l, creating a // new service goroutine for each. The service goroutines read requests and // then call su.server.Handler to reply to them. // // For HTTP/2 support, server.TLSConfig should be initialized to the // provided listener's TLS Config before calling Serve. If // server.TLSConfig is non-nil and doesn't include the string "h2" in // Config.NextProtos, HTTP/2 support is not enabled. // // Serve always returns a non-nil error. After Shutdown or Close, the // returned error is http.ErrServerClosed. func (su *Supervisor) Serve(l net.Listener) error { su.setAddress(l.Addr().String()) return su.supervise(func() error { return su.Server.Serve(l) }) } // ListenAndServe listens on the TCP network address addr // and then calls Serve with handler to handle requests // on incoming connections. // Accepted connections are configured to enable TCP keep-alives. func (su *Supervisor) ListenAndServe() error { l, err := su.newListener() if err != nil { return err } return su.Serve(l) } func loadCertificate(c, k string) (*tls.Certificate, error) { var ( cert tls.Certificate err error ) if fileExists(c) && fileExists(k) { // act them as files in the system. cert, err = tls.LoadX509KeyPair(c, k) } else { // act them as raw contents. cert, err = tls.X509KeyPair([]byte(c), []byte(k)) } if err != nil { return nil, err } return &cert, nil } // ListenAndServeTLS acts identically to ListenAndServe, except that it // expects HTTPS connections. Additionally, files containing a certificate and // matching private key for the server must be provided. If the certificate // is signed by a certificate authority, the certFile should be the concatenation // of the server's certificate, any intermediates, and the CA's certificate. func (su *Supervisor) ListenAndServeTLS(certFileOrContents string, keyFileOrContents string) error { var getCertificate func(*tls.ClientHelloInfo) (*tls.Certificate, error) // If tls.Config configured manually through a host configurator then skip that // and let the redirection service registered alone. // e.g. https://github.com/kataras/iris/issues/1481#issuecomment-605621255 if su.Server.TLSConfig == nil { if certFileOrContents == "" && keyFileOrContents == "" { return errors.New("empty certFileOrContents or keyFileOrContents and Server.TLSConfig") } cert, err := loadCertificate(certFileOrContents, keyFileOrContents) if err != nil { return err } getCertificate = func(*tls.ClientHelloInfo) (*tls.Certificate, error) { return cert, nil } } su.manuallyTLS = true return su.runTLS(getCertificate, nil) } // ListenAndServeAutoTLS acts identically to ListenAndServe, except that it // expects HTTPS connections. Server's certificates are auto generated from LETSENCRYPT using // the golang/x/net/autocert package. // // The whitelisted domains are separated by whitespace in "domain" argument, i.e "iris-go.com". // If empty, all hosts are currently allowed. This is not recommended, // as it opens a potential attack where clients connect to a server // by IP address and pretend to be asking for an incorrect host name. // Manager will attempt to obtain a certificate for that host, incorrectly, // eventually reaching the CA's rate limit for certificate requests // and making it impossible to obtain actual certificates. // // For an "e-mail" use a non-public one, letsencrypt needs that for your own security. // // The "cacheDir" is being, optionally, used to provide cache // stores and retrieves previously-obtained certificates. // If empty, certs will only be cached for the lifetime of the auto tls manager. // // Note: The domain should be like "iris-go.com www.iris-go.com", // the e-mail like "kataras2006@hotmail.com" and the cacheDir like "letscache" // The `ListenAndServeAutoTLS` will start a new server for you, // which will redirect all http versions to their https, including subdomains as well. func (su *Supervisor) ListenAndServeAutoTLS(domain string, email string, cacheDir string) error { var ( cache autocert.Cache hostPolicy autocert.HostPolicy ) if cacheDir != "" { cache = autocert.DirCache(cacheDir) } if strings.TrimSpace(domain) != "" { domains := strings.Split(domain, " ") su.FriendlyAddr = strings.Join(domains, ", ") hostPolicy = autocert.HostWhitelist(domains...) } su.autoTLS = true autoTLSManager := &autocert.Manager{ Prompt: autocert.AcceptTOS, HostPolicy: hostPolicy, Email: email, Cache: cache, } return su.runTLS(autoTLSManager.GetCertificate, autoTLSManager.HTTPHandler) } func (su *Supervisor) runTLS(getCertificate func(*tls.ClientHelloInfo) (*tls.Certificate, error), challengeHandler func(fallback http.Handler) http.Handler) error { if su.manuallyTLS && !su.disableHTTP1ToHTTP2Redirection { // If manual TLS and auto-redirection is enabled, // then create an empty challenge handler so the :80 server starts. challengeHandler = func(h http.Handler) http.Handler { // it is always nil on manual TLS. target, _ := url.Parse("https://" + netutil.ResolveVHost(su.Server.Addr)) // e.g. https://localhost:443 http1Handler := RedirectHandler(target, http.StatusMovedPermanently) return http1Handler } } if challengeHandler != nil { http1Server := &http.Server{ Addr: ":http", Handler: challengeHandler(nil), // nil for redirection. ReadTimeout: su.Server.ReadTimeout, ReadHeaderTimeout: su.Server.ReadHeaderTimeout, WriteTimeout: su.Server.WriteTimeout, IdleTimeout: su.Server.IdleTimeout, MaxHeaderBytes: su.Server.MaxHeaderBytes, } if su.Fallback == nil { if !su.manuallyTLS && su.disableHTTP1ToHTTP2Redirection { // automatic redirection was disabled but Fallback was not registered. return fmt.Errorf("autotls: use iris.AutoTLSNoRedirect instead") } go http1Server.ListenAndServe() } else { // if it's manual TLS still can have its own Fallback server here, // the handler will be the redirect one, the difference is that it can run on any port. srv := su.Fallback(challengeHandler) if srv == nil { if !su.manuallyTLS { return fmt.Errorf("autotls: relies on an HTTP/1.1 server") } // for any case the end-developer decided to return nil here, // we proceed with the automatic redirection. srv = http1Server go srv.ListenAndServe() } else { if srv.Addr == "" { srv.Addr = ":http" } // } else if !su.manuallyTLS && srv.Addr != ":80" && srv.Addr != ":http" { // hostname, _, _ := net.SplitHostPort(su.Server.Addr) // return fmt.Errorf("autotls: The HTTP-01 challenge relies on http://%s:80/.well-known/acme-challenge/", hostname) // } if srv.Handler == nil { // handler was nil, caller wanted to change the server's options like read/write timeout. srv.Handler = http1Server.Handler go srv.ListenAndServe() // automatically start it, we assume the above ^ } http1Server = srv // to register the shutdown event. } } su.RegisterOnShutdown(func() { timeout := 10 * time.Second ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() http1Server.Shutdown(ctx) }) } if su.Server.TLSConfig == nil { // If tls.Config is NOT configured manually through a host configurator, // then create it. su.Server.TLSConfig = &tls.Config{ MinVersion: tls.VersionTLS12, GetCertificate: getCertificate, PreferServerCipherSuites: true, NextProtos: []string{"h2", "http/1.1"}, CurvePreferences: []tls.CurveID{ tls.CurveP521, tls.CurveP384, tls.CurveP256, }, CipherSuites: []uint16{ tls.TLS_AES_128_GCM_SHA256, tls.TLS_CHACHA20_POLY1305_SHA256, tls.TLS_AES_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, // tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, G402: TLS Bad Cipher Suite 0xC028, /* TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384 */ }, } } ln, err := netutil.TCP(su.Server.Addr, su.SocketSharding) if err != nil { return err } return su.supervise(func() error { return su.Server.ServeTLS(ln, "", "") }) } // RegisterOnShutdown registers a function to call on Shutdown. // This can be used to gracefully shutdown connections that have // undergone NPN/ALPN protocol upgrade or that have been hijacked. // This function should start protocol-specific graceful shutdown, // but should not wait for shutdown to complete. // // Callbacks will run as separate go routines. func (su *Supervisor) RegisterOnShutdown(cb func()) { su.Server.RegisterOnShutdown(cb) } // Shutdown gracefully shuts down the server without interrupting any // active connections. Shutdown works by first closing all open // listeners, then closing all idle connections, and then waiting // indefinitely for connections to return to idle and then shut down. // If the provided context expires before the shutdown is complete, // then the context's error is returned. // // Shutdown does not attempt to close nor wait for hijacked // connections such as WebSockets. The caller of Shutdown should // separately notify such long-lived connections of shutdown and wait // for them to close, if desired. func (su *Supervisor) Shutdown(ctx context.Context) error { if ctx == nil { ctx = context.Background() } return su.Server.Shutdown(ctx) } func (su *Supervisor) shutdownOnInterrupt(ctx context.Context) { atomic.StoreUint32(&su.closedByInterruptHandler, 1) su.Shutdown(ctx) } // fileExists tries to report whether a local physical file of "filename" exists. func fileExists(filename string) bool { info, err := os.Stat(filename) if err != nil { return false } return !info.IsDir() } ================================================ FILE: core/host/supervisor_task_example_test.go ================================================ // white-box testing package host import ( "context" "fmt" "net/http" "time" ) func ExampleSupervisor_RegisterOnError() { su := New(&http.Server{Addr: ":8273", Handler: http.DefaultServeMux}) su.RegisterOnError(func(err error) { fmt.Println(err.Error()) }) su.RegisterOnError(func(err error) { fmt.Println(err.Error()) }) su.RegisterOnError(func(err error) { fmt.Println(err.Error()) }) go su.ListenAndServe() time.Sleep(1 * time.Second) if err := su.Shutdown(context.TODO()); err != nil { panic(err) } time.Sleep(1 * time.Second) // Output: // http: Server closed // http: Server closed // http: Server closed } ================================================ FILE: core/host/supervisor_test.go ================================================ // white-box testing package host import ( "bytes" "context" "crypto/tls" "log" "net" "net/http" "strings" "sync" "testing" "time" "github.com/iris-contrib/httpexpect/v2" ) const ( debugMode = false ) func newTester(t *testing.T, baseURL string, handler http.Handler) *httpexpect.Expect { var transporter http.RoundTripper if strings.HasPrefix(baseURL, "http") { // means we are testing real serve time transporter = &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true, MinVersion: tls.VersionTLS13}, } } else { // means we are testing the handler itself transporter = httpexpect.NewBinder(handler) } testConfiguration := httpexpect.Config{ BaseURL: baseURL, Client: &http.Client{ Transport: transporter, Jar: httpexpect.NewCookieJar(), }, Reporter: httpexpect.NewAssertReporter(t), } if debugMode { testConfiguration.Printers = []httpexpect.Printer{ httpexpect.NewDebugPrinter(t, true), } } return httpexpect.WithConfig(testConfiguration) } func testSupervisor(t *testing.T, creator func(*http.Server, []func(TaskHost)) *Supervisor) { loggerOutput := &bytes.Buffer{} logger := log.New(loggerOutput, "", 0) mu := new(sync.RWMutex) const ( expectedHelloMessage = "Hello\n" ) // http routing expectedBody := "this is the response body\n" mux := http.NewServeMux() mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { _, err := w.Write([]byte(expectedBody)) if err != nil { t.Fatal(err) } }) // host (server wrapper and adapter) construction srv := &http.Server{Handler: mux, ErrorLog: logger} addr := "localhost:5525" // serving ln, err := net.Listen("tcp4", addr) if err != nil { t.Fatal(err) } helloMe := func(_ TaskHost) { mu.Lock() logger.Print(expectedHelloMessage) mu.Unlock() } host := creator(srv, []func(TaskHost){helloMe}) defer host.Shutdown(context.TODO()) go host.Serve(ln) // http testsing and various calls // no need for time sleep because the following will take some time by theirselves tester := newTester(t, "http://"+addr, mux) tester.Request("GET", "/").Expect().Status(http.StatusOK).Body().IsEqual(expectedBody) // WARNING: Data Race here because we try to read the logs // but it's "safe" here. time.Sleep(500 * time.Millisecond) // wait a bit // testing Task (recorded) message: mu.RLock() got := loggerOutput.String() mu.RUnlock() if expectedHelloMessage != got { t.Fatalf("expected hello Task's message to be '%s' but got '%s'", expectedHelloMessage, got) } } func TestSupervisor(t *testing.T) { testSupervisor(t, func(srv *http.Server, tasks []func(TaskHost)) *Supervisor { su := New(srv) for _, t := range tasks { su.RegisterOnServe(t) } return su }) } ================================================ FILE: core/host/task.go ================================================ package host // the 24hour name was "Supervisor" but it's not cover its usage // 100%, best name is Task or Thead, I'll chouse Task. // and re-name the host to "Supervisor" because that is the really // supervisor. import ( "context" "errors" "fmt" "io" "net" "net/http" "strings" "time" "github.com/kataras/iris/v12/core/netutil" ) // WriteStartupLogOnServe is a task which accepts a logger(io.Writer) // and logs the listening address // by a generated message based on the host supervisor's server and writes it to the "w". // This function should be registered on Serve. func WriteStartupLogOnServe(w io.Writer) func(TaskHost) { return func(h TaskHost) { guessScheme := netutil.ResolveScheme(h.Supervisor.autoTLS || h.Supervisor.manuallyTLS || h.Supervisor.Fallback != nil) addr := h.Supervisor.FriendlyAddr if addr == "" { addr = h.Supervisor.Server.Addr } var listeningURIs = make([]string, 0, 1) if host, port, err := net.SplitHostPort(addr); err == nil { // Improve for the issue #2175. if host == "" || host == "0.0.0.0" { if ifaces, err := net.Interfaces(); err == nil { var ips []string for _, i := range ifaces { addrs, err := i.Addrs() if err != nil { continue } for _, localAddr := range addrs { var ip net.IP switch v := localAddr.(type) { case *net.IPNet: ip = v.IP case *net.IPAddr: ip = v.IP } if ip != nil && ip.To4() != nil { if !ip.IsPrivate() { // let's don't print ips that are not accessible through browser. continue } ips = append(ips, ip.String()) } } } for _, ip := range ips { listeningURI := netutil.ResolveURL(guessScheme, fmt.Sprintf("%s:%s", ip, port)) listeningURI = "> Network: " + listeningURI listeningURIs = append(listeningURIs, listeningURI) } } } } // if len(listeningURIs) == 0 { // ^ check no need, we want to print the virtual addr too. listeningURI := netutil.ResolveURL(guessScheme, addr) if len(listeningURIs) > 0 { listeningURIs[0] = "\n" + listeningURIs[0] listeningURI = "> Local: " + listeningURI } listeningURIs = append(listeningURIs, listeningURI) /* Now listening on: > Network: http://192.168.1.109:8080 > Network: http://172.25.224.1:8080 > Local: http://localhost:8080 Otherwise: Now listening on: http://192.168.1.109:8080 */ _, _ = fmt.Fprintf(w, "Now listening on: %s\nApplication started. Press CTRL+C to shut down.\n", strings.Join(listeningURIs, "\n")) } } // ShutdownOnInterrupt terminates the supervisor and its underline server when CMD+C/CTRL+C pressed. // This function should be registered on Interrupt. func ShutdownOnInterrupt(su *Supervisor, shutdownTimeout time.Duration) func() { return func() { ctx, cancel := context.WithTimeout(context.Background(), shutdownTimeout) defer cancel() su.shutdownOnInterrupt(ctx) } } // TaskHost contains all the necessary information // about the host supervisor, its server // and the exports the whole flow controller of it. type TaskHost struct { Supervisor *Supervisor } // Serve can (re)run the server with the latest known configuration. func (h TaskHost) Serve() error { // the underline server's serve, using the "latest known" listener from the supervisor. l, err := h.Supervisor.newListener() if err != nil { return err } // if http.serverclosed ignore the error, it will have this error // from the previous close if err := h.Supervisor.Server.Serve(l); !errors.Is(err, http.ErrServerClosed) { return err } return nil } // HostURL returns the listening full url (scheme+host) // based on the supervisor's server's address. func (h TaskHost) HostURL() string { return netutil.ResolveURLFromServer(h.Supervisor.Server) } // Hostname returns the underline server's hostname. func (h TaskHost) Hostname() string { return netutil.ResolveHostname(h.Supervisor.Server.Addr) } // Shutdown gracefully shuts down the server without interrupting any // active connections. Shutdown works by first closing all open // listeners, then closing all idle connections, and then waiting // indefinitely for connections to return to idle and then shut down. // If the provided context expires before the shutdown is complete, // then the context's error is returned. // // Shutdown does not attempt to close nor wait for hijacked // connections such as WebSockets. The caller of Shutdown should // separately notify such long-lived connections of shutdown and wait // for them to close, if desired. // // This Shutdown calls the underline's Server's Shutdown, in order to be able to re-start the server // from a task. func (h TaskHost) Shutdown(ctx context.Context) error { // the underline server's Shutdown (otherwise we will cancel all tasks and do cycles) return h.Supervisor.Server.Shutdown(ctx) } func createTaskHost(su *Supervisor) TaskHost { return TaskHost{ Supervisor: su, } } ================================================ FILE: core/host/waiter.go ================================================ package host import ( "context" "fmt" "math" "net" "sync" "time" ) // Waiter is a helper for waiting for a server to be up and running. type Waiter struct { defaultMaxRetries int addressFunc func() string failure error // or runError for app.Run. mu sync.RWMutex } // NewWaiter returns a new Waiter. func NewWaiter(defaultMaxRetries int, addressFunc func() string) *Waiter { if defaultMaxRetries <= 0 { defaultMaxRetries = 7 // 256 seconds max. } return &Waiter{ defaultMaxRetries: defaultMaxRetries, addressFunc: addressFunc, } } // Wait blocks the main goroutine until the application is up and running. func (w *Waiter) Wait(ctx context.Context) error { // First check if there is an error already from Done. if err := w.getFailure(); err != nil { return err } // Set the base for exponential backoff. base := 2.0 // Get the maximum number of retries by context or force to default max retries (e.g. 7). var maxRetries int // Get the deadline of the context. if deadline, ok := ctx.Deadline(); ok { now := time.Now() timeout := deadline.Sub(now) maxRetries = getMaxRetries(timeout, base) } else { maxRetries = w.defaultMaxRetries } // Set the initial retry interval. retryInterval := time.Second return w.tryConnect(ctx, w.addressFunc, maxRetries, retryInterval, base) } // getMaxRetries calculates the maximum number of retries from the retry interval and the base. func getMaxRetries(retryInterval time.Duration, base float64) int { // Convert the retry interval to seconds. seconds := retryInterval.Seconds() // Apply the inverse formula. retries := math.Log(seconds)/math.Log(base) - 1 return int(math.Round(retries)) } // tryConnect tries to connect to the server with the given context and retry parameters. func (w *Waiter) tryConnect(ctx context.Context, addressFunc func() string, maxRetries int, retryInterval time.Duration, base float64) error { // Try to connect to the server in a loop. for i := 0; i < maxRetries; i++ { // Check the context before each attempt. select { case <-ctx.Done(): // Context is canceled, return the context error. return ctx.Err() default: address := addressFunc() // Get this server's listening address. if address == "" { i-- // Note that this may be modified at another go routine of the serve method. So it may be empty at first chance. So retry fetching the VHost every 1 second. time.Sleep(time.Second) continue } // Context is not canceled, proceed with the attempt. conn, err := net.Dial("tcp", address) if err == nil { // Connection successful, close the connection and return nil. conn.Close() return nil // exit. } // ignore error. // Connection failed, wait for the retry interval and try again. time.Sleep(retryInterval) // After each failed attempt, check the server Run's error again. if err := w.getFailure(); err != nil { return err } // Increase the retry interval by the base raised to the power of the number of attempts. /* 0 2 seconds 1 4 seconds 2 8 seconds 3 ~16 seconds 4 ~32 seconds 5 ~64 seconds 6 ~128 seconds 7 ~256 seconds 8 ~512 seconds ... */ retryInterval = time.Duration(math.Pow(base, float64(i+1))) * time.Second } } // All attempts failed, return an error. return fmt.Errorf("failed to connect to the server after %d retries", maxRetries) } // Fail is called by the server's Run method when the server failed to start. func (w *Waiter) Fail(err error) { w.mu.Lock() w.failure = err w.mu.Unlock() } func (w *Waiter) getFailure() error { w.mu.RLock() err := w.failure w.mu.RUnlock() return err } ================================================ FILE: core/memstore/gob.go ================================================ package memstore import ( "bytes" "encoding/gob" "io" "time" ) // why? // on the future we may change how these encoders/decoders // and we may need different method for store and other for entry. func init() { gob.Register(Store{}) gob.Register(Entry{}) gob.Register(time.Time{}) } // GobEncode accepts a store and writes // as series of bytes to the "w" writer. func GobEncode(store Store, w io.Writer) error { enc := gob.NewEncoder(w) err := enc.Encode(store) return err } // GobSerialize same as GobEncode but it returns // the bytes using a temp buffer. func GobSerialize(store Store) ([]byte, error) { w := new(bytes.Buffer) err := GobEncode(store, w) return w.Bytes(), err } // GobEncodeEntry accepts an entry and writes // as series of bytes to the "w" writer. func GobEncodeEntry(entry Entry, w io.Writer) error { enc := gob.NewEncoder(w) err := enc.Encode(entry) return err } // GobSerializeEntry same as GobEncodeEntry but it returns // the bytes using a temp buffer. func GobSerializeEntry(entry Entry) ([]byte, error) { w := new(bytes.Buffer) err := GobEncodeEntry(entry, w) return w.Bytes(), err } // GobDecode accepts a series of bytes and returns // the store. func GobDecode(b []byte) (store Store, err error) { dec := gob.NewDecoder(bytes.NewBuffer(b)) // no reference because of: // gob: decoding into local type *memstore.Store, received remote type Entry err = dec.Decode(&store) return } // GobDecodeEntry accepts a series of bytes and returns // the entry. func GobDecodeEntry(b []byte) (entry Entry, err error) { dec := gob.NewDecoder(bytes.NewBuffer(b)) err = dec.Decode(&entry) return } ================================================ FILE: core/memstore/lifetime.go ================================================ package memstore import ( "sync" "time" ) var ( // Clock is the default clock to get the current time, // it can be used for testing purposes too. // // Defaults to time.Now. Clock func() time.Time = time.Now // ExpireDelete may be set on Cookie.Expire for expiring the given cookie. ExpireDelete = time.Unix(0, 0) // time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC) ) // LifeTime controls the session expiration datetime. type LifeTime struct { // Remember, tip for the future: // No need of gob.Register, because we embed the time.Time. // And serious bug which has a result of me spending my whole evening: // Because of gob encoding it doesn't encodes/decodes the other fields if time.Time is embedded // (this should be a bug(go1.9-rc1) or not. We don't care atm) time.Time timer *time.Timer // StartTime holds the Now of the Begin. Begun time.Time mu sync.RWMutex } // NewLifeTime returns a pointer to an empty LifeTime instance. func NewLifeTime() *LifeTime { return &LifeTime{} } // Begin will begin the life based on the Clock (time.Now()).Add(d). // Use `Continue` to continue from a stored time(database-based session does that). func (lt *LifeTime) Begin(d time.Duration, onExpire func()) { if d <= 0 { return } now := Clock() lt.mu.Lock() lt.Begun = now lt.Time = now.Add(d) lt.timer = time.AfterFunc(d, onExpire) lt.mu.Unlock() } // Revive will continue the life based on the stored Time. // Other words that could be used for this func are: Continue, Restore, Resc. func (lt *LifeTime) Revive(onExpire func()) { lt.mu.RLock() t := lt.Time lt.mu.RUnlock() if t.IsZero() { return } now := Clock() if t.After(now) { d := t.Sub(now) lt.mu.Lock() if lt.timer != nil { lt.timer.Stop() // Stop the existing timer, if any. } lt.Begun = now lt.timer = time.AfterFunc(d, onExpire) // and execute on-time the new onExpire function. lt.mu.Unlock() } } // Shift resets the lifetime based on "d". func (lt *LifeTime) Shift(d time.Duration) { lt.mu.Lock() if d > 0 && lt.timer != nil { now := Clock() lt.Begun = now lt.Time = now.Add(d) lt.timer.Reset(d) } lt.mu.Unlock() } // ExpireNow reduce the lifetime completely. func (lt *LifeTime) ExpireNow() { lt.mu.Lock() lt.Time = ExpireDelete if lt.timer != nil { lt.timer.Stop() } lt.mu.Unlock() } // HasExpired reports whether "lt" represents is expired. func (lt *LifeTime) HasExpired() bool { lt.mu.RLock() t := lt.Time lt.mu.RUnlock() if t.IsZero() { return false } return t.Before(Clock()) } // DurationUntilExpiration returns the duration until expires, it can return negative number if expired, // a call to `HasExpired` may be useful before calling this `Dur` function. func (lt *LifeTime) DurationUntilExpiration() time.Duration { lt.mu.RLock() t := lt.Time lt.mu.RUnlock() return t.Sub(Clock()) } ================================================ FILE: core/memstore/memstore.go ================================================ // Package memstore contains a store which is just // a collection of key-value entries with immutability capabilities. // // Developers can use that storage to their own apps if they like its behavior. // It's fast and in the same time you get read-only access (safety) when you need it. package memstore import ( "fmt" "math" "reflect" "strconv" "strings" "time" ) type ( // ValueSetter is the interface which can be accepted as a generic solution of RequestParams or memstore when Set is the only requirement, // i.e internally on macro/template/TemplateParam#Eval:paramChanger. ValueSetter interface { Set(key string, newValue any) (Entry, bool) } // Entry is the entry of the context storage Store - .Values() Entry struct { Key string `json:"key" msgpack:"key" yaml:"Key" toml:"Value"` ValueRaw any `json:"value" msgpack:"value" yaml:"Value" toml:"Value"` immutable bool // if true then it can't change by its caller. } // StringEntry is just a key-value wrapped by a struct. // See Context.URLParamsSorted method. StringEntry struct { Key string `json:"key" msgpack:"key" yaml:"Key" toml:"Value"` Value string `json:"value" msgpack:"value" yaml:"Value" toml:"Value"` } // Store is a collection of key-value entries with immutability capabilities. Store []Entry ) var _ ValueSetter = (*Store)(nil) // GetByKindOrNil will try to get this entry's value of "k" kind, // if value is not that kind it will NOT try to convert it the "k", instead // it will return nil, except if boolean; then it will return false // even if the value was not bool. // // If the "k" kind is not a string or int or int64 or bool // then it will return the raw value of the entry as it's. func (e Entry) GetByKindOrNil(k reflect.Kind) any { switch k { case reflect.String: v := e.StringDefault("__$nf") if v == "__$nf" { return nil } return v case reflect.Int: v, err := e.IntDefault(-1) if err != nil || v == -1 { return nil } return v case reflect.Int64: v, err := e.Int64Default(-1) if err != nil || v == -1 { return nil } return v case reflect.Bool: v, err := e.BoolDefault(false) if err != nil { return nil } return v default: return e.ValueRaw } } // StringDefault returns the entry's value as string. // If not found returns "def". func (e Entry) StringDefault(def string) string { v := e.ValueRaw if v == nil { return def } if vString, ok := v.(string); ok { return vString } val := fmt.Sprintf("%v", v) if val != "" { return val } return def } // String returns the entry's value as string. func (e Entry) String() string { return e.StringDefault("") } // StringTrim returns the entry's string value without trailing spaces. func (e Entry) StringTrim() string { return strings.TrimSpace(e.String()) } // ErrEntryNotFound may be returned from memstore methods if // a key (with a certain kind) was not found. // Usage: // // var e *ErrEntryNotFound // errors.As(err, &e) // To check for specific key error: // errors.As(err, &ErrEntryNotFound{Key: "key"}) // To check for specific key-kind error: // errors.As(err, &ErrEntryNotFound{Key: "key", Kind: reflect.Int}) type ErrEntryNotFound struct { Key string // the entry's key. Kind reflect.Kind // i.e bool, int, string... Type reflect.Type // i.e time.Time{} or custom struct. } func (e *ErrEntryNotFound) Error() string { return fmt.Sprintf("not found: %s as %s (%s)", e.Key, e.Kind.String(), e.Type.String()) } // As can be used to manually check if the error came from the memstore // is a not found entry, the key must much in order to return true. // Usage: // errors.As(err, &ErrEntryNotFound{Key: "key", Kind: reflect.Int}) // // Do NOT use this method directly, prefer` errors.As` method as explained above. // // Implements: go/src/errors/wrap.go#84 func (e *ErrEntryNotFound) As(target any) bool { v, ok := target.(*ErrEntryNotFound) if !ok { return false } if v.Key != "" && v.Key != e.Key { return false } if v.Kind != reflect.Invalid && v.Kind != e.Kind { return false } return true } func (e Entry) notFound(typ reflect.Type) *ErrEntryNotFound { return &ErrEntryNotFound{Key: e.Key, Kind: typ.Kind(), Type: typ} } var intType = reflect.TypeOf(int(0)) // IntDefault returns the entry's value as int. // If not found returns "def" and a non-nil error. func (e Entry) IntDefault(def int) (int, error) { v := e.ValueRaw if v == nil { return def, e.notFound(intType) } switch vv := v.(type) { case string: val, err := strconv.Atoi(vv) if err != nil { return def, err } return val, nil case int: return vv, nil case int8: return int(vv), nil case int16: return int(vv), nil case int32: return int(vv), nil case int64: return int(vv), nil case uint: return int(vv), nil case uint8: return int(vv), nil case uint16: return int(vv), nil case uint32: return int(vv), nil case uint64: return int(vv), nil } return def, e.notFound(intType) } var int8Type = reflect.TypeOf(int8(0)) // Int8Default returns the entry's value as int8. // If not found returns "def" and a non-nil error. func (e Entry) Int8Default(def int8) (int8, error) { v := e.ValueRaw if v == nil { return def, e.notFound(int8Type) } switch vv := v.(type) { case string: val, err := strconv.ParseInt(vv, 10, 8) if err != nil { return def, err } return int8(val), nil case int: return int8(vv), nil case int8: return vv, nil case int16: return int8(vv), nil case int32: return int8(vv), nil case int64: return int8(vv), nil case uint: return int8(vv), nil case uint8: return int8(vv), nil case uint16: return int8(vv), nil case uint32: return int8(vv), nil case uint64: return int8(vv), nil } return def, e.notFound(int8Type) } var int16Type = reflect.TypeOf(int16(0)) // Int16Default returns the entry's value as int16. // If not found returns "def" and a non-nil error. func (e Entry) Int16Default(def int16) (int16, error) { v := e.ValueRaw if v == nil { return def, e.notFound(int16Type) } switch vv := v.(type) { case string: val, err := strconv.ParseInt(vv, 10, 16) if err != nil { return def, err } return int16(val), nil case int: return int16(vv), nil case int8: return int16(vv), nil case int16: return vv, nil case int32: return int16(vv), nil case int64: return int16(vv), nil case uint: return int16(vv), nil case uint8: return int16(vv), nil case uint16: return int16(vv), nil case uint32: return int16(vv), nil case uint64: return int16(vv), nil } return def, e.notFound(int16Type) } var int32Type = reflect.TypeOf(int32(0)) // Int32Default returns the entry's value as int32. // If not found returns "def" and a non-nil error. func (e Entry) Int32Default(def int32) (int32, error) { v := e.ValueRaw if v == nil { return def, e.notFound(int32Type) } switch vv := v.(type) { case string: val, err := strconv.ParseInt(vv, 10, 32) if err != nil { return def, err } return int32(val), nil case int: return int32(vv), nil case int8: return int32(vv), nil case int16: return int32(vv), nil case int32: return vv, nil case int64: return int32(vv), nil } return def, e.notFound(int32Type) } var int64Type = reflect.TypeOf(int64(0)) // Int64Default returns the entry's value as int64. // If not found returns "def" and a non-nil error. func (e Entry) Int64Default(def int64) (int64, error) { v := e.ValueRaw if v == nil { return def, e.notFound(int64Type) } switch vv := v.(type) { case string: return strconv.ParseInt(vv, 10, 64) case int64: return vv, nil case int32: return int64(vv), nil case int8: return int64(vv), nil case int: return int64(vv), nil case uint: return int64(vv), nil case uint8: return int64(vv), nil case uint16: return int64(vv), nil case uint32: return int64(vv), nil case uint64: return int64(vv), nil } return def, e.notFound(int64Type) } var uintType = reflect.TypeOf(uint(0)) // UintDefault returns the entry's value as uint. // If not found returns "def" and a non-nil error. func (e Entry) UintDefault(def uint) (uint, error) { v := e.ValueRaw if v == nil { return def, e.notFound(uintType) } x64 := strconv.IntSize == 64 var maxValue uint64 = math.MaxUint32 if x64 { maxValue = math.MaxUint64 } switch vv := v.(type) { case string: val, err := strconv.ParseUint(vv, 10, strconv.IntSize) if err != nil { return def, err } return uint(val), nil case uint: return vv, nil case uint8: return uint(vv), nil case uint16: return uint(vv), nil case uint32: return uint(vv), nil case uint64: if vv > uint64(maxValue) { return def, e.notFound(uintType) } return uint(vv), nil case int: if vv < 0 || vv > int(maxValue) { return def, e.notFound(uintType) } return uint(vv), nil case int8: return uint(vv), nil case int16: return uint(vv), nil case int32: return uint(vv), nil case int64: return uint(vv), nil } return def, e.notFound(uintType) } var uint8Type = reflect.TypeOf(uint8(0)) // Uint8Default returns the entry's value as uint8. // If not found returns "def" and a non-nil error. func (e Entry) Uint8Default(def uint8) (uint8, error) { v := e.ValueRaw if v == nil { return def, e.notFound(uint8Type) } switch vv := v.(type) { case string: val, err := strconv.ParseUint(vv, 10, 8) if err != nil { return def, err } return uint8(val), nil case uint: if vv > math.MaxUint8 { return def, e.notFound(uint8Type) } return uint8(vv), nil case uint8: return vv, nil case uint16: if vv > math.MaxUint8 { return def, e.notFound(uint8Type) } return uint8(vv), nil case uint32: if vv > math.MaxUint8 { return def, e.notFound(uint8Type) } return uint8(vv), nil case uint64: if vv > math.MaxUint8 { return def, e.notFound(uint8Type) } return uint8(vv), nil case int: if vv < 0 || vv > math.MaxUint8 { return def, e.notFound(uint8Type) } return uint8(vv), nil } return def, e.notFound(uint8Type) } var uint16Type = reflect.TypeOf(uint16(0)) // Uint16Default returns the entry's value as uint16. // If not found returns "def" and a non-nil error. func (e Entry) Uint16Default(def uint16) (uint16, error) { v := e.ValueRaw if v == nil { return def, e.notFound(uint16Type) } switch vv := v.(type) { case string: val, err := strconv.ParseUint(vv, 10, 16) if err != nil { return def, err } return uint16(val), nil case uint: if vv > math.MaxUint16 { return def, e.notFound(uint16Type) } return uint16(vv), nil case uint8: return uint16(vv), nil case uint16: return vv, nil case uint32: if vv > math.MaxUint16 { return def, e.notFound(uint16Type) } return uint16(vv), nil case uint64: if vv > math.MaxUint16 { return def, e.notFound(uint16Type) } return uint16(vv), nil case int: if vv < 0 || vv > math.MaxUint16 { return def, e.notFound(uint16Type) } return uint16(vv), nil } return def, e.notFound(uint16Type) } var uint32Type = reflect.TypeOf(uint32(0)) // Uint32Default returns the entry's value as uint32. // If not found returns "def" and a non-nil error. func (e Entry) Uint32Default(def uint32) (uint32, error) { v := e.ValueRaw if v == nil { return def, e.notFound(uint32Type) } switch vv := v.(type) { case string: val, err := strconv.ParseUint(vv, 10, 32) if err != nil { return def, err } return uint32(val), nil case uint: if vv > math.MaxUint32 { return def, e.notFound(uint32Type) } return uint32(vv), nil case uint8: return uint32(vv), nil case uint16: return uint32(vv), nil case uint32: return vv, nil case uint64: if vv > math.MaxUint32 { return def, e.notFound(uint32Type) } return uint32(vv), nil case int32: return uint32(vv), nil case int64: if vv < 0 || vv > math.MaxUint32 { return def, e.notFound(uint32Type) } return uint32(vv), nil } return def, e.notFound(uint32Type) } var uint64Type = reflect.TypeOf(uint64(0)) // Uint64Default returns the entry's value as uint64. // If not found returns "def" and a non-nil error. func (e Entry) Uint64Default(def uint64) (uint64, error) { v := e.ValueRaw if v == nil { return def, e.notFound(uint64Type) } switch vv := v.(type) { case string: val, err := strconv.ParseUint(vv, 10, 64) if err != nil { return def, err } return uint64(val), nil case uint8: return uint64(vv), nil case uint16: return uint64(vv), nil case uint32: return uint64(vv), nil case uint64: return vv, nil case int64: return uint64(vv), nil case int: return uint64(vv), nil } return def, e.notFound(uint64Type) } var float32Type = reflect.TypeOf(float32(0)) // Float32Default returns the entry's value as float32. // If not found returns "def" and a non-nil error. func (e Entry) Float32Default(key string, def float32) (float32, error) { v := e.ValueRaw if v == nil { return def, e.notFound(float32Type) } switch vv := v.(type) { case string: val, err := strconv.ParseFloat(vv, 32) if err != nil { return def, err } return float32(val), nil case float32: return vv, nil case float64: if vv > math.MaxFloat32 { return def, e.notFound(float32Type) } return float32(vv), nil case int: return float32(vv), nil } return def, e.notFound(float32Type) } var float64Type = reflect.TypeOf(float64(0)) // Float64Default returns the entry's value as float64. // If not found returns "def" and a non-nil error. func (e Entry) Float64Default(def float64) (float64, error) { v := e.ValueRaw if v == nil { return def, e.notFound(float64Type) } switch vv := v.(type) { case string: val, err := strconv.ParseFloat(vv, 64) if err != nil { return def, err } return val, nil case float32: return float64(vv), nil case float64: return vv, nil case int: return float64(vv), nil case int64: return float64(vv), nil case uint: return float64(vv), nil case uint64: return float64(vv), nil } return def, e.notFound(float64Type) } var boolType = reflect.TypeOf(false) // BoolDefault returns the user's value as bool. // a string which is "1" or "t" or "T" or "TRUE" or "true" or "True" // or "0" or "f" or "F" or "FALSE" or "false" or "False". // Any other value returns an error. // // If not found returns "def" and a non-nil error. func (e Entry) BoolDefault(def bool) (bool, error) { v := e.ValueRaw if v == nil { return def, e.notFound(boolType) } switch vv := v.(type) { case string: val, err := strconv.ParseBool(vv) if err != nil { return def, err } return val, nil case bool: return vv, nil case int: if vv == 1 { return true, nil } return false, nil } return def, e.notFound(boolType) } var timeType = reflect.TypeOf(time.Time{}) // TimeDefault returns the stored time.Time value based on its "key". // If does not exist or the stored key's value is not a time // it returns the "def" time value and a not found error. func (e Entry) TimeDefault(def time.Time) (time.Time, error) { v := e.ValueRaw if v == nil { return def, e.notFound(timeType) } vv, ok := v.(time.Time) if !ok { return def, nil } return vv, nil } var weekdayType = reflect.TypeOf(time.Weekday(0)) // WeekdayDefault returns the stored time.Weekday value based on its "key". // If does not exist or the stored key's value is not a weekday // it returns the "def" weekday value and a not found error. func (e Entry) WeekdayDefault(def time.Weekday) (time.Weekday, error) { v := e.ValueRaw if v == nil { return def, e.notFound(weekdayType) } vv, ok := v.(time.Weekday) if !ok { return def, nil } return vv, nil } // Value returns the value of the entry, // respects the immutable. func (e Entry) Value() any { if e.immutable { // take its value, no pointer even if set with a reference. vv := reflect.Indirect(reflect.ValueOf(e.ValueRaw)) // return copy of that slice if vv.Type().Kind() == reflect.Slice { newSlice := reflect.MakeSlice(vv.Type(), vv.Len(), vv.Cap()) reflect.Copy(newSlice, vv) return newSlice.Interface() } // return a copy of that map if vv.Type().Kind() == reflect.Map { newMap := reflect.MakeMap(vv.Type()) for _, k := range vv.MapKeys() { newMap.SetMapIndex(k, vv.MapIndex(k)) } return newMap.Interface() } // if was *value it will return value{}. return vv.Interface() } return e.ValueRaw } // Save same as `Set` // However, if "immutable" is true then saves it as immutable (same as `SetImmutable`). // // Returns the entry and true if it was just inserted, meaning that // it will return the entry and a false boolean if the entry exists and it has been updated. func (r *Store) Save(key string, value any, immutable bool) (Entry, bool) { args := *r n := len(args) // replace if we can, else just return for i := 0; i < n; i++ { kv := &args[i] if kv.Key == key { if immutable && kv.immutable { // if called by `SetImmutable` // then allow the update, maybe it's a slice that user wants to update by SetImmutable method, // we should allow this kv.ValueRaw = value kv.immutable = immutable } else if !kv.immutable { // if it was not immutable then user can alt it via `Set` and `SetImmutable` kv.ValueRaw = value kv.immutable = immutable } // else it was immutable and called by `Set` then disallow the update return *kv, false } } // expand slice to add it c := cap(args) if c > n { args = args[:n+1] kv := &args[n] kv.Key = key kv.ValueRaw = value kv.immutable = immutable *r = args return *kv, true } // add kv := Entry{ Key: key, ValueRaw: value, immutable: immutable, } *r = append(args, kv) return kv, true } // Set saves a value to the key-value storage. // Returns the entry and true if it was just inserted, meaning that // it will return the entry and a false boolean if the entry exists and it has been updated. // // See `SetImmutable` and `Get`. func (r *Store) Set(key string, value any) (Entry, bool) { return r.Save(key, value, false) } // SetImmutable saves a value to the key-value storage. // Unlike `Set`, the output value cannot be changed by the caller later on (when .Get OR .Set) // // An Immutable entry should be only changed with a `SetImmutable`, simple `Set` will not work // if the entry was immutable, for your own safety. // // Returns the entry and true if it was just inserted, meaning that // it will return the entry and a false boolean if the entry exists and it has been updated. // // Use it consistently, it's far slower than `Set`. // Read more about muttable and immutable go types: https://stackoverflow.com/a/8021081 func (r *Store) SetImmutable(key string, value any) (Entry, bool) { return r.Save(key, value, true) } var emptyEntry Entry // GetEntry returns a pointer to the "Entry" found with the given "key" // if nothing found then it returns an empty Entry and false. func (r *Store) GetEntry(key string) (Entry, bool) { args := *r n := len(args) for i := 0; i < n; i++ { if kv := args[i]; kv.Key == key { return kv, true } } return emptyEntry, false } // GetEntryAt returns the internal Entry of the memstore based on its index, // the stored index by the router. // If not found then it returns a zero Entry and false. func (r *Store) GetEntryAt(index int) (Entry, bool) { args := *r if len(args) > index { return args[index], true } return emptyEntry, false } // GetDefault returns the entry's value based on its key. // If not found returns "def". // This function checks for immutability as well, the rest don't. func (r *Store) GetDefault(key string, def any) any { v, ok := r.GetEntry(key) if !ok || v.ValueRaw == nil { return def } vv := v.Value() if vv == nil { return def } return vv } // Exists is a small helper which reports whether a key exists. // It's not recommended to be used outside of templates. // Use Get or GetEntry instead which will give you back the entry value too, // so you don't have to loop again the key-value storage to get its value. func (r *Store) Exists(key string) bool { _, ok := r.GetEntry(key) return ok } // Get returns the entry's value based on its key. // If not found returns nil. func (r *Store) Get(key string) any { return r.GetDefault(key, nil) } // GetOrSet is like `GetDefault` but it accepts a function which is // fired and its result is used to `Set` if // the "key" was not found or its value is nil. func (r *Store) GetOrSet(key string, setFunc func() any) any { if v, ok := r.GetEntry(key); ok && v.ValueRaw != nil { return v.Value() } value := setFunc() r.Set(key, value) return value } // Visit accepts a visitor which will be filled // by the key-value objects. func (r *Store) Visit(visitor func(key string, value any)) { args := *r for i, n := 0, len(args); i < n; i++ { kv := args[i] visitor(kv.Key, kv.Value()) } } // GetStringDefault returns the entry's value as string, based on its key. // If not found returns "def". func (r *Store) GetStringDefault(key string, def string) string { v, ok := r.GetEntry(key) if !ok { return def } return v.StringDefault(def) } // GetString returns the entry's value as string, based on its key. func (r *Store) GetString(key string) string { return r.GetStringDefault(key, "") } // GetStringTrim returns the entry's string value without trailing spaces. func (r *Store) GetStringTrim(name string) string { return strings.TrimSpace(r.GetString(name)) } // GetInt returns the entry's value as int, based on its key. // If not found returns -1 and a non-nil error. func (r *Store) GetInt(key string) (int, error) { v, ok := r.GetEntry(key) if !ok { return 0, v.notFound(intType) } return v.IntDefault(-1) } // GetIntDefault returns the entry's value as int, based on its key. // If not found returns "def". func (r *Store) GetIntDefault(key string, def int) int { if v, err := r.GetInt(key); err == nil { return v } return def } // GetInt8 returns the entry's value as int8, based on its key. // If not found returns -1 and a non-nil error. func (r *Store) GetInt8(key string) (int8, error) { v, ok := r.GetEntry(key) if !ok { return -1, v.notFound(int8Type) } return v.Int8Default(-1) } // GetInt8Default returns the entry's value as int8, based on its key. // If not found returns "def". func (r *Store) GetInt8Default(key string, def int8) int8 { if v, err := r.GetInt8(key); err == nil { return v } return def } // GetInt16 returns the entry's value as int16, based on its key. // If not found returns -1 and a non-nil error. func (r *Store) GetInt16(key string) (int16, error) { v, ok := r.GetEntry(key) if !ok { return -1, v.notFound(int16Type) } return v.Int16Default(-1) } // GetInt16Default returns the entry's value as int16, based on its key. // If not found returns "def". func (r *Store) GetInt16Default(key string, def int16) int16 { if v, err := r.GetInt16(key); err == nil { return v } return def } // GetInt32 returns the entry's value as int32, based on its key. // If not found returns -1 and a non-nil error. func (r *Store) GetInt32(key string) (int32, error) { v, ok := r.GetEntry(key) if !ok { return -1, v.notFound(int32Type) } return v.Int32Default(-1) } // GetInt32Default returns the entry's value as int32, based on its key. // If not found returns "def". func (r *Store) GetInt32Default(key string, def int32) int32 { if v, err := r.GetInt32(key); err == nil { return v } return def } // GetInt64 returns the entry's value as int64, based on its key. // If not found returns -1 and a non-nil error. func (r *Store) GetInt64(key string) (int64, error) { v, ok := r.GetEntry(key) if !ok { return -1, v.notFound(int64Type) } return v.Int64Default(-1) } // GetInt64Default returns the entry's value as int64, based on its key. // If not found returns "def". func (r *Store) GetInt64Default(key string, def int64) int64 { if v, err := r.GetInt64(key); err == nil { return v } return def } // GetUint returns the entry's value as uint, based on its key. // If not found returns 0 and a non-nil error. func (r *Store) GetUint(key string) (uint, error) { v, ok := r.GetEntry(key) if !ok { return 0, v.notFound(uintType) } return v.UintDefault(0) } // GetUintDefault returns the entry's value as uint, based on its key. // If not found returns "def". func (r *Store) GetUintDefault(key string, def uint) uint { if v, err := r.GetUint(key); err == nil { return v } return def } // GetUint8 returns the entry's value as uint8, based on its key. // If not found returns 0 and a non-nil error. func (r *Store) GetUint8(key string) (uint8, error) { v, ok := r.GetEntry(key) if !ok { return 0, v.notFound(uint8Type) } return v.Uint8Default(0) } // GetUint8Default returns the entry's value as uint8, based on its key. // If not found returns "def". func (r *Store) GetUint8Default(key string, def uint8) uint8 { if v, err := r.GetUint8(key); err == nil { return v } return def } // GetUint16 returns the entry's value as uint16, based on its key. // If not found returns 0 and a non-nil error. func (r *Store) GetUint16(key string) (uint16, error) { v, ok := r.GetEntry(key) if !ok { return 0, v.notFound(uint16Type) } return v.Uint16Default(0) } // GetUint16Default returns the entry's value as uint16, based on its key. // If not found returns "def". func (r *Store) GetUint16Default(key string, def uint16) uint16 { if v, err := r.GetUint16(key); err == nil { return v } return def } // GetUint32 returns the entry's value as uint32, based on its key. // If not found returns 0 and a non-nil error. func (r *Store) GetUint32(key string) (uint32, error) { v, ok := r.GetEntry(key) if !ok { return 0, v.notFound(uint32Type) } return v.Uint32Default(0) } // GetUint32Default returns the entry's value as uint32, based on its key. // If not found returns "def". func (r *Store) GetUint32Default(key string, def uint32) uint32 { if v, err := r.GetUint32(key); err == nil { return v } return def } // GetUint64 returns the entry's value as uint64, based on its key. // If not found returns 0 and a non-nil error. func (r *Store) GetUint64(key string) (uint64, error) { v, ok := r.GetEntry(key) if !ok { return 0, v.notFound(uint64Type) } return v.Uint64Default(0) } // GetUint64Default returns the entry's value as uint64, based on its key. // If not found returns "def". func (r *Store) GetUint64Default(key string, def uint64) uint64 { if v, err := r.GetUint64(key); err == nil { return v } return def } // GetFloat64 returns the entry's value as float64, based on its key. // If not found returns -1 and a non nil error. func (r *Store) GetFloat64(key string) (float64, error) { v, ok := r.GetEntry(key) if !ok { return -1, v.notFound(float64Type) } return v.Float64Default(-1) } // GetFloat64Default returns the entry's value as float64, based on its key. // If not found returns "def". func (r *Store) GetFloat64Default(key string, def float64) float64 { if v, err := r.GetFloat64(key); err == nil { return v } return def } // GetBool returns the user's value as bool, based on its key. // a string which is "1" or "t" or "T" or "TRUE" or "true" or "True" // or "0" or "f" or "F" or "FALSE" or "false" or "False". // Any other value returns an error. // // If not found returns false and a non-nil error. func (r *Store) GetBool(key string) (bool, error) { v, ok := r.GetEntry(key) if !ok { return false, v.notFound(boolType) } return v.BoolDefault(false) } // GetBoolDefault returns the user's value as bool, based on its key. // a string which is "1" or "t" or "T" or "TRUE" or "true" or "True" // or "0" or "f" or "F" or "FALSE" or "false" or "False". // // If not found returns "def". func (r *Store) GetBoolDefault(key string, def bool) bool { if v, err := r.GetBool(key); err == nil { return v } return def } var zeroTime = time.Time{} // GetTime returns the stored time.Time value based on its "key". // If does not exist or the stored key's value is not a time // it returns a zero time value and a not found error. func (r *Store) GetTime(key string) (time.Time, error) { v, ok := r.GetEntry(key) if !ok { return zeroTime, v.notFound(timeType) } return v.TimeDefault(zeroTime) } const simpleDateLayout = "2006/01/02" // SimpleDate calls GetTime and formats the time as "yyyyy/mm/dd". // It returns an empty string if the key does not exist or the // stored value on "key" is not a time.Time type. func (r *Store) SimpleDate(key string) string { v, ok := r.GetEntry(key) if !ok { return "" } tt, err := v.TimeDefault(zeroTime) if err != nil { return "" } return tt.Format(simpleDateLayout) } const zeroWeekday = time.Sunday // GetWeekday returns the stored time.Weekday value based on its "key". // If does not exist or the stored key's value is not a weekday // it returns the time.Sunday value and a not found error. func (r *Store) GetWeekday(key string) (time.Weekday, error) { v, ok := r.GetEntry(key) if !ok { return zeroWeekday, v.notFound(timeType) } return v.WeekdayDefault(zeroWeekday) } // Remove deletes an entry linked to that "key", // returns true if an entry is actually removed. func (r *Store) Remove(key string) bool { args := *r n := len(args) for i := 0; i < n; i++ { kv := &args[i] if kv.Key == key { // we found the index, // let's remove the item by appending to the temp and // after set the pointer of the slice to this temp args args = append(args[:i], args[i+1:]...) *r = args return true } } return false } // Reset clears all the request entries. func (r *Store) Reset() { *r = (*r)[0:0] } // Len returns the full length of the entries. func (r *Store) Len() int { args := *r return len(args) } // Serialize returns the byte representation of the current Store. func (r Store) Serialize() []byte { // note: no pointer here, ignore linters if shows up. b, _ := GobSerialize(r) return b } ================================================ FILE: core/memstore/memstore_test.go ================================================ // white-box testing package memstore import ( "bytes" "encoding/json" "fmt" "testing" ) type myTestObject struct { Name string `json:"name"` } func TestMuttable(t *testing.T) { var p Store // slice p.Set("slice", []myTestObject{{"value 1"}, {"value 2"}}) v := p.Get("slice").([]myTestObject) v[0].Name = "modified" vv := p.Get("slice").([]myTestObject) if vv[0].Name != "modified" { t.Fatalf("expected slice to be muttable but caller was not able to change its value") } // map p.Set("map", map[string]myTestObject{"key 1": {"value 1"}, "key 2": {"value 2"}}) vMap := p.Get("map").(map[string]myTestObject) vMap["key 1"] = myTestObject{"modified"} vvMap := p.Get("map").(map[string]myTestObject) if vvMap["key 1"].Name != "modified" { t.Fatalf("expected map to be muttable but caller was not able to change its value") } // object pointer of a value, it can change like maps or slices and arrays. p.Set("objp", &myTestObject{"value"}) // we expect pointer here, as we set it. vObjP := p.Get("objp").(*myTestObject) vObjP.Name = "modified" vvObjP := p.Get("objp").(*myTestObject) if vvObjP.Name != "modified" { t.Fatalf("expected objp to be muttable but caller was able to change its value") } } func TestImmutable(t *testing.T) { var p Store // slice p.SetImmutable("slice", []myTestObject{{"value 1"}, {"value 2"}}) v := p.Get("slice").([]myTestObject) v[0].Name = "modified" vv := p.Get("slice").([]myTestObject) if vv[0].Name == "modified" { t.Fatalf("expected slice to be immutable but caller was able to change its value") } // map p.SetImmutable("map", map[string]myTestObject{"key 1": {"value 1"}, "key 2": {"value 2"}}) vMap := p.Get("map").(map[string]myTestObject) vMap["key 1"] = myTestObject{"modified"} vvMap := p.Get("map").(map[string]myTestObject) if vvMap["key 1"].Name == "modified" { t.Fatalf("expected map to be immutable but caller was able to change its value") } // object value, it's immutable at all cases. p.SetImmutable("obj", myTestObject{"value"}) vObj := p.Get("obj").(myTestObject) vObj.Name = "modified" vvObj := p.Get("obj").(myTestObject) if vvObj.Name == "modified" { t.Fatalf("expected obj to be immutable but caller was able to change its value") } // object pointer of a value, it's immutable at all cases. p.SetImmutable("objp", &myTestObject{"value"}) // we expect no pointer here if SetImmutable. // so it can't be changed by-design vObjP := p.Get("objp").(myTestObject) vObjP.Name = "modified" vvObjP := p.Get("objp").(myTestObject) if vvObjP.Name == "modified" { t.Fatalf("expected objp to be immutable but caller was able to change its value") } } func TestImmutableSetOnlyWithSetImmutable(t *testing.T) { var p Store p.SetImmutable("objp", &myTestObject{"value"}) p.Set("objp", &myTestObject{"modified"}) vObjP := p.Get("objp").(myTestObject) if vObjP.Name == "modified" { t.Fatalf("caller should not be able to change the immutable entry with a simple `Set`") } p.SetImmutable("objp", &myTestObject{"value with SetImmutable"}) vvObjP := p.Get("objp").(myTestObject) if vvObjP.Name != "value with SetImmutable" { t.Fatalf("caller should be able to change the immutable entry with a `SetImmutable`") } } func TestGetInt64Default(t *testing.T) { var p Store p.Set("a uint16", uint16(2)) if v := p.GetInt64Default("a uint16", 0); v != 2 { t.Fatalf("unexpected value of %d", v) } } func TestJSON(t *testing.T) { var p Store p.Set("key1", "value1") p.Set("key2", 2) p.Set("key3", myTestObject{Name: "makis"}) b, err := json.Marshal(p) if err != nil { t.Fatal(err) } expectedJSON := []byte(`[{"key":"key1","value":"value1"},{"key":"key2","value":2},{"key":"key3","value":{"name":"makis"}}]`) if !bytes.Equal(b, expectedJSON) { t.Fatalf("expected: %s but got: %s", string(expectedJSON), string(b)) } var newStore Store if err = json.Unmarshal(b, &newStore); err != nil { t.Fatal(err) } for i, v := range newStore { expected, got := p.Get(v.Key), v.ValueRaw if ex, g := fmt.Sprintf("%v", expected), fmt.Sprintf("%v", got); ex != g { if _, isMap := got.(map[string]any); isMap { // was struct but converted into map (as expected). b1, _ := json.Marshal(expected) b2, _ := json.Marshal(got) if !bytes.Equal(b1, b2) { t.Fatalf("[%d] JSON expected: %s but got: %s", i, string(b1), string(b2)) } continue } t.Fatalf("[%d] expected: %s but got: %s", i, ex, g) } } } ================================================ FILE: core/netutil/addr.go ================================================ package netutil import ( "os" "regexp" "strconv" "strings" ) var ( // LoopbackRegex the regex if matched a host:port is a loopback. LoopbackRegex = regexp.MustCompile(`^localhost$|^127(?:\.[0-9]+){0,2}\.[0-9]+$|^(?:0*\:)*?:?0*1$`) loopbackSubRegex = regexp.MustCompile(`^127(?:\.[0-9]+){0,2}\.[0-9]+$|^(?:0*\:)*?:?0*1$`) machineHostname string ) func init() { machineHostname, _ = os.Hostname() } // IsLoopbackSubdomain checks if a string is a subdomain or a hostname. var IsLoopbackSubdomain = func(s string) bool { if strings.HasPrefix(s, "127.0.0.1:") || s == "127.0.0.1" || strings.HasPrefix(s, "0.0.0.0:") || s == "0.0.0.0" /* let's resolve that without regex (see below)*/ { return true } valid := loopbackSubRegex.MatchString(s) if !valid { // if regex failed to match it, then try with the pc's name. if !strings.Contains(machineHostname, ".") { // if machine name's is not a loopback by itself valid = s == machineHostname } } return valid } // GetLoopbackSubdomain returns the part of the loopback subdomain. func GetLoopbackSubdomain(s string) string { if strings.HasPrefix(s, "127.0.0.1:") || s == "127.0.0.1" || strings.HasPrefix(s, "0.0.0.0:") || s == "0.0.0.0" { return s } return loopbackSubRegex.FindString(s) } // IsLoopbackHost tries to catch the local addresses when a developer // navigates to a subdomain that its hostname differs from Application.Configuration.VHost. // Developer may want to override this function to return always false // in order to not allow different hostname from Application.Configuration.VHost in local environment (remote is not reached). var IsLoopbackHost = func(requestHost string) bool { // this func will be called if we have a subdomain actually, not otherwise, so we are // safe to do some hacks. // if subdomain.127.0.0.1:8080/path, we need to compare the 127.0.0.1 // if subdomain.localhost:8080/mypath, we need to compare the localhost // if subdomain.127.0.0.1/mypath, we need to compare the 127.0.0.1 // if subdomain.127.0.0.1, we need to compare the 127.0.0.1 // find the first index of [:]8080 or [/]mypath or nothing(root with loopback address like 127.0.0.1) // remember: we are not looking for .com or these things, if is up and running then the developer // would probably not want to reach the server with different Application.Configuration.VHost than // he/she declared. portOrPathIdx := strings.LastIndexByte(requestHost, ':') if portOrPathIdx == 0 { // 0.0.0.0:[...]/localhost:[...]/127.0.0.1:[...]/ipv6 local... return true } // this will not catch ipv6 loopbacks like subdomain.0000:0:0000::01.1:8080 // but, again, is for developers only, is hard to try to navigate with something like this, // and if that happened, I provide a way to override the whole "algorithm" to a custom one via "IsLoopbackHost". if portOrPathIdx == -1 { portOrPathIdx = strings.LastIndexByte(requestHost, '/') if portOrPathIdx == -1 { portOrPathIdx = len(requestHost) // if not port or / then it should be something like subodmain.127.0.0.1 } } // remove the left part of subdomain[.]<- and the right part of ->[:]8080/[/]mypath // so result should be 127.0.0.1/localhost/0.0.0.0 or any ip subdomainFinishIdx := strings.IndexByte(requestHost, '.') + 1 if l := len(requestHost); l <= subdomainFinishIdx || l < portOrPathIdx { return false // for any case to not panic here. } hostname := requestHost[subdomainFinishIdx:portOrPathIdx] if hostname == "" { return false } // we use regex here to catch all posibilities, we compiled the regex at init func // so it shouldn't hurt so much, but we don't care a lot because it's a special case here // because this function will be called only if developer him/herself can reach the server // with a loopback/local address, so we are totally safe. valid := LoopbackRegex.MatchString(hostname) if !valid { // if regex failed to match it, then try with the pc's name. valid = hostname == machineHostname } return valid } /* func isLoopbackHostGoVersion(host string) bool { ip := net.ParseIP(host) if ip != nil { return ip.IsLoopback() } // Host is not an ip, perform lookup. addrs, err := net.LookupHost(host) if err != nil { return false } for _, addr := range addrs { if !net.ParseIP(addr).IsLoopback() { return false } } return true } */ const ( // defaultServerHostname returns the default hostname which is "localhost" defaultServerHostname = "localhost" ) // ResolveAddr tries to convert a given string to an address which is compatible with net.Listener and server func ResolveAddr(addr string) string { // check if addr has :port, if not do it +:80 ,we need the hostname for many cases a := addr if a == "" { // check for os environments if oshost := os.Getenv("ADDR"); oshost != "" { a = oshost } else if oshost := os.Getenv("HOST"); oshost != "" { a = oshost } else if oshost := os.Getenv("HOSTNAME"); oshost != "" { a = oshost // check for port also here if osport := os.Getenv("PORT"); osport != "" { a += ":" + osport } } else if osport := os.Getenv("PORT"); osport != "" { a = ":" + osport } else { a = ":http" } } if portIdx := strings.IndexByte(a, ':'); portIdx == 0 { if a[portIdx:] == ":https" { a = defaultServerHostname + ":443" } else { // if contains only :port ,then the : is the first letter, so we dont have set a hostname, lets set it a = defaultServerHostname + a } } return a } // ResolveHostname receives an addr of form host[:port] and returns the hostname part of it // ex: localhost:8080 will return the `localhost`, mydomain.com:8080 will return the 'mydomain' func ResolveHostname(addr string) string { if idx := strings.IndexByte(addr, ':'); idx == 0 { // only port, then return the localhost hostname return "localhost" } else if idx > 0 { return addr[0:idx] } // it's already hostname return addr } // ResolveVHost tries to get the hostname if port is no needed for Addr's usage. // Addr is being used inside router->subdomains // and inside {{ url }} template funcs. // It should be the same as "browser's" // usually they removing :80 or :443. func ResolveVHost(addr string) string { if addr == ":https" || addr == ":http" { return "localhost" } if idx := strings.IndexByte(addr, ':'); idx == 0 { // only port, then return the 0.0.0.0:PORT return /* "0.0.0.0" */ "localhost" + addr[idx:] } else if idx > 0 { // if 0.0.0.0:80 let's just convert it to localhost. if addr[0:idx] == "0.0.0.0" { if addr[idx:] == ":80" { return "localhost" } return "localhost" + addr[idx:] } } // with ':' in order to not replace the ipv6 loopback addresses // addr = strings.Replace(addr, "0.0.0.0:", "localhost:", 1) // some users are confusing from the log output ^. port := ResolvePort(addr) if port == 80 || port == 443 { return ResolveHostname(addr) } return addr } const ( // SchemeHTTPS the "https" url scheme. SchemeHTTPS = "https" // SchemeHTTP the "http" url scheme. SchemeHTTP = "http" ) // ResolvePort receives an addr of form host[:port] and returns the port part of it // ex: localhost:8080 will return the `8080`, mydomain.com will return the '80' func ResolvePort(addr string) int { if portIdx := strings.IndexByte(addr, ':'); portIdx != -1 { afP := addr[portIdx+1:] p, err := strconv.Atoi(afP) if err == nil { return p } else if afP == SchemeHTTPS { // it's not number, check if it's :https return 443 } } return 80 } // ResolveScheme returns "https" if "isTLS" receiver is true, // otherwise "http". func ResolveScheme(isTLS bool) string { if isTLS { return SchemeHTTPS } return SchemeHTTP } // ResolveSchemeFromVHost returns the scheme based on the "vhost". func ResolveSchemeFromVHost(vhost string) string { // pure check isTLS := strings.HasPrefix(vhost, SchemeHTTPS) || ResolvePort(vhost) == 443 return ResolveScheme(isTLS) } // ResolveURL takes the scheme and an address // and returns its URL, pure implementation but it does the job. func ResolveURL(scheme string, addr string) string { host := ResolveVHost(addr) return scheme + "://" + host } ================================================ FILE: core/netutil/addr_test.go ================================================ package netutil import ( "testing" ) func TestIsLoopbackHost(t *testing.T) { tests := []struct { host string valid bool }{ {"subdomain.127.0.0.1:8080", true}, {"subdomain.127.0.0.1", true}, {"subdomain.localhost:8080", true}, {"subdomain.localhost", true}, {"subdomain.127.0000.0000.1:8080", true}, {"subdomain.127.0000.0000.1", true}, {"subdomain.127.255.255.254:8080", true}, {"subdomain.127.255.255.254", true}, {"subdomain.0000:0:0000::01.1:8080", false}, {"subdomain.0000:0:0000::01", false}, {"subdomain.0000:0:0000::01.1:8080", false}, {"subdomain.0000:0:0000::01", false}, {"subdomain.0000:0000:0000:0000:0000:0000:0000:0001:8080", true}, {"subdomain.0000:0000:0000:0000:0000:0000:0000:0001", false}, {"subdomain.example:8080", false}, {"subdomain.example", false}, {"subdomain.example.com:8080", false}, {"subdomain.example.com", false}, {"subdomain.com", false}, {"subdomain", false}, {".subdomain", false}, {"127.0.0.1.com", false}, } for i, tt := range tests { if expected, got := tt.valid, IsLoopbackHost(tt.host); expected != got { t.Fatalf("[%d] expected %t but got %t for %s", i, expected, got, tt.host) } } } ================================================ FILE: core/netutil/client.go ================================================ package netutil import ( "net" "net/http" "time" "github.com/kataras/golog" ) // Client returns a new http.Client using // the "timeout" for open connection. func Client(timeout time.Duration) *http.Client { transport := http.Transport{ Dial: func(network string, addr string) (net.Conn, error) { conn, err := net.DialTimeout(network, addr, timeout) if err != nil { golog.Debugf("%v", err) return nil, err } return conn, err }, } client := &http.Client{ Transport: &transport, } return client } ================================================ FILE: core/netutil/ip.go ================================================ package netutil import ( "bytes" "net" "strings" ) /* Based on: https://husobee.github.io/golang/ip-address/2015/12/17/remote-ip-go.html requested at: https://github.com/kataras/iris/issues/1453 */ // IPRange is a structure that holds the start and end of a range of IP Addresses. type IPRange struct { Start string `ini:"start" json:"start" yaml:"Start" toml:"Start"` End string `ini:"end" json:"end" yaml:"End" toml:"End"` } // IPInRange reports whether a given IP Address is within a range given. func IPInRange(r IPRange, ipAddress net.IP) bool { return bytes.Compare(ipAddress, net.ParseIP(r.Start)) >= 0 && bytes.Compare(ipAddress, net.ParseIP(r.End)) <= 0 } // IPIsPrivateSubnet reports whether this "ipAddress" is in a private subnet. func IPIsPrivateSubnet(ipAddress net.IP, privateRanges []IPRange) bool { // IPv4 for now. if ipCheck := ipAddress.To4(); ipCheck != nil { // iterate over all our ranges. for _, r := range privateRanges { // check if this ip is in a private range. if IPInRange(r, ipAddress) { return true } } } return false } // GetIPAddress returns a valid public IP Address from a collection of IP Addresses // and a range of private subnets. // // Reports whether a valid IP was found. func GetIPAddress(ipAddresses []string, privateRanges []IPRange) (string, bool) { // march from right to left until we get a public address // that will be the address right before our proxy. for i := len(ipAddresses) - 1; i >= 0; i-- { ip := strings.TrimSpace(ipAddresses[i]) realIP := net.ParseIP(ip) if !realIP.IsGlobalUnicast() || IPIsPrivateSubnet(realIP, privateRanges) { // bad address, go to next continue } return ip, true } return "", false } ================================================ FILE: core/netutil/ip_test.go ================================================ package netutil import ( "testing" ) func TestIP(t *testing.T) { privateRanges := []IPRange{ { Start: "10.0.0.0", End: "10.255.255.255", }, { Start: "100.64.0.0", End: "100.127.255.255", }, { Start: "172.16.0.0", End: "172.31.255.255", }, { Start: "192.0.0.0", End: "192.0.0.255", }, { Start: "192.168.0.0", End: "192.168.255.255", }, { Start: "198.18.0.0", End: "198.19.255.255", }, } addresses := []string{ "201.37.138.59", "159.117.3.153", "166.192.97.84", "225.181.213.210", "124.50.84.134", "87.53.250.102", "106.79.33.62", "242.120.17.144", "131.179.101.254", "103.11.11.174", "115.97.0.114", "219.202.120.251", "37.72.123.120", "154.94.78.101", "126.105.144.250", } got, ok := GetIPAddress(addresses, privateRanges) if !ok { t.Logf("expected addr to be matched") } if expected := "126.105.144.250"; expected != got { t.Logf("expected addr to be found: %s but got: %s", expected, got) } addresses = []string{ "10.10.233.1", "126.105.144.250", "192.168.99.33", "172.18.22.23", "10.0.0.0", "10.255.255.255", } got, ok = GetIPAddress(addresses, privateRanges) if !ok { t.Logf("expected addr to be matched") } if expected := "126.105.144.250"; expected != got { t.Logf("expected addr to be found: %s but got: %s", expected, got) } addresses = []string{ "10.0.0.0", "10.10.233.1", "192.168.99.33", "172.18.22.23", } got, ok = GetIPAddress(addresses, privateRanges) if ok { t.Logf("expected addr to not be matched") } } ================================================ FILE: core/netutil/server.go ================================================ package netutil import ( "net/http" ) // used on host/supervisor/task and router/path // IsTLS returns true if the "srv" contains any certificates // or a get certificate function, meaning that is secure. func IsTLS(srv *http.Server) bool { if cfg := srv.TLSConfig; cfg != nil && (len(cfg.Certificates) > 0 || cfg.GetCertificate != nil) { return true } return false } // ResolveSchemeFromServer tries to resolve a url scheme // based on the server's configuration. // Returns "https" on secure server, // otherwise "http". func ResolveSchemeFromServer(srv *http.Server) string { return ResolveScheme(IsTLS(srv)) } // ResolveURLFromServer returns the scheme+host from a server. func ResolveURLFromServer(srv *http.Server) string { scheme := ResolveSchemeFromServer(srv) host := ResolveVHost(srv.Addr) return scheme + "://" + host } ================================================ FILE: core/netutil/tcp.go ================================================ package netutil import ( "context" "crypto/tls" "errors" "fmt" "net" "os" "strings" "time" "golang.org/x/crypto/acme/autocert" ) // tcpKeepAliveListener sets TCP keep-alive timeouts on accepted // connections. It's used by Run, ListenAndServe and ListenAndServeTLS so // dead TCP connections (e.g. closing laptop mid-download) eventually // go away. // // A raw copy of standar library. type tcpKeepAliveListener struct { *net.TCPListener keepAliveDur time.Duration } // Accept accepts tcp connections aka clients. func (l tcpKeepAliveListener) Accept() (net.Conn, error) { tc, err := l.AcceptTCP() if err != nil { return tc, err } if err = tc.SetKeepAlive(true); err != nil { return tc, err } if err = tc.SetKeepAlivePeriod(l.keepAliveDur); err != nil { return tc, err } return tc, nil } // TCP returns a new tcp(ipv6 if supported by network) and an error on failure. func TCP(addr string, reuse bool) (net.Listener, error) { var cfg net.ListenConfig if reuse { cfg.Control = control } return cfg.Listen(context.Background(), "tcp", addr) } // TCPKeepAlive returns a new tcp keep alive Listener and an error on failure. func TCPKeepAlive(addr string, reuse bool, keepAliveDur time.Duration) (ln net.Listener, err error) { // if strings.HasPrefix(addr, "127.0.0.1") { // // it's ipv4, use ipv4 tcp listener instead of the default ipv6. Don't. // ln, err = net.Listen("tcp4", addr) // } else { // ln, err = TCP(addr) // } ln, err = TCP(addr, reuse) if err != nil { return nil, err } return tcpKeepAliveListener{ln.(*net.TCPListener), keepAliveDur}, nil } // UNIX returns a new unix(file) Listener. func UNIX(socketFile string, mode os.FileMode) (net.Listener, error) { if errOs := os.Remove(socketFile); errOs != nil && !os.IsNotExist(errOs) { return nil, fmt.Errorf("%s: %w", socketFile, errOs) } l, err := net.Listen("unix", socketFile) if err != nil { return nil, fmt.Errorf("port already in use: %w", err) } if err = os.Chmod(socketFile, mode); err != nil { return nil, fmt.Errorf("cannot chmod %#o for %q: %w", mode, socketFile, err) } return l, nil } // TLS returns a new TLS Listener and an error on failure. func TLS(addr, certFile, keyFile string) (net.Listener, error) { if certFile == "" || keyFile == "" { return nil, errors.New("empty certFile or KeyFile") } cert, err := tls.LoadX509KeyPair(certFile, keyFile) if err != nil { return nil, err } return CERT(addr, cert) } // CERT returns a listener which contans tls.Config with the provided certificate, use for ssl. func CERT(addr string, cert tls.Certificate) (net.Listener, error) { l, err := net.Listen("tcp", addr) if err != nil { return nil, err } tlsConfig := &tls.Config{ Certificates: []tls.Certificate{cert}, PreferServerCipherSuites: true, MinVersion: tls.VersionTLS13, } return tls.NewListener(l, tlsConfig), nil } // LETSENCRYPT returns a new Automatic TLS Listener using letsencrypt.org service // receives three parameters, // the first is the host of the server, // second one should declare if the underline tcp listener can be binded more than once, // third can be the server name(domain) or empty if skip verification is the expected behavior (not recommended), // and the forth is optionally, the cache directory, if you skip it then the cache directory is "./certcache" // if you want to disable cache directory then simple give it a value of empty string "" // // does NOT supports localhost domains for testing. // // this is the recommended function to use when you're ready for production state. func LETSENCRYPT(addr string, reuse bool, serverName string, cacheDirOptional ...string) (net.Listener, error) { if portIdx := strings.IndexByte(addr, ':'); portIdx == -1 { addr += ":443" } l, err := TCP(addr, reuse) if err != nil { return nil, err } cacheDir := "./certcache" if len(cacheDirOptional) > 0 { cacheDir = cacheDirOptional[0] } m := autocert.Manager{ Prompt: autocert.AcceptTOS, } // HostPolicy is missing, if user wants it, then she/he should manually if cacheDir == "" { // then the user passed empty by own will, then I guess she/he doesnt' want any cache directory } else { m.Cache = autocert.DirCache(cacheDir) } tlsConfig := &tls.Config{GetCertificate: m.GetCertificate, MinVersion: tls.VersionTLS13} // use InsecureSkipVerify or ServerName to a value if serverName == "" { // if server name is invalid then bypass it tlsConfig.InsecureSkipVerify = true } else { tlsConfig.ServerName = serverName } return tls.NewListener(l, tlsConfig), nil } ================================================ FILE: core/netutil/tcp_soreuse_control_unix.go ================================================ //go:build !windows && !wasm // +build !windows,!wasm package netutil import ( "syscall" "golang.org/x/sys/unix" ) func control(network, address string, c syscall.RawConn) (err error) { c.Control(func(fd uintptr) { err = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEADDR, 1) if err != nil { return } err = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1) if err != nil { return } }) return } ================================================ FILE: core/netutil/tcp_soreuse_control_wasm.go ================================================ //go:build wasm // +build wasm package netutil import "syscall" func control(network, address string, c syscall.RawConn) error { return nil } ================================================ FILE: core/netutil/tcp_soreuse_control_windows.go ================================================ package netutil import ( "syscall" "golang.org/x/sys/windows" ) func control(network, address string, c syscall.RawConn) (err error) { return c.Control(func(fd uintptr) { err = windows.SetsockoptInt(windows.Handle(fd), windows.SOL_SOCKET, windows.SO_REUSEADDR, 1) }) } ================================================ FILE: core/router/TestUseRouterParentDisallow_fuzz_test.go ================================================ package router_test import ( "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/httptest" "testing" ) func FuzzTestUseRouterParentDisallow(f *testing.F) { f.Add("no_userouter_allowed", "always", "_2", "_3", "/index", "/", "/user") f.Fuzz(func(t *testing.T, data1 string, data2 string, data3 string, data4 string, data5 string, data6 string, data7 string) { app := iris.New() app.UseRouter(func(ctx iris.Context) { ctx.WriteString(data2) ctx.Next() }) app.Get(data5, func(ctx iris.Context) { ctx.WriteString(data1) }) app.SetPartyMatcher(func(ctx iris.Context, p iris.Party) bool { // modifies the PartyMatcher to not match any UseRouter, // tests should receive the handlers response alone. return false }) app.PartyFunc(data6, func(p iris.Party) { // it's the same instance of app. p.UseRouter(func(ctx iris.Context) { ctx.WriteString(data3) ctx.Next() }) p.Get(data6, func(ctx iris.Context) { ctx.WriteString(data1) }) }) app.PartyFunc(data7, func(p iris.Party) { p.UseRouter(func(ctx iris.Context) { ctx.WriteString(data4) ctx.Next() }) p.Get(data6, func(ctx iris.Context) { ctx.WriteString(data1) }) }) e := httptest.New(t, app) e.GET(data5) e.GET(data6) e.GET(data7) }) } ================================================ FILE: core/router/api_builder.go ================================================ package router import ( "errors" "fmt" "net/http" "os" "path" "reflect" "strings" "time" "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/hero" "github.com/kataras/iris/v12/macro" macroHandler "github.com/kataras/iris/v12/macro/handler" "github.com/kataras/golog" ) // MethodNone is a Virtual method // to store the "offline" routes. const MethodNone = "NONE" // AllMethods contains the valid HTTP Methods: // "GET", "POST", "PUT", "DELETE", "CONNECT", "HEAD", // "PATCH", "OPTIONS", "TRACE". var AllMethods = []string{ http.MethodGet, http.MethodHead, http.MethodPatch, http.MethodPut, http.MethodPost, http.MethodDelete, http.MethodOptions, http.MethodConnect, http.MethodTrace, } // RegisterMethods adds custom http methods to the "AllMethods" list. // Use it on initialization of your program. func RegisterMethods(newCustomHTTPVerbs ...string) { newMethods := append(AllMethods, newCustomHTTPVerbs...) AllMethods = removeDuplicates(newMethods) } // repository passed to all parties(subrouters), it's the object witch keeps // all the routes. type repository struct { routes []*Route paths map[string]*Route // only the fullname path part, required at CreateRoutes for registering index page. } func (repo *repository) get(routeName string) *Route { for _, r := range repo.routes { if r.Name == routeName { return r } } return nil } func (repo *repository) getRelative(r *Route) *Route { if r.tmpl.IsTrailing() || !macroHandler.CanMakeHandler(r.tmpl) { return nil } for _, route := range repo.routes { if r.tmpl.Src == route.tmpl.Src { // No topLink on the same route syntax. // Fixes #2008, because of APIBuilder.handle, repo.getRelative and repo.register replacement but with a toplink of the old route. continue } if r.Subdomain == route.Subdomain && r.StatusCode == route.StatusCode && r.Method == route.Method && r.FormattedPath == route.FormattedPath && !route.tmpl.IsTrailing() { return route } } return nil } func (repo *repository) getByPath(tmplPath string) *Route { if r, ok := repo.paths[tmplPath]; ok { return r } return nil } func (repo *repository) getAll() []*Route { return repo.routes } func (repo *repository) remove(routeName string) bool { for i, r := range repo.routes { if r.Name == routeName { lastIdx := len(repo.routes) - 1 if lastIdx == i { repo.routes = repo.routes[0:lastIdx] } else { cp := make([]*Route, 0, lastIdx) cp = append(cp, repo.routes[:i]...) repo.routes = append(cp, repo.routes[i+1:]...) } delete(repo.paths, r.tmpl.Src) return true } } return false } func (repo *repository) register(route *Route, rule RouteRegisterRule) (*Route, error) { for i, r := range repo.routes { // 14 August 2019 allow register same path pattern with different macro functions, // see #1058 if route.DeepEqual(r) { if rule == RouteSkip { return r, nil } else if rule == RouteError { return nil, fmt.Errorf("new route: %s conflicts with an already registered one: %s route", route.String(), r.String()) } else if rule == RouteOverlap { overlapRoute(r, route) return route, nil } else { // replace existing with the latest one, the default behavior. repo.routes = append(repo.routes[:i], repo.routes[i+1:]...) } break // continue } } repo.routes = append(repo.routes, route) if route.StatusCode == 0 { // a common resource route, not a status code error handler. if repo.paths == nil { repo.paths = make(map[string]*Route) } repo.paths[route.tmpl.Src] = route } return route, nil } var defaultOverlapFilter = func(ctx *context.Context) bool { if ctx.IsStopped() { // It's stopped and the response can be overridden by a new handler. // An exception of compress writer, which does not implement Reseter (and it shouldn't): rs, ok := ctx.ResponseWriter().(context.ResponseWriterReseter) return ok && rs.Reset() } // It's not stopped, all OK no need to execute the alternative route. return false } func overlapRoute(r *Route, next *Route) { next.BuildHandlers() nextHandlers := next.Handlers[0:] isErrorRoutes := r.StatusCode > 0 && next.StatusCode > 0 decisionHandler := func(ctx *context.Context) { ctx.Next() if isErrorRoutes { // fixes issue #1602. // If it's an error we don't need to reset (see defaultOverlapFilter) // its status code(!) and its body, we just check if it was proceed or not. if !ctx.IsStopped() { return } } else { prevStatusCode := ctx.GetStatusCode() if !defaultOverlapFilter(ctx) { return } // set the status code that it was stopped with. // useful for dependencies with StopWithStatus(XXX) // instead of raw ctx.StopExecution(). // The func_result now also catch the last registered status code // of the chain, unless the controller returns an integer. // See _examples/authenticated-controller. if prevStatusCode > 0 { // An exception when stored error // exists and it's type of ErrNotFound. // Example: // Version was not found: // we need to be able to send the status on the last not found version // but reset the status code if a next available matched version was found. // see the versioning package. if !errors.Is(ctx.GetErr(), context.ErrNotFound) { ctx.StatusCode(prevStatusCode) } } } ctx.SetErr(nil) // clear any stored error. // Set the route to the next one and execute it. ctx.SetCurrentRoute(next.ReadOnly) ctx.HandlerIndex(0) ctx.Do(nextHandlers) } r.builtinBeginHandlers = append(context.Handlers{decisionHandler}, r.builtinBeginHandlers...) r.overlappedLink = next } // APIBuilder the visible API for constructing the router // and child routers. type APIBuilder struct { // the application logger. logger *golog.Logger // parent is the creator of this Party. // It is nil on Root. parent *APIBuilder // currently it's not used anywhere. // the per-party APIBuilder with DI. apiBuilderDI *APIContainer // the api builder global macros registry macros *macro.Macros // the per-party (and its children) values map // that may help on building the API // when source code is splitted between projects. // Initialized on Properties method. properties context.Map // the api builder global routes repository routes *repository // disables the debug logging of routes under a per-party and its children. routesNoLog bool // the per-party handlers, order // of handlers registration matters, // inherited by children unless Reset is called. middleware context.Handlers middlewareErrorCode context.Handlers // the global middleware handlers, order of call doesn't matters, order // of handlers registration matters. We need a secondary field for this // because `UseGlobal` registers handlers that should be executed // even before the `middleware` handlers, and in the same time keep the order // of handlers registration, so the same type of handlers are being called in order. beginGlobalHandlers context.Handlers // the per-party done handlers, order matters. doneHandlers context.Handlers // global done handlers, order doesn't matter. doneGlobalHandlers context.Handlers // the per-party relative path. relativePath string // allowMethods are filled with the `AllowMethods` method. // They are used to create new routes // per any party's (and its children) routes registered // if the method "x" wasn't registered already via the `Handle` (and its extensions like `Get`, `Post`...). allowMethods []string // the per-party (and its children) execution rules for begin, main and done handlers. handlerExecutionRules ExecutionRules // the per-party (and its children) route registration rule, see `SetRegisterRule`. routeRegisterRule RouteRegisterRule // routerFilterHandlers holds a reference // of the handlers used by the current and its parent Party's registered // router filters. Inherited by children unless `Reset` (see `UseRouter`), routerFilterHandlers context.Handlers // routerFilters field is shared across Parties. Each Party registers // one or more middlewares to run before the router itself using the `UseRouter` method. // Each Party calls the shared filter (`partyMatcher`) that decides if its `UseRouter` handlers // can be executed. By default it's based on party's static path and/or subdomain, // it can be modified through an `Application.SetPartyMatcher` call // once before or after routerFilters filled. // // The Key is the Party (instance of APIBuilder), // value wraps the partyFilter + the handlers registered through `UseRouter`. // See `GetRouterFilters` too. routerFilters map[Party]*Filter // partyMatcher field is shared across all Parties, // can be modified through the Application level only. // // It defaults to the internal, simple, "defaultPartyMatcher". // It applies when "routerFilters" are used. partyMatcher PartyMatcherFunc } var ( _ Party = (*APIBuilder)(nil) _ PartyMatcher = (*APIBuilder)(nil) _ RoutesProvider = (*APIBuilder)(nil) // passed to the default request handler (routerHandler) ) // NewAPIBuilder creates & returns a new builder // which is responsible to build the API and the router handler. func NewAPIBuilder(logger *golog.Logger) *APIBuilder { return &APIBuilder{ logger: logger, parent: nil, macros: macro.Defaults, relativePath: "/", routes: new(repository), apiBuilderDI: &APIContainer{Container: hero.New().WithLogger(logger)}, routerFilters: make(map[Party]*Filter), partyMatcher: defaultPartyMatcher, } } // Logger returns the Application Logger. func (api *APIBuilder) Logger() *golog.Logger { return api.logger } // IsRoot reports whether this Party is the root Application's one. // It will return false on all children Parties, no exception. func (api *APIBuilder) IsRoot() bool { return api.parent == nil } /* If requested: // GetRoot returns the very first Party (the Application). func (api *APIBuilder) GetRoot() *APIBuilder { root := api.parent for root != nil { root = api.parent } return root }*/ // ConfigureContainer accepts one or more functions that can be used // to configure dependency injection features of this Party // such as register dependency and register handlers that will automatically inject any valid dependency. // However, if the "builder" parameter is nil or not provided then it just returns the *APIContainer, // which automatically initialized on Party allocation. // // It returns the same `APIBuilder` featured with Dependency Injection. func (api *APIBuilder) ConfigureContainer(builder ...func(*APIContainer)) *APIContainer { if api.apiBuilderDI.Self == nil { api.apiBuilderDI.Self = api } for _, b := range builder { if b != nil { b(api.apiBuilderDI) } } return api.apiBuilderDI } // EnsureStaticBindings panics on struct handler (controller) // if at least one input binding depends on the request and not in a static structure. // Should be called before `RegisterDependency`. func (api *APIBuilder) EnsureStaticBindings() Party { diContainer := api.ConfigureContainer() diContainer.Container.DisableStructDynamicBindings = true return api } // RegisterDependency calls the `ConfigureContainer.RegisterDependency` method // with the provided value(s). See `HandleFunc` and `PartyConfigure` methods too. func (api *APIBuilder) RegisterDependency(dependencies ...any) { diContainer := api.ConfigureContainer() for i, dependency := range dependencies { if dependency == nil { api.logger.Warnf("Party: %s: nil dependency on position: %d", api.relativePath, i) continue } diContainer.RegisterDependency(dependency) } } // HandleFunc registers a route on HTTP verb "method" and relative, to this Party, path. // It is like the `Handle` method but it accepts one or more "handlersFn" functions // that each one of them can accept any input arguments as the HTTP request and // output a result as the HTTP response. Specifically, // the input of the "handlersFn" can be any registered dependency // (see ConfigureContainer().RegisterDependency) // or leave the framework to parse the request and fill the values accordingly. // The output of the "handlersFn" can be any output result: // // custom structs , string, []byte, int, error, // a combination of the above, hero.Result(hero.View | hero.Response) and more. // // If more than one handler function is registered // then the execution happens without the nessecity of the `Context.Next` method, // simply, to stop the execution and not continue to the next "handlersFn" in chain // you should return an `iris.ErrStopExecution`. // // Example Code: // // The client's request body and server's response body Go types. // Could be any data structure. // // type ( // request struct { // Firstname string `json:"firstname"` // Lastname string `json:"lastname"` // } // // response struct { // ID uint64 `json:"id"` // Message string `json:"message"` // } // ) // // Register the route hander. // // HTTP VERB ROUTE PATH ROUTE HANDLER // app.HandleFunc("PUT", "/users/{id:uint64}", updateUser) // // Code the route handler function. // Path parameters and request body are binded // automatically. // The "id" uint64 binds to "{id:uint64}" route path parameter and // the "input" binds to client request data such as JSON. // // func updateUser(id uint64, input request) response { // // [custom logic...] // // return response{ // ID:id, // Message: "User updated successfully", // } // } // // Simulate a client request which sends data // to the server and prints out the response. // // curl --request PUT -d '{"firstname":"John","lastname":"Doe"}' \ // -H "Content-Type: application/json" \ // http://localhost:8080/users/42 // // { // "id": 42, // "message": "User updated successfully" // } // // See the `ConfigureContainer` for more features regrading // the dependency injection, mvc and function handlers. // // This method is just a shortcut of the `ConfigureContainer().Handle`. func (api *APIBuilder) HandleFunc(method, relativePath string, handlersFn ...any) *Route { return api.ConfigureContainer().Handle(method, relativePath, handlersFn...) } // UseFunc registers a function which can accept one or more // dependencies (see RegisterDependency) and returns an iris.Handler // or a result of and/or an error. // // This method is just a shortcut of the `ConfigureContainer().Use`. func (api *APIBuilder) UseFunc(handlersFn ...any) { api.ConfigureContainer().Use(handlersFn...) } // GetRelPath returns the current party's relative path. // i.e: // if r := app.Party("/users"), then the `r.GetRelPath()` is the "/users". // if r := app.Party("www.") or app.Subdomain("www") then the `r.GetRelPath()` is the "www.". func (api *APIBuilder) GetRelPath() string { return api.relativePath } // AllowMethods will re-register the future routes that will be registered // via `Handle`, `Get`, `Post`, ... to the given "methods" on that Party and its children "Parties", // duplicates are not registered. // // Call of `AllowMethod` will override any previous allow methods. func (api *APIBuilder) AllowMethods(methods ...string) Party { api.allowMethods = methods return api } // SetExecutionRules alters the execution flow of the route handlers outside of the handlers themselves. // // For example, if for some reason the desired result is the (done or all) handlers to be executed no matter what // even if no `ctx.Next()` is called in the previous handlers, including the begin(`Use`), // the main(`Handle`) and the done(`Done`) handlers themselves, then: // // Party#SetExecutionRules(iris.ExecutionRules { // Begin: iris.ExecutionOptions{Force: true}, // Main: iris.ExecutionOptions{Force: true}, // Done: iris.ExecutionOptions{Force: true}, // }) // // Note that if : true then the only remained way to "break" the handler chain is by `ctx.StopExecution()` now that `ctx.Next()` does not matter. // // These rules are per-party, so if a `Party` creates a child one then the same rules will be applied to that as well. // Reset of these rules (before `Party#Handle`) can be done with `Party#SetExecutionRules(iris.ExecutionRules{})`. // // The most common scenario for its use can be found inside Iris MVC Applications; // when we want the `Done` handlers of that specific mvc app's `Party` // to be executed but we don't want to add `ctx.Next()` on the `OurController#EndRequest`. // // Returns this Party. // // Example: https://github.com/kataras/iris/tree/main/_examples/mvc/middleware/without-ctx-next func (api *APIBuilder) SetExecutionRules(executionRules ExecutionRules) Party { api.handlerExecutionRules = executionRules return api } // RouteRegisterRule is a type of uint8. // Defines the register rule for new routes that already exists. // Available values are: RouteOverride, RouteSkip and RouteError. // // See `Party#SetRegisterRule`. type RouteRegisterRule uint8 const ( // RouteOverride replaces an existing route with the new one, the default rule. RouteOverride RouteRegisterRule = iota // RouteSkip keeps the original route and skips the new one. RouteSkip // RouteError log when a route already exists, shown after the `Build` state, // server never starts. RouteError // RouteOverlap will overlap the new route to the previous one. // If the route stopped and its response can be reset then the new route will be execute. RouteOverlap ) // SetRegisterRule sets a `RouteRegisterRule` for this Party and its children. // Available values are: // * RouteOverride (the default one) // * RouteSkip // * RouteError // * RouteOverlap. func (api *APIBuilder) SetRegisterRule(rule RouteRegisterRule) Party { api.routeRegisterRule = rule return api } // Handle registers a route to this Party. // if empty method is passed then handler(s) are being registered to all methods, same as .Any. // // Returns a *Route, app will throw any errors later on. func (api *APIBuilder) Handle(method string, relativePath string, handlers ...context.Handler) *Route { return api.handle(0, method, relativePath, handlers...) } // handle registers a full route to this Party. // Use Handle or Get, Post, Put, Delete and et.c. instead. func (api *APIBuilder) handle(errorCode int, method string, relativePath string, handlers ...context.Handler) *Route { if relativePath == "" { relativePath = "/" } routes := api.createRoutes(errorCode, []string{method}, relativePath, handlers...) var route *Route // the last one is returned. var err error for _, route = range routes { if route == nil { continue } // global route.topLink = api.routes.getRelative(route) if route, err = api.routes.register(route, api.routeRegisterRule); err != nil { api.logger.Error(err) break } } return route } // HandleMany works like `Handle` but can receive more than one // paths separated by spaces and returns always a slice of *Route instead of a single instance of Route. // // It's useful only if the same handler can handle more than one request paths, // otherwise use `Party` which can handle many paths with different handlers and middlewares. // // Usage: // // app.HandleMany("GET", "/user /user/{id:uint64} /user/me", genericUserHandler) // // At the other side, with `Handle` we've had to write: // // app.Handle("GET", "/user", userHandler) // app.Handle("GET", "/user/{id:uint64}", userByIDHandler) // app.Handle("GET", "/user/me", userMeHandler) // // app.HandleMany("GET POST", "/path", handler) func (api *APIBuilder) HandleMany(methodOrMulti string, relativePathorMulti string, handlers ...context.Handler) (routes []*Route) { // at least slash // a space // at least one other slash for the next path paths := splitPath(relativePathorMulti) methods := splitMethod(methodOrMulti) for _, p := range paths { if p != "" { for _, method := range methods { if method == "" { method = "ANY" } if method == "ANY" || method == "ALL" { routes = append(routes, api.Any(p, handlers...)...) continue } routes = append(routes, api.Handle(method, p, handlers...)) } } } return } // HandleDir registers a handler that serves HTTP requests // with the contents of a file system (physical or embedded). // // first parameter : the route path // second parameter : the file system needs to be served // third parameter : not required, the serve directory options. // // Alternatively, to get just the handler for that look the FileServer function instead. // // api.HandleDir("/static", iris.Dir("./assets"), iris.DirOptions{IndexName: "/index.html", Compress: true}) // // Returns all the registered routes, including GET index and path patterm and HEAD. // // Usage: // HandleDir("/public", "./assets", DirOptions{...}) or // HandleDir("/public", iris.Dir("./assets"), DirOptions{...}) // OR // //go:embed assets/* // var filesystem embed.FS // HandleDir("/public",filesystem, DirOptions{...}) // OR to pick a specific folder of the embedded filesystem: // import "io/fs" // subFilesystem, err := fs.Sub(filesystem, "assets") // HandleDir("/public",subFilesystem, DirOptions{...}) // // Examples: // https://github.com/kataras/iris/tree/main/_examples/file-server func (api *APIBuilder) HandleDir(requestPath string, fsOrDir any, opts ...DirOptions) (routes []*Route) { options := DefaultDirOptions if len(opts) > 0 { options = opts[0] } fs := context.ResolveHTTPFS(fsOrDir) h := FileServer(fs, options) description := "file server" if d, ok := fs.(http.Dir); ok { description = string(d) } fileName, lineNumber := context.HandlerFileLine(h) // take those before StripPrefix. // if subdomain, we get the full path of the path only, // because a subdomain can have parties as well // and we need that path to call the `StripPrefix`. _, fullpath := splitSubdomainAndPath(joinPath(api.relativePath, requestPath)) if fullpath != "/" { h = StripPrefix(fullpath, h) } if api.GetRouteByPath(fullpath) == nil { // register index if not registered by the end-developer. routes = api.CreateRoutes([]string{http.MethodGet, http.MethodHead}, requestPath, h) } requestPath = joinPath(requestPath, WildcardFileParam()) routes = append(routes, api.CreateRoutes([]string{http.MethodGet, http.MethodHead}, requestPath, h)...) for _, route := range routes { if route.Method == http.MethodHead { } else { route.Describe(description) route.SetSourceLine(fileName, lineNumber) } if _, err := api.routes.register(route, api.routeRegisterRule); err != nil { api.logger.Error(err) break } } return routes } // CreateRoutes returns a list of Party-based Routes. // It does NOT registers the route. Use `Handle, Get...` methods instead. // This method can be used for third-parties Iris helpers packages and tools // that want a more detailed view of Party-based Routes before take the decision to register them. func (api *APIBuilder) CreateRoutes(methods []string, relativePath string, handlers ...context.Handler) []*Route { return api.createRoutes(0, methods, relativePath, handlers...) } // RemoveRoute deletes a registered route by its name before `Application.Listen`. // The default naming for newly created routes is: method + subdomain + path. // Reports whether a route with that name was found and removed successfully. // // Note that this method applies to all Parties (sub routers) // even if each of the Parties have access to this method, // as the route name is unique per Iris Application. func (api *APIBuilder) RemoveRoute(routeName string) bool { return api.routes.remove(routeName) } func (api *APIBuilder) createRoutes(errorCode int, methods []string, relativePath string, handlers ...context.Handler) []*Route { if statusCodeSuccessful(errorCode) { errorCode = 0 } mainHandlers := context.CopyHandlers(handlers) if errorCode == 0 { if len(methods) == 0 || methods[0] == "ALL" || methods[0] == "ANY" { // then use like it was .Any return api.Any(relativePath, mainHandlers...) } } // no clean path yet because of subdomain indicator/separator which contains a dot. // but remove the first slash if the relative has already ending with a slash // it's not needed because later on we do normalize/clean the path, but better do it here too // for any future updates. if api.relativePath[len(api.relativePath)-1] == '/' { if relativePath[0] == '/' { relativePath = relativePath[1:] } } filename, line := hero.GetCaller() fullpath := api.relativePath + relativePath // for now, keep the last "/" if any, "/xyz/" if len(mainHandlers) == 0 { api.logger.Errorf("missing handlers for route[%s:%d] %s: %s", filename, line, strings.Join(methods, ", "), fullpath) return nil } // note: this can not change the caller's handlers as they're but the entry values(handlers) // of `middleware`, `doneHandlers` and `handlers` can. // So if we just put `api.middleware` or `api.doneHandlers` // then the next `Party` will have those updated handlers // but dev may change the rules for that child Party, so we have to make clones of them here. var ( // global middleware to error handlers as well. beginHandlers = api.beginGlobalHandlers doneHandlers = api.doneGlobalHandlers ) if errorCode == 0 { beginHandlers = context.JoinHandlers(beginHandlers, api.middleware) doneHandlers = context.JoinHandlers(doneHandlers, api.doneHandlers) } else { beginHandlers = context.JoinHandlers(beginHandlers, api.middlewareErrorCode) } // before join the middleware + handlers + done handlers and apply the execution rules. mainHandlerName, mainHandlerIndex := context.MainHandlerName(mainHandlers) mainHandlerFileName, mainHandlerFileNumber := context.HandlerFileLineRel(mainHandlers[mainHandlerIndex]) // TODO: think of it. if mainHandlerFileName == "" { // At PartyConfigure, 2nd+ level of routes it will get but in reallity will be the same as the caller. mainHandlerFileName = filename mainHandlerFileNumber = line } // re-calculate mainHandlerIndex in favor of the middlewares. mainHandlerIndex = len(beginHandlers) + mainHandlerIndex // TODO: for UseGlobal/DoneGlobal that doesn't work. applyExecutionRules(api.handlerExecutionRules, &beginHandlers, &doneHandlers, &mainHandlers) // global begin handlers -> middleware that are registered before route registration // -> handlers that are passed to this Handle function. routeHandlers := context.JoinHandlers(beginHandlers, mainHandlers) // -> done handlers routeHandlers = context.JoinHandlers(routeHandlers, doneHandlers) // here we separate the subdomain and relative path subdomain, path := splitSubdomainAndPath(fullpath) // if allowMethods are empty, then simply register with the passed, main, method. methods = removeDuplicates(append(api.allowMethods, methods...)) routes := make([]*Route, len(methods)) for i, m := range methods { // single, empty method for error handlers. route, err := NewRoute(api, errorCode, m, subdomain, path, routeHandlers, *api.macros) if err != nil { // template path parser errors: api.logger.Errorf("[%s:%d] %v -> %s:%s:%s", filename, line, err, m, subdomain, path) continue } // The caller tiself, if anonymous, it's the first line of `app.X("/path", here)` route.RegisterFileName = mainHandlerFileName // filename route.RegisterLineNumber = mainHandlerFileNumber // line route.MainHandlerName = mainHandlerName route.MainHandlerIndex = mainHandlerIndex // The main handler source, could be the same as the register's if anonymous. route.SourceFileName = mainHandlerFileName route.SourceLineNumber = mainHandlerFileNumber // Add UseGlobal & DoneGlobal Handlers // route.Use(api.beginGlobalHandlers...) // route.Done(api.doneGlobalHandlers...) route.NoLog = api.routesNoLog routes[i] = route } return routes } func removeDuplicates(elements []string) (result []string) { seen := make(map[string]struct{}) for v := range elements { val := elements[v] if _, ok := seen[val]; !ok { seen[val] = struct{}{} result = append(result, val) } } return result } // Party returns a new child Party which inherites its // parent's options and middlewares. // A Party groups routes which may have the same prefix or subdomain and share same middlewares. // // To create a group of routes for subdomains // use the `Subdomain` or `WildcardSubdomain` methods // or pass a "relativePath" of "admin." or "*." respectfully. func (api *APIBuilder) Party(relativePath string, handlers ...context.Handler) Party { if relativePath == "" { relativePath = "/" } // if app.Party("/"), root party or app.Party("/user") == app.Party("/user") // then just add the middlewares and return itself. // if relativePath == "" || api.relativePath == relativePath { // api.Use(handlers...) // return api // } // ^ No, this is wrong, let the developer do its job, if she/he wants a copy let have it, // it's a pure check as well, a path can be the same even if it's the same as its parent, i.e. // app.Party("/user").Party("/user") should result in a /user/user, not a /user. parentPath := api.relativePath dot := string(SubdomainPrefix[0]) if len(parentPath) > 0 && parentPath[0] == '/' && strings.HasSuffix(relativePath, dot) { // if ends with . , i.e admin., it's subdomain-> parentPath = parentPath[1:] // remove first slash } // this is checked later on but for easier debug is better to do it here: if api.relativePath[len(api.relativePath)-1] == '/' && relativePath[0] == '/' { relativePath = relativePath[1:] // remove first slash if parent ended with / and new one started with /. } // if it's subdomain then it has priority, i.e: // api.relativePath == "admin." // relativePath == "panel." // then it should be panel.admin. // instead of admin.panel. if hasSubdomain(parentPath) && hasSubdomain(relativePath) { relativePath = relativePath + parentPath parentPath = "" } fullpath := parentPath + relativePath // append the parent's + child's handlers middleware := context.JoinHandlers(api.middleware, handlers) // the allow methods per party and its children. allowMethods := make([]string, len(api.allowMethods)) copy(allowMethods, api.allowMethods) // make a copy of the parent properties. properties := make(context.Map, len(api.properties)) for k, v := range api.properties { properties[k] = v } childAPI := &APIBuilder{ // global/api builder logger: api.logger, macros: api.macros, properties: properties, routes: api.routes, routesNoLog: api.routesNoLog, beginGlobalHandlers: api.beginGlobalHandlers, doneGlobalHandlers: api.doneGlobalHandlers, // per-party/children parent: api, middleware: middleware, middlewareErrorCode: context.JoinHandlers(api.middlewareErrorCode, context.Handlers{}), doneHandlers: api.doneHandlers[0:], routerFilters: api.routerFilters, routerFilterHandlers: api.routerFilterHandlers, partyMatcher: api.partyMatcher, relativePath: fullpath, allowMethods: allowMethods, handlerExecutionRules: api.handlerExecutionRules, routeRegisterRule: api.routeRegisterRule, apiBuilderDI: &APIContainer{ // attach a new Container with correct dynamic path parameter start index for input arguments // based on the fullpath. Container: api.apiBuilderDI.Container.Clone(), }, } return childAPI } // PartyFunc same as `Party`, groups routes that share a base path or/and same handlers. // However this function accepts a function that receives this created Party instead. // Returns the Party in order the caller to be able to use this created Party to continue the // top-bottom routes "tree". // // Note: `iris#Party` and `core/router#Party` describes the exactly same interface. // // Usage: // // app.PartyFunc("/users", func(u iris.Party){ // u.Use(authMiddleware, logMiddleware) // u.Get("/", getAllUsers) // u.Post("/", createOrUpdateUser) // u.Delete("/", deleteUser) // }) // // Look `Party` for more. func (api *APIBuilder) PartyFunc(relativePath string, partyBuilderFunc func(p Party)) Party { p := api.Party(relativePath) partyBuilderFunc(p) return p } type ( // PartyConfigurator is an interface which all child parties that are registered // through `PartyConfigure` should implement. PartyConfigurator interface { Configure(parent Party) } // StrictlyPartyConfigurator is an optional interface which a `PartyConfigurator` // can implement to make sure that all exported fields having a not-nin, non-zero // value before server starts. // StrictlyPartyConfigurator interface { // Strict() bool // } // Good idea but a `mvc or bind:"required"` is a better one I think. ) // PartyConfigure like `Party` and `PartyFunc` registers a new children Party // but instead it accepts a struct value which should implement the PartyConfigurator interface. // // PartyConfigure accepts the relative path of the child party // (As an exception, if it's empty then all configurators are applied to the current Party) // and one or more Party configurators and // executes the PartyConfigurator's Configure method. // // If the end-developer registered one or more dependencies upfront through // RegisterDependencies or ConfigureContainer.RegisterDependency methods // and "p" is a pointer to a struct then try to bind the unset/zero exported fields // to the registered dependencies, just like we do with Controllers. // Useful when the api's dependencies amount are too much to pass on a function. // // Usage: // // app.PartyConfigure("/users", &api.UsersAPI{UserRepository: ..., ...}) // // Where UsersAPI looks like: // // type UsersAPI struct { [...] } // func(api *UsersAPI) Configure(router iris.Party) { // router.Get("/{id:uuid}", api.getUser) // [...] // } // // Usage with (static) dependencies: // // app.RegisterDependency(userRepo, ...) // app.PartyConfigure("/users", new(api.UsersAPI)) func (api *APIBuilder) PartyConfigure(relativePath string, partyReg ...PartyConfigurator) Party { var child Party if relativePath == "" { child = api } else { child = api.Party(relativePath) } for _, p := range partyReg { if p == nil { continue } if len(api.apiBuilderDI.Container.Dependencies) > 0 { if typ := reflect.TypeOf(p); typ.Kind() == reflect.Ptr && typ.Elem().Kind() == reflect.Struct { api.apiBuilderDI.Container.Struct(p, -1) } } p.Configure(child) } return child } // Subdomain returns a new party which is responsible to register routes to // this specific "subdomain". // // If called from a child party then the subdomain will be prepended to the path instead of appended. // So if app.Subdomain("admin").Subdomain("panel") then the result is: "panel.admin.". func (api *APIBuilder) Subdomain(subdomain string, middleware ...context.Handler) Party { if api.relativePath == SubdomainWildcardIndicator { // cannot concat wildcard subdomain with something else api.logger.Errorf("cannot concat parent wildcard subdomain with anything else -> %s , %s", api.relativePath, subdomain) return api } if l := len(subdomain); l < 1 { return api } else if subdomain[l-1] != '.' { subdomain += "." } return api.Party(subdomain, middleware...) } // WildcardSubdomain returns a new party which is responsible to register routes to // a dynamic, wildcard(ed) subdomain. A dynamic subdomain is a subdomain which // can reply to any subdomain requests. Server will accept any subdomain // (if not static subdomain found) and it will search and execute the handlers of this party. func (api *APIBuilder) WildcardSubdomain(middleware ...context.Handler) Party { if hasSubdomain(api.relativePath) { // cannot concat static subdomain with a dynamic one, wildcard should be at the root level api.logger.Errorf("cannot concat static subdomain with a dynamic one. Dynamic subdomains should be at the root level -> %s", api.relativePath) return api } return api.Subdomain(SubdomainWildcardIndicator, middleware...) } // Macros returns the macro collection that is responsible // to register custom macros with their own parameter types and their macro functions for all routes. // // Learn more at: https://github.com/kataras/iris/tree/main/_examples/routing/dynamic-path func (api *APIBuilder) Macros() *macro.Macros { return api.macros } // Properties returns the original Party's properties map, // it can be modified before server startup but not afterwards. func (api *APIBuilder) Properties() context.Map { if api.properties == nil { api.properties = make(context.Map) } return api.properties } // GetRoutes returns the routes information, // some of them can be changed at runtime some others not. // // Needs refresh of the router to Method or Path or Handlers changes to take place. func (api *APIBuilder) GetRoutes() []*Route { return api.routes.getAll() } // CountHandlers returns the total number of all unique // registered route handlers. func (api *APIBuilder) CountHandlers() int { uniqueNames := make(map[string]struct{}) for _, r := range api.GetRoutes() { for _, h := range r.Handlers { handlerName := context.HandlerName(h) if _, exists := uniqueNames[handlerName]; !exists { uniqueNames[handlerName] = struct{}{} } } } return len(uniqueNames) } // GetRoute returns the registered route based on its name, otherwise nil. // One note: "routeName" should be case-sensitive. func (api *APIBuilder) GetRoute(routeName string) *Route { return api.routes.get(routeName) } // GetRouteByPath returns the registered route based on the template path (`Route.Tmpl().Src`). func (api *APIBuilder) GetRouteByPath(tmplPath string) *Route { return api.routes.getByPath(tmplPath) } // GetRoutesReadOnly returns the registered routes with "read-only" access, // you cannot and you should not change any of these routes' properties on request state, // you can use the `GetRoutes()` for that instead. // // It returns interface-based slice instead of the real ones in order to apply // safe fetch between context(request-state) and the builded application. // // Look `GetRouteReadOnly` too. func (api *APIBuilder) GetRoutesReadOnly() []context.RouteReadOnly { routes := api.GetRoutes() readOnlyRoutes := make([]context.RouteReadOnly, len(routes)) for i, r := range routes { readOnlyRoutes[i] = r.ReadOnly } return readOnlyRoutes } // GetRouteReadOnly returns the registered "read-only" route based on its name, otherwise nil. // One note: "routeName" should be case-sensitive. Used by the context to get the current route. // It returns an interface instead to reduce wrong usage and to keep the decoupled design between // the context and the routes. // Look `GetRoutesReadOnly` to fetch a list of all registered routes. // // Look `GetRoute` for more. func (api *APIBuilder) GetRouteReadOnly(routeName string) context.RouteReadOnly { r := api.GetRoute(routeName) if r == nil { return nil } return r.ReadOnly } // GetRouteReadOnlyByPath returns the registered read-only route based on the template path (`Route.Tmpl().Src`). func (api *APIBuilder) GetRouteReadOnlyByPath(tmplPath string) context.RouteReadOnly { r := api.GetRouteByPath(tmplPath) if r == nil { return nil } return r.ReadOnly } // SetRoutesNoLog disables (true) the verbose logging for the next registered // routes under this Party and its children. // // To disable logging for controllers under MVC Application, // see `mvc/Application.SetControllersNoLog` instead. // // Defaults to false when log level is "debug". func (api *APIBuilder) SetRoutesNoLog(disable bool) Party { api.routesNoLog = disable return api } type ( // PartyMatcherFunc used to build a filter which decides // if the given Party is responsible to fire its `UseRouter` handlers or not. // Can be customized through `SetPartyMatcher` method. See `Match` method too. PartyMatcherFunc func(*context.Context, Party) bool // PartyMatcher decides if `UseRouter` handlers should be executed or not. // A different interface becauwe we want to separate // the Party's public API from `UseRouter` internals. PartyMatcher interface { Match(ctx *context.Context) bool } // Filter is a wraper for a Router Filter contains information // for its Party's fullpath, subdomain the Party's // matcher and the associated handlers to be executed before main router's request handler. Filter struct { Matcher PartyMatcher // it's a Party, for freedom that can be changed through a custom matcher which accepts the same filter. Skippers map[*APIBuilder]struct{} // skip execution on these builders ( see `Reset`) Subdomain string Path string Handlers context.Handlers } ) // SetPartyMatcher accepts a function which runs against // a Party and should report whether its `UseRouter` handlers should be executed. // PartyMatchers are run through parent to children. // It modifies the default Party filter that decides // which `UseRouter` middlewares to run before the Router, // each one of those middlewares can skip `Context.Next` or call `Context.StopXXX` // to stop the main router from searching for a route match. // Can be called before or after `UseRouter`, it doesn't matter. func (api *APIBuilder) SetPartyMatcher(matcherFunc PartyMatcherFunc) { if matcherFunc == nil { matcherFunc = defaultPartyMatcher } api.partyMatcher = matcherFunc } // Match reports whether the `UseRouter` handlers should be executed. // Calls its parent's Match if possible. // Implements the `PartyMatcher` interface. func (api *APIBuilder) Match(ctx *context.Context) bool { return api.partyMatcher(ctx, api) } func defaultPartyMatcher(ctx *context.Context, p Party) bool { subdomain, path := splitSubdomainAndPath(p.GetRelPath()) staticPath := staticPath(path) hosts := subdomain != "" if p.IsRoot() { // ALWAYS executed first when registered // through an `Application.UseRouter` call. return true } if hosts { // Note(@kataras): do NOT try to implement something like party matcher for each party // separately. We will introduce a new problem with subdomain inside a subdomain: // they are not by prefix, so parenting calls will not help // e.g. admin. and control.admin, control.admin is a sub of the admin. if !canHandleSubdomain(ctx, subdomain) { return false } } // this is the longest static path. return strings.HasPrefix(ctx.Path(), staticPath) } // GetRouterFilters returns the global router filters. // Read `UseRouter` for more. // The map can be altered before router built. // The router internally prioritized them by the subdomains and // longest static path. // Implements the `RoutesProvider` interface. func (api *APIBuilder) GetRouterFilters() map[Party]*Filter { return api.routerFilters } // UseRouter upserts one or more handlers that will be fired // right before the main router's request handler. // // Use this method to register handlers, that can ran // independently of the incoming request's values, // that they will be executed ALWAYS against ALL children incoming requests. // Example of use-case: CORS. // // Note that because these are executed before the router itself // the Context should not have access to the `GetCurrentRoute` // as it is not decided yet which route is responsible to handle the incoming request. // It's one level higher than the `WrapRouter`. // The context SHOULD call its `Next` method in order to proceed to // the next handler in the chain or the main request handler one. func (api *APIBuilder) UseRouter(handlers ...context.Handler) { if len(handlers) == 0 || handlers[0] == nil { return } beginHandlers := context.Handlers(handlers) // respect any execution rules (begin). api.handlerExecutionRules.Begin.apply(&beginHandlers) beginHandlers = context.JoinHandlers(api.routerFilterHandlers, beginHandlers) if f := api.routerFilters[api]; f != nil && len(f.Handlers) > 0 { // exists. beginHandlers = context.UpsertHandlers(f.Handlers, beginHandlers) // remove dupls. } // we are not using the parent field here, // we need to have control over those values in order to be able to `Reset`. api.routerFilterHandlers = beginHandlers subdomain, path := splitSubdomainAndPath(api.relativePath) api.routerFilters[api] = &Filter{ Matcher: api, Subdomain: subdomain, Path: path, Handlers: beginHandlers, } } // GetDefaultErrorMiddleware returns the application's error pre handlers // registered through `UseError` for the default error handlers. // This is used when no matching error handlers registered // for a specific status code but `UseError` is called to register a middleware, // so the default error handler should make use of those middleware now. func (api *APIBuilder) GetDefaultErrorMiddleware() context.Handlers { return api.middlewareErrorCode } // UseError upserts one or more handlers that will be fired, // as middleware, before any error handler registered through `On(Any)ErrorCode`. // See `OnErrorCode` too. func (api *APIBuilder) UseError(handlers ...context.Handler) { api.middlewareErrorCode = context.UpsertHandlers(api.middlewareErrorCode, handlers) } // Use appends Handler(s) to the current Party's routes and child routes. // If the current Party is the root, then it registers the middleware to all child Parties' routes too. // The given "handlers" will be executed only on matched routes. // // Call order matters, it should be called right before the routes that they care about these handlers. // // If it's called after the routes then these handlers will never be executed. // Use `UseGlobal` if you want to register begin handlers(middleware) // that should be always run before all application's routes. // To register a middleware for error handlers, look `UseError` method instead. func (api *APIBuilder) Use(handlers ...context.Handler) { api.middleware = append(api.middleware, handlers...) } // UseOnce either inserts a middleware, // or on the basis of the middleware already existing, // replace that existing middleware instead. // To register a middleware for error handlers, look `UseError` method instead. func (api *APIBuilder) UseOnce(handlers ...context.Handler) { api.middleware = context.UpsertHandlers(api.middleware, handlers) } // UseGlobal registers handlers that should run at the very beginning. // It prepends those handler(s) to all routes, // including all parties, subdomains and errors. // It doesn't care about call order, it will prepend the handlers to all // existing routes and the future routes that may being registered. // // The given "handlers" will be executed only on matched routes and registered errors. // See `UseRouter` if you want to register middleware that will always run, even on 404 not founds. // // The difference from `.DoneGlobal` is that this/or these Handler(s) are being always running first. // Use of `ctx.Next()` of those handler(s) is necessary to call the main handler or the next middleware. // It's always a good practise to call it right before the `Application#Run` function. func (api *APIBuilder) UseGlobal(handlers ...context.Handler) { for _, r := range api.routes.routes { // r.beginHandlers = append(handlers, r.beginHandlers...) // ^ this is correct but we act global begin handlers as one chain, so // if called last more than one time, after all routes registered, we must somehow // register them by order, so: r.Use(handlers...) } // set as begin handlers for the next routes as well. api.beginGlobalHandlers = append(api.beginGlobalHandlers, handlers...) } // Done appends to the very end, Handler(s) to the current Party's routes and child routes. // The given "handlers" will be executed only on matched routes. // // Call order matters, it should be called right before the routes that they care about these handlers. // // The difference from .Use is that this/or these Handler(s) are being always running last. func (api *APIBuilder) Done(handlers ...context.Handler) { api.doneHandlers = append(api.doneHandlers, handlers...) } // DoneGlobal registers handlers that should run at the very end. // It appends those handler(s) to all routes, // including all parties, subdomains. // It doesn't care about call order, it will append the handlers to all // existing routes and the future routes that may being registered. // // The given "handlers" will be executed only on matched and registered error routes. // // The difference from `.UseGlobal` is that this/or these Handler(s) are being always running last. // Use of `ctx.Next()` at the previous handler is necessary. // It's always a good practise to call it right before the `Application#Run` function. func (api *APIBuilder) DoneGlobal(handlers ...context.Handler) { for _, r := range api.routes.routes { r.Done(handlers...) // append the handlers to the existing routes } // set as done handlers for the next routes as well. api.doneGlobalHandlers = append(api.doneGlobalHandlers, handlers...) } // MiddlewareExists reports whether the given handler exists in the middleware chain. func (api *APIBuilder) MiddlewareExists(handlerNameOrFunc any) bool { if handlerNameOrFunc == nil { return false } var handlers context.Handlers if filter, ok := api.routerFilters[api]; ok { handlers = append(handlers, filter.Handlers...) } handlers = append(handlers, api.middleware...) handlers = append(handlers, api.doneHandlers...) handlers = append(handlers, api.beginGlobalHandlers...) handlers = append(handlers, api.doneGlobalHandlers...) return context.HandlerExists(handlers, handlerNameOrFunc) } // RemoveHandler deletes a handler from begin and done handlers // based on its name or the handler pc function. // Note that UseGlobal and DoneGlobal handlers cannot be removed // through this method as they were registered to the routes already. // // As an exception, if one of the arguments is a pointer to an int, // then this is used to set the total amount of removed handlers. // // Returns the Party itself for chain calls. // // Should be called before children routes regitration. func (api *APIBuilder) RemoveHandler(namesOrHandlers ...any) Party { var counter *int for _, nameOrHandler := range namesOrHandlers { handlerName := "" switch h := nameOrHandler.(type) { case string: handlerName = h case context.Handler: //, func(*context.Context): handlerName = context.HandlerName(h) case *int: counter = h default: panic(fmt.Sprintf("remove handler: unexpected type of %T", h)) } api.middleware = removeHandler(handlerName, api.middleware, counter) api.doneHandlers = removeHandler(handlerName, api.doneHandlers, counter) } return api } // Reset removes all the begin and done handlers that may derived from the parent party via `Use` & `Done`, // and the execution rules. // Note that the `Reset` will not reset the handlers that are registered via `UseGlobal` & `DoneGlobal`. // // Returns this Party. func (api *APIBuilder) Reset() Party { api.middleware = api.middleware[0:0] api.middlewareErrorCode = api.middlewareErrorCode[0:0] api.ResetRouterFilters() api.doneHandlers = api.doneHandlers[0:0] api.handlerExecutionRules = ExecutionRules{} api.routeRegisterRule = RouteOverride // keep container as it's. return api } // ResetRouterFilters deactivates any previous registered // router filters and the parents ones for this Party. // // Returns this Party. func (api *APIBuilder) ResetRouterFilters() Party { api.routerFilterHandlers = api.routerFilterHandlers[0:0] delete(api.routerFilters, api) if api.parent == nil { // it's the root, stop, nothing else to do here. return api } // Set a filter with empty handlers, the router will find it, execute nothing // and continue with the request handling. This works on Reset() and no UseRouter // and with Reset().UseRouter. subdomain, path := splitSubdomainAndPath(api.relativePath) api.routerFilters[api] = &Filter{ Matcher: api, Handlers: nil, Subdomain: subdomain, Path: path, } return api } // None registers an "offline" route // see context.ExecRoute(routeName) and // party.Routes().Online(handleResultRouteInfo, "GET") and // Offline(handleResultRouteInfo) // // Returns a *Route and an error which will be filled if route wasn't registered successfully. func (api *APIBuilder) None(relativePath string, handlers ...context.Handler) *Route { return api.Handle(MethodNone, relativePath, handlers...) } // Get registers a route for the Get HTTP Method. // // Returns a *Route and an error which will be filled if route wasn't registered successfully. func (api *APIBuilder) Get(relativePath string, handlers ...context.Handler) *Route { return api.Handle(http.MethodGet, relativePath, handlers...) } // Post registers a route for the Post HTTP Method. // // Returns a *Route and an error which will be filled if route wasn't registered successfully. func (api *APIBuilder) Post(relativePath string, handlers ...context.Handler) *Route { return api.Handle(http.MethodPost, relativePath, handlers...) } // Put registers a route for the Put HTTP Method. // // Returns a *Route and an error which will be filled if route wasn't registered successfully. func (api *APIBuilder) Put(relativePath string, handlers ...context.Handler) *Route { return api.Handle(http.MethodPut, relativePath, handlers...) } // Delete registers a route for the Delete HTTP Method. // // Returns a *Route and an error which will be filled if route wasn't registered successfully. func (api *APIBuilder) Delete(relativePath string, handlers ...context.Handler) *Route { return api.Handle(http.MethodDelete, relativePath, handlers...) } // Connect registers a route for the Connect HTTP Method. // // Returns a *Route and an error which will be filled if route wasn't registered successfully. func (api *APIBuilder) Connect(relativePath string, handlers ...context.Handler) *Route { return api.Handle(http.MethodConnect, relativePath, handlers...) } // Head registers a route for the Head HTTP Method. // // Returns a *Route and an error which will be filled if route wasn't registered successfully. func (api *APIBuilder) Head(relativePath string, handlers ...context.Handler) *Route { return api.Handle(http.MethodHead, relativePath, handlers...) } // Options registers a route for the Options HTTP Method. // // Returns a *Route and an error which will be filled if route wasn't registered successfully. func (api *APIBuilder) Options(relativePath string, handlers ...context.Handler) *Route { return api.Handle(http.MethodOptions, relativePath, handlers...) } // Patch registers a route for the Patch HTTP Method. // // Returns a *Route and an error which will be filled if route wasn't registered successfully. func (api *APIBuilder) Patch(relativePath string, handlers ...context.Handler) *Route { return api.Handle(http.MethodPatch, relativePath, handlers...) } // Trace registers a route for the Trace HTTP Method. // // Returns a *Route and an error which will be filled if route wasn't registered successfully. func (api *APIBuilder) Trace(relativePath string, handlers ...context.Handler) *Route { return api.Handle(http.MethodTrace, relativePath, handlers...) } // Any registers a route for ALL of the HTTP methods: // Get // Post // Put // Delete // Head // Patch // Options // Connect // Trace func (api *APIBuilder) Any(relativePath string, handlers ...context.Handler) (routes []*Route) { for _, m := range AllMethods { r := api.HandleMany(m, relativePath, handlers...) routes = append(routes, r...) } return } type ( // ServerHandler is the interface which all server handlers should implement. // The Iris Application implements it. // See `Party.HandleServer` method for more. ServerHandler interface { ServeHTTPC(*context.Context) } serverBuilder interface { Build() error } ) // HandleServer registers a route for all HTTP methods which forwards the requests to the given server. // // Usage: // // app.HandleServer("/api/identity/{first:string}/orgs/{second:string}/{p:path}", otherApp) // // OR // // app.HandleServer("/api/identity", otherApp) func (api *APIBuilder) HandleServer(path string, server ServerHandler) { if server == nil { return } if app, ok := server.(serverBuilder); ok { // Do an extra check for Build() error at any case // the end-developer didn't call Build before. if err := app.Build(); err != nil { panic(err) } } pathParameterName := "" // Check and get the last parameter name if it's a wildcard one by the end-developer. parsedPath, err := macro.Parse(path, *api.macros) if err != nil { panic(err) } if n := len(parsedPath.Params); n > 0 { lastParam := parsedPath.Params[n-1] if lastParam.IsMacro(macro.Path) { pathParameterName = lastParam.Name // path remains as it was defined by the end-developer. } } // if pathParameterName == "" { pathParameterName = fmt.Sprintf("iris_wildcard_path_parameter%d", len(api.routes.routes)) path = fmt.Sprintf("%s/{%s:path}", path, pathParameterName) } handler := makeServerHandler(pathParameterName, server.ServeHTTPC) api.Any(path, handler) } func makeServerHandler(givenPathParameter string, handler context.Handler) context.Handler { return func(ctx *context.Context) { pathValue := "" if givenPathParameter == "" { pathValue = ctx.Params().GetEntryAt(ctx.Params().Len() - 1).ValueRaw.(string) } else { pathValue = ctx.Params().Get(givenPathParameter) } apiPath := "/" + pathValue r := ctx.Request() r.URL.Path = apiPath r.URL.RawPath = apiPath ctx.Params().Reset() handler(ctx) } } func (api *APIBuilder) registerResourceRoute(reqPath string, h context.Handler) *Route { api.Head(reqPath, h) return api.Get(reqPath, h) } // StaticContent registers a GET and HEAD method routes to the requestPath // that are ready to serve raw static bytes, memory cached. // // Returns the GET *Route. func (api *APIBuilder) StaticContent(reqPath string, cType string, content []byte) *Route { modtime := time.Now() h := func(ctx *context.Context) { ctx.ContentType(cType) if _, err := ctx.WriteWithExpiration(content, modtime); err != nil { ctx.StatusCode(http.StatusInternalServerError) // ctx.Application().Logger().Infof("error while serving []byte via StaticContent: %s", err.Error()) } } return api.registerResourceRoute(reqPath, h) } // Favicon serves static favicon // accepts 2 parameters, second is optional // favPath (string), declare the system directory path of the __.ico // requestPath (string), it's the route's path, by default this is the "/favicon.ico" because some browsers tries to get this by default first, // you can declare your own path if you have more than one favicon (desktop, mobile and so on) // // this func will add a route for you which will static serve the /yuorpath/yourfile.ico to the /yourfile.ico // (nothing special that you can't handle by yourself). // Note that you have to call it on every favicon you have to serve automatically (desktop, mobile and so on). // // Returns the GET *Route. func (api *APIBuilder) Favicon(favPath string, requestPath ...string) *Route { description := favPath favPath = Abs(favPath) f, err := os.Open(favPath) if err != nil { api.logger.Errorf("favicon: file or directory %s not found: %w", favPath, err) return nil } defer f.Close() fi, _ := f.Stat() if fi.IsDir() { // if it's dir the try to get the favicon.ico return api.Favicon(path.Join(favPath, "favicon.ico")) } // copy the bytes here in order to cache and not read the ico on each request. cacheFav := make([]byte, fi.Size()) if _, err = f.Read(cacheFav); err != nil { // Here we are before actually run the server. // So we could panic but we don't, // we just interrupt with a message // to the (user-defined) logger. api.logger.Errorf("favicon: couldn't read the data bytes for %s: %w", favPath, err) return nil } modtime := time.Now() cType := TypeByFilename(favPath) h := func(ctx *context.Context) { ctx.ContentType(cType) if _, err := ctx.WriteWithExpiration(cacheFav, modtime); err != nil { ctx.StatusCode(http.StatusInternalServerError) ctx.Application().Logger().Debugf("while trying to serve the favicon: %s", err.Error()) } } reqPath := "/favicon" + path.Ext(fi.Name()) // we could use the filename, but because standards is /favicon.ico if len(requestPath) > 0 && requestPath[0] != "" { reqPath = requestPath[0] } return api.registerResourceRoute(reqPath, h).Describe(description) } // OnErrorCode registers a handlers chain for this `Party` for a specific HTTP status code. // Read more at: http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml // Look `UseError` and `OnAnyErrorCode` too. func (api *APIBuilder) OnErrorCode(statusCode int, handlers ...context.Handler) (routes []*Route) { routes = append(routes, api.handle(statusCode, "", "/", handlers...)) if api.relativePath != "/" { routes = append(routes, api.handle(statusCode, "", "/{tail:path}", handlers...)) } return } // OnAnyErrorCode registers a handlers chain for all error codes // (4xxx and 5xxx, change the `context.ClientErrorCodes` and `context.ServerErrorCodes` variables to modify those) // Look `UseError` and `OnErrorCode` too. func (api *APIBuilder) OnAnyErrorCode(handlers ...context.Handler) (routes []*Route) { for _, statusCode := range context.ClientAndServerErrorCodes { routes = append(routes, api.OnErrorCode(statusCode, handlers...)...) } if n := len(routes); n > 1 { for _, r := range routes[1:n] { r.NoLog = true } routes[0].Title = "ERR" } return } // RegisterView registers and loads a view engine middleware for this group of routes. // It overrides any of the application's root registered view engines. // To register a view engine per handler chain see the `Context.ViewEngine` instead. // Read `Configuration.ViewEngineContextKey` documentation for more. func (api *APIBuilder) RegisterView(viewEngine context.ViewEngine) { if err := viewEngine.Load(); err != nil { api.logger.Error(err) return } handler := func(ctx *context.Context) { ctx.ViewEngine(viewEngine) ctx.Next() } api.Use(handler) api.UseError(handler) // Note (@kataras): It does not return the Party in order // to keep the iris.Application a compatible Party. } // FallbackView registers one or more fallback views for a template or a template layout. // Usage: // // FallbackView(iris.FallbackView("fallback.html")) // FallbackView(iris.FallbackViewLayout("layouts/fallback.html")) // OR // FallbackView(iris.FallbackViewFunc(ctx iris.Context, err iris.ErrViewNotExist) error { // err.Name is the previous template name. // err.IsLayout reports whether the failure came from the layout template. // err.Data is the template data provided to the previous View call. // [...custom logic e.g. ctx.View("fallback", err.Data)] // }) func (api *APIBuilder) FallbackView(provider context.FallbackViewProvider) { handler := func(ctx *context.Context) { ctx.FallbackView(provider) ctx.Next() } api.Use(handler) api.UseError(handler) } // Layout overrides the parent template layout with a more specific layout for this Party. // It returns the current Party. // // The "tmplLayoutFile" should be a relative path to the templates dir. // Usage: // // app := iris.New() // app.RegisterView(iris.$VIEW_ENGINE("./views", ".$extension")) // my := app.Party("/my").Layout("layouts/mylayout.html") // // my.Get("/", func(ctx iris.Context) { // if err := ctx.View("page1.html"); err != nil { // ctx.HTML(fmt.Sprintf("

      %s

      ", err.Error())) // return // } // }) // // Examples: https://github.com/kataras/iris/tree/main/_examples/view func (api *APIBuilder) Layout(tmplLayoutFile string) Party { handler := func(ctx *context.Context) { ctx.ViewLayout(tmplLayoutFile) ctx.Next() } api.Use(handler) api.UseError(handler) return api } ================================================ FILE: core/router/api_builder_benchmark_test.go ================================================ package router import ( "bytes" "math/rand" "strings" "testing" "time" "github.com/kataras/iris/v12/context" "github.com/kataras/golog" ) // randStringBytesMaskImprSrc helps us to generate random paths for the test, // the below piece of code is external, as an answer to a stackoverflow question. // // START. const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" const ( letterIdxBits = 6 // 6 bits to represent a letter index letterIdxMask = 1<= 0; { if remain == 0 { cache, remain = src.Int63(), letterIdxMax } if idx := int(cache & letterIdxMask); idx < len(letterBytes) { b[i] = letterBytes[idx] i-- } cache >>= letterIdxBits remain-- } return strings.ToLower(string(b)) } // END. func genPaths(routesLength, minCharLength, maxCharLength int) []string { // b := new(strings.Builder) b := new(bytes.Buffer) paths := make([]string, routesLength) pathStart := '/' for i := 0; i < routesLength; i++ { pathSegmentCharsLength := rand.Intn(maxCharLength-minCharLength) + minCharLength b.WriteRune(pathStart) b.WriteString(randStringBytesMaskImprSrc(pathSegmentCharsLength)) b.WriteString("/{name:string}/") // sugar. b.WriteString(randStringBytesMaskImprSrc(pathSegmentCharsLength)) b.WriteString("/{age:int}/end") paths[i] = b.String() b.Reset() } return paths } // Build 1296(=144*9(the available http methods)) routes // with up to 2*range(15-42)+ 2 named paths lowercase letters // and 12 request handlers each. // // GOCACHE=off && go test -run=XXX -bench=BenchmarkAPIBuilder$ -benchtime=10s func BenchmarkAPIBuilder(b *testing.B) { rand.New(rand.NewSource(time.Now().Unix())) noOpHandler := func(ctx *context.Context) {} handlersPerRoute := make(context.Handlers, 12) for i := 0; i < cap(handlersPerRoute); i++ { handlersPerRoute[i] = noOpHandler } routesLength := 144 // i.e /gzhyweumidvelqewrvoyqmzopvuxli/{name:string}/bibrkratnrrhvsjwsxygfwmqwhcstc/{age:int}/end paths := genPaths(routesLength, 15, 42) api := NewAPIBuilder(golog.Default) requestHandler := NewDefaultHandler(nil, nil) b.ReportAllocs() b.ResetTimer() for i := 0; i < routesLength; i++ { api.Any(paths[i], handlersPerRoute...) } if err := requestHandler.Build(api); err != nil { b.Fatal(err) } b.StopTimer() b.Logf("%d routes have just builded\n", len(api.GetRoutes())) } ================================================ FILE: core/router/api_container.go ================================================ package router import ( "net/http" "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/hero" "github.com/kataras/iris/v12/macro" ) // APIContainer is a wrapper of a common `Party` featured by Dependency Injection. // See `Party.ConfigureContainer` for more. type APIContainer struct { // Self returns the original `Party` without DI features. Self Party // Container is the per-party (and its children gets a clone) DI container.. Container *hero.Container } // Party returns a child of this `APIContainer` featured with Dependency Injection. // Like the `Self.Party` method does for the common Router Groups. func (api *APIContainer) Party(relativePath string, handlersFn ...any) *APIContainer { handlers := api.convertHandlerFuncs(relativePath, handlersFn...) p := api.Self.Party(relativePath, handlers...) return p.ConfigureContainer() } // PartyFunc same as `Party` but it accepts a party builder function instead. // Returns the new Party's APIContainer func (api *APIContainer) PartyFunc(relativePath string, fn func(*APIContainer)) *APIContainer { childContainer := api.Party(relativePath) fn(childContainer) return childContainer } // OnError adds an error handler for this Party's DI Hero Container and its handlers (or controllers). // The "errorHandler" handles any error may occurred and returned // during dependencies injection of the Party's hero handlers or from the handlers themselves. // // Same as: // Container.GetErrorHandler = func(ctx iris.Context) hero.ErrorHandler { return errorHandler } // // See `RegisterDependency`, `Use`, `Done` and `Handle` too. func (api *APIContainer) OnError(errorHandler func(*context.Context, error)) { errHandler := hero.ErrorHandlerFunc(errorHandler) api.Container.GetErrorHandler = func(ctx *context.Context) hero.ErrorHandler { return errHandler } } // RegisterDependency adds a dependency. // The value can be a single struct value or a function. // Follow the rules: // * {structValue} // * func(accepts ) returns or (, error) // * func(accepts iris.Context) returns or (, error) // // A Dependency can accept a previous registered dependency and return a new one or the same updated. // * func(accepts1 , accepts2 ) returns or (, error) or error // * func(acceptsPathParameter1 string, id uint64) returns or (, error) // // Usage: // // - RegisterDependency(loggerService{prefix: "dev"}) // - RegisterDependency(func(ctx iris.Context) User {...}) // - RegisterDependency(func(User) OtherResponse {...}) // // See `OnError`, `Use`, `Done` and `Handle` too. func (api *APIContainer) RegisterDependency(dependency any) *hero.Dependency { return api.Container.Register(dependency) } // UseResultHandler adds a result handler to the Container. // A result handler can be used to inject the returned struct value // from a request handler or to replace the default renderer. func (api *APIContainer) UseResultHandler(handler func(next hero.ResultHandler) hero.ResultHandler) *APIContainer { api.Container.UseResultHandler(handler) return api } // EnableStrictMode sets the container's DisablePayloadAutoBinding and MarkExportedFieldsAsRequired to true. // Meaning that all struct's fields (or function's parameters) should be binded manually (except the path parameters). // // Note that children will clone the same properties. // Call the same method with `false` for children // to enable automatic binding on missing dependencies. // // Strict mode is disabled by default; // structs or path parameters that don't match to registered dependencies // are automatically binded from the request context (body and url path parameters respectfully). func (api *APIContainer) EnableStrictMode(strictMode bool) *APIContainer { api.Container.DisablePayloadAutoBinding = strictMode api.Container.MarkExportedFieldsAsRequired = strictMode return api } // EnableStructDependents sets the container's EnableStructDependents to true. // It's used to automatically fill the dependencies of a struct's fields // based on the previous registered dependencies, just like function inputs. func (api *APIContainer) EnableStructDependents() *APIContainer { api.Container.EnableStructDependents = true return api } // SetDependencyMatcher replaces the function that compares equality between // a dependency and an input (struct field or function parameter). // // Defaults to hero.DefaultMatchDependencyFunc. func (api *APIContainer) SetDependencyMatcher(fn hero.DependencyMatcher) *APIContainer { if fn == nil { panic("api container: set dependency matcher: fn cannot be nil") } api.Container.DependencyMatcher = fn return api } // convertHandlerFuncs accepts Iris hero handlers and returns a slice of native Iris handlers. func (api *APIContainer) convertHandlerFuncs(relativePath string, handlersFn ...any) context.Handlers { fullpath := api.Self.GetRelPath() + relativePath paramsCount := macro.CountParams(fullpath, *api.Self.Macros()) handlers := make(context.Handlers, 0, len(handlersFn)) for _, h := range handlersFn { handlers = append(handlers, api.Container.HandlerWithParams(h, paramsCount)) } // Note: let end-developer to decide that through Party.SetExecutionRules. // On that type of handlers the end-developer does not have to include the Context in the handler, // so the ctx.Next is automatically called unless an `ErrStopExecution` returned (implementation inside hero pkg). // // o := ExecutionOptions{Force: true} // o.apply(&handlers) return handlers } func fixRouteInfo(route *Route, handlersFn []any) { // Fix main handler name and source modified by execution rules wrapper. route.MainHandlerName, route.MainHandlerIndex = context.MainHandlerName(handlersFn...) if len(handlersFn) > route.MainHandlerIndex { route.SourceFileName, route.SourceLineNumber = context.HandlerFileLineRel(handlersFn[route.MainHandlerIndex]) } } // Handler receives a function which can receive dependencies and output result // and returns a common Iris Handler, useful for Versioning API integration otherwise // the `Handle/Get/Post...` methods are preferable. func (api *APIContainer) Handler(handlerFn any, handlerParamsCount int) context.Handler { paramsCount := macro.CountParams(api.Self.GetRelPath(), *api.Self.Macros()) + handlerParamsCount return api.Container.HandlerWithParams(handlerFn, paramsCount) } // Use same as `Self.Use` but it accepts dynamic functions as its "handlersFn" input. // // See `OnError`, `RegisterDependency`, `Done` and `Handle` for more. func (api *APIContainer) Use(handlersFn ...any) { handlers := api.convertHandlerFuncs("/", handlersFn...) api.Self.Use(handlers...) } // Done same as `Self.Done` but it accepts dynamic functions as its "handlersFn" input. // See `OnError`, `RegisterDependency`, `Use` and `Handle` for more. func (api *APIContainer) Done(handlersFn ...any) { handlers := api.convertHandlerFuncs("/", handlersFn...) api.Self.Done(handlers...) } // Handle same as `Self.Handle` but it accepts one or more "handlersFn" functions which each one of them // can accept any input arguments that match with the Party's registered Container's `Dependencies` and // any output result; like custom structs , string, []byte, int, error, // a combination of the above, hero.Result(hero.View | hero.Response) and more. // // It's common from a hero handler to not even need to accept a `Context`, for that reason, // the "handlersFn" will call `ctx.Next()` automatically when not called manually. // To stop the execution and not continue to the next "handlersFn" // the end-developer should output an error and return `iris.ErrStopExecution`. // // See `OnError`, `RegisterDependency`, `Use`, `Done`, `Get`, `Post`, `Put`, `Patch` and `Delete` too. func (api *APIContainer) Handle(method, relativePath string, handlersFn ...any) *Route { handlers := api.convertHandlerFuncs(relativePath, handlersFn...) route := api.Self.Handle(method, relativePath, handlers...) fixRouteInfo(route, handlersFn) return route } // Get registers a route for the Get HTTP Method. // // Returns a *Route and an error which will be filled if route wasn't registered successfully. func (api *APIContainer) Get(relativePath string, handlersFn ...any) *Route { return api.Handle(http.MethodGet, relativePath, handlersFn...) } // Post registers a route for the Post HTTP Method. // // Returns a *Route and an error which will be filled if route wasn't registered successfully. func (api *APIContainer) Post(relativePath string, handlersFn ...any) *Route { return api.Handle(http.MethodPost, relativePath, handlersFn...) } // Put registers a route for the Put HTTP Method. // // Returns a *Route and an error which will be filled if route wasn't registered successfully. func (api *APIContainer) Put(relativePath string, handlersFn ...any) *Route { return api.Handle(http.MethodPut, relativePath, handlersFn...) } // Delete registers a route for the Delete HTTP Method. // // Returns a *Route and an error which will be filled if route wasn't registered successfully. func (api *APIContainer) Delete(relativePath string, handlersFn ...any) *Route { return api.Handle(http.MethodDelete, relativePath, handlersFn...) } // Connect registers a route for the Connect HTTP Method. // // Returns a *Route and an error which will be filled if route wasn't registered successfully. func (api *APIContainer) Connect(relativePath string, handlersFn ...any) *Route { return api.Handle(http.MethodConnect, relativePath, handlersFn...) } // Head registers a route for the Head HTTP Method. // // Returns a *Route and an error which will be filled if route wasn't registered successfully. func (api *APIContainer) Head(relativePath string, handlersFn ...any) *Route { return api.Handle(http.MethodHead, relativePath, handlersFn...) } // Options registers a route for the Options HTTP Method. // // Returns a *Route and an error which will be filled if route wasn't registered successfully. func (api *APIContainer) Options(relativePath string, handlersFn ...any) *Route { return api.Handle(http.MethodOptions, relativePath, handlersFn...) } // Patch registers a route for the Patch HTTP Method. // // Returns a *Route and an error which will be filled if route wasn't registered successfully. func (api *APIContainer) Patch(relativePath string, handlersFn ...any) *Route { return api.Handle(http.MethodPatch, relativePath, handlersFn...) } // Trace registers a route for the Trace HTTP Method. // // Returns a *Route and an error which will be filled if route wasn't registered successfully. func (api *APIContainer) Trace(relativePath string, handlersFn ...any) *Route { return api.Handle(http.MethodTrace, relativePath, handlersFn...) } // Any registers a route for ALL of the HTTP methods: // Get // Post // Put // Delete // Head // Patch // Options // Connect // Trace func (api *APIContainer) Any(relativePath string, handlersFn ...any) (routes []*Route) { handlers := api.convertHandlerFuncs(relativePath, handlersFn...) for _, m := range AllMethods { r := api.Self.HandleMany(m, relativePath, handlers...) routes = append(routes, r...) } return } /* TODO: fix those // OnErrorCode registers a handlers chain for this `Party` for a specific HTTP status code. // Read more at: http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml // Look `OnAnyErrorCode` too. func (api *APIContainer) OnErrorCode(statusCode int, handlersFn ...any) []*Route { handlers := api.convertHandlerFuncs("/{tail:path}", handlersFn...) return api.Self.OnErrorCode(statusCode, handlers...) } // OnAnyErrorCode registers a handlers chain for all error codes // (4xxx and 5xxx, change the `ClientErrorCodes` and `ServerErrorCodes` variables to modify those) // Look `OnErrorCode` too. func (api *APIContainer) OnAnyErrorCode(handlersFn ...any) []*Route { handlers := api.convertHandlerFuncs("/{tail:path}", handlersFn...) return api.Self.OnAnyErrorCode(handlers...) } */ ================================================ FILE: core/router/fs.go ================================================ package router import ( "bytes" stdContext "context" "fmt" "html" "html/template" "io" "net/http" "net/url" "os" "path" "path/filepath" "reflect" "regexp" "runtime" "sort" "strconv" "strings" "sync" "time" "github.com/kataras/iris/v12/context" ) const indexName = "/index.html" // DirListFunc is the function signature for customizing directory and file listing. // See `DirList` and `DirListRich` functions for its implementations. type DirListFunc func(ctx *context.Context, dirOptions DirOptions, dirName string, dir http.File) error // Attachments options for files to be downloaded and saved locally by the client. // See `DirOptions`. type Attachments struct { // Set to true to enable the files to be downloaded and // saved locally by the client, instead of serving the file. Enable bool // Options to send files with a limit of bytes sent per second. Limit float64 Burst int // Use this function to change the sent filename. NameFunc func(systemName string) (attachmentName string) } // DirCacheOptions holds the options for the cached file system. // See `DirOptions`structure for more. type DirCacheOptions struct { // Enable or disable cache. Enable bool // Minimum content size for compression in bytes. CompressMinSize int64 // Ignore compress files that match this pattern. CompressIgnore *regexp.Regexp // The available sever's encodings to be negotiated with the client's needs, // common values: gzip, br. Encodings []string // If greater than zero then prints information about cached files to the stdout. // If it's 1 then it prints only the total cached and after-compression reduced file sizes // If it's 2 then it prints it per file too. Verbose uint8 } // DirOptions contains the settings that `FileServer` can use to serve files. // See `DefaultDirOptions`. type DirOptions struct { // Defaults to "/index.html", if request path is ending with **/*/$IndexName // then it redirects to **/*(/). // That index handler is registered automatically // by the framework unless but it can be overridden. IndexName string // PushTargets filenames (map's value) to // be served without additional client's requests (HTTP/2 Push) // when a specific request path (map's key WITHOUT prefix) // is requested and it's not a directory (it's an `IndexFile`). // // Example: // "/": { // "favicon.ico", // "js/main.js", // "css/main.css", // } PushTargets map[string][]string // PushTargetsRegexp like `PushTargets` but accepts regexp which // is compared against all files under a directory (recursively). // The `IndexName` should be set. // // Example: // "/": regexp.MustCompile("((.*).js|(.*).css|(.*).ico)$") // See `iris.MatchCommonAssets` too. PushTargetsRegexp map[string]*regexp.Regexp // Cache to enable in-memory cache and pre-compress files. Cache DirCacheOptions // When files should served under compression. Compress bool // List the files inside the current requested // directory if `IndexName` not found. ShowList bool // If `ShowList` is true then this function will be used instead // of the default one to show the list of files // of a current requested directory(dir). // See `DirListRich` package-level function too. DirList DirListFunc // Show hidden files or directories or not when `ShowList` is true. ShowHidden bool // Files downloaded and saved locally. Attachments Attachments // Optional validator that loops through each requested resource. AssetValidator func(ctx *context.Context, name string) bool // If enabled then the router will render the index file on any not-found file // instead of firing the 404 error code handler. // Make sure the `IndexName` field is set. // // Usage: // app.HandleDir("/", iris.Dir("./public"), iris.DirOptions{ // IndexName: "index.html", // SPA: true, // }) SPA bool } // DefaultDirOptions holds the default settings for `FileServer`. var DefaultDirOptions = DirOptions{ IndexName: indexName, PushTargets: make(map[string][]string), PushTargetsRegexp: make(map[string]*regexp.Regexp), Cache: DirCacheOptions{ // Disable by-default. Enable: false, // Don't compress files smaller than 300 bytes. CompressMinSize: 300, // Gzip, deflate, br(brotli), snappy. Encodings: context.AllEncodings, // Log to the stdout (no iris logger) the total reduced file size. Verbose: 1, }, Compress: true, ShowList: false, DirList: DirListRich(DirListRichOptions{ Tmpl: DirListRichTemplate, TmplName: "dirlist", }), Attachments: Attachments{ Enable: false, Limit: 0, Burst: 0, }, AssetValidator: nil, SPA: false, } // FileServer returns a Handler which serves files from a specific file system. // The first parameter is the file system, // if it's a `http.Dir` the files should be located near the executable program. // The second parameter is the settings that the caller can use to customize the behavior. // // See `Party#HandleDir` too. // Examples can be found at: https://github.com/kataras/iris/tree/main/_examples/file-server func FileServer(fs http.FileSystem, options DirOptions) context.Handler { if fs == nil { panic("FileServer: fs is nil. The fs parameter should point to a file system of physical system directory or to an embedded one") } // Make sure index name starts with a slash. if options.IndexName != "" { options.IndexName = prefix(options.IndexName, "/") } // Make sure PushTarget's paths are in the proper form. for path, filenames := range options.PushTargets { for idx, filename := range filenames { filenames[idx] = filepath.ToSlash(filename) } options.PushTargets[path] = filenames } if !options.Attachments.Enable { // make sure rate limiting is not used when attachments are not. options.Attachments.Limit = 0 options.Attachments.Burst = 0 } plainStatusCode := func(ctx *context.Context, statusCode int) { if writer, ok := ctx.ResponseWriter().(*context.CompressResponseWriter); ok { writer.Disabled = true } ctx.StatusCode(statusCode) } dirList := options.DirList if dirList == nil { dirList = DirList } open := fsOpener(fs, options.Cache) // We only need its opener, the "fs" is NOT used below. h := func(ctx *context.Context) { r := ctx.Request() name := prefix(r.URL.Path, "/") r.URL.Path = name var ( indexFound bool noRedirect bool ) f, err := open(name, r) if err != nil { if options.SPA && name != options.IndexName { oldname := name name = prefix(options.IndexName, "/") // to match push targets. r.URL.Path = name f, err = open(name, r) // try find the main index. if err != nil { r.URL.Path = oldname plainStatusCode(ctx, http.StatusNotFound) return } indexFound = true // to support push targets. noRedirect = true // to disable redirecting back to /. } else { plainStatusCode(ctx, http.StatusNotFound) return } } defer f.Close() info, err := f.Stat() if err != nil { plainStatusCode(ctx, http.StatusNotFound) return } // use contents of index.html for directory, if present if info.IsDir() && options.IndexName != "" { // Note that, in contrast of the default net/http mechanism; // here different handlers may serve the indexes // if manually then this will block will never fire, // if index handler are automatically registered by the framework // then this block will be fired on indexes because the static site routes are registered using the static route's handler. // // End-developers must have the chance to register different logic and middlewares // to an index file, useful on Single Page Applications. index := strings.TrimSuffix(name, "/") + options.IndexName fIndex, err := open(index, r) if err == nil { defer fIndex.Close() infoIndex, err := fIndex.Stat() if err == nil { indexFound = true f = fIndex info = infoIndex } } } // Still a directory? (we didn't find an index.html file) if info.IsDir() { if !options.ShowList { plainStatusCode(ctx, http.StatusNotFound) return } if modified, err := ctx.CheckIfModifiedSince(info.ModTime()); !modified && err == nil { ctx.WriteNotModified() ctx.StatusCode(http.StatusNotModified) ctx.Next() return } ctx.SetLastModified(info.ModTime()) err = dirList(ctx, options, info.Name(), f) if err != nil { ctx.Application().Logger().Errorf("FileServer: dirList: %v", err) plainStatusCode(ctx, http.StatusInternalServerError) return } ctx.Next() return } // index requested, send a moved permanently status // and navigate back to the route without the index suffix. if !noRedirect && options.IndexName != "" && strings.HasSuffix(name, options.IndexName) { localRedirect(ctx, "./") return } if options.AssetValidator != nil { if !options.AssetValidator(ctx, name) { errCode := ctx.GetStatusCode() if ctx.ResponseWriter().Written() <= context.StatusCodeWritten { // if nothing written as body from the AssetValidator but 200 status code(which is the default), // then we assume that the end-developer just returned false expecting this to be not found. if errCode == http.StatusOK { errCode = http.StatusNotFound } plainStatusCode(ctx, errCode) } return } } // try to find and send the correct content type based on the filename // and the binary data inside "f". detectOrWriteContentType(ctx, info.Name(), f) // if not index file and attachments should be force-sent: if !indexFound && options.Attachments.Enable { destName := info.Name() // diposition := "attachment" if nameFunc := options.Attachments.NameFunc; nameFunc != nil { destName = nameFunc(destName) } ctx.ResponseWriter().Header().Set(context.ContentDispositionHeaderKey, context.MakeDisposition(destName)) } // the encoding saved from the negotiation. encoding, isCached := getFileEncoding(f) if isCached { // if it's cached and its settings didnt allow this file to be compressed // then don't try to compress it on the fly, even if the options.Compress was set to true. if encoding != "" { if ctx.ResponseWriter().Header().Get(context.ContentEncodingHeaderKey) != "" { // disable any compression writer if that header exist, // note that, we don't directly check for CompressResponseWriter type // because it may be a ResponseRecorder. ctx.CompressWriter(false) } // Set the response header we need, the data are already compressed. context.AddCompressHeaders(ctx.ResponseWriter().Header(), encoding) } } else if options.Compress { ctx.CompressWriter(true) } if indexFound && !options.Attachments.Enable { if indexAssets, ok := options.PushTargets[name]; ok { if pusher, ok := ctx.ResponseWriter().Naive().(http.Pusher); ok { var pushOpts *http.PushOptions if encoding != "" { pushOpts = &http.PushOptions{Header: r.Header} } for _, indexAsset := range indexAssets { if indexAsset[0] != '/' { // it's relative path. indexAsset = path.Join(r.RequestURI, indexAsset) } if err = pusher.Push(indexAsset, pushOpts); err != nil { break } } } } if regex, ok := options.PushTargetsRegexp[r.URL.Path]; ok { if pusher, ok := ctx.ResponseWriter().Naive().(http.Pusher); ok { var pushOpts *http.PushOptions if encoding != "" { pushOpts = &http.PushOptions{Header: r.Header} } prefixURL := strings.TrimSuffix(r.RequestURI, name) names, err := context.FindNames(fs, name) if err == nil { for _, indexAsset := range names { // it's an index file, do not pushed that. if strings.HasSuffix(prefix(indexAsset, "/"), options.IndexName) { continue } // match using relative path (without the first '/' slash) // to keep consistency between the `PushTargets` behavior if regex.MatchString(indexAsset) { // println("Regex Matched: " + indexAsset) if err = pusher.Push(path.Join(prefixURL, indexAsset), pushOpts); err != nil { break } } } } } } } // If limit is 0 then same as ServeContent. ctx.ServeContentWithRate(f, info.Name(), info.ModTime(), options.Attachments.Limit, options.Attachments.Burst) if serveCode := ctx.GetStatusCode(); context.StatusCodeNotSuccessful(serveCode) { plainStatusCode(ctx, serveCode) return } ctx.Next() // fire any middleware, if any. } return h } // StripPrefix returns a handler that serves HTTP requests // by removing the given prefix from the request URL's Path // and invoking the handler h. StripPrefix handles a // request for a path that doesn't begin with prefix by // replying with an HTTP 404 not found error. // // Usage: // fileserver := FileServer("./static_files", DirOptions {...}) // h := StripPrefix("/static", fileserver) // app.Get("/static/{file:path}", h) // app.Head("/static/{file:path}", h) func StripPrefix(prefix string, h context.Handler) context.Handler { if prefix == "" { return h } // here we separate the path from the subdomain (if any), we care only for the path // fixes a bug when serving static files via a subdomain canonicalPrefix := prefix if dotWSlashIdx := strings.Index(canonicalPrefix, SubdomainPrefix); dotWSlashIdx > 0 { canonicalPrefix = canonicalPrefix[dotWSlashIdx+1:] } canonicalPrefix = toWebPath(canonicalPrefix) return func(ctx *context.Context) { u := ctx.Request().URL if p := strings.TrimPrefix(u.Path, canonicalPrefix); len(p) < len(u.Path) { if p == "" { p = "/" } u.Path = p h(ctx) } else { ctx.NotFound() } } } func toWebPath(systemPath string) string { // winos slash to slash webpath := strings.ReplaceAll(systemPath, "\\", "/") // double slashes to single webpath = strings.ReplaceAll(webpath, "//", "/") return webpath } // Abs calls filepath.Abs but ignores the error and // returns the original value if any error occurred. func Abs(path string) string { absPath, err := filepath.Abs(path) if err != nil { return path } return absPath } // The algorithm uses at most sniffLen bytes to make its decision. const sniffLen = 512 func detectOrWriteContentType(ctx *context.Context, name string, content io.ReadSeeker) (string, error) { // If Content-Type isn't set, use the file's extension to find it, but // if the Content-Type is unset explicitly, do not sniff the type. ctypes, haveType := ctx.ResponseWriter().Header()["Content-Type"] var ctype string if !haveType { ctype = TypeByExtension(filepath.Ext(name)) if ctype == "" { // read a chunk to decide between utf-8 text and binary var buf [sniffLen]byte n, _ := io.ReadFull(content, buf[:]) ctype = http.DetectContentType(buf[:n]) _, err := content.Seek(0, io.SeekStart) // rewind to output whole file if err != nil { return "", err } } ctx.ContentType(ctype) } else if len(ctypes) > 0 { ctype = ctypes[0] } return ctype, nil } // localRedirect gives a Moved Permanently response. // It does not convert relative paths to absolute paths like Redirect does. func localRedirect(ctx *context.Context, newPath string) { if q := ctx.Request().URL.RawQuery; q != "" { newPath += "?" + q } ctx.Header("Location", newPath) ctx.StatusCode(http.StatusMovedPermanently) } // DirectoryExists returns true if a directory(or file) exists, otherwise false func DirectoryExists(dir string) bool { if _, err := os.Stat(dir); os.IsNotExist(err) { return false } return true } // Instead of path.Base(filepath.ToSlash(s)) // let's do something like that, it is faster // (used to list directories on serve-time too): func toBaseName(s string) string { n := len(s) - 1 for i := n; i >= 0; i-- { if c := s[i]; c == '/' || c == '\\' { if i == n { // "s" ends with a slash, remove it and retry. return toBaseName(s[:n]) } return s[i+1:] // return the rest, trimming the slash. } } return s } // IsHidden checks a file is hidden or not func IsHidden(file os.FileInfo) bool { isHidden := false if runtime.GOOS == "windows" { fa := reflect.ValueOf(file.Sys()).Elem().FieldByName("FileAttributes").Uint() bytefa := []byte(strconv.FormatUint(fa, 2)) if bytefa[len(bytefa)-2] == '1' { isHidden = true } } else { isHidden = file.Name()[0] == '.' } return isHidden } // DirList is a `DirListFunc` which renders directories and files in html, but plain, mode. // See `DirListRich` for more. func DirList(ctx *context.Context, dirOptions DirOptions, dirName string, dir http.File) error { dirs, err := dir.Readdir(-1) if err != nil { return err } sort.Slice(dirs, func(i, j int) bool { return dirs[i].Name() < dirs[j].Name() }) ctx.ContentType(context.ContentHTMLHeaderValue) _, err = ctx.WriteString("
      \n") if err != nil { return err } // show current directory _, err = ctx.Writef("

      Current Directory: %s

      ", ctx.Request().RequestURI) if err != nil { return err } _, err = ctx.WriteString("
      \n") return err } // DirListRichOptions the options for the `DirListRich` helper function. type DirListRichOptions struct { // If not nil then this template's "dirlist" is used to render the listing page. Tmpl *template.Template // If not empty then this view file is used to render the listing page. // The view should be registered with `Application.RegisterView`. // E.g. "dirlist.html" TmplName string } // DirListRich is a `DirListFunc` which can be passed to `DirOptions.DirList` field // to override the default file listing appearance. // See `DirListRichTemplate` to modify the template, if necessary. func DirListRich(opts ...DirListRichOptions) DirListFunc { var options DirListRichOptions if len(opts) > 0 { options = opts[0] } if options.TmplName == "" && options.Tmpl == nil { options.Tmpl = DirListRichTemplate } return func(ctx *context.Context, dirOptions DirOptions, dirName string, dir http.File) error { dirs, err := dir.Readdir(-1) if err != nil { return err } sortBy := ctx.URLParam("sort") switch sortBy { case "name": sort.Slice(dirs, func(i, j int) bool { return dirs[i].Name() < dirs[j].Name() }) case "size": sort.Slice(dirs, func(i, j int) bool { return dirs[i].Size() < dirs[j].Size() }) default: sort.Slice(dirs, func(i, j int) bool { return dirs[i].ModTime().After(dirs[j].ModTime()) }) } pageData := listPageData{ Title: fmt.Sprintf("List of %d files", len(dirs)), Files: make([]fileInfoData, 0, len(dirs)), } for _, d := range dirs { if !dirOptions.ShowHidden && IsHidden(d) { continue } name := toBaseName(d.Name()) u, err := url.Parse(ctx.Request().RequestURI) // clone url and remove query (#1882). if err != nil { return fmt.Errorf("name: %s: error: %w", name, err) } u.RawQuery = "" upath := url.URL{Path: path.Join(u.String(), name)} viewName := name if d.IsDir() { viewName += "/" } shouldDownload := dirOptions.Attachments.Enable && !d.IsDir() pageData.Files = append(pageData.Files, fileInfoData{ Info: d, ModTime: d.ModTime().UTC().Format(http.TimeFormat), Path: upath.String(), RelPath: path.Join(ctx.Path(), name), Name: html.EscapeString(viewName), Download: shouldDownload, }) } if options.TmplName != "" { return ctx.View(options.TmplName, pageData) } return options.Tmpl.ExecuteTemplate(ctx, "dirlist", pageData) } } type ( listPageData struct { Title string // the document's title. Files []fileInfoData } fileInfoData struct { Info os.FileInfo ModTime string // format-ed time. Path string // the request path. RelPath string // file path without the system directory itself (we are not exposing it to the user). Name string // the html-escaped name. Download bool // the file should be downloaded (attachment instead of inline view). } ) // FormatBytes returns a string representation of the "b" bytes. func FormatBytes(b int64) string { const unit = 1000 if b < unit { return fmt.Sprintf("%d B", b) } div, exp := int64(unit), 0 for n := b / unit; n >= unit; n /= unit { div *= unit exp++ } return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "kMGTPE"[exp]) } // DirListRichTemplate is the html template the `DirListRich` function is using to render // the directories and files. var DirListRichTemplate = template.Must(template.New("dirlist"). Funcs(template.FuncMap{ "formatBytes": FormatBytes, }).Parse(` {{.Title}} {{ range $idx, $file := .Files }} {{ if $file.Download }} {{ else }} {{ end }} {{ if $file.Info.IsDir }} {{ else }} {{ end }} {{ end }}
      # Name Size
      {{ $idx }}{{ $file.Name }}{{ $file.Name }}Dir{{ formatBytes $file.Info.Size }}
      `)) // fsOpener returns the file system opener, cached one or the original based on the options Enable field. func fsOpener(fs http.FileSystem, options DirCacheOptions) func(name string, r *http.Request) (http.File, error) { if !options.Enable { // if it's not enabled return the opener original one. return func(name string, _ *http.Request) (http.File, error) { return fs.Open(name) } } c, err := cache(fs, options) if err != nil { panic(err) } return c.Ropen } // cache returns a http.FileSystem which serves in-memory cached (compressed) files. // Look `Verbose` function to print out information while in development status. func cache(fs http.FileSystem, options DirCacheOptions) (*cacheFS, error) { start := time.Now() names, err := context.FindNames(fs, "/") if err != nil { return nil, err } sort.Slice(names, func(i, j int) bool { return strings.Count(names[j], "/") > strings.Count(names[i], "/") }) dirs, err := findDirs(fs, names) if err != nil { return nil, err } files, err := cacheFiles(stdContext.Background(), fs, names, options.Encodings, options.CompressMinSize, options.CompressIgnore) if err != nil { return nil, err } ttc := time.Since(start) c := &cacheFS{dirs: dirs, files: files, algs: options.Encodings} go logCacheFS(c, ttc, len(names), options.Verbose) return c, nil } func logCacheFS(fs *cacheFS, ttc time.Duration, n int, level uint8) { if level == 0 { return } var ( totalLength int64 totalCompressedLength = make(map[string]int64) totalCompressedContents int64 ) for name, f := range fs.files { uncompressed := f.algs[""] totalLength += int64(len(uncompressed)) if level == 2 { fmt.Printf("%s (%s)\n", name, FormatBytes(int64(len(uncompressed)))) } for alg, contents := range f.algs { if alg == "" { continue } totalCompressedContents++ if len(alg) < 7 { alg += strings.Repeat(" ", 7-len(alg)) } totalCompressedLength[alg] += int64(len(contents)) if level == 2 { fmt.Printf("%s (%s)\n", alg, FormatBytes(int64(len(contents)))) } } } fmt.Printf("Time to complete the compression and caching of [%d/%d] files: %s\n", totalCompressedContents/int64(len(fs.algs)), n, ttc) fmt.Printf("Total size reduced from %s to:\n", FormatBytes(totalLength)) for alg, length := range totalCompressedLength { // https://en.wikipedia.org/wiki/Data_compression_ratio reducedRatio := 1 - float64(length)/float64(totalLength) fmt.Printf("%s (%s) [%.2f%%]\n", alg, FormatBytes(length), reducedRatio*100) } } type cacheFS struct { dirs map[string]*dir files fileMap algs []string } var _ http.FileSystem = (*cacheFS)(nil) // Open returns the http.File based on "name". // If file, it always returns a cached file of uncompressed data. // See `Ropen` too. func (c *cacheFS) Open(name string) (http.File, error) { // we always fetch with the sep, // as http requests will do, // and the filename's info.Name() is always base // and without separator prefix // (keep note, we need that fileInfo // wrapper because go-bindata's Name originally // returns the fullname while the http.Dir returns the basename). if name == "" || name[0] != '/' { name = "/" + name } if d, ok := c.dirs[name]; ok { return d, nil } if f, ok := c.files[name]; ok { return f.Get("") } return nil, os.ErrNotExist } // Ropen returns the http.File based on "name". // If file, it negotiates the content encoding, // based on the given algorithms, and // returns the cached file with compressed data, // if the encoding was empty then it // returns the cached file with its original, uncompressed data. // // A check of `GetEncoding(file)` is required to set // response headers. // // Note: We don't require a response writer to set the headers // because the caller of this method may stop the operation // before file's contents are written to the client. func (c *cacheFS) Ropen(name string, r *http.Request) (http.File, error) { if name == "" || name[0] != '/' { name = "/" + name } if d, ok := c.dirs[name]; ok { return d, nil } if f, ok := c.files[name]; ok { encoding, _ := context.GetEncoding(r, c.algs) return f.Get(encoding) } return nil, os.ErrNotExist } // getFileEncoding returns the encoding of an http.File. // If the "f" file was created by a `Cache` call then // it returns the content encoding that this file was cached with. // It returns empty string for files that // were too small or ignored to be compressed. // // It also reports whether the "f" is a cached file or not. func getFileEncoding(f http.File) (string, bool) { if f == nil { return "", false } ff, ok := f.(*file) if !ok { return "", false } return ff.alg, true } // type fileMap map[string] /* path */ map[string] /*compression alg or empty for original */ []byte /*contents */ type fileMap map[string]*file func cacheFiles(ctx stdContext.Context, fs http.FileSystem, names []string, compressAlgs []string, compressMinSize int64, compressIgnore *regexp.Regexp) (fileMap, error) { ctx, cancel := stdContext.WithCancel(ctx) defer cancel() list := make(fileMap, len(names)) mutex := new(sync.Mutex) cache := func(name string) error { f, err := fs.Open(name) if err != nil { return err } inf, err := f.Stat() if err != nil { f.Close() return err } fi := newFileInfo(path.Base(name), inf.Mode(), inf.ModTime()) contents, err := io.ReadAll(f) f.Close() if err != nil { return err } algs := make(map[string][]byte, len(compressAlgs)+1) algs[""] = contents // original contents. mutex.Lock() list[name] = newFile(name, fi, algs) mutex.Unlock() if compressMinSize > 0 && compressMinSize > int64(len(contents)) { return nil } if compressIgnore != nil && compressIgnore.MatchString(name) { return nil } // Note: // We can fire a new goroutine for each compression of the same file // but this will have an impact on CPU cost if // thousands of files running 4 compressions at the same time, // so, unless requested keep it as it's. buf := new(bytes.Buffer) for _, alg := range compressAlgs { select { case <-ctx.Done(): return ctx.Err() // stop all compressions if at least one file failed to. default: } if alg == "brotli" { alg = "br" } w, err := context.NewCompressWriter(buf, strings.ToLower(alg), -1) if err != nil { return err } _, err = w.Write(contents) w.Close() if err != nil { return err } bs := buf.Bytes() dest := make([]byte, len(bs)) copy(dest, bs) algs[alg] = dest buf.Reset() } return nil } var ( err error wg sync.WaitGroup errOnce sync.Once ) for _, name := range names { wg.Add(1) go func(name string) { defer wg.Done() if fnErr := cache(name); fnErr != nil { errOnce.Do(func() { err = fnErr cancel() }) } }(name) } wg.Wait() return list, err } type cacheStoreFile interface { Get(compressionAlgorithm string) (http.File, error) } type file struct { io.ReadSeeker // nil on cache store and filled on file Get. algs map[string][]byte // non empty for store and nil for files. alg string // empty for cache store, filled with the compression algorithm of this file (useful to decompress). name string baseName string info os.FileInfo } var ( _ http.File = (*file)(nil) _ cacheStoreFile = (*file)(nil) ) func newFile(name string, fi os.FileInfo, algs map[string][]byte) *file { return &file{ name: name, baseName: path.Base(name), info: fi, algs: algs, } } func (f *file) Close() error { return nil } func (f *file) Readdir(count int) ([]os.FileInfo, error) { return nil, os.ErrNotExist } func (f *file) Stat() (os.FileInfo, error) { return f.info, nil } // Get returns a new http.File to be served. // Caller should check if a specific http.File has this method as well. func (f *file) Get(alg string) (http.File, error) { // The "alg" can be empty for non-compressed file contents. // We don't need a new structure. if contents, ok := f.algs[alg]; ok { return &file{ name: f.name, baseName: f.baseName, info: f.info, alg: alg, ReadSeeker: bytes.NewReader(contents), }, nil } // When client accept compression but cached contents are not compressed, // e.g. file too small or ignored one. return f.Get("") } type fileInfo struct { baseName string modTime time.Time isDir bool mode os.FileMode } var _ os.FileInfo = (*fileInfo)(nil) func newFileInfo(baseName string, mode os.FileMode, modTime time.Time) *fileInfo { return &fileInfo{ baseName: baseName, modTime: modTime, mode: mode, isDir: mode == os.ModeDir, } } func (fi *fileInfo) Close() error { return nil } func (fi *fileInfo) Name() string { return fi.baseName } func (fi *fileInfo) Mode() os.FileMode { return fi.mode } func (fi *fileInfo) ModTime() time.Time { return fi.modTime } func (fi *fileInfo) IsDir() bool { return fi.isDir } func (fi *fileInfo) Size() int64 { return 0 } func (fi *fileInfo) Sys() any { return fi } type dir struct { os.FileInfo // *fileInfo io.ReadSeeker // nil name string // fullname, for any case. baseName string children []os.FileInfo // a slice of *fileInfo } var ( _ os.FileInfo = (*dir)(nil) _ http.File = (*dir)(nil) ) func (d *dir) Close() error { return nil } func (d *dir) Name() string { return d.baseName } func (d *dir) Stat() (os.FileInfo, error) { return d.FileInfo, nil } func (d *dir) Readdir(count int) ([]os.FileInfo, error) { return d.children, nil } func newDir(fi os.FileInfo, fullname string) *dir { baseName := path.Base(fullname) return &dir{ FileInfo: newFileInfo(baseName, os.ModeDir, fi.ModTime()), name: fullname, baseName: baseName, } } var _ http.File = (*dir)(nil) // returns unorderded map of directories both reclusive and flat. func findDirs(fs http.FileSystem, names []string) (map[string]*dir, error) { dirs := make(map[string]*dir) for _, name := range names { f, err := fs.Open(name) if err != nil { return nil, err } inf, err := f.Stat() if err != nil { return nil, err } dirName := path.Dir(name) d, ok := dirs[dirName] if !ok { fi := newFileInfo(path.Base(dirName), os.ModeDir, inf.ModTime()) d = newDir(fi, dirName) dirs[dirName] = d } fi := newFileInfo(path.Base(name), inf.Mode(), inf.ModTime()) // Add the directory file info (=this dir) to the parent one, // so `ShowList` can render sub-directories of this dir. parentName := path.Dir(dirName) if parent, hasParent := dirs[parentName]; hasParent { parent.children = append(parent.children, d) } d.children = append(d.children, fi) } return dirs, nil } ================================================ FILE: core/router/handler.go ================================================ package router import ( "errors" "net/http" "sort" "strings" "sync" "sync/atomic" "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/core/errgroup" "github.com/kataras/iris/v12/core/netutil" macroHandler "github.com/kataras/iris/v12/macro/handler" "github.com/kataras/golog" ) type ( // RequestHandler the middle man between acquiring a context and releasing it. // By-default is the router algorithm. RequestHandler interface { // Note: A different interface in order to hide the rest of the implementation. // We only need the `FireErrorCode` to be accessible through the Iris application (see `iris.go#Build`) HTTPErrorHandler // HandleRequest should handle the request based on the Context. HandleRequest(ctx *context.Context) // Build should builds the handler, it's being called on router's BuildRouter. Build(provider RoutesProvider) error // RouteExists reports whether a particular route exists. RouteExists(ctx *context.Context, method, path string) bool } // HTTPErrorHandler should contain a method `FireErrorCode` which // handles http unsuccessful status codes. HTTPErrorHandler interface { // FireErrorCode should send an error response to the client based // on the given context's response status code. FireErrorCode(ctx *context.Context) } // RouteAdder is an optional interface that can be implemented by a `RequestHandler`. RouteAdder interface { // AddRoute should add a route to the request handler directly. AddRoute(*Route) error } ) // ErrNotRouteAdder throws on `AddRouteUnsafe` when a registered `RequestHandler` // does not implements the optional `AddRoute(*Route) error` method. var ErrNotRouteAdder = errors.New("request handler does not implement AddRoute method") type routerHandler struct { // Config. disablePathCorrection bool disablePathCorrectionRedirection bool fireMethodNotAllowed bool enablePathIntelligence bool forceLowercaseRouting bool // logger *golog.Logger trees []*trie errorTrees []*trie hosts bool // true if at least one route contains a Subdomain. errorHosts bool // true if error handlers are registered to at least one Subdomain. errorDefaultHandlers context.Handlers // the main handler(s) for default error code handlers, when not registered directly by the end-developer. } var ( _ RequestHandler = (*routerHandler)(nil) _ HTTPErrorHandler = (*routerHandler)(nil) ) type routerHandlerDynamic struct { RequestHandler rw sync.RWMutex locked uint32 } // RouteExists reports whether a particular route exists. func (h *routerHandlerDynamic) RouteExists(ctx *context.Context, method, path string) (exists bool) { h.lock(false, func() error { exists = h.RequestHandler.RouteExists(ctx, method, path) return nil }) return } func (h *routerHandlerDynamic) AddRoute(r *Route) error { if v, ok := h.RequestHandler.(RouteAdder); ok { return h.lock(true, func() error { return v.AddRoute(r) }) } return ErrNotRouteAdder } func (h *routerHandlerDynamic) lock(writeAccess bool, fn func() error) error { if atomic.CompareAndSwapUint32(&h.locked, 0, 1) { if writeAccess { h.rw.Lock() } else { h.rw.RLock() } err := fn() // check agan because fn may called the unlock method. if atomic.CompareAndSwapUint32(&h.locked, 1, 0) { if writeAccess { h.rw.Unlock() } else { h.rw.RUnlock() } } return err } return fn() } func (h *routerHandlerDynamic) Build(provider RoutesProvider) error { // Build can be called inside HandleRequest if the route handler // calls the RefreshRouter method, and it will stuck on the rw.Lock() call, // so use a custom version of it. // h.rw.Lock() // defer h.rw.Unlock() return h.lock(true, func() error { return h.RequestHandler.Build(provider) }) } func (h *routerHandlerDynamic) HandleRequest(ctx *context.Context) { h.lock(false, func() error { h.RequestHandler.HandleRequest(ctx) return nil }) } func (h *routerHandlerDynamic) FireErrorCode(ctx *context.Context) { h.lock(false, func() error { h.RequestHandler.FireErrorCode(ctx) return nil }) } // NewDynamicHandler returns a new router handler which is responsible handle each request // with routes that can be added in serve-time. // It's a wrapper of the `NewDefaultHandler`. // It's being used when the `ConfigurationReadOnly.GetEnableDynamicHandler` is true. func NewDynamicHandler(config context.ConfigurationReadOnly, logger *golog.Logger) RequestHandler /* #2167 */ { handler := NewDefaultHandler(config, logger) return wrapDynamicHandler(handler) } func wrapDynamicHandler(handler RequestHandler) RequestHandler { return &routerHandlerDynamic{ RequestHandler: handler, } } // NewDefaultHandler returns the handler which is responsible // to map the request with a route (aka mux implementation). func NewDefaultHandler(config context.ConfigurationReadOnly, logger *golog.Logger) RequestHandler { var ( disablePathCorrection bool disablePathCorrectionRedirection bool fireMethodNotAllowed bool enablePathIntelligence bool forceLowercaseRouting bool dynamicHandlerEnabled bool ) if config != nil { // #2147 disablePathCorrection = config.GetDisablePathCorrection() disablePathCorrectionRedirection = config.GetDisablePathCorrectionRedirection() fireMethodNotAllowed = config.GetFireMethodNotAllowed() enablePathIntelligence = config.GetEnablePathIntelligence() forceLowercaseRouting = config.GetForceLowercaseRouting() dynamicHandlerEnabled = config.GetEnableDynamicHandler() } handler := &routerHandler{ disablePathCorrection: disablePathCorrection, disablePathCorrectionRedirection: disablePathCorrectionRedirection, fireMethodNotAllowed: fireMethodNotAllowed, enablePathIntelligence: enablePathIntelligence, forceLowercaseRouting: forceLowercaseRouting, logger: logger, } if dynamicHandlerEnabled { return wrapDynamicHandler(handler) } return handler } func (h *routerHandler) getTree(statusCode int, method, subdomain string) *trie { if statusCode > 0 { for i := range h.errorTrees { t := h.errorTrees[i] if t.statusCode == statusCode && t.subdomain == subdomain { return t } } return nil } for i := range h.trees { t := h.trees[i] if t.method == method && t.subdomain == subdomain { return t } } return nil } // AddRoute registers a route. See `Router.AddRouteUnsafe`. func (h *routerHandler) AddRoute(r *Route) error { var ( method = r.Method statusCode = r.StatusCode subdomain = r.Subdomain path = r.Path handlers = r.Handlers ) t := h.getTree(statusCode, method, subdomain) if t == nil { n := newTrieNode() // first time we register a route to this method with this subdomain t = &trie{statusCode: statusCode, method: method, subdomain: subdomain, root: n} if statusCode > 0 { h.errorTrees = append(h.errorTrees, t) } else { h.trees = append(h.trees, t) } } t.insert(path, r.ReadOnly, handlers) return nil } // RoutesProvider should be implemented by // iteral which contains the registered routes. type RoutesProvider interface { // api builder GetRoutes() []*Route GetRoute(routeName string) *Route // GetRouterFilters returns the app's router filters. // Read `UseRouter` for more. // The map can be altered before router built. GetRouterFilters() map[Party]*Filter // GetDefaultErrorMiddleware should return // the default error handler middleares. GetDefaultErrorMiddleware() context.Handlers } func defaultErrorHandler(ctx *context.Context) { if ok, err := ctx.GetErrPublic(); ok { // If an error is stored and it's not a private one // write it to the response body. ctx.WriteString(err.Error()) return } // Otherwise, write the code's text instead. ctx.WriteString(context.StatusText(ctx.GetStatusCode())) } func (h *routerHandler) Build(provider RoutesProvider) error { h.trees = h.trees[0:0] // reset, inneed when rebuilding. h.errorTrees = h.errorTrees[0:0] // set the default error code handler, will be fired on error codes // that are not handled by a specific handler (On(Any)ErrorCode). h.errorDefaultHandlers = append(provider.GetDefaultErrorMiddleware(), defaultErrorHandler) rp := errgroup.New("Routes Builder") registeredRoutes := provider.GetRoutes() // before sort. for _, r := range registeredRoutes { if r.topLink != nil { bindMultiParamTypesHandler(r) } } // sort, subdomains go first. sort.Slice(registeredRoutes, func(i, j int) bool { first, second := registeredRoutes[i], registeredRoutes[j] lsub1 := len(first.Subdomain) lsub2 := len(second.Subdomain) firstSlashLen := strings.Count(first.Path, "/") secondSlashLen := strings.Count(second.Path, "/") if lsub1 == lsub2 && first.Method == second.Method { if secondSlashLen < firstSlashLen { // fixes order when wildcard root is registered before other wildcard paths return true } if secondSlashLen == firstSlashLen { // fixes order when static path with the same prefix with a wildcard path // is registered after the wildcard path, although this is managed // by the low-level node but it couldn't work if we registered a root level wildcard, this fixes it. if len(first.tmpl.Params) == 0 { return false } if len(second.tmpl.Params) == 0 { return true } // No don't fix the order by framework's suggestion, // let it as it is today; {string} and {path} should be registered before {id} {uint} and e.t.c. // see `bindMultiParamTypesHandler` for the reason. Order of registration matters. } } // the rest are handled inside the node return lsub1 > lsub2 }) noLogCount := 0 for _, r := range registeredRoutes { if r.NoLog { noLogCount++ } if h.forceLowercaseRouting { // only in that state, keep everything else as end-developer registered. r.Path = strings.ToLower(r.Path) } if r.Subdomain != "" { if r.StatusCode > 0 { h.errorHosts = true } else { h.hosts = true } } if r.topLink == nil { // build the r.Handlers based on begin and done handlers, if any. r.BuildHandlers() // the only "bad" with this is if the user made an error // on route, it will be stacked shown in this build state // and no in the lines of the user's action, they should read // the docs better. Or TODO: add a link here in order to help new users. if err := h.AddRoute(r); err != nil { // node errors: rp.Addf("%s: %w", r.String(), err) continue } } } printRoutesInfo(h.logger, registeredRoutes, noLogCount) return errgroup.Check(rp) } func bindMultiParamTypesHandler(r *Route) { // like overlap feature but specifically for path parameters. r.BuildHandlers() h := r.Handlers[1:] // remove the macro evaluator handler as we manually check below. f := macroHandler.MakeFilter(r.tmpl) if f == nil { return // should never happen, previous checks made to set the top link. } currentStatusCode := r.StatusCode if currentStatusCode == 0 { currentStatusCode = http.StatusOK } decisionHandler := func(ctx *context.Context) { // println("core/router/handler.go: decision handler; " + ctx.Path() + " route.Name: " + r.Name + " vs context's " + ctx.GetCurrentRoute().Name()) currentRoute := ctx.GetCurrentRoute() // Different path parameters types in the same path, fallback should registered first e.g. {path} {string}, // because the handler on this case is executing from last to top. if f(ctx) { // println("core/router/handler.go: filter for : " + r.Name + " passed") ctx.SetCurrentRoute(r.ReadOnly) // Note: error handlers will be the same, routes came from the same party, // no need to update them. ctx.HandlerIndex(0) ctx.Do(h) return } ctx.SetCurrentRoute(currentRoute) ctx.StatusCode(currentStatusCode) ctx.Next() } r.topLink.builtinBeginHandlers = append(context.Handlers{decisionHandler}, r.topLink.builtinBeginHandlers...) } func canHandleSubdomain(ctx *context.Context, subdomain string) bool { if subdomain == "" { return true } requestHost := ctx.Host() if netutil.IsLoopbackSubdomain(requestHost) { // this fixes a bug when listening on // 127.0.0.1:8080 for example // and have a wildcard subdomain and a route registered to root domain. return false // it's not a subdomain, it's something like 127.0.0.1 probably } // it's a dynamic wildcard subdomain, we have just to check if ctx.subdomain is not empty if subdomain == SubdomainWildcardIndicator { // mydomain.com -> invalid // localhost -> invalid // sub.mydomain.com -> valid // sub.localhost -> valid serverHost := ctx.Application().ConfigurationReadOnly().GetVHost() if serverHost == requestHost { return false // it's not a subdomain, it's a full domain (with .com...) } dotIdx := strings.IndexByte(requestHost, '.') slashIdx := strings.IndexByte(requestHost, '/') if dotIdx > 0 && (slashIdx == -1 || slashIdx > dotIdx) { // if "." was found anywhere but not at the first path segment (host). } else { return false } // continue to that, any subdomain is valid. } else if !strings.HasPrefix(requestHost, subdomain) { // subdomain contains the dot, e.g. "admin." return false } return true } func (h *routerHandler) HandleRequest(ctx *context.Context) { method := ctx.Method() path := ctx.Path() if !h.disablePathCorrection { if len(path) > 1 && strings.HasSuffix(path, "/") { // Remove trailing slash and client-permanent rule for redirection, // if confgiuration allows that and path has an extra slash. // update the new path and redirect. u := ctx.Request().URL // use Trim to ensure there is no open redirect due to two leading slashes path = "/" + strings.Trim(path, "/") u.Path = path if !h.disablePathCorrectionRedirection { // do redirect, else continue with the modified path without the last "/". url := u.String() // Fixes https://github.com/kataras/iris/issues/921 // This is caused for security reasons, imagine a payment shop, // you can't just permantly redirect a POST request, so just 307 (RFC 7231, 6.4.7). if method == http.MethodPost || method == http.MethodPut { ctx.Redirect(url, http.StatusTemporaryRedirect) return } ctx.Redirect(url, http.StatusMovedPermanently) return } } } for i := range h.trees { t := h.trees[i] if method != t.method { continue } if h.hosts && !canHandleSubdomain(ctx, t.subdomain) { continue } n := t.search(path, ctx.Params()) if n != nil { ctx.SetCurrentRoute(n.Route) ctx.Do(n.Handlers) // found return } // not found or method not allowed. break } if h.fireMethodNotAllowed { for i := range h.trees { t := h.trees[i] // if `Configuration#FireMethodNotAllowed` is kept as defaulted(false) then this function will not // run, therefore performance kept as before. if h.subdomainAndPathAndMethodExists(ctx, t, "", path) { // RCF rfc2616 https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html // The response MUST include an Allow header containing a list of valid methods for the requested resource. ctx.Header("Allow", t.method) ctx.StatusCode(http.StatusMethodNotAllowed) return } } } if h.enablePathIntelligence && method == http.MethodGet { closestPaths := ctx.FindClosest(1) if len(closestPaths) > 0 { u := ctx.Request().URL u.Path = closestPaths[0] ctx.Redirect(u.String(), http.StatusMovedPermanently) return } } ctx.StatusCode(http.StatusNotFound) } func statusCodeSuccessful(statusCode int) bool { return !context.StatusCodeNotSuccessful(statusCode) } // FireErrorCode handles the response's error response. // If `Configuration.ResetOnFireErrorCode()` is true // and the response writer was a recorder one // then it will try to reset the headers and the body before calling the // registered (or default) error handler for that error code set by // `ctx.StatusCode` method. func (h *routerHandler) FireErrorCode(ctx *context.Context) { // On common response writer, always check // if we can't reset the body and the body has been filled // which means that the status code already sent, // then do not fire this custom error code, // rel: context/context.go#EndRequest. // // Note that, this is set to 0 on recorder because it holds the response before sent, // so we check their len(Body()) instead, look below. if ctx.ResponseWriter().Written() > 0 { return } statusCode := ctx.GetStatusCode() // the response's cached one. if ctx.Application().ConfigurationReadOnly().GetResetOnFireErrorCode() /* could be an argument too but we must not break the method */ { // if we can reset the body, probably manual call of `Application.FireErrorCode`. if w, ok := ctx.IsRecording(); ok { if statusCodeSuccessful(w.StatusCode()) { // if not an error status code w.WriteHeader(statusCode) // then set it manually here, otherwise it should be set via ctx.StatusCode(...) } // reset if previous content and it's recorder, keep the status code. w.ClearHeaders() w.ResetBody() if cw, ok := w.ResponseWriter.(*context.CompressResponseWriter); ok { // recorder wraps a compress writer. cw.Disabled = true } } else if w, ok := ctx.ResponseWriter().(*context.CompressResponseWriter); ok { // reset and disable the gzip in order to be an expected form of http error result w.Disabled = true } } else { // check if a body already set (the error response is handled by the handler itself, // see `Context.EndRequest`) if w, ok := ctx.IsRecording(); ok { if len(w.Body()) > 0 { return } } } for i := range h.errorTrees { t := h.errorTrees[i] if statusCode != t.statusCode { continue } if h.errorHosts && !canHandleSubdomain(ctx, t.subdomain) { continue } n := t.search(ctx.Path(), ctx.Params()) if n == nil { // try to take the root's one. n = t.root.getChild(pathSep) } if n != nil { // Note: handlers can contain macro filters here, // they are registered as error handlers, see macro/handler.go#42. // fmt.Println("Error Handlers") // for _, h := range n.Handlers { // f, l := context.HandlerFileLine(h) // fmt.Printf("%s: %s:%d\n", ctx.Path(), f, l) // } // fire this http status code's error handlers chain. // ctx.StopExecution() // not uncomment this, is here to remember why to. // note for me: I don't stopping the execution of the other handlers // because may the user want to add a fallback error code // i.e // users := app.Party("/users") // users.Done(func(ctx *context.Context){ if ctx.StatusCode() == 400 { /* custom error code for /users */ }}) // use .HandlerIndex // that sets the current handler index to zero // in order to: // ignore previous runs that may changed the handler index, // via ctx.Next or ctx.StopExecution, if any. // // use .Do // that overrides the existing handlers and sets and runs these error handlers. // in order to: // ignore the route's after-handlers, if any. ctx.SetCurrentRoute(n.Route) // Should work with: // Manual call of ctx.Application().FireErrorCode(ctx) (handlers length > 0) // And on `ctx.SetStatusCode`: Context -> EndRequest -> FireErrorCode (handlers length > 0) // And on router: HandleRequest -> SetStatusCode -> Context -> // EndRequest -> FireErrorCode (handlers' length is always 0) ctx.HandlerIndex(0) ctx.Do(n.Handlers) return } break } // not error handler found, // see if failed with a stored error, and if so // then render it, otherwise write a default message. ctx.Do(h.errorDefaultHandlers) } func (h *routerHandler) subdomainAndPathAndMethodExists(ctx *context.Context, t *trie, method, path string) bool { if method != "" && method != t.method { return false } if h.hosts && t.subdomain != "" { requestHost := ctx.Host() if netutil.IsLoopbackSubdomain(requestHost) { // this fixes a bug when listening on // 127.0.0.1:8080 for example // and have a wildcard subdomain and a route registered to root domain. return false // it's not a subdomain, it's something like 127.0.0.1 probably } // it's a dynamic wildcard subdomain, we have just to check if ctx.subdomain is not empty if t.subdomain == SubdomainWildcardIndicator { // mydomain.com -> invalid // localhost -> invalid // sub.mydomain.com -> valid // sub.localhost -> valid serverHost := ctx.Application().ConfigurationReadOnly().GetVHost() if serverHost == requestHost { return false // it's not a subdomain, it's a full domain (with .com...) } dotIdx := strings.IndexByte(requestHost, '.') slashIdx := strings.IndexByte(requestHost, '/') if dotIdx > 0 && (slashIdx == -1 || slashIdx > dotIdx) { // if "." was found anywhere but not at the first path segment (host). } else { return false } // continue to that, any subdomain is valid. } else if !strings.HasPrefix(requestHost, t.subdomain) { // t.subdomain contains the dot. return false } } n := t.search(path, ctx.Params()) return n != nil } // RouteExists reports whether a particular route exists // It will search from the current subdomain of context's host, if not inside the root domain. func (h *routerHandler) RouteExists(ctx *context.Context, method, path string) bool { for i := range h.trees { t := h.trees[i] if h.subdomainAndPathAndMethodExists(ctx, t, method, path) { return true } } return false } ================================================ FILE: core/router/handler_debug.go ================================================ package router import ( "fmt" "time" "github.com/kataras/golog" "github.com/kataras/pio" ) func printRoutesInfo(logger *golog.Logger, registeredRoutes []*Route, noLogCount int) { if !(logger != nil && logger.Level == golog.DebugLevel && noLogCount < len(registeredRoutes)) { return } // group routes by method and print them without the [DBUG] and time info, // the route logs are colorful. // Note: don't use map, we need to keep registered order, use // different slices for each method. collect := func(method string) (methodRoutes []*Route) { for _, r := range registeredRoutes { if r.NoLog { continue } if r.Method == method { methodRoutes = append(methodRoutes, r) } } return } type MethodRoutes struct { method string routes []*Route } allMethods := append(AllMethods, []string{MethodNone, ""}...) methodRoutes := make([]MethodRoutes, 0, len(allMethods)) for _, method := range allMethods { routes := collect(method) if len(routes) > 0 { methodRoutes = append(methodRoutes, MethodRoutes{method, routes}) } } if n := len(methodRoutes); n > 0 { tr := "routes" if len(registeredRoutes) == 1 { tr = tr[0 : len(tr)-1] } bckpNewLine := logger.NewLine logger.NewLine = false debugLevel := golog.Levels[golog.DebugLevel] // Replace that in order to not transfer it to the log handler (e.g. json) // logger.Debugf("API: %d registered %s (", len(registeredRoutes), tr) // with: pio.WriteRich(logger.Printer, debugLevel.Title, debugLevel.ColorCode, debugLevel.Style...) fmt.Fprintf(logger.Printer, " %s %sAPI: %d registered %s (", time.Now().Format(logger.TimeFormat), logger.Prefix, len(registeredRoutes)-noLogCount, tr) // logger.NewLine = bckpNewLine for i, m := range methodRoutes { // @method: @count if i > 0 { if i == n-1 { fmt.Fprint(logger.Printer, " and ") } else { fmt.Fprint(logger.Printer, ", ") } } if m.method == "" { m.method = "ERROR" } fmt.Fprintf(logger.Printer, "%d ", len(m.routes)) pio.WriteRich(logger.Printer, m.method, TraceTitleColorCode(m.method)) } fmt.Fprint(logger.Printer, ")\n") } for i, m := range methodRoutes { for _, r := range m.routes { r.Trace(logger.Printer, -1) } if i != len(allMethods)-1 { logger.Printer.Write(pio.NewLine) } } } ================================================ FILE: core/router/handler_execution_rules.go ================================================ package router import ( "github.com/kataras/iris/v12/context" ) // ExecutionRules gives control to the execution of the route handlers outside of the handlers themselves. // Usage: // // Party#SetExecutionRules(ExecutionRules { // Done: ExecutionOptions{Force: true}, // }) // // See `Party#SetExecutionRules` for more. type ExecutionRules struct { // Begin applies from `Party#Use`/`APIBUilder#UseGlobal` to the first...!last `Party#Handle`'s IF main handlers > 1. Begin ExecutionOptions // Done applies to the latest `Party#Handle`'s (even if one) and all done handlers. Done ExecutionOptions // Main applies to the `Party#Handle`'s all handlers, plays nice with the `Done` rule // when more than one handler was registered in `Party#Handle` without `ctx.Next()` (for Force: true). Main ExecutionOptions } func applyExecutionRules(rules ExecutionRules, begin, done, main *context.Handlers) { if !rules.Begin.Force && !rules.Done.Force && !rules.Main.Force { return // do not proceed and spend build-time here if nothing changed. } beginOK := rules.Begin.apply(begin) mainOK := rules.Main.apply(main) doneOK := rules.Done.apply(done) if !mainOK { mainCp := (*main)[0:] lastIdx := len(mainCp) - 1 if beginOK { if len(mainCp) > 1 { mainCpFirstButNotLast := make(context.Handlers, lastIdx) copy(mainCpFirstButNotLast, mainCp[:lastIdx]) for i, h := range mainCpFirstButNotLast { (*main)[i] = rules.Begin.buildHandler(h) } } } if doneOK { latestMainHandler := mainCp[lastIdx] (*main)[lastIdx] = rules.Done.buildHandler(latestMainHandler) } } } // ExecutionOptions is a set of default behaviors that can be changed in order to customize the execution flow of the routes' handlers with ease. // // See `ExecutionRules` and `Party#SetExecutionRules` for more. type ExecutionOptions struct { // Force if true then the handler9s) will execute even if the previous (or/and current, depends on the type of the rule) // handler does not calling the `ctx.Next()`, // note that the only way remained to stop a next handler is with the `ctx.StopExecution()` if this option is true. // // If true and `ctx.Next()` exists in the handlers that it shouldn't be, the framework will understand it but use it wisely. // // Defaults to false. Force bool } func (e ExecutionOptions) buildHandler(h context.Handler) context.Handler { if !e.Force { return h } return func(ctx *context.Context) { // Proceed will fire the handler and return false here if it doesn't contain a `ctx.Next()`, // so we add the `ctx.Next()` wherever is necessary in order to eliminate any dev's misuse. // // 26 Feb 2022: check if manually stopped, and if it's then don't call ctx.Next. if hasStopped, hasNext := ctx.ProceedAndReportIfStopped(h); !hasStopped && !hasNext { // `ctx.Next()` always checks for `ctx.IsStopped()` and handler(s) positions by-design. ctx.Next() } } } func (e ExecutionOptions) apply(handlers *context.Handlers) bool { if !e.Force { return false } tmp := *handlers for i, h := range tmp { if h == nil { if len(tmp) == 1 { return false } continue } (*handlers)[i] = e.buildHandler(h) } return true } ================================================ FILE: core/router/handler_execution_rules_test.go ================================================ package router_test import ( "testing" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/core/router" "github.com/kataras/iris/v12/httptest" ) var ( finalExecutionRulesResponse = "1234" testExecutionResponse = func(t *testing.T, app *iris.Application, path string) { e := httptest.New(t, app) e.GET(path).Expect().Status(httptest.StatusOK).Body().IsEqual(finalExecutionRulesResponse) } ) func writeStringHandler(text string, withNext bool) context.Handler { return func(ctx *context.Context) { ctx.WriteString(text) if withNext { ctx.Next() } } } func TestRouterExecutionRulesForceMain(t *testing.T) { app := iris.New() begin := app.Party("/") begin.SetExecutionRules(router.ExecutionRules{Main: router.ExecutionOptions{Force: true}}) // no need of `ctx.Next()` all main handlers should be executed with the Main.Force:True rule. begin.Get("/", writeStringHandler("12", false), writeStringHandler("3", false), writeStringHandler("4", false)) testExecutionResponse(t, app, "/") } func TestRouterExecutionRulesForceBegin(t *testing.T) { app := iris.New() begin := app.Party("/begin_force") begin.SetExecutionRules(router.ExecutionRules{Begin: router.ExecutionOptions{Force: true}}) // should execute, begin rule is to force execute them without `ctx.Next()`. begin.Use(writeStringHandler("1", false)) begin.Use(writeStringHandler("2", false)) // begin starts with begin and ends to the main handlers but not last, so this done should not be executed. begin.Done(writeStringHandler("5", false)) begin.Get("/", writeStringHandler("3", false), writeStringHandler("4", false)) testExecutionResponse(t, app, "/begin_force") } func TestRouterExecutionRulesForceDone(t *testing.T) { app := iris.New() done := app.Party("/done_force") done.SetExecutionRules(router.ExecutionRules{Done: router.ExecutionOptions{Force: true}}) // these done should be executed without `ctx.Next()` done.Done(writeStringHandler("3", false), writeStringHandler("4", false)) // first with `ctx.Next()`, because Done.Force:True rule will alter the latest of the main handler(s) only. done.Get("/", writeStringHandler("1", true), writeStringHandler("2", false)) // rules should be kept in children. doneChild := done.Party("/child") // even if only one, it's the latest, Done.Force:True rule should modify it. doneChild.Get("/", writeStringHandler("12", false)) testExecutionResponse(t, app, "/done_force") testExecutionResponse(t, app, "/done_force/child") } func TestRouterExecutionRulesShouldNotModifyTheCallersHandlerAndChildrenCanResetExecutionRules(t *testing.T) { app := iris.New() app.SetExecutionRules(router.ExecutionRules{Done: router.ExecutionOptions{Force: true}}) h := writeStringHandler("4", false) app.Done(h) app.Get("/", writeStringHandler("123", false)) // remember: the handler stored in var didn't had a `ctx.Next()`, modified its clone above with adding a `ctx.Next()` // note the "clone" word, the original handler shouldn't be changed. app.Party("/c").SetExecutionRules(router.ExecutionRules{}).Get("/", h, writeStringHandler("err caller modified!", false)) testExecutionResponse(t, app, "/") e := httptest.New(t, app) e.GET("/c").Expect().Status(httptest.StatusOK).Body().IsEqual("4") // the "should not" should not be written. } ================================================ FILE: core/router/mime.go ================================================ package router import ( "mime" "path/filepath" "github.com/kataras/iris/v12/context" ) var types = map[string]string{ ".3dm": "x-world/x-3dmf", ".3dmf": "x-world/x-3dmf", ".7z": "application/x-7z-compressed", ".a": "application/octet-stream", ".aab": "application/x-authorware-bin", ".aam": "application/x-authorware-map", ".aas": "application/x-authorware-seg", ".abc": "text/vndabc", ".ace": "application/x-ace-compressed", ".acgi": "text/html", ".afl": "video/animaflex", ".ai": "application/postscript", ".aif": "audio/aiff", ".aifc": "audio/aiff", ".aiff": "audio/aiff", ".aim": "application/x-aim", ".aip": "text/x-audiosoft-intra", ".alz": "application/x-alz-compressed", ".ani": "application/x-navi-animation", ".aos": "application/x-nokia-9000-communicator-add-on-software", ".aps": "application/mime", ".apk": "application/vnd.android.package-archive", ".arc": "application/x-arc-compressed", ".arj": "application/arj", ".art": "image/x-jg", ".asf": "video/x-ms-asf", ".asm": "text/x-asm", ".asp": "text/asp", ".asx": "application/x-mplayer2", ".au": "audio/basic", ".avi": "video/x-msvideo", ".avs": "video/avs-video", ".bcpio": "application/x-bcpio", ".bin": "application/mac-binary", ".bmp": "image/bmp", ".boo": "application/book", ".book": "application/book", ".boz": "application/x-bzip2", ".bsh": "application/x-bsh", ".bz2": "application/x-bzip2", ".bz": "application/x-bzip", ".c++": context.ContentTextHeaderValue, ".c": "text/x-c", ".cab": "application/vnd.ms-cab-compressed", ".cat": "application/vndms-pkiseccat", ".cc": "text/x-c", ".ccad": "application/clariscad", ".cco": "application/x-cocoa", ".cdf": "application/cdf", ".cer": "application/pkix-cert", ".cha": "application/x-chat", ".chat": "application/x-chat", ".chrt": "application/vnd.kde.kchart", ".class": "application/java", ".com": context.ContentTextHeaderValue, ".conf": context.ContentTextHeaderValue, ".cpio": "application/x-cpio", ".cpp": "text/x-c", ".cpt": "application/mac-compactpro", ".crl": "application/pkcs-crl", ".crt": "application/pkix-cert", ".crx": "application/x-chrome-extension", ".csh": "text/x-scriptcsh", ".css": "text/css", ".csv": "text/csv", ".cxx": context.ContentTextHeaderValue, ".dar": "application/x-dar", ".dcr": "application/x-director", ".deb": "application/x-debian-package", ".deepv": "application/x-deepv", ".def": context.ContentTextHeaderValue, ".der": "application/x-x509-ca-cert", ".dif": "video/x-dv", ".dir": "application/x-director", ".divx": "video/divx", ".dl": "video/dl", ".dmg": "application/x-apple-diskimage", ".doc": "application/msword", ".dot": "application/msword", ".dp": "application/commonground", ".drw": "application/drafting", ".dump": "application/octet-stream", ".dv": "video/x-dv", ".dvi": "application/x-dvi", ".dwf": "drawing/x-dwf=(old)", ".dwg": "application/acad", ".dxf": "application/dxf", ".dxr": "application/x-director", ".el": "text/x-scriptelisp", ".elc": "application/x-bytecodeelisp=(compiled=elisp)", ".eml": "message/rfc822", ".env": "application/x-envoy", ".eps": "application/postscript", ".es": "application/x-esrehber", ".etx": "text/x-setext", ".evy": "application/envoy", ".exe": "application/octet-stream", ".f77": "text/x-fortran", ".f90": "text/x-fortran", ".f": "text/x-fortran", ".fdf": "application/vndfdf", ".fif": "application/fractals", ".fli": "video/fli", ".flo": "image/florian", ".flv": "video/x-flv", ".flx": "text/vndfmiflexstor", ".fmf": "video/x-atomic3d-feature", ".for": "text/x-fortran", ".fpx": "image/vndfpx", ".frl": "application/freeloader", ".funk": "audio/make", ".g3": "image/g3fax", ".g": context.ContentTextHeaderValue, ".gif": "image/gif", ".gl": "video/gl", ".gsd": "audio/x-gsm", ".gsm": "audio/x-gsm", ".gsp": "application/x-gsp", ".gss": "application/x-gss", ".gtar": "application/x-gtar", ".gz": "application/x-compressed", ".gzip": "application/x-gzip", ".h": "text/x-h", ".hdf": "application/x-hdf", ".help": "application/x-helpfile", ".hgl": "application/vndhp-hpgl", ".hh": "text/x-h", ".hlb": "text/x-script", ".hlp": "application/hlp", ".hpg": "application/vndhp-hpgl", ".hpgl": "application/vndhp-hpgl", ".hqx": "application/binhex", ".hta": "application/hta", ".htc": "text/x-component", ".htm": "text/html", ".html": "text/html", ".htmls": "text/html", ".htt": "text/webviewhtml", ".htx": "text/html", ".ice": "x-conference/x-cooltalk", ".ico": "image/x-icon", ".ics": "text/calendar", ".icz": "text/calendar", ".idc": context.ContentTextHeaderValue, ".ief": "image/ief", ".iefs": "image/ief", ".iges": "application/iges", ".igs": "application/iges", ".ima": "application/x-ima", ".imap": "application/x-httpd-imap", ".inf": "application/inf", ".ins": "application/x-internett-signup", ".ip": "application/x-ip2", ".isu": "video/x-isvideo", ".it": "audio/it", ".iv": "application/x-inventor", ".ivr": "i-world/i-vrml", ".ivy": "application/x-livescreen", ".jam": "audio/x-jam", ".jav": "text/x-java-source", ".java": "text/x-java-source", ".jcm": "application/x-java-commerce", ".jfif-tbnl": "image/jpeg", ".jfif": "image/jpeg", ".jnlp": "application/x-java-jnlp-file", ".jpe": "image/jpeg", ".jpeg": "image/jpeg", ".jpg": "image/jpeg", ".jps": "image/x-jps", ".js": context.ContentJavascriptHeaderValue, ".mjs": context.ContentJavascriptHeaderValue, ".json": context.ContentJSONHeaderValue, ".vue": context.ContentJavascriptHeaderValue, ".jut": "image/jutvision", ".kar": "audio/midi", ".karbon": "application/vnd.kde.karbon", ".kfo": "application/vnd.kde.kformula", ".flw": "application/vnd.kde.kivio", ".kml": "application/vnd.google-earth.kml+xml", ".kmz": "application/vnd.google-earth.kmz", ".kon": "application/vnd.kde.kontour", ".kpr": "application/vnd.kde.kpresenter", ".kpt": "application/vnd.kde.kpresenter", ".ksp": "application/vnd.kde.kspread", ".kwd": "application/vnd.kde.kword", ".kwt": "application/vnd.kde.kword", ".ksh": "text/x-scriptksh", ".la": "audio/nspaudio", ".lam": "audio/x-liveaudio", ".latex": "application/x-latex", ".lha": "application/lha", ".lhx": "application/octet-stream", ".list": context.ContentTextHeaderValue, ".lma": "audio/nspaudio", ".log": context.ContentTextHeaderValue, ".lsp": "text/x-scriptlisp", ".lst": context.ContentTextHeaderValue, ".lsx": "text/x-la-asf", ".ltx": "application/x-latex", ".lzh": "application/octet-stream", ".lzx": "application/lzx", ".m1v": "video/mpeg", ".m2a": "audio/mpeg", ".m2v": "video/mpeg", ".m3u": "audio/x-mpegurl", ".m": "text/x-m", ".man": "application/x-troff-man", ".manifest": "text/cache-manifest", ".map": "application/x-navimap", ".mar": context.ContentTextHeaderValue, ".mbd": "application/mbedlet", ".mc$": "application/x-magic-cap-package-10", ".mcd": "application/mcad", ".mcf": "text/mcf", ".mcp": "application/netmc", ".me": "application/x-troff-me", ".mht": "message/rfc822", ".mhtml": "message/rfc822", ".mid": "application/x-midi", ".midi": "application/x-midi", ".mif": "application/x-frame", ".mime": "message/rfc822", ".mjf": "audio/x-vndaudioexplosionmjuicemediafile", ".mjpg": "video/x-motion-jpeg", ".mm": "application/base64", ".mme": "application/base64", ".mod": "audio/mod", ".moov": "video/quicktime", ".mov": "video/quicktime", ".movie": "video/x-sgi-movie", ".mp2": "audio/mpeg", ".mp3": "audio/mpeg", ".mp4": "video/mp4", ".mpa": "audio/mpeg", ".mpc": "application/x-project", ".mpe": "video/mpeg", ".mpeg": "video/mpeg", ".mpg": "video/mpeg", ".mpga": "audio/mpeg", ".mpp": "application/vndms-project", ".mpt": "application/x-project", ".mpv": "application/x-project", ".mpx": "application/x-project", ".mrc": "application/marc", ".ms": "application/x-troff-ms", ".mv": "video/x-sgi-movie", ".my": "audio/make", ".mzz": "application/x-vndaudioexplosionmzz", ".nap": "image/naplps", ".naplps": "image/naplps", ".nc": "application/x-netcdf", ".ncm": "application/vndnokiaconfiguration-message", ".nif": "image/x-niff", ".niff": "image/x-niff", ".nix": "application/x-mix-transfer", ".nsc": "application/x-conference", ".nvd": "application/x-navidoc", ".o": "application/octet-stream", ".oda": "application/oda", ".odb": "application/vnd.oasis.opendocument.database", ".odc": "application/vnd.oasis.opendocument.chart", ".odf": "application/vnd.oasis.opendocument.formula", ".odg": "application/vnd.oasis.opendocument.graphics", ".odi": "application/vnd.oasis.opendocument.image", ".odm": "application/vnd.oasis.opendocument.text-master", ".odp": "application/vnd.oasis.opendocument.presentation", ".ods": "application/vnd.oasis.opendocument.spreadsheet", ".odt": "application/vnd.oasis.opendocument.text", ".oga": "audio/ogg", ".ogg": "audio/ogg", ".ogv": "video/ogg", ".omc": "application/x-omc", ".omcd": "application/x-omcdatamaker", ".omcr": "application/x-omcregerator", ".otc": "application/vnd.oasis.opendocument.chart-template", ".otf": "application/vnd.oasis.opendocument.formula-template", ".otg": "application/vnd.oasis.opendocument.graphics-template", ".oth": "application/vnd.oasis.opendocument.text-web", ".oti": "application/vnd.oasis.opendocument.image-template", ".otm": "application/vnd.oasis.opendocument.text-master", ".otp": "application/vnd.oasis.opendocument.presentation-template", ".ots": "application/vnd.oasis.opendocument.spreadsheet-template", ".ott": "application/vnd.oasis.opendocument.text-template", ".p10": "application/pkcs10", ".p12": "application/pkcs-12", ".p7a": "application/x-pkcs7-signature", ".p7c": "application/pkcs7-mime", ".p7m": "application/pkcs7-mime", ".p7r": "application/x-pkcs7-certreqresp", ".p7s": "application/pkcs7-signature", ".p": "text/x-pascal", ".part": "application/pro_eng", ".pas": "text/pascal", ".pbm": "image/x-portable-bitmap", ".pcl": "application/vndhp-pcl", ".pct": "image/x-pict", ".pcx": "image/x-pcx", ".pdb": "chemical/x-pdb", ".pdf": "application/pdf", ".pfunk": "audio/make", ".pgm": "image/x-portable-graymap", ".pic": "image/pict", ".pict": "image/pict", ".pkg": "application/x-newton-compatible-pkg", ".pko": "application/vndms-pkipko", ".pl": "text/x-scriptperl", ".plx": "application/x-pixclscript", ".pm4": "application/x-pagemaker", ".pm5": "application/x-pagemaker", ".pm": "text/x-scriptperl-module", ".png": "image/png", ".pnm": "application/x-portable-anymap", ".pot": "application/mspowerpoint", ".pov": "model/x-pov", ".ppa": "application/vndms-powerpoint", ".ppm": "image/x-portable-pixmap", ".pps": "application/mspowerpoint", ".ppt": "application/mspowerpoint", ".ppz": "application/mspowerpoint", ".pre": "application/x-freelance", ".prt": "application/pro_eng", ".ps": "application/postscript", ".psd": "application/octet-stream", ".pvu": "paleovu/x-pv", ".pwz": "application/vndms-powerpoint", ".py": "text/x-scriptphyton", ".pyc": "application/x-bytecodepython", ".qcp": "audio/vndqcelp", ".qd3": "x-world/x-3dmf", ".qd3d": "x-world/x-3dmf", ".qif": "image/x-quicktime", ".qt": "video/quicktime", ".qtc": "video/x-qtc", ".qti": "image/x-quicktime", ".qtif": "image/x-quicktime", ".ra": "audio/x-pn-realaudio", ".ram": "audio/x-pn-realaudio", ".rar": "application/x-rar-compressed", ".ras": "application/x-cmu-raster", ".rast": "image/cmu-raster", ".rexx": "text/x-scriptrexx", ".rf": "image/vndrn-realflash", ".rgb": "image/x-rgb", ".rm": "application/vndrn-realmedia", ".rmi": "audio/mid", ".rmm": "audio/x-pn-realaudio", ".rmp": "audio/x-pn-realaudio", ".rng": "application/ringing-tones", ".rnx": "application/vndrn-realplayer", ".roff": "application/x-troff", ".rp": "image/vndrn-realpix", ".rpm": "audio/x-pn-realaudio-plugin", ".rt": "text/vndrn-realtext", ".rtf": "text/richtext", ".rtx": "text/richtext", ".rv": "video/vndrn-realvideo", ".s": "text/x-asm", ".s3m": "audio/s3m", ".s7z": "application/x-7z-compressed", ".saveme": "application/octet-stream", ".sbk": "application/x-tbook", ".scm": "text/x-scriptscheme", ".sdml": context.ContentTextHeaderValue, ".sdp": "application/sdp", ".sdr": "application/sounder", ".sea": "application/sea", ".set": "application/set", ".sgm": "text/x-sgml", ".sgml": "text/x-sgml", ".sh": "text/x-scriptsh", ".shar": "application/x-bsh", ".shtml": "text/x-server-parsed-html", ".sid": "audio/x-psid", ".skd": "application/x-koan", ".skm": "application/x-koan", ".skp": "application/x-koan", ".skt": "application/x-koan", ".sit": "application/x-stuffit", ".sitx": "application/x-stuffitx", ".sl": "application/x-seelogo", ".smi": "application/smil", ".smil": "application/smil", ".snd": "audio/basic", ".sol": "application/solids", ".spc": "text/x-speech", ".spl": "application/futuresplash", ".spr": "application/x-sprite", ".sprite": "application/x-sprite", ".spx": "audio/ogg", ".src": "application/x-wais-source", ".ssi": "text/x-server-parsed-html", ".ssm": "application/streamingmedia", ".sst": "application/vndms-pkicertstore", ".step": "application/step", ".stl": "application/sla", ".stp": "application/step", ".sv4cpio": "application/x-sv4cpio", ".sv4crc": "application/x-sv4crc", ".svf": "image/vnddwg", ".svg": "image/svg+xml", ".svr": "application/x-world", ".swf": "application/x-shockwave-flash", ".t": "application/x-troff", ".talk": "text/x-speech", ".tar": "application/x-tar", ".tbk": "application/toolbook", ".tcl": "text/x-scripttcl", ".tcsh": "text/x-scripttcsh", ".tex": "application/x-tex", ".texi": "application/x-texinfo", ".texinfo": "application/x-texinfo", ".text": context.ContentTextHeaderValue, ".tgz": "application/gnutar", ".tif": "image/tiff", ".tiff": "image/tiff", ".tr": "application/x-troff", ".tsi": "audio/tsp-audio", ".tsp": "application/dsptype", ".tsv": "text/tab-separated-values", ".turbot": "image/florian", ".txt": context.ContentTextHeaderValue, ".uil": "text/x-uil", ".uni": "text/uri-list", ".unis": "text/uri-list", ".unv": "application/i-deas", ".uri": "text/uri-list", ".uris": "text/uri-list", ".ustar": "application/x-ustar", ".uu": "text/x-uuencode", ".uue": "text/x-uuencode", ".vcd": "application/x-cdlink", ".vcf": "text/x-vcard", ".vcard": "text/x-vcard", ".vcs": "text/x-vcalendar", ".vda": "application/vda", ".vdo": "video/vdo", ".vew": "application/groupwise", ".viv": "video/vivo", ".vivo": "video/vivo", ".vmd": "application/vocaltec-media-desc", ".vmf": "application/vocaltec-media-file", ".voc": "audio/voc", ".vos": "video/vosaic", ".vox": "audio/voxware", ".vqe": "audio/x-twinvq-plugin", ".vqf": "audio/x-twinvq", ".vql": "audio/x-twinvq-plugin", ".vrml": "application/x-vrml", ".vrt": "x-world/x-vrt", ".vsd": "application/x-visio", ".vst": "application/x-visio", ".vsw": "application/x-visio", ".w60": "application/wordperfect60", ".w61": "application/wordperfect61", ".w6w": "application/msword", ".wav": "audio/wav", ".wb1": "application/x-qpro", ".wbmp": "image/vnd.wap.wbmp", ".web": "application/vndxara", ".wiz": "application/msword", ".wk1": "application/x-123", ".wmf": "windows/metafile", ".wml": "text/vnd.wap.wml", ".wmlc": "application/vnd.wap.wmlc", ".wmls": "text/vnd.wap.wmlscript", ".wmlsc": "application/vnd.wap.wmlscriptc", ".word": "application/msword", ".wp5": "application/wordperfect", ".wp6": "application/wordperfect", ".wp": "application/wordperfect", ".wpd": "application/wordperfect", ".wq1": "application/x-lotus", ".wri": "application/mswrite", ".wrl": "application/x-world", ".wrz": "model/vrml", ".wsc": "text/scriplet", ".wsrc": "application/x-wais-source", ".wtk": "application/x-wintalk", ".x-png": "image/png", ".xbm": "image/x-xbitmap", ".xdr": "video/x-amt-demorun", ".xgz": "xgl/drawing", ".xif": "image/vndxiff", ".xl": "application/excel", ".xla": "application/excel", ".xlb": "application/excel", ".xlc": "application/excel", ".xld": "application/excel", ".xlk": "application/excel", ".xll": "application/excel", ".xlm": "application/excel", ".xls": "application/excel", ".xlt": "application/excel", ".xlv": "application/excel", ".xlw": "application/excel", ".xm": "audio/xm", ".xml": context.ContentXMLHeaderValue, ".xmz": "xgl/movie", ".xpix": "application/x-vndls-xpix", ".xpm": "image/x-xpixmap", ".xsr": "video/x-amt-showrun", ".xwd": "image/x-xwd", ".xyz": "chemical/x-pdb", ".z": "application/x-compress", ".zip": "application/zip", ".zoo": "application/octet-stream", ".zsh": "text/x-scriptzsh", ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", ".docm": "application/vnd.ms-word.document.macroEnabled.12", ".dotx": "application/vnd.openxmlformats-officedocument.wordprocessingml.template", ".dotm": "application/vnd.ms-word.template.macroEnabled.12", ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ".xlsm": "application/vnd.ms-excel.sheet.macroEnabled.12", ".xltx": "application/vnd.openxmlformats-officedocument.spreadsheetml.template", ".xltm": "application/vnd.ms-excel.template.macroEnabled.12", ".xlsb": "application/vnd.ms-excel.sheet.binary.macroEnabled.12", ".xlam": "application/vnd.ms-excel.addin.macroEnabled.12", ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation", ".pptm": "application/vnd.ms-powerpoint.presentation.macroEnabled.12", ".ppsx": "application/vnd.openxmlformats-officedocument.presentationml.slideshow", ".ppsm": "application/vnd.ms-powerpoint.slideshow.macroEnabled.12", ".potx": "application/vnd.openxmlformats-officedocument.presentationml.template", ".potm": "application/vnd.ms-powerpoint.template.macroEnabled.12", ".ppam": "application/vnd.ms-powerpoint.addin.macroEnabled.12", ".sldx": "application/vnd.openxmlformats-officedocument.presentationml.slide", ".sldm": "application/vnd.ms-powerpoint.slide.macroEnabled.12", ".thmx": "application/vnd.ms-officetheme", ".onetoc": "application/onenote", ".onetoc2": "application/onenote", ".onetmp": "application/onenote", ".onepkg": "application/onenote", ".xpi": "application/x-xpinstall", ".wasm": "application/wasm", } func init() { for ext, typ := range types { // skip errors _ = mime.AddExtensionType(ext, typ) } } // TypeByExtension returns the MIME type associated with the file extension ext. // The extension ext should begin with a leading dot, as in ".html". // When ext has no associated type, typeByExtension returns "". // // Extensions are looked up first case-sensitively, then case-insensitively. // // The built-in table is small but on unix it is augmented by the local // system's mime.types file(s) if available under one or more of these // names: // // /etc/mime.types // /etc/apache2/mime.types // /etc/apache/mime.types // // On Windows, MIME types are extracted from the registry. // // Text types have the charset parameter set to "utf-8" by default. func TypeByExtension(ext string) (typ string) { if len(ext) < 2 { return } if ext[0] != '.' { // try to take it by filename typ = context.TrimHeaderValue(TypeByFilename(ext)) if typ == "" { ext = "." + ext // if error or something wrong then prepend the dot } } if typ == "" { typ = context.TrimHeaderValue(mime.TypeByExtension(ext)) } // mime.TypeByExtension returns as text/plain; | charset=utf-8 the static .js (not always) if ext == ".js" || ext == ".mjs" && (typ == context.ContentJavascriptHeaderValue || typ == context.ContentTextHeaderValue) { typ = context.ContentJavascriptHeaderValue } return typ } // TypeByFilename same as TypeByExtension // but receives a filename path instead. func TypeByFilename(fullFilename string) string { ext := filepath.Ext(fullFilename) return TypeByExtension(ext) } ================================================ FILE: core/router/party.go ================================================ package router import ( "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/macro" "github.com/kataras/golog" ) // Party is just a group joiner of routes which have the same prefix and share same middleware(s) also. // Party could also be named as 'Join' or 'Node' or 'Group' , Party chosen because it is fun. // // Look the `APIBuilder` structure for its implementation. type Party interface { // Logger returns the Application Logger. Logger() *golog.Logger // IsRoot reports whether this Party is the root Application's one. // It will return false on all children Parties, no exception. IsRoot() bool // ConfigureContainer accepts one or more functions that can be used // to configure dependency injection features of this Party // such as register dependency and register handlers that will automatically inject any valid dependency. // However, if the "builder" parameter is nil or not provided then it just returns the *APIContainer, // which automatically initialized on Party allocation. // // It returns the same `APIBuilder` featured with Dependency Injection. ConfigureContainer(builder ...func(*APIContainer)) *APIContainer // EnsureStaticBindings panics on struct handler (controller) // if at least one input binding depends on the request and not in a static structure. // Should be called before `RegisterDependency`. EnsureStaticBindings() Party // RegisterDependency calls the `ConfigureContainer.RegisterDependency` method // with the provided value(s). See `HandleFunc` and `PartyConfigure` methods too. RegisterDependency(dependencies ...any) // HandleFunc registers a route on HTTP verb "method" and relative, to this Party, path. // It is like the `Handle` method but it accepts one or more "handlersFn" functions // that each one of them can accept any input arguments as the HTTP request and // output a result as the HTTP response. Specifically, // the input of the "handlersFn" can be any registered dependency // (see ConfigureContainer().RegisterDependency) // or leave the framework to parse the request and fill the values accordingly. // The output of the "handlersFn" can be any output result: // custom structs , string, []byte, int, error, // a combination of the above, hero.Result(hero.View | hero.Response) and more. // // If more than one handler function is registered // then the execution happens without the nessecity of the `Context.Next` method, // simply, to stop the execution and not continue to the next "handlersFn" in chain // you should return an `iris.ErrStopExecution`. // // Example Code: // // The client's request body and server's response body Go types. // Could be any data structure. // // type ( // request struct { // Firstname string `json:"firstname"` // Lastname string `json:"lastname"` // } // // response struct { // ID uint64 `json:"id"` // Message string `json:"message"` // } // ) // // Register the route hander. // // HTTP VERB ROUTE PATH ROUTE HANDLER // app.HandleFunc("PUT", "/users/{id:uint64}", updateUser) // // Code the route handler function. // Path parameters and request body are binded // automatically. // The "id" uint64 binds to "{id:uint64}" route path parameter and // the "input" binds to client request data such as JSON. // // func updateUser(id uint64, input request) response { // // [custom logic...] // // return response{ // ID:id, // Message: "User updated successfully", // } // } // // Simulate a client request which sends data // to the server and prints out the response. // // curl --request PUT -d '{"firstname":"John","lastname":"Doe"}' \ // -H "Content-Type: application/json" \ // http://localhost:8080/users/42 // // { // "id": 42, // "message": "User updated successfully" // } // // See the `ConfigureContainer` for more features regrading // the dependency injection, mvc and function handlers. // // This method is just a shortcut for the `ConfigureContainer().Handle` one. HandleFunc(method, relativePath string, handlersFn ...any) *Route // UseFunc registers a function which can accept one or more // dependencies (see RegisterDependency) and returns an iris.Handler // or a result of and/or an error. // // This method is just a shortcut of the `ConfigureContainer().Use`. UseFunc(handlersFn ...any) // GetRelPath returns the current party's relative path. // i.e: // if r := app.Party("/users"), then the `r.GetRelPath()` is the "/users". // if r := app.Party("www.") or app.Subdomain("www") then the `r.GetRelPath()` is the "www.". GetRelPath() string // Macros returns the macro collection that is responsible // to register custom macros with their own parameter types and their macro functions for all routes. // // Learn more at: https://github.com/kataras/iris/tree/main/_examples/routing/dynamic-path Macros() *macro.Macros // Properties returns the original Party's properties map, // it can be modified before server startup but not afterwards. Properties() context.Map // SetRoutesNoLog disables (true) the verbose logging for the next registered // routes under this Party and its children. // // To disable logging for controllers under MVC Application, // see `mvc/Application.SetControllersNoLog` instead. // // Defaults to false when log level is "debug". SetRoutesNoLog(disable bool) Party // OnErrorCode registers a handlers chain for this `Party` for a specific HTTP status code. // Read more at: http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml // Look `UseError` and `OnAnyErrorCode` too. OnErrorCode(statusCode int, handlers ...context.Handler) []*Route // OnAnyErrorCode registers a handlers chain for all error codes // (4xxx and 5xxx, change the `ClientErrorCodes` and `ServerErrorCodes` variables to modify those) // Look `UseError` and `OnErrorCode` too. OnAnyErrorCode(handlers ...context.Handler) []*Route // Party returns a new child Party which inherites its // parent's options and middlewares. // If "relativePath" matches the parent's one then it returns the current Party. // A Party groups routes which may have the same prefix or subdomain and share same middlewares. // // To create a group of routes for subdomains // use the `Subdomain` or `WildcardSubdomain` methods // or pass a "relativePath" as "admin." or "*." respectfully. Party(relativePath string, middleware ...context.Handler) Party // PartyFunc same as `Party`, groups routes that share a base path or/and same handlers. // However this function accepts a function that receives this created Party instead. // Returns the Party in order the caller to be able to use this created Party to continue the // top-bottom routes "tree". // // Note: `iris#Party` and `core/router#Party` describes the exactly same interface. // // Usage: // app.PartyFunc("/users", func(u iris.Party){ // u.Use(authMiddleware, logMiddleware) // u.Get("/", getAllUsers) // u.Post("/", createOrUpdateUser) // u.Delete("/", deleteUser) // }) // // Look `Party` for more. PartyFunc(relativePath string, partyBuilderFunc func(p Party)) Party // PartyConfigure like `Party` and `PartyFunc` registers a new children Party // but instead it accepts a struct value which should implement the PartyConfigurator interface. // // PartyConfigure accepts the relative path of the child party // (As an exception, if it's empty then all configurators are applied to the current Party) // and one or more Party configurators and // executes the PartyConfigurator's Configure method. // // If the end-developer registered one or more dependencies upfront through // RegisterDependencies or ConfigureContainer.RegisterDependency methods // and "p" is a pointer to a struct then try to bind the unset/zero exported fields // to the registered dependencies, just like we do with Controllers. // Useful when the api's dependencies amount are too much to pass on a function. // // Usage: // app.PartyConfigure("/users", &api.UsersAPI{UserRepository: ..., ...}) // Where UsersAPI looks like: // type UsersAPI struct { [...] } // func(api *UsersAPI) Configure(router iris.Party) { // router.Get("/{id:uuid}", api.getUser) // [...] // } // Usage with (static) dependencies: // app.RegisterDependency(userRepo, ...) // app.PartyConfigure("/users", &api.UsersAPI{}) PartyConfigure(relativePath string, partyReg ...PartyConfigurator) Party // Subdomain returns a new party which is responsible to register routes to // this specific "subdomain". // // If called from a child party then the subdomain will be prepended to the path instead of appended. // So if app.Subdomain("admin").Subdomain("panel") then the result is: "panel.admin.". Subdomain(subdomain string, middleware ...context.Handler) Party // UseRouter upserts one or more handlers that will be fired // right before the main router's request handler. // // Use this method to register handlers, that can ran // independently of the incoming request's values, // that they will be executed ALWAYS against ALL children incoming requests. // Example of use-case: CORS. // // Note that because these are executed before the router itself // the Context should not have access to the `GetCurrentRoute` // as it is not decided yet which route is responsible to handle the incoming request. // It's one level higher than the `WrapRouter`. // The context SHOULD call its `Next` method in order to proceed to // the next handler in the chain or the main request handler one. UseRouter(handlers ...context.Handler) // UseError upserts one or more handlers that will be fired, // as middleware, before any error handler registered through `On(Any)ErrorCode`. // See `OnErrorCode` too. UseError(handlers ...context.Handler) // Use appends Handler(s) to the current Party's routes and child routes. // If the current Party is the root, then it registers the middleware to all child Parties' routes too. // To register a middleware for error handlers, look `UseError` method instead. Use(middleware ...context.Handler) // UseOnce either inserts a middleware, // or on the basis of the middleware already existing, // replace that existing middleware instead. // To register a middleware for error handlers, look `UseError` method instead. UseOnce(handlers ...context.Handler) // Done appends to the very end, Handler(s) to the current Party's routes and child routes. // The difference from .Use is that this/or these Handler(s) are being always running last. Done(handlers ...context.Handler) // MiddlewareExists reports whether the given handler exists in the middleware chain. MiddlewareExists(handlerNameOrFunc any) bool // RemoveHandler deletes a handler from begin and done handlers // based on its name or the handler pc function. // // As an exception, if one of the arguments is a pointer to an int, // then this is used to set the total amount of removed handlers. // // Returns the Party itself for chain calls. // // Should be called before children routes regitration. RemoveHandler(namesOrHandlers ...any) Party // Reset removes all the begin and done handlers that may derived from the parent party via `Use` & `Done`, // and the execution rules. // Note that the `Reset` will not reset the handlers that are registered via `UseGlobal` & `DoneGlobal`. // // Returns this Party. Reset() Party // ResetRouterFilters deactivates any previous registered // router filters and the parents ones for this Party. // // Returns this Party. ResetRouterFilters() Party // AllowMethods will re-register the future routes that will be registered // via `Handle`, `Get`, `Post`, ... to the given "methods" on that Party and its children "Parties", // duplicates are not registered. // // Call of `AllowMethod` will override any previous allow methods. AllowMethods(methods ...string) Party // SetExecutionRules alters the execution flow of the route handlers outside of the handlers themselves. // // For example, if for some reason the desired result is the (done or all) handlers to be executed no matter what // even if no `ctx.Next()` is called in the previous handlers, including the begin(`Use`), // the main(`Handle`) and the done(`Done`) handlers themselves, then: // Party#SetExecutionRules(iris.ExecutionRules { // Begin: iris.ExecutionOptions{Force: true}, // Main: iris.ExecutionOptions{Force: true}, // Done: iris.ExecutionOptions{Force: true}, // }) // // Note that if : true then the only remained way to "break" the handler chain is by `ctx.StopExecution()` now that `ctx.Next()` does not matter. // // These rules are per-party, so if a `Party` creates a child one then the same rules will be applied to that as well. // Reset of these rules (before `Party#Handle`) can be done with `Party#SetExecutionRules(iris.ExecutionRules{})`. // // The most common scenario for its use can be found inside Iris MVC Applications; // when we want the `Done` handlers of that specific mvc app's `Party` // to be executed but we don't want to add `ctx.Next()` on the `OurController#EndRequest`. // // Returns this Party. // // Example: https://github.com/kataras/iris/tree/main/_examples/mvc/middleware/without-ctx-next SetExecutionRules(executionRules ExecutionRules) Party // SetRegisterRule sets a `RouteRegisterRule` for this Party and its children. // Available values are: // * RouteOverride (the default one) // * RouteSkip // * RouteError // * RouteOverlap. SetRegisterRule(rule RouteRegisterRule) Party // Handle registers a route to the server's router. // if empty method is passed then handler(s) are being registered to all methods, same as .Any. // // Returns the read-only route information. Handle(method string, registeredPath string, handlers ...context.Handler) *Route // HandleMany works like `Handle` but can receive more than one // paths separated by spaces and returns always a slice of *Route instead of a single instance of Route. // // It's useful only if the same handler can handle more than one request paths, // otherwise use `Party` which can handle many paths with different handlers and middlewares. // // Usage: // app.HandleMany(iris.MethodGet, "/user /user/{id:uint64} /user/me", userHandler) // At the other side, with `Handle` we've had to write: // app.Handle(iris.MethodGet, "/user", userHandler) // app.Handle(iris.MethodGet, "/user/{id:uint64}", userHandler) // app.Handle(iris.MethodGet, "/user/me", userHandler) // // This method is used behind the scenes at the `Controller` function // in order to handle more than one paths for the same controller instance. HandleMany(method string, relativePath string, handlers ...context.Handler) []*Route // HandleDir registers a handler that serves HTTP requests // with the contents of a file system (physical or embedded). // // first parameter : the route path // second parameter : the file system needs to be served // third parameter : not required, the serve directory options. // // Alternatively, to get just the handler for that look the FileServer function instead. // // api.HandleDir("/static", iris.Dir("./assets"), iris.DirOptions{IndexName: "/index.html", Compress: true}) // // Returns all the registered routes, including GET index and path patterm and HEAD. // // Usage: // HandleDir("/public", "./assets", DirOptions{...}) or // HandleDir("/public", iris.Dir("./assets"), DirOptions{...}) // // Examples: // https://github.com/kataras/iris/tree/main/_examples/file-server HandleDir(requestPath string, fileSystem any, opts ...DirOptions) []*Route // None registers an "offline" route // see context.ExecRoute(routeName) and // party.Routes().Online(handleResultregistry.*Route, "GET") and // Offline(handleResultregistry.*Route) // // Returns the read-only route information. None(path string, handlers ...context.Handler) *Route // Get registers a route for the Get HTTP Method. // // Returns the read-only route information. Get(path string, handlers ...context.Handler) *Route // Post registers a route for the Post HTTP Method. // // Returns the read-only route information. Post(path string, handlers ...context.Handler) *Route // Put registers a route for the Put HTTP Method. // // Returns the read-only route information. Put(path string, handlers ...context.Handler) *Route // Delete registers a route for the Delete HTTP Method. // // Returns the read-only route information. Delete(path string, handlers ...context.Handler) *Route // Connect registers a route for the Connect HTTP Method. // // Returns the read-only route information. Connect(path string, handlers ...context.Handler) *Route // Head registers a route for the Head HTTP Method. // // Returns the read-only route information. Head(path string, handlers ...context.Handler) *Route // Options registers a route for the Options HTTP Method. // // Returns the read-only route information. Options(path string, handlers ...context.Handler) *Route // Patch registers a route for the Patch HTTP Method. // // Returns the read-only route information. Patch(path string, handlers ...context.Handler) *Route // Trace registers a route for the Trace HTTP Method. // // Returns the read-only route information. Trace(path string, handlers ...context.Handler) *Route // Any registers a route for ALL of the HTTP methods: // Get // Post // Put // Delete // Head // Patch // Options // Connect // Trace Any(registeredPath string, handlers ...context.Handler) []*Route // HandleServer registers a route for all HTTP methods which forwards the requests to the given server. // // Usage: // // app.HandleServer("/api/identity/{first:string}/orgs/{second:string}/{p:path}", otherApp) // // OR // // app.HandleServer("/api/identity", otherApp) HandleServer(path string, server ServerHandler) // CreateRoutes returns a list of Party-based Routes. // It does NOT registers the route. Use `Handle, Get...` methods instead. // This method can be used for third-parties Iris helpers packages and tools // that want a more detailed view of Party-based Routes before take the decision to register them. CreateRoutes(methods []string, relativePath string, handlers ...context.Handler) []*Route // RemoveRoute deletes a registered route by its name before `Application.Listen`. // The default naming for newly created routes is: method + subdomain + path. // Reports whether a route with that name was found and removed successfully. // // Note that this method applies to all Parties (sub routers) // even if each of the Parties have access to this method, // as the route name is unique per Iris Application. RemoveRoute(routeName string) bool // StaticContent registers a GET and HEAD method routes to the requestPath // that are ready to serve raw static bytes, memory cached. // // Returns the GET *Route. StaticContent(requestPath string, cType string, content []byte) *Route // Favicon serves static favicon // accepts 2 parameters, second is optional // favPath (string), declare the system directory path of the __.ico // requestPath (string), it's the route's path, by default this is the "/favicon.ico" because some browsers tries to get this by default first, // you can declare your own path if you have more than one favicon (desktop, mobile and so on) // // this func will add a route for you which will static serve the /yuorpath/yourfile.ico to the /yourfile.ico // (nothing special that you can't handle by yourself). // Note that you have to call it on every favicon you have to serve automatically (desktop, mobile and so on). // // Returns the GET *Route. Favicon(favPath string, requestPath ...string) *Route // RegisterView registers and loads a view engine middleware for this group of routes. // It overrides any of the application's root registered view engine. // To register a view engine per handler chain see the `Context.ViewEngine` instead. // Read `Configuration.ViewEngineContextKey` documentation for more. RegisterView(viewEngine context.ViewEngine) // FallbackView registers one or more fallback views for a template or a template layout. // Usage: // FallbackView(iris.FallbackView("fallback.html")) // FallbackView(iris.FallbackViewLayout("layouts/fallback.html")) // OR // FallbackView(iris.FallbackViewFunc(ctx iris.Context, err iris.ErrViewNotExist) error { // err.Name is the previous template name. // err.IsLayout reports whether the failure came from the layout template. // err.Data is the template data provided to the previous View call. // [...custom logic e.g. ctx.View("fallback", err.Data)] // }) FallbackView(provider context.FallbackViewProvider) // Layout overrides the parent template layout with a more specific layout for this Party. // It returns the current Party. // // The "tmplLayoutFile" should be a relative path to the templates dir. // Usage: // // app := iris.New() // app.RegisterView(iris.$VIEW_ENGINE("./views", ".$extension")) // my := app.Party("/my").Layout("layouts/mylayout.html") // my.Get("/", func(ctx iris.Context) { // if err := ctx.View("page1.html"); err != nil { // ctx.HTML(fmt.Sprintf("

      %s

      ", err.Error())) // return // } // }) // // Examples: https://github.com/kataras/iris/tree/main/_examples/view Layout(tmplLayoutFile string) Party } ================================================ FILE: core/router/path.go ================================================ package router import ( "net/http" "path" "strconv" "strings" "github.com/kataras/iris/v12/core/netutil" "github.com/kataras/iris/v12/macro" "github.com/kataras/iris/v12/macro/interpreter/ast" "github.com/kataras/iris/v12/macro/interpreter/lexer" ) // Param receives a parameter name prefixed with the ParamStart symbol. func Param(name string) string { return prefix(name, ParamStart) } // WildcardParam receives a parameter name prefixed with the WildcardParamStart symbol. func WildcardParam(name string) string { if name == "" { return "" } return prefix(name, WildcardParamStart) } // WildcardFileParam wraps a named parameter "file" with the trailing "path" macro parameter type. // At build state this "file" parameter is prefixed with the request handler's `WildcardParamStart`. // Created mostly for routes that serve static files to be visibly collected by // the `Application#GetRouteReadOnly` via the `Route.Tmpl().Src` instead of // the underline request handler's representation (`Route.Path()`). func WildcardFileParam() string { return "{file:path}" } func convertMacroTmplToNodePath(tmpl macro.Template) string { routePath := tmpl.Src if len(routePath) > 1 && routePath[len(routePath)-1] == '/' { routePath = routePath[0 : len(routePath)-1] // remove any last "/" } // if it has started with {} and it's valid // then the tmpl.Params will be filled, // so no any further check needed. for i := range tmpl.Params { p := tmpl.Params[i] if ast.IsTrailing(p.Type) { routePath = strings.Replace(routePath, p.Src, WildcardParam(p.Name), 1) } else { routePath = strings.Replace(routePath, p.Src, Param(p.Name), 1) } } return routePath } func prefix(s string, prefix string) string { if !strings.HasPrefix(s, prefix) { return prefix + s } return s } func splitMethod(methodMany string) []string { methodMany = strings.Trim(methodMany, " ") return strings.Split(methodMany, " ") } func splitPath(pathMany string) (paths []string) { pathMany = strings.Trim(pathMany, " ") pathsWithoutSlashFromFirstAndSoOn := strings.Split(pathMany, " /") for _, path := range pathsWithoutSlashFromFirstAndSoOn { if path == "" { continue } if path[0] != '/' { path = "/" + path } paths = append(paths, path) } return } func joinPath(path1 string, path2 string) string { return path.Join(path1, path2) } // cleanPath applies the following rules // iteratively until no further processing can be done: // // 1. Replace multiple slashes with a single slash. // 2. Replace '\' with '/' // 3. Replace "\\" with '/' // 4. Ignore anything inside '{' and '}' // 5. Makes sure that prefixed with '/' // 6. Remove trailing '/'. // // The returned path ends in a slash only if it is the root "/". // The function does not modify the dynamic path parts. func cleanPath(path string) string { // note that we don't care about the performance here, it's before the server ran. if path == "" || path == "." { return "/" } // remove suffix "/", if it's root "/" then it will add it as a prefix below. if lidx := len(path) - 1; path[lidx] == '/' { path = path[:lidx] } // prefix with "/". path = prefix(path, "/") s := []rune(path) // If you're learning go through Iris I will ask you to ignore the // following part, it's not the recommending way to do that, // but it's understable to me. var ( insideMacro = false i = -1 ) for { i++ if len(s) <= i { break } if s[i] == lexer.Begin { insideMacro = true continue } if s[i] == lexer.End { insideMacro = false continue } // when inside {} then don't try to clean it. if !insideMacro { if s[i] == '\\' { s[i] = '/' if len(s)-1 > i+1 && s[i+1] == '\\' { s = deleteCharacter(s, i+1) } else { i-- // set to minus in order for the next check to be applied for prev tokens too. } } if s[i] == '/' && len(s)-1 > i+1 && s[i+1] == '/' { s[i] = '/' s = deleteCharacter(s, i+1) i-- continue } } } if len(s) > 1 && s[len(s)-1] == '/' { // remove any last //. s = s[:len(s)-1] } return string(s) } func deleteCharacter(s []rune, index int) []rune { return append(s[0:index], s[index+1:]...) } const ( // SubdomainWildcardIndicator where a registered path starts with '*.'. // if subdomain == "*." then its wildcard. // // used internally by router and api builder. SubdomainWildcardIndicator = "*." // SubdomainWildcardPrefix where a registered path starts with "*./", // then this route should accept any subdomain. SubdomainWildcardPrefix = SubdomainWildcardIndicator + "/" // SubdomainPrefix where './' exists in a registered path then it contains subdomain // // used on api builder. SubdomainPrefix = "./" // i.e subdomain./ -> Subdomain: subdomain. Path: / ) func hasSubdomain(s string) bool { if s == "" { return false } // subdomain./path // .*/path // // remember: path always starts with "/" // if not start with "/" then it should be something else, // we don't assume anything else but subdomain. slashIdx := strings.IndexByte(s, '/') return slashIdx > 0 || // for route paths s[0] == SubdomainPrefix[0] || // for route paths (len(s) >= 2 && s[0:2] == SubdomainWildcardIndicator) || // for party rel path or route paths (len(s) >= 2 && slashIdx != 0 && s[len(s)-1] == '.') // for party rel, i.e www., or subsub.www. } // splitSubdomainAndPath checks if the path has subdomain and if it's // it splits the subdomain and path and returns them, otherwise it returns // an empty subdomain and the clean path. // // First return value is the subdomain, second is the path. func splitSubdomainAndPath(fullUnparsedPath string) (subdomain string, path string) { s := fullUnparsedPath if s == "" || s == "/" { return "", "/" } splitPath := strings.Split(s, ".") if len(splitPath) == 2 && splitPath[1] == "" { return splitPath[0] + ".", "/" } slashIdx := strings.IndexByte(s, '/') if slashIdx > 0 { // has subdomain subdomain = s[0:slashIdx] } if slashIdx == -1 { // this will only happen when this function // is called to Party's relative path (e.g. control.admin.), // and not a route's one (the route's one always contains a slash). // return all as subdomain and "/" as path. return s, "/" } path = s[slashIdx:] if !strings.Contains(path, "{") { path = strings.ReplaceAll(path, "//", "/") path = strings.ReplaceAll(path, "\\", "/") } // remove any left trailing slashes, i.e "//api/users". for i := 1; i < len(path); i++ { if path[i] == '/' { path = path[0:i] + path[i+1:] } else { break } } // remove last /. path = strings.TrimRight(path, "/") // no cleanPath(path) in order // to be able to parse macro function regexp(\\). return // return subdomain without slash, path with slash } func staticPath(src string) string { bidx := strings.IndexByte(src, '{') if bidx == -1 || len(src) <= bidx { return src // no dynamic part found } if bidx <= 1 { // found at first{...} or second index (/{...}), // although first index should never happen because of the prepended slash. return "/" } return src[:bidx-1] // (/static/{...} -> /static) } // RoutePathReverserOption option signature for the RoutePathReverser. type RoutePathReverserOption func(*RoutePathReverser) // WithScheme is an option for the RoutepathReverser, // it sets the optional field "vscheme", // v for virtual. // if vscheme is empty then it will try to resolve it from // the RoutePathReverser's vhost field. // // See WithHost or WithServer to enable the URL feature. func WithScheme(scheme string) RoutePathReverserOption { return func(ps *RoutePathReverser) { ps.vscheme = scheme } } // WithHost enables the RoutePathReverser's URL feature. // Both "WithHost" and "WithScheme" can be different from // the real server's listening address, i.e nginx in front. func WithHost(host string) RoutePathReverserOption { return func(ps *RoutePathReverser) { ps.vhost = host if ps.vscheme == "" { ps.vscheme = netutil.ResolveSchemeFromVHost(host) } } } // WithServer enables the RoutePathReverser's URL feature. // It receives an *http.Server and tries to resolve // a scheme and a host to be used in the URL function. func WithServer(srv *http.Server) RoutePathReverserOption { return func(ps *RoutePathReverser) { ps.vhost = netutil.ResolveVHost(srv.Addr) ps.vscheme = netutil.ResolveSchemeFromServer(srv) } } // RoutePathReverser contains methods that helps to reverse a // (dynamic) path from a specific route, // route name is required because a route may being registered // on more than one http method. type RoutePathReverser struct { provider RoutesProvider // both vhost and vscheme are being used, optionally, for the URL feature. vhost string vscheme string } // NewRoutePathReverser returns a new path reverser based on // a routes provider, needed to get a route based on its name. // Options is required for the URL function. // See WithScheme and WithHost or WithServer. func NewRoutePathReverser(apiRoutesProvider RoutesProvider, options ...RoutePathReverserOption) *RoutePathReverser { ps := &RoutePathReverser{ provider: apiRoutesProvider, } for _, o := range options { o(ps) } return ps } // Path returns a route path based on a route name and any dynamic named parameter's values-only. func (ps *RoutePathReverser) Path(routeName string, paramValues ...any) string { r := ps.provider.GetRoute(routeName) if r == nil { return "" } if len(paramValues) == 0 { return r.Path } return r.ResolvePath(toStringSlice(paramValues)...) } func toStringSlice(args []any) (argsString []string) { argsSize := len(args) if argsSize <= 0 { return } argsString = make([]string, argsSize) for i, v := range args { if s, ok := v.(string); ok { argsString[i] = s } else if num, ok := v.(int); ok { argsString[i] = strconv.Itoa(num) } else if b, ok := v.(bool); ok { argsString[i] = strconv.FormatBool(b) } else if arr, ok := v.([]string); ok { if len(arr) > 0 { argsString[i] = arr[0] argsString = append(argsString, arr[1:]...) } } } return } // Remove the URL for now, it complicates things for the whole framework without a specific benefits, // developers can just concat the subdomain, (host can be auto-retrieve by browser using the Path). // URL same as Path but returns the full uri, i.e https://mysubdomain.mydomain.com/hello/iris func (ps *RoutePathReverser) URL(routeName string, paramValues ...any) (url string) { if ps.vhost == "" || ps.vscheme == "" { return "not supported" } r := ps.provider.GetRoute(routeName) if r == nil { return } host := ps.vhost scheme := ps.vscheme args := toStringSlice(paramValues) // if it's dynamic subdomain then the first argument is the subdomain part // for this part we are responsible not the custom routers if len(args) > 0 && r.Subdomain == SubdomainWildcardIndicator { subdomain := args[0] host = subdomain + "." + host args = args[1:] // remove the subdomain part for the arguments, } if parsedPath := r.ResolvePath(args...); parsedPath != "" { url = scheme + "://" + host + parsedPath } return } ================================================ FILE: core/router/path_test.go ================================================ package router import ( "testing" ) func TestCleanPath(t *testing.T) { tests := []struct { path string expected string }{ { "/", "/", }, { "noslashPrefix", "/noslashPrefix", }, { "slashSuffix/", "/slashSuffix", }, { "noSlashPrefixAndslashSuffix/", "/noSlashPrefixAndslashSuffix", }, // don't do any clean up inside {}, // fixes #927. { "/total/{year:string regexp(\\d{4})}", "/total/{year:string regexp(\\d{4})}", }, { "/total/{year:string regexp(\\d{4})}/more", "/total/{year:string regexp(\\d{4})}/more", }, { "/total/{year:string regexp(\\d{4})}/more/{s:string regexp(\\d{7})}", "/total/{year:string regexp(\\d{4})}/more/{s:string regexp(\\d{7})}", }, { "/single_no_params", "/single_no_params", }, { "/single/{id:uint64}", "/single/{id:uint64}", }, { "0\\\\\\0", "/0/0", }, { "*\\*\\*", "/*/*/*", }, { "\\", "/", }, } for i, tt := range tests { if expected, got := tt.expected, cleanPath(tt.path); expected != got { t.Fatalf("[%d] - expected path '%s' but got '%s'", i, expected, got) } } } func TestSplitPath(t *testing.T) { tests := []struct { path string expected []string }{ { "/v2/stores/{id:string format(uuid)} /v3", []string{"/v2/stores/{id:string format(uuid)}", "/v3"}, }, { "/user/{id:uint64} /admin/{id:uint64}", []string{"/user/{id:uint64}", "/admin/{id:uint64}"}, }, { "/users/{id:int} /admins/{id:int64}", []string{"/users/{id:int}", "/admins/{id:int64}"}, }, { "/user /admin", []string{"/user", "/admin"}, }, { "/single_no_params", []string{"/single_no_params"}, }, { "/single/{id:int}", []string{"/single/{id:int}"}, }, } equalSlice := func(s1 []string, s2 []string) bool { if len(s1) != len(s2) { return false } for i := range s1 { if s2[i] != s1[i] { return false } } return true } for i, tt := range tests { paths := splitPath(tt.path) if expected, got := tt.expected, paths; !equalSlice(expected, got) { t.Fatalf("[%d] - expected paths '%#v' but got '%#v'", i, expected, got) } } } func TestSplitSubdomainAndPath(t *testing.T) { tests := []struct { original string subdomain string path string }{ {"admin./users/42", "admin.", "/users/42"}, {"static.", "static.", "/"}, {"static./" + WildcardFileParam(), "static.", "/" + WildcardFileParam()}, {"//api/users\\42", "", "/api/users/42"}, {"admin./users//42", "admin.", "/users/42"}, {"*./users/42/", "*.", "/users/42"}, } for i, tt := range tests { subdomain, path := splitSubdomainAndPath(tt.original) if expected, got := tt.subdomain, subdomain; expected != got { t.Fatalf("[%d] - expected subdomain '%s' but got '%s'", i, expected, got) } if expected, got := tt.path, path; expected != got { t.Fatalf("[%d] - expected path '%s' but got '%s'", i, expected, got) } } } ================================================ FILE: core/router/route.go ================================================ package router import ( "fmt" "io" "net/http" "path/filepath" "strings" "time" "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/macro" "github.com/kataras/iris/v12/macro/handler" "github.com/kataras/pio" ) // Route contains the information about a registered Route. // If any of the following fields are changed then the // caller should Refresh the router. type Route struct { // The Party which this Route was created and registered on. Party Party Title string `json:"title"` // custom name to replace the method on debug logging. Name string `json:"name"` // "userRoute" Description string `json:"description"` // "lists a user" Method string `json:"method"` // "GET" StatusCode int `json:"statusCode"` // 404 (only for HTTP error handlers). methodBckp string // if Method changed to something else (which is possible at runtime as well, via RefreshRouter) then this field will be filled with the old one. Subdomain string `json:"subdomain"` // "admin." tmpl macro.Template // Tmpl().Src: "/api/user/{id:uint64}" // temp storage, they're appended to the Handlers on build. // Execution happens before Handlers, can be empty. // They run right after any builtinBeginHandlers. beginHandlers context.Handlers // temp storage, these are always registered first as Handlers on Build. // There are the handlers may be added by the framework and // can NOT be modified by the end-developer (i.e overlapRoute & bindMultiParamTypesHandler), // even if a function like UseGlobal is used. builtinBeginHandlers context.Handlers // Handlers are the main route's handlers, executed by order. // Cannot be empty. Handlers context.Handlers `json:"-"` MainHandlerName string `json:"mainHandlerName"` MainHandlerIndex int `json:"mainHandlerIndex"` // temp storage, they're appended to the Handlers on build. // Execution happens after Begin and main Handler(s), can be empty. doneHandlers context.Handlers Path string `json:"path"` // the underline router's representation, i.e "/api/user/:id" // FormattedPath all dynamic named parameters (if any) replaced with %v, // used by Application to validate param values of a Route based on its name. FormattedPath string `json:"formattedPath"` // the source code's filename:filenumber that this route was created from. SourceFileName string `json:"sourceFileName"` SourceLineNumber int `json:"sourceLineNumber"` // where the route registered. RegisterFileName string `json:"registerFileName"` RegisterLineNumber int `json:"registerLineNumber"` // see APIBuilder.handle, routerHandler.bindMultiParamTypesHandler and routerHandler.Build, // it's the parent route of the last registered of the same path parameter. Specifically for path parameters. topLink *Route // overlappedLink specifically for overlapRoute feature. // keeps the second route of the same path pattern registered. // It's used ONLY for logging. overlappedLink *Route // Sitemap properties: https://www.sitemaps.org/protocol.html NoSitemap bool // when this route should be hidden from sitemap. LastMod time.Time `json:"lastMod,omitempty"` ChangeFreq string `json:"changeFreq,omitempty"` Priority float32 `json:"priority,omitempty"` // ReadOnly is the read-only structure of the Route. ReadOnly context.RouteReadOnly // OnBuild runs right before BuildHandlers. OnBuild func(r *Route) NoLog bool // disables debug logging. } // NewRoute returns a new route based on its method, // subdomain, the path (unparsed or original), // handlers and the macro container which all routes should share. // It parses the path based on the "macros", // handlers are being changed to validate the macros at serve time, if needed. func NewRoute(p Party, statusErrorCode int, method, subdomain, unparsedPath string, handlers context.Handlers, macros macro.Macros) (*Route, error) { path := cleanPath(unparsedPath) // required. Before macro template parse as the cleanPath does not modify the dynamic path route parts. tmpl, err := macro.Parse(path, macros) if err != nil { return nil, err } path = convertMacroTmplToNodePath(tmpl) // prepend the macro handler to the route, now, // right before the register to the tree, so APIBuilder#UseGlobal will work as expected. if handler.CanMakeHandler(tmpl) { macroEvaluatorHandler := handler.MakeHandler(tmpl) handlers = append(context.Handlers{macroEvaluatorHandler}, handlers...) } defaultName := method + subdomain + tmpl.Src if statusErrorCode > 0 { defaultName = fmt.Sprintf("%d_%s", statusErrorCode, defaultName) } formattedPath := formatPath(path) route := &Route{ Party: p, StatusCode: statusErrorCode, Name: defaultName, Method: method, methodBckp: method, Subdomain: subdomain, tmpl: tmpl, Path: path, Handlers: handlers, FormattedPath: formattedPath, } route.ReadOnly = routeReadOnlyWrapper{route} return route, nil } // Use adds explicit begin handlers to this route. // Alternatively the end-dev can prepend to the `Handlers` field. // Should be used before the `BuildHandlers` which is // called by the framework itself on `Application#Run` (build state). // // Used internally at `APIBuilder#UseGlobal` -> `beginGlobalHandlers` -> `APIBuilder#Handle`. func (r *Route) Use(handlers ...context.Handler) { if len(handlers) == 0 { return } r.beginHandlers = append(r.beginHandlers, handlers...) } // UseOnce like Use but it replaces any duplicate handlers with // the new ones. // Should be called before Application Build. func (r *Route) UseOnce(handlers ...context.Handler) { r.beginHandlers = context.UpsertHandlers(r.beginHandlers, handlers) } // RemoveHandler deletes a handler from begin, main and done handlers // based on its name or the handler pc function. // Returns the total amount of handlers removed. // // Should be called before Application Build. func (r *Route) RemoveHandler(namesOrHandlers ...any) (count int) { for _, nameOrHandler := range namesOrHandlers { handlerName := "" switch h := nameOrHandler.(type) { case string: handlerName = h case context.Handler: //, func(*context.Context): handlerName = context.HandlerName(h) default: panic(fmt.Sprintf("remove handler: unexpected type of %T", h)) } r.beginHandlers = removeHandler(handlerName, r.beginHandlers, &count) r.Handlers = removeHandler(handlerName, r.Handlers, &count) r.doneHandlers = removeHandler(handlerName, r.doneHandlers, &count) } return } func removeHandler(handlerName string, handlers context.Handlers, counter *int) (newHandlers context.Handlers) { for _, h := range handlers { if h == nil { continue } if context.HandlerName(h) == handlerName { if counter != nil { *counter++ } continue } newHandlers = append(newHandlers, h) } return } // Done adds explicit finish handlers to this route. // Alternatively the end-dev can append to the `Handlers` field. // Should be used before the `BuildHandlers` which is // called by the framework itself on `Application#Run` (build state). // // Used internally at `APIBuilder#DoneGlobal` -> `doneGlobalHandlers` -> `APIBuilder#Handle`. func (r *Route) Done(handlers ...context.Handler) { if len(handlers) == 0 { return } r.doneHandlers = append(r.doneHandlers, handlers...) } // ChangeMethod will try to change the HTTP Method of this route instance. // A call of `RefreshRouter` is required after this type of change in order to change to be really applied. func (r *Route) ChangeMethod(newMethod string) bool { if newMethod != r.Method { r.methodBckp = r.Method r.Method = newMethod return true } return false } // SetStatusOffline will try make this route unavailable. // A call of `RefreshRouter` is required after this type of change in order to change to be really applied. func (r *Route) SetStatusOffline() bool { return r.ChangeMethod(MethodNone) } // Describe sets the route's description // that will be logged alongside with the route information // in DEBUG log level. // Returns the `Route` itself. func (r *Route) Describe(description string) *Route { r.Description = description return r } // SetSourceLine sets the route's source caller, useful for debugging. // Returns the `Route` itself. func (r *Route) SetSourceLine(fileName string, lineNumber int) *Route { r.SourceFileName = fileName r.SourceLineNumber = lineNumber return r } // RestoreStatus will try to restore the status of this route instance, i.e if `SetStatusOffline` called on a "GET" route, // then this function will make this route available with "GET" HTTP Method. // Note if that you want to set status online for an offline registered route then you should call the `ChangeMethod` instead. // It will return true if the status restored, otherwise false. // A call of `RefreshRouter` is required after this type of change in order to change to be really applied. func (r *Route) RestoreStatus() bool { return r.ChangeMethod(r.methodBckp) } // BuildHandlers is executed automatically by the router handler // at the `Application#Build` state. Do not call it manually, unless // you were defined your own request mux handler. func (r *Route) BuildHandlers() { if r.OnBuild != nil { r.OnBuild(r) } // prepend begin handlers. r.Handlers = append(r.builtinBeginHandlers, append(r.beginHandlers, r.Handlers...)...) // append done handlers. r.Handlers = append(r.Handlers, r.doneHandlers...) // reset the temp storage, so a second call of // BuildHandlers will not re-add them (i.e RefreshRouter). r.builtinBeginHandlers = r.builtinBeginHandlers[0:0] r.beginHandlers = r.beginHandlers[0:0] r.doneHandlers = r.doneHandlers[0:0] } // String returns the form of METHOD, SUBDOMAIN, TMPL PATH. func (r *Route) String() string { start := r.GetTitle() // if r.StatusCode > 0 { // start = fmt.Sprintf("%d (%s)", r.StatusCode, http.StatusText(r.StatusCode)) // } return fmt.Sprintf("%s %s%s", start, r.Subdomain, r.Tmpl().Src) } // Equal compares the method, subdomain and the // underline representation of the route's path, // instead of the `String` function which returns the front representation. func (r *Route) Equal(other *Route) bool { return r.StatusCode == other.StatusCode && r.Method == other.Method && r.Subdomain == other.Subdomain && r.Path == other.Path } // DeepEqual compares the method, subdomain, the // underline representation of the route's path, // and the template source. func (r *Route) DeepEqual(other *Route) bool { return r.Equal(other) && r.tmpl.Src == other.tmpl.Src } // SetName overrides the default route name which defaults to // method + subdomain + path and // statusErrorCode_method+subdomain+path for error routes. // // Note that the route name MUST BE unique per Iris Application. func (r *Route) SetName(newRouteName string) *Route { r.Name = newRouteName return r } // ExcludeSitemap excludes this route page from sitemap generator. // It sets the NoSitemap field to true. // // See `SetLastMod`, `SetChangeFreq`, `SetPriority` methods // and `iris.WithSitemap`. func (r *Route) ExcludeSitemap() *Route { r.NoSitemap = true return r } // SetLastMod sets the date of last modification of the file served by this static GET route. func (r *Route) SetLastMod(t time.Time) *Route { r.LastMod = t return r } // SetChangeFreq sets how frequently this static GET route's page is likely to change, // possible values: // - "always" // - "hourly" // - "daily" // - "weekly" // - "monthly" // - "yearly" // - "never" func (r *Route) SetChangeFreq(freq string) *Route { r.ChangeFreq = freq return r } // SetPriority sets the priority of this static GET route's URL relative to other URLs on your site. func (r *Route) SetPriority(prio float32) *Route { r.Priority = prio return r } // Tmpl returns the path template, // it contains the parsed template // for the route's path. // May contain zero named parameters. // // Developer can get his registered path // via Tmpl().Src, Route.Path is the path // converted to match the underline router's specs. func (r *Route) Tmpl() macro.Template { return r.tmpl } // RegisteredHandlersLen returns the end-developer's registered handlers, all except the macro evaluator handler // if was required by the build process. func (r *Route) RegisteredHandlersLen() int { n := len(r.Handlers) if handler.CanMakeHandler(r.tmpl) { n-- } return n } // IsOnline returns true if the route is marked as "online" (state). func (r *Route) IsOnline() bool { return r.Method != MethodNone } // formats the parsed to the underline path syntax. // path = "/api/users/:id" // return "/api/users/%v" // // path = "/files/*file" // return /files/%v // // path = "/:username/messages/:messageid" // return "/%v/messages/%v" // we don't care about performance here, it's prelisten. func formatPath(path string) string { if strings.Contains(path, ParamStart) || strings.Contains(path, WildcardParamStart) { var ( startRune = ParamStart[0] wildcardStartRune = WildcardParamStart[0] ) var formattedParts []string parts := strings.Split(path, "/") for _, part := range parts { if part == "" { continue } if part[0] == startRune || part[0] == wildcardStartRune { // is param or wildcard param part = "%v" } formattedParts = append(formattedParts, part) } return "/" + strings.Join(formattedParts, "/") } // the whole path is static just return it return path } // IsStatic reports whether this route is a static route. // Does not contain dynamic path parameters, // is online and registered on GET HTTP Method. func (r *Route) IsStatic() bool { return r.IsOnline() && len(r.Tmpl().Params) == 0 && r.Method == "GET" } // StaticPath returns the static part of the original, registered route path. // if /user/{id} it will return /user // if /user/{id}/friend/{friendid:uint64} it will return /user too // if /assets/{filepath:path} it will return /assets. func (r *Route) StaticPath() string { src := r.tmpl.Src return staticPath(src) } // ResolvePath returns the formatted path's %v replaced with the args. func (r *Route) ResolvePath(args ...string) string { rpath, formattedPath := r.Path, r.FormattedPath if rpath == formattedPath { // static, no need to pass args return rpath } // check if we have /*, if yes then join all arguments to one as path and pass that as parameter if rpath[len(rpath)-1] == WildcardParamStart[0] { parameter := strings.Join(args, "/") return fmt.Sprintf(formattedPath, parameter) } // else return the formattedPath with its args, // the order matters. for _, s := range args { formattedPath = strings.Replace(formattedPath, "%v", s, 1) } return formattedPath } func traceHandlerFile(title, name, line string, number int) string { file := fmt.Sprintf("(%s:%d)", filepath.ToSlash(line), number) if context.IgnoreHandlerName(name) { return "" } space := strings.Repeat(" ", len(title)+1) return fmt.Sprintf("\n%s • %s %s", space, name, file) } var methodColors = map[string]int{ http.MethodGet: pio.Green, http.MethodPost: pio.Magenta, http.MethodPut: pio.Blue, http.MethodDelete: pio.Red, http.MethodConnect: pio.Green, http.MethodHead: 23, http.MethodPatch: pio.Blue, http.MethodOptions: pio.Gray, http.MethodTrace: pio.Yellow, MethodNone: 203, // orange-red. } // TraceTitleColorCode returns the color code depending on the method or the status. func TraceTitleColorCode(method string) int { if color, ok := methodColors[method]; ok { return color } return 131 // for error handlers, of "ERROR [%STATUSCODE]" } // GetTitle returns the custom Title or the method or the error code. func (r *Route) GetTitle() string { title := r.Title if title == "" { if r.StatusCode > 0 { title = fmt.Sprintf("%d", r.StatusCode) // if error code then title is the status code, e.g. 400. } else { title = r.Method // else is its method, e.g. GET } } return title } // Trace prints some debug info about the Route to the "w". // Should be called after `Build` state. // // It prints the @method: @path (@description) (@route_rel_location) // - @handler_name (@handler_rel_location) // - @second_handler ... // // If route and handler line:number locations are equal then the second is ignored. func (r *Route) Trace(w io.Writer, stoppedIndex int) { title := r.GetTitle() // Color the method. color := TraceTitleColorCode(title) // @method: @path // space := strings.Repeat(" ", len(http.MethodConnect)-len(method)) // s := fmt.Sprintf("%s: %s", pio.Rich(title, color), path) pio.WriteRich(w, title, color) path := r.tmpl.Src if path == "" { path = "/" } fmt.Fprintf(w, ": %s", path) // (@description) description := r.Description if description == "" { if title == MethodNone { description = "offline" } if subdomain := r.Subdomain; subdomain != "" { if subdomain == "*." { // wildcard. subdomain = "subdomain" } if description == "offline" { description += ", " } description += subdomain } } if description != "" { // s += fmt.Sprintf(" %s", pio.Rich(description, pio.Cyan, pio.Underline)) fmt.Fprint(w, " ") pio.WriteRich(w, description, pio.Cyan, pio.Underline) } // (@route_rel_location) // s += fmt.Sprintf(" (%s:%d)", r.RegisterFileName, r.RegisterLineNumber) fmt.Fprintf(w, " (%s:%d)", r.RegisterFileName, r.RegisterLineNumber) for i, h := range r.Handlers { var ( name string file string line int ) if i == r.MainHandlerIndex && r.MainHandlerName != "" { // Main handler info can be programmatically // changed to be more specific, respect these changes. name = r.MainHandlerName file = r.SourceFileName line = r.SourceLineNumber } else { name = context.HandlerName(h) file, line = context.HandlerFileLineRel(h) // If a middleware, e.g (macro) which changes the main handler index, // skip it. // TODO: think of it. if file == "" { // At PartyConfigure, 2nd+ level of routes it will get but in reallity will be the same as the caller. file = r.RegisterFileName line = r.RegisterLineNumber } if file == r.SourceFileName && line == r.SourceLineNumber { continue } } // If a handler is an anonymous function then it was already // printed in the first line, skip it. if file == r.RegisterFileName && line == r.RegisterLineNumber { continue } // * @handler_name (@handler_rel_location) fmt.Fprint(w, traceHandlerFile(title, name, file, line)) if stoppedIndex != -1 && stoppedIndex <= len(r.Handlers) { if i <= stoppedIndex { pio.WriteRich(w, " ✓", pio.Green) // } else { // pio.WriteRich(w, " ✕", pio.Red, pio.Underline) } } } fmt.Fprintln(w) if r.overlappedLink != nil { bckpDesc := r.overlappedLink.Description r.overlappedLink.Description += " (overlapped)" r.overlappedLink.Trace(w, -1) r.overlappedLink.Description = bckpDesc } } type routeReadOnlyWrapper struct { *Route } var _ context.RouteReadOnly = routeReadOnlyWrapper{} func (rd routeReadOnlyWrapper) StatusErrorCode() int { return rd.Route.StatusCode } func (rd routeReadOnlyWrapper) Method() string { return rd.Route.Method } func (rd routeReadOnlyWrapper) Name() string { return rd.Route.Name } func (rd routeReadOnlyWrapper) Subdomain() string { return rd.Route.Subdomain } func (rd routeReadOnlyWrapper) Path() string { return rd.Route.tmpl.Src } func (rd routeReadOnlyWrapper) Trace(w io.Writer, stoppedIndex int) { rd.Route.Trace(w, stoppedIndex) } func (rd routeReadOnlyWrapper) Tmpl() macro.Template { return rd.Route.Tmpl() } func (rd routeReadOnlyWrapper) MainHandlerName() string { return rd.Route.MainHandlerName } func (rd routeReadOnlyWrapper) MainHandlerIndex() int { return rd.Route.MainHandlerIndex } func (rd routeReadOnlyWrapper) Property(key string) (any, bool) { properties := rd.Route.Party.Properties() if properties != nil { if property, ok := properties[key]; ok { return property, true } } return nil, false } func (rd routeReadOnlyWrapper) GetLastMod() time.Time { return rd.Route.LastMod } func (rd routeReadOnlyWrapper) GetChangeFreq() string { return rd.Route.ChangeFreq } func (rd routeReadOnlyWrapper) GetPriority() float32 { return rd.Route.Priority } ================================================ FILE: core/router/route_register_rule_test.go ================================================ package router_test import ( "bytes" "reflect" "testing" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/core/router" "github.com/kataras/iris/v12/httptest" ) func TestRegisterRule(t *testing.T) { app := iris.New() app.Configure(iris.WithDynamicHandler) // collect the error on RouteError rule. buf := new(bytes.Buffer) app.Logger().SetTimeFormat("").DisableNewLine().SetOutput(buf) v1 := app.Party("/v1") v1.SetRegisterRule(iris.RouteSkip) getHandler := func(ctx iris.Context) { ctx.Writef("[get] %s", ctx.Method()) } anyHandler := func(ctx iris.Context) { ctx.Writef("[any] %s", ctx.Method()) } getRoute := v1.Get("/", getHandler) v1.Any("/", anyHandler) if route := v1.Get("/", getHandler); !reflect.DeepEqual(route, getRoute) { t.Fatalf("expected route to be equal with the original get route") } // test RouteSkip. e := httptest.New(t, app, httptest.LogLevel("error")) testRegisterRule(e, "[get] GET") // test RouteOverride (default behavior). v1.SetRegisterRule(iris.RouteOverride) v1.Any("/", anyHandler) app.RefreshRouter() testRegisterRule(e, "[any] GET") // test RouteError. v1.SetRegisterRule(iris.RouteError) if route := v1.Get("/", getHandler); route != nil { t.Fatalf("expected duplicated route, with RouteError rule, to be nil but got: %#+v", route) } if expected, got := "[ERRO] new route: GET /v1 conflicts with an already registered one: GET /v1 route", buf.String(); expected != got { t.Fatalf("expected api builder's error to be:\n'%s'\nbut got:\n'%s'", expected, got) } } func testRegisterRule(e *httptest.Expect, expectedGetBody string) { for _, method := range router.AllMethods { tt := e.Request(method, "/v1").Expect().Status(httptest.StatusOK).Body() if method == iris.MethodGet { tt.IsEqual(expectedGetBody) } else { tt.IsEqual("[any] " + method) } } } func TestRegisterRuleOverlap(t *testing.T) { app := iris.New() // TODO(@kataras) the overlapping does not work per-party yet, // it just checks compares from the total app's routes (which is the best possible action to do // because MVC applications can be separated into different parties too?). usersRouter := app.Party("/users") usersRouter.SetRegisterRule(iris.RouteOverlap) // second handler will be executed, status will be reset-ed as well, // stop without data written. usersRouter.Get("/", func(ctx iris.Context) { ctx.StopWithStatus(iris.StatusUnauthorized) }) usersRouter.Get("/", func(ctx iris.Context) { ctx.StatusCode(iris.StatusOK) ctx.WriteString("data") }) // first handler will be executed, no stop called. usersRouter.Get("/p1", func(ctx iris.Context) { ctx.StatusCode(iris.StatusUnauthorized) }) usersRouter.Get("/p1", func(ctx iris.Context) { ctx.WriteString("not written") }) // first handler will be executed, stop but with data sent on default writer // (body sent cannot be reset-ed here). usersRouter.Get("/p2", func(ctx iris.Context) { ctx.StopWithText(iris.StatusUnauthorized, "no access") }) usersRouter.Get("/p2", func(ctx iris.Context) { ctx.WriteString("not written") }) // second will be executed, response can be reset-ed on recording. usersRouter.Get("/p3", func(ctx iris.Context) { ctx.Record() ctx.StopWithText(iris.StatusUnauthorized, "no access") }) usersRouter.Get("/p3", func(ctx iris.Context) { ctx.StatusCode(iris.StatusOK) ctx.WriteString("p3 data") }) e := httptest.New(t, app) e.GET("/users").Expect().Status(httptest.StatusOK).Body().IsEqual("data") e.GET("/users/p1").Expect().Status(httptest.StatusUnauthorized).Body().IsEqual("Unauthorized") e.GET("/users/p2").Expect().Status(httptest.StatusUnauthorized).Body().IsEqual("no access") e.GET("/users/p3").Expect().Status(httptest.StatusOK).Body().IsEqual("p3 data") } ================================================ FILE: core/router/route_test.go ================================================ // white-box testing package router import ( "testing" "github.com/kataras/iris/v12/macro" ) func TestRouteStaticPath(t *testing.T) { tests := []struct { tmpl string static string }{ { tmpl: "/files/{file:path}", static: "/files", }, { tmpl: "/path", static: "/path", }, { tmpl: "/path/segment", static: "/path/segment", }, { tmpl: "/path/segment/{n:int}", static: "/path/segment", }, { tmpl: "/path/{n:uint64}/{n:int}", static: "/path", }, { tmpl: "/path/{n:uint64}/static", static: "/path", }, { tmpl: "/{name}", static: "/", }, { tmpl: "/", static: "/", }, } for i, tt := range tests { route := Route{tmpl: macro.Template{Src: tt.tmpl}} if expected, got := tt.static, route.StaticPath(); expected != got { t.Fatalf("[%d:%s] expected static path to be: '%s' but got: '%s'", i, tt.tmpl, expected, got) } } } ================================================ FILE: core/router/router.go ================================================ package router import ( "errors" "net/http" "sort" "strings" "sync" "time" "github.com/kataras/iris/v12/context" "github.com/schollz/closestmatch" ) // Router is the "director". // Caller should provide a request handler (router implementation or root handler). // Router is responsible to build the received request handler and run it // to serve requests, based on the received context.Pool. // // User can refresh the router with `RefreshRouter` whenever a route's field is changed by him. type Router struct { mu sync.Mutex // for Downgrade, WrapRouter & BuildRouter, requestHandler RequestHandler // build-accessible, can be changed to define a custom router or proxy, used on RefreshRouter too. mainHandler http.HandlerFunc // init-accessible wrapperFunc WrapperFunc // wrappers to be built on BuildRouter state, // first is executed first at this case. // Case: // - SubdomainRedirect on user call, registers a wrapper, on design state // - i18n,if loaded and Subdomain or PathRedirect is true, registers a wrapper too, on build state // the SubdomainRedirect should be the first(subdomainWrap(i18nWrap)) wrapper // to be executed instead of last(i18nWrap(subdomainWrap)). wrapperFuncs []WrapperFunc cPool *context.Pool // used on RefreshRouter routesProvider RoutesProvider // key = subdomain // value = closest of static routes, filled on `BuildRouter/RefreshRouter`. closestPaths map[string]*closestmatch.ClosestMatch } // NewRouter returns a new empty Router. func NewRouter() *Router { return &Router{} } // RefreshRouter re-builds the router. Should be called when a route's state // changed (i.e Method changed at serve-time). // // Note that in order to use RefreshRouter while in serve-time, // you have to set the `EnableDynamicHandler` Iris Application setting to true, // e.g. `app.Listen(":8080", iris.WithEnableDynamicHandler)` func (router *Router) RefreshRouter() error { return router.BuildRouter(router.cPool, router.requestHandler, router.routesProvider, true) } // AddRouteUnsafe adds a route directly to the router's request handler. // Works before or after Build state. // Mainly used for internal cases like `iris.WithSitemap`. // Do NOT use it on serve-time. func (router *Router) AddRouteUnsafe(routes ...*Route) error { if h := router.requestHandler; h != nil { if v, ok := h.(RouteAdder); ok { for _, r := range routes { return v.AddRoute(r) } } } return ErrNotRouteAdder } // FindClosestPaths returns a list of "n" paths close to "path" under the given "subdomain". // // Order may change. func (router *Router) FindClosestPaths(subdomain, searchPath string, n int) []string { if router.closestPaths == nil { return nil } cm, ok := router.closestPaths[subdomain] if !ok { return nil } list := cm.ClosestN(searchPath, n) if len(list) == 1 && list[0] == "" { // yes, it may return empty string as its first slice element when not found. return nil } return list } func (router *Router) buildMainHandler(cPool *context.Pool, requestHandler RequestHandler) { router.mainHandler = func(w http.ResponseWriter, r *http.Request) { ctx := cPool.Acquire(w, r) router.requestHandler.HandleRequest(ctx) cPool.Release(ctx) } } func (router *Router) buildMainHandlerWithFilters(routerFilters map[Party]*Filter, cPool *context.Pool, requestHandler RequestHandler) { sortedFilters := make([]*Filter, 0, len(routerFilters)) // key was just there to enforce uniqueness on API level. for _, f := range routerFilters { sortedFilters = append(sortedFilters, f) // append it as one handlers so execution rules are being respected in that step too. f.Handlers = append(f.Handlers, func(ctx *context.Context) { // set the handler index back to 0 so the route's handlers can be executed as expected. ctx.HandlerIndex(0) // execute the main request handler, this will fire the found route's handlers // or if error the error code's associated handler. router.requestHandler.HandleRequest(ctx) }) } sort.SliceStable(sortedFilters, func(i, j int) bool { left, right := sortedFilters[i], sortedFilters[j] var ( leftSubLen = len(left.Subdomain) rightSubLen = len(right.Subdomain) leftSlashLen = strings.Count(left.Path, "/") rightSlashLen = strings.Count(right.Path, "/") ) if leftSubLen == rightSubLen { if leftSlashLen > rightSlashLen { return true } } if leftSubLen > rightSubLen { return true } if leftSlashLen > rightSlashLen { return true } if leftSlashLen == rightSlashLen { return len(left.Path) > len(right.Path) } return len(left.Path) > len(right.Path) }) router.mainHandler = func(w http.ResponseWriter, r *http.Request) { ctx := cPool.Acquire(w, r) filterExecuted := false for _, f := range sortedFilters { // from subdomain, largest path to shortest. // fmt.Printf("Sorted filter execution: [%s] [%s]\n", f.Subdomain, f.Path) if f.Matcher.Match(ctx) { // fmt.Printf("Matched [%s] and execute [%d] handlers [%s]\n\n", ctx.Path(), len(f.Handlers), context.HandlersNames(f.Handlers)) filterExecuted = true // execute the final handlers chain. ctx.Do(f.Handlers) break // and break on first found. } } if !filterExecuted { // If not at least one match filter found and executed, // then just run the router. router.requestHandler.HandleRequest(ctx) } cPool.Release(ctx) } } // BuildRouter builds the router based on // the context factory (explicit pool in this case), // the request handler which manages how the main handler will multiplexes the routes // provided by the third parameter, routerProvider (it's the api builder in this case) and // its wrapper. // // Use of RefreshRouter to re-build the router if needed. func (router *Router) BuildRouter(cPool *context.Pool, requestHandler RequestHandler, routesProvider RoutesProvider, force bool) error { if requestHandler == nil { return errors.New("router: request handler is nil") } if cPool == nil { return errors.New("router: context pool is nil") } // build the handler using the routesProvider if err := requestHandler.Build(routesProvider); err != nil { return err } router.mu.Lock() defer router.mu.Unlock() // store these for RefreshRouter's needs. if force { router.cPool = cPool router.requestHandler = requestHandler router.routesProvider = routesProvider } else { if router.cPool == nil { router.cPool = cPool } if router.requestHandler == nil { router.requestHandler = requestHandler } if router.routesProvider == nil && routesProvider != nil { router.routesProvider = routesProvider } } // the important stuff. if routerFilters := routesProvider.GetRouterFilters(); len(routerFilters) > 0 { router.buildMainHandlerWithFilters(routerFilters, cPool, requestHandler) } else { router.buildMainHandler(cPool, requestHandler) } for i := len(router.wrapperFuncs) - 1; i >= 0; i-- { w := router.wrapperFuncs[i] if w == nil { continue } router.WrapRouter(w) } if router.wrapperFunc != nil { // if wrapper used then attach that as the router service router.mainHandler = newWrapper(router.wrapperFunc, router.mainHandler).ServeHTTP } // build closest. subdomainPaths := make(map[string][]string) for _, r := range router.routesProvider.GetRoutes() { if !r.IsStatic() { continue } subdomainPaths[r.Subdomain] = append(subdomainPaths[r.Subdomain], r.Path) } router.closestPaths = make(map[string]*closestmatch.ClosestMatch) for subdomain, paths := range subdomainPaths { router.closestPaths[subdomain] = closestmatch.New(paths, []int{3, 4, 6}) } return nil } // Downgrade "downgrades", alters the router supervisor service(Router.mainHandler) // algorithm to a custom one, // be aware to change the global variables of 'ParamStart' and 'ParamWildcardStart'. // can be used to implement a custom proxy or // a custom router which should work with raw ResponseWriter, *Request // instead of the Context(which again, can be retrieved by the Framework's context pool). // // Note: Downgrade will by-pass the Wrapper, the caller is responsible for everything. // Downgrade is thread-safe. func (router *Router) Downgrade(newMainHandler http.HandlerFunc) { router.mu.Lock() router.mainHandler = newMainHandler router.mu.Unlock() } // Downgraded returns true if this router is downgraded. func (router *Router) Downgraded() bool { return router.mainHandler != nil && router.requestHandler == nil } // SetTimeoutHandler overrides the main handler with a timeout handler. // // TimeoutHandler supports the Pusher interface but does not support // the Hijacker or Flusher interfaces. // // All previous registered wrappers and middlewares are still executed as expected. func (router *Router) SetTimeoutHandler(timeout time.Duration, msg string) { if timeout <= 0 { return } mainHandler := router.mainHandler h := func(w http.ResponseWriter, r *http.Request) { mainHandler(w, r) } router.mainHandler = http.TimeoutHandler(http.HandlerFunc(h), timeout, msg).ServeHTTP } // WrapRouter adds a wrapper on the top of the main router. // Usually it's useful for third-party middleware // when need to wrap the entire application with a middleware like CORS. // // Developers can add more than one wrappers, // those wrappers' execution comes from last to first. // That means that the second wrapper will wrap the first, and so on. // // Before build. func (router *Router) WrapRouter(wrapperFunc WrapperFunc) { // logger := context.DefaultLogger("router wrapper") // file, line := context.HandlerFileLineRel(wrapperFunc) // if router.wrapperFunc != nil { // wrappedFile, wrappedLine := context.HandlerFileLineRel(router.wrapperFunc) // logger.Infof("%s:%d wraps %s:%d", file, line, wrappedFile, wrappedLine) // } else { // logger.Infof("%s:%d wraps the main router", file, line) // } router.wrapperFunc = makeWrapperFunc(router.wrapperFunc, wrapperFunc) } // AddRouterWrapper adds a router wrapper. // Unlike `WrapRouter` the first registered will be executed first // so a wrapper wraps its next not the previous one. // it defers the wrapping until the `BuildRouter`. // Redirection wrappers should be added using this method // e.g. SubdomainRedirect. func (router *Router) AddRouterWrapper(wrapperFunc WrapperFunc) { router.wrapperFuncs = append(router.wrapperFuncs, wrapperFunc) } // PrependRouterWrapper like `AddRouterWrapper` but this wrapperFunc // will always be executed before the previous `AddRouterWrapper`. // Path form (no modification) wrappers should be added using this method // e.g. ForceLowercaseRouting. func (router *Router) PrependRouterWrapper(wrapperFunc WrapperFunc) { router.wrapperFuncs = append([]WrapperFunc{wrapperFunc}, router.wrapperFuncs...) } // ServeHTTPC serves the raw context, useful if we have already a context, it by-pass the wrapper. func (router *Router) ServeHTTPC(ctx *context.Context) { router.requestHandler.HandleRequest(ctx) } func (router *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) { router.mainHandler(w, r) } // RouteExists reports whether a particular route exists // It will search from the current subdomain of context's host, if not inside the root domain. func (router *Router) RouteExists(ctx *context.Context, method, path string) bool { return router.requestHandler.RouteExists(ctx, method, path) } ================================================ FILE: core/router/router_handlers_order_test.go ================================================ // black-box testing // // see _examples/routing/main_test.go for the most common router tests that you may want to see, // this is a test which makes sure that the APIBuilder's `UseGlobal`, `Use` and `Done` functions are // working as expected. package router_test import ( "fmt" "net/http" "testing" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/httptest" ) // test registering of below handlers // with a different order but the route's final // response should be the same at all cases. var ( writeHandler = func(s string) iris.Handler { return func(ctx iris.Context) { ctx.WriteString(s) ctx.Next() } } mainResponse = "main" mainHandler = writeHandler(mainResponse) firstUseResponse = "use1" firstUseHandler = writeHandler(firstUseResponse) secondUseResponse = "use2" secondUseHandler = writeHandler(secondUseResponse) firstUseRouterResponse = "userouter1" // Use inline handler, no the `writeHandler`, // because it will be overridden by `secondUseRouterHandler` otherwise, // look `UseRouter:context.UpsertHandlers` for more. firstUseRouterHandler = func(ctx iris.Context) { ctx.WriteString(firstUseRouterResponse) ctx.Next() } secondUseRouterResponse = "userouter2" secondUseRouterHandler = writeHandler(secondUseRouterResponse) firstUseGlobalResponse = "useglobal1" firstUseGlobalHandler = writeHandler(firstUseGlobalResponse) secondUseGlobalResponse = "useglobal2" secondUseGlobalHandler = writeHandler(secondUseGlobalResponse) firstDoneResponse = "done1" firstDoneHandler = writeHandler(firstDoneResponse) secondDoneResponse = "done2" secondDoneHandler = func(ctx iris.Context) { ctx.WriteString(secondDoneResponse) } finalResponse = firstUseRouterResponse + secondUseRouterResponse + firstUseGlobalResponse + secondUseGlobalResponse + firstUseResponse + secondUseResponse + mainResponse + firstDoneResponse + secondDoneResponse testResponse = func(t *testing.T, app *iris.Application, path string) { t.Helper() e := httptest.New(t, app) e.GET(path).Expect().Status(httptest.StatusOK).Body().IsEqual(finalResponse) } ) func TestMiddlewareByRouteDef(t *testing.T) { app := iris.New() app.UseRouter(firstUseRouterHandler) app.UseRouter(secondUseRouterHandler) app.Get("/mypath", firstUseGlobalHandler, secondUseGlobalHandler, firstUseHandler, secondUseHandler, mainHandler, firstDoneHandler, secondDoneHandler) testResponse(t, app, "/mypath") } func TestMiddlewareByUseAndDoneDef(t *testing.T) { app := iris.New() app.UseRouter(firstUseRouterHandler, secondUseRouterHandler) app.Use(firstUseGlobalHandler, secondUseGlobalHandler, firstUseHandler, secondUseHandler) app.Done(firstDoneHandler, secondDoneHandler) app.Get("/mypath", mainHandler) testResponse(t, app, "/mypath") } func TestMiddlewareByUseUseGlobalAndDoneDef(t *testing.T) { app := iris.New() app.Use(firstUseHandler, secondUseHandler) // if failed then UseGlobal didnt' registered these handlers even before the // existing middleware. app.UseGlobal(firstUseGlobalHandler, secondUseGlobalHandler) app.Done(firstDoneHandler, secondDoneHandler) app.UseRouter(firstUseRouterHandler, secondUseRouterHandler) app.Get("/mypath", mainHandler) testResponse(t, app, "/mypath") } func TestMiddlewareByUseDoneAndUseGlobalDef(t *testing.T) { app := iris.New() app.UseRouter(firstUseRouterHandler, secondUseRouterHandler) app.Use(firstUseHandler, secondUseHandler) app.Done(firstDoneHandler, secondDoneHandler) app.Get("/mypath", mainHandler) // if failed then UseGlobal was unable to // prepend these handlers to the route was registered before // OR // when order failed because these should be executed in order, first the firstUseGlobalHandler, // because they are the same type (global begin handlers) app.UseGlobal(firstUseGlobalHandler) app.UseGlobal(secondUseGlobalHandler) testResponse(t, app, "/mypath") } func TestMiddlewareByUseGlobalUseAndDoneGlobalDef(t *testing.T) { app := iris.New() app.UseRouter(firstUseRouterHandler) app.UseRouter(secondUseRouterHandler) app.UseGlobal(firstUseGlobalHandler) app.UseGlobal(secondUseGlobalHandler) app.Use(firstUseHandler, secondUseHandler) app.Get("/mypath", mainHandler) app.DoneGlobal(firstDoneHandler, secondDoneHandler) testResponse(t, app, "/mypath") } func TestMiddlewareByDoneUseAndUseGlobalDef(t *testing.T) { app := iris.New() app.UseRouter(firstUseRouterHandler, secondUseRouterHandler) app.Done(firstDoneHandler, secondDoneHandler) app.Use(firstUseHandler, secondUseHandler) app.Get("/mypath", mainHandler) app.UseGlobal(firstUseGlobalHandler) app.UseGlobal(secondUseGlobalHandler) testResponse(t, app, "/mypath") } func TestUseRouterStopExecution(t *testing.T) { app := iris.New() app.UseRouter(func(ctx iris.Context) { ctx.WriteString("stop") // no ctx.Next, so the router has not even the chance to work. }) app.Get("/", writeHandler("index")) e := httptest.New(t, app) e.GET("/").Expect().Status(iris.StatusOK).Body().IsEqual("stop") app = iris.New() app.OnErrorCode(iris.StatusForbidden, func(ctx iris.Context) { ctx.Writef("err: %v", ctx.GetErr()) }) app.UseRouter(func(ctx iris.Context) { ctx.StopWithPlainError(iris.StatusForbidden, fmt.Errorf("custom error")) // stopped but not data written yet, the error code handler // should be responsible of it (use StopWithError to write and close). }) app.Get("/", writeHandler("index")) e = httptest.New(t, app) e.GET("/").Expect().Status(iris.StatusForbidden).Body().IsEqual("err: custom error") } func TestUseRouterParentDisallow(t *testing.T) { const expectedResponse = "no_userouter_allowed" app := iris.New() app.UseRouter(func(ctx iris.Context) { ctx.WriteString("always") ctx.Next() }) app.Get("/index", func(ctx iris.Context) { ctx.WriteString(expectedResponse) }) app.SetPartyMatcher(func(ctx iris.Context, p iris.Party) bool { // modifies the PartyMatcher to not match any UseRouter, // tests should receive the handlers response alone. return false }) app.PartyFunc("/", func(p iris.Party) { // it's the same instance of app. p.UseRouter(func(ctx iris.Context) { ctx.WriteString("_2") ctx.Next() }) p.Get("/", func(ctx iris.Context) { ctx.WriteString(expectedResponse) }) }) app.PartyFunc("/user", func(p iris.Party) { p.UseRouter(func(ctx iris.Context) { ctx.WriteString("_3") ctx.Next() }) p.Get("/", func(ctx iris.Context) { ctx.WriteString(expectedResponse) }) }) e := httptest.New(t, app) e.GET("/index").Expect().Status(iris.StatusOK).Body().IsEqual(expectedResponse) e.GET("/").Expect().Status(iris.StatusOK).Body().IsEqual(expectedResponse) e.GET("/user").Expect().Status(iris.StatusOK).Body().IsEqual(expectedResponse) } func TestUseRouterSubdomains(t *testing.T) { app := iris.New() app.UseRouter(func(ctx iris.Context) { if ctx.Subdomain() == "old" { ctx.Next() // call the router, do not write. return } // if we write here, it will always give 200 OK, // even on not registered routes, that's the point at the end, // full control here when we need it. ctx.WriteString("always_") ctx.Next() }) adminAPI := app.Subdomain("admin") adminAPI.UseRouter(func(ctx iris.Context) { ctx.WriteString("admin always_") ctx.Next() }) adminAPI.Get("/", func(ctx iris.Context) { ctx.WriteString("admin") }) adminControlAPI := adminAPI.Subdomain("control") adminControlAPI.UseRouter(func(ctx iris.Context) { ctx.WriteString("control admin always_") ctx.Next() }) adminControlAPI.Get("/", func(ctx iris.Context) { ctx.WriteString("control admin") }) oldAPI := app.Subdomain("old") oldAPI.Get("/", func(ctx iris.Context) { ctx.WriteString("chat") }) e := httptest.New(t, app, httptest.URL("http://example.com")) e.GET("/notfound").Expect().Status(iris.StatusOK).Body().IsEqual("always_") e.GET("/").WithURL("http://admin.example.com").Expect().Status(iris.StatusOK).Body(). IsEqual("always_admin always_admin") e.GET("/").WithURL("http://control.admin.example.com").Expect().Status(iris.StatusOK).Body(). IsEqual("always_admin always_control admin always_control admin") // It has a route, and use router just proceeds to the router. e.GET("/").WithURL("http://old.example.com").Expect().Status(iris.StatusOK).Body(). IsEqual("chat") // this is not a registered path, should fire 404, the UseRouter does not write // anything to the response writer, so the router has control over it. e.GET("/notfound").WithURL("http://old.example.com").Expect().Status(iris.StatusNotFound).Body(). IsEqual("Not Found") } func TestUseWrapOrder(t *testing.T) { var ( expectedBody = "#1 .WrapRouter\n#2 .UseRouter\n#3 .UseGlobal\n#4 .Use\n#5 Main Handler\n" expectedNotFoundBody = "#3 .UseGlobal\n#1 .UseError\n#2 Main Error Handler\n" makeMiddleware = func(body string) iris.Handler { return func(ctx iris.Context) { ctx.WriteString(body) ctx.Next() } } handler = func(ctx iris.Context) { ctx.WriteString("#5 Main Handler\n") } errorHandler = func(ctx iris.Context) { ctx.WriteString("#2 Main Error Handler\n") } useHandler = makeMiddleware("#4 .Use\n") useGlobal = makeMiddleware("#3 .UseGlobal\n") useError = func(ctx iris.Context) { // UseError has captured the status code, because it runs // after the router itself but only one error handlers. ctx.WriteString("#1 .UseError\n") ctx.Next() } useRouter = func(ctx iris.Context) { if ctx.Path() == "/" { ctx.WriteString("#2 .UseRouter\n") } ctx.Next() } wrapRouter = func(w http.ResponseWriter, r *http.Request, router http.HandlerFunc) { if r.URL.Path == "/" { w.Write([]byte("#1 .WrapRouter\n")) /* Note for new Gophers: If we write something here, on a not found resource, in the raw `net/http` wrapper like this one, then the response writer will send `200` status OK (on first write). Any error handler will not be fired as expected. Also, when `w.WriteHeader` is called you can NOT change the status code later on. In Iris Handlers, if you write before status code set, then it sends 200 status OK and it cannot change as well. However if we just called `ctx.StatusCode` inside an Iris Handler without any content written then we would able to change the status code later on. When you need to change that behavior you should start the handler with a ctx.Record() call. */ } // Continue by executing the Iris Router and leave it do its job. router(w, r) } ) app := iris.New() app.Use(useHandler) app.UseGlobal(useGlobal) app.UseError(useError) app.UseRouter(useRouter) app.WrapRouter(wrapRouter) app.OnErrorCode(iris.StatusNotFound, errorHandler) app.Get("/", handler) e := httptest.New(t, app) e.GET("/NotFound").Expect().Status(iris.StatusNotFound).Body().IsEqual(expectedNotFoundBody) e.GET("/").Expect().Status(iris.StatusOK).Body().IsEqual(expectedBody) } func TestResumeExecution(t *testing.T) { before := func(ctx iris.Context) { ctx.WriteString("1") curIdx := ctx.HandlerIndex(-1) ctx.StopExecution() ctx.Next() ctx.StopExecution() ctx.Next() ctx.ResumeExecution() if ctx.HandlerIndex(-1) != curIdx { ctx.WriteString("| 1. NOT OK") } ctx.StopExecution() ctx.ResumeExecution() if ctx.HandlerIndex(-1) != curIdx { ctx.WriteString("| 2. NOT OK") } ctx.Next() if ctx.HandlerIndex(-1) != curIdx+2 /* 2 and 3 */ { ctx.WriteString("| 3. NOT OK") } } handler := func(ctx iris.Context) { ctx.WriteString("2") ctx.Next() } after := func(ctx iris.Context) { ctx.WriteString("3") if !ctx.Proceed(func(ctx iris.Context) { ctx.Next() }) { ctx.WriteString(" | 4. NOT OK") } } expectedBody := "123" app := iris.New() app.Get("/", before, handler, after) e := httptest.New(t, app) e.GET("/").Expect().Status(iris.StatusOK).Body().IsEqual(expectedBody) } ================================================ FILE: core/router/router_subdomain_redirect.go ================================================ package router import ( "fmt" "net/http" "strconv" "strings" "text/template" "unicode/utf8" "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/core/netutil" ) type subdomainRedirectWrapper struct { // the func which will give us the root domain, // it's declared as a func because in that state the application is not configurated neither ran yet. root func() string // the from and to locations, if subdomains must end with dot('.'). from, to string // true if from wildcard subdomain is given by 'from' ("*." or '*'). isFromAny bool // true for the location that is the root domain ('/', '.' or ""). isFromRoot, isToRoot bool } func pathIsRootDomain(partyRelPath string) bool { return partyRelPath == "/" || partyRelPath == "" || partyRelPath == "." } func pathIsWildcard(partyRelPath string) bool { return partyRelPath == SubdomainWildcardIndicator || partyRelPath == "*" } // NewSubdomainRedirectWrapper returns a router wrapper which // if it's registered to the router via `router#WrapRouter` it // redirects(StatusMovedPermanently) a subdomain or the root domain to another subdomain or to the root domain. // // It receives three arguments, // the first one is a function which returns the root domain, (in the application it's the app.ConfigurationReadOnly().GetVHost()). // The second and third are the from and to locations, 'from' can be a wildcard subdomain as well (*. or *) // 'to' is not allowed to be a wildcard for obvious reasons, // 'from' can be the root domain when the 'to' is not the root domain and visa-versa. // To declare a root domain as 'from' or 'to' you MUST pass an empty string or a slash('/') or a dot('.'). // Important note: the 'from' and 'to' should end with "." like we use the `APIBuilder#Party`, if they are subdomains. // // Usage(package-level): // sd := NewSubdomainRedirectWrapper(func() string { return "mydomain.com" }, ".", "www.") // router.AddRouterWrapper(sd) // // Usage(high-level using `iris#Application.SubdomainRedirect`) // www := app.Subdomain("www") // app.SubdomainRedirect(app, www) // Because app's rel path is "/" it translates it to the root domain // and www's party's rel path is the "www.", so it's the target subdomain. // // All the above code snippets will register a router wrapper which will // redirect all http(s)://mydomain.com/%anypath% to http(s)://www.mydomain.com/%anypath%. // // One or more subdomain redirect wrappers can be used to the same router instance. // // NewSubdomainRedirectWrapper may return nil if not allowed input arguments values were received // but in that case, the `AddRouterWrapper` will, simply, ignore that wrapper. // // Example: https://github.com/kataras/iris/tree/main/_examples/routing/subdomains/redirect func NewSubdomainRedirectWrapper(rootDomainGetter func() string, from, to string) WrapperFunc { // we can return nil, // because if wrapper is nil then it's not be used on the `router#AddRouterWrapper`. if from == to { // cannot redirect to the same location, cycle. return nil } if pathIsWildcard(to) { // cannot redirect to "any location". return nil } isFromRoot, isToRoot := pathIsRootDomain(from), pathIsRootDomain(to) if isFromRoot && isToRoot { // cannot redirect to the root domain from the root domain. return nil } sd := &subdomainRedirectWrapper{ root: rootDomainGetter, from: from, to: to, isFromAny: pathIsWildcard(from), isFromRoot: isFromRoot, isToRoot: isToRoot, } return sd.Wrapper } // Wrapper is the function that is being used to wrap the router with a redirect // service that is able to redirect between (sub)domains as fast as possible. // Please take a look at the `NewSubdomainRedirectWrapper` function for more. func (s *subdomainRedirectWrapper) Wrapper(w http.ResponseWriter, r *http.Request, router http.HandlerFunc) { // Author's note: // I use the StatusMovedPermanently(301) instead of the the StatusPermanentRedirect(308) // because older browsers may not be able to recognise that status code (the RFC 7538, is not so old) // although note that move is not the same thing as redirect: move reminds a specific address or location moved while // redirect is a new location. host := context.GetHost(r) root := s.root() if loopback := netutil.GetLoopbackSubdomain(root); loopback != "" { root = strings.Replace(root, loopback, context.GetDomain(host), 1) } hasSubdomain := host != root if !hasSubdomain && !s.isFromRoot { // if the current endpoint is not a subdomain // and the redirect is not configured to be used from root domain to a subdomain. // This check comes first because it's the most common scenario. router(w, r) return } if hasSubdomain { // the current endpoint is a subdomain and // redirect is used for a subdomain to another subdomain or to its root domain. subdomain := strings.TrimSuffix(host, root) // with dot '.'. if s.to == subdomain { // we are in the subdomain we wanted to be redirected, // remember: a redirect response will fire a new request. // This check is needed to not allow cycles (too many redirects). router(w, r) return } if subdomain == s.from || s.isFromAny { resturi := r.URL.RequestURI() if s.isToRoot { // from a specific subdomain or any subdomain to the root domain. RedirectAbsolute(w, r, context.GetScheme(r)+root+resturi, http.StatusMovedPermanently) return } // from a specific subdomain or any subdomain to a specific subdomain. RedirectAbsolute(w, r, context.GetScheme(r)+s.to+root+resturi, http.StatusMovedPermanently) return } /* Think of another way. As it's a breaking change. if s.isFromRoot && !s.isFromAny { // Then we must not continue, // the subdomain didn't match the "to" but the from // was the application root itself, which is not a wildcard // so it shouldn't accept any subdomain, we must fire 404 here. // Something like: // http://registered_host_but_not_in_app.your.mydomain.com http.NotFound(w, r) return } */ // the from subdomain is not matched and it's not from root. router(w, r) return } if s.isFromRoot { resturi := r.URL.RequestURI() // we are not inside a subdomain, so we are in the root domain // and the redirect is configured to be used from root domain to a subdomain. RedirectAbsolute(w, r, context.GetScheme(r)+s.to+root+resturi, http.StatusMovedPermanently) return } router(w, r) } // NewSubdomainPartyRedirectHandler returns a handler which can be registered // through `UseRouter` or `Use` to redirect from the current request's // subdomain to the one which the given `to` Party can handle. func NewSubdomainPartyRedirectHandler(to Party) context.Handler { return NewSubdomainRedirectHandler(to.GetRelPath()) } // NewSubdomainRedirectHandler returns a handler which can be registered // through `UseRouter` or `Use` to redirect from the current request's // subdomain to the given "toSubdomain". func NewSubdomainRedirectHandler(toSubdomain string) context.Handler { toSubdomain, _ = splitSubdomainAndPath(toSubdomain) // let it here so users can just pass the GetRelPath of a Party. if pathIsWildcard(toSubdomain) { return nil } return func(ctx *context.Context) { // en-us.test.mydomain.com host := ctx.Host() fullSubdomain := ctx.SubdomainFull() targetHost := strings.Replace(host, fullSubdomain, toSubdomain, 1) // resturi := ctx.Request().URL.RequestURI() // urlToRedirect := ctx.Scheme() + newHost + resturi r := ctx.Request() r.Host = targetHost r.URL.Host = targetHost urlToRedirect := r.URL.String() RedirectAbsolute(ctx.ResponseWriter(), r, urlToRedirect, http.StatusMovedPermanently) } } // RedirectAbsolute replies to the request with a redirect to an absolute URL. // // The provided code should be in the 3xx range and is usually // StatusMovedPermanently, StatusFound or StatusSeeOther. // // If the Content-Type header has not been set, Redirect sets it // to "text/html; charset=utf-8" and writes a small HTML body. // Setting the Content-Type header to any value, including nil, // disables that behavior. func RedirectAbsolute(w http.ResponseWriter, r *http.Request, url string, code int) { h := w.Header() // RFC 7231 notes that a short HTML body is usually included in // the response because older user agents may not understand 301/307. // Do it only if the request didn't already have a Content-Type header. _, hadCT := h[context.ContentTypeHeaderKey] h.Set("Location", hexEscapeNonASCII(url)) if !hadCT && (r.Method == http.MethodGet || r.Method == http.MethodHead) { h.Set(context.ContentTypeHeaderKey, "text/html; charset=utf-8") } w.WriteHeader(code) // Shouldn't send the body for POST or HEAD; that leaves GET. if !hadCT && r.Method == "GET" { body := "" + http.StatusText(code) + ".\n" fmt.Fprintln(w, body) } } func hexEscapeNonASCII(s string) string { // part of the standard library. newLen := 0 for i := 0; i < len(s); i++ { if s[i] >= utf8.RuneSelf { newLen += 3 } else { newLen++ } } if newLen == len(s) { return s } b := make([]byte, 0, newLen) for i := 0; i < len(s); i++ { if s[i] >= utf8.RuneSelf { b = append(b, '%') b = strconv.AppendInt(b, int64(s[i]), 16) } else { b = append(b, s[i]) } } return string(b) } ================================================ FILE: core/router/router_test.go ================================================ package router_test import ( "fmt" "io" "net/http" "strings" "testing" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/core/router" "github.com/kataras/iris/v12/httptest" ) func TestRouteExists(t *testing.T) { // build the api app := iris.New() emptyHandler := func(*context.Context) {} // setup the tested routes app.Handle("GET", "/route-exists", emptyHandler) app.Handle("POST", "/route-with-param/{param}", emptyHandler) // check RouteExists app.Handle("GET", "/route-test", func(ctx *context.Context) { if ctx.RouteExists("GET", "/route-not-exists") { t.Error("Route with path should not exists") } if ctx.RouteExists("POST", "/route-exists") { t.Error("Route with method should not exists") } if !ctx.RouteExists("GET", "/route-exists") { t.Error("Route 1 should exists") } if !ctx.RouteExists("POST", "/route-with-param/a-param") { t.Error("Route 2 should exists") } }) // run the tests httptest.New(t, app, httptest.Debug(false)).Request("GET", "/route-test").Expect().Status(iris.StatusOK) } func TestLowercaseRouting(t *testing.T) { app := iris.New() app.WrapRouter(func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { // test bottom to begin wrapper, the last ones should execute first. // The ones that are registered at `Build` state, after this `WrapRouter` call. // So path should be already lowecased. if expected, got := strings.ToLower(r.URL.Path), r.URL.Path; expected != got { t.Fatalf("expected path: %s but got: %s", expected, got) } next(w, r) }) h := func(ctx iris.Context) { ctx.WriteString(ctx.Path()) } // Register routes. tests := []string{"/", "/lowercase", "/UPPERCASE", "/Title", "/m1xEd2"} for _, tt := range tests { app.Get(tt, h) } app.Configure(iris.WithLowercaseRouting) // Test routes. e := httptest.New(t, app) for _, tt := range tests { s := strings.ToLower(tt) e.GET(tt).Expect().Status(httptest.StatusOK).Body().IsEqual(s) e.GET(s).Expect().Status(httptest.StatusOK).Body().IsEqual(s) e.GET(strings.ToUpper(tt)).Expect().Status(httptest.StatusOK).Body().IsEqual(s) } } func TestRouterWrapperOrder(t *testing.T) { // last is wrapping the previous. // first is executed last. userWrappers := []router.WrapperFunc{ func(w http.ResponseWriter, r *http.Request, main http.HandlerFunc) { io.WriteString(w, "6") main(w, r) }, func(w http.ResponseWriter, r *http.Request, main http.HandlerFunc) { io.WriteString(w, "5") main(w, r) }, } // should be executed before userWrappers. redirectionWrappers := []router.WrapperFunc{ func(w http.ResponseWriter, r *http.Request, main http.HandlerFunc) { io.WriteString(w, "3") main(w, r) }, func(w http.ResponseWriter, r *http.Request, main http.HandlerFunc) { io.WriteString(w, "4") main(w, r) }, } // should be executed before redirectionWrappers. afterRedirectionWrappers := []router.WrapperFunc{ func(w http.ResponseWriter, r *http.Request, main http.HandlerFunc) { io.WriteString(w, "2") main(w, r) }, func(w http.ResponseWriter, r *http.Request, main http.HandlerFunc) { io.WriteString(w, "1") main(w, r) }, } testOrder1 := iris.New() for _, w := range userWrappers { testOrder1.WrapRouter(w) // this always wraps the previous one, but it's not accessible after Build state, // the below are simulating the SubdomainRedirect and ForceLowercaseRouting. } for _, w := range redirectionWrappers { testOrder1.AddRouterWrapper(w) } for _, w := range afterRedirectionWrappers { testOrder1.PrependRouterWrapper(w) } testOrder2 := iris.New() for _, w := range redirectionWrappers { testOrder2.AddRouterWrapper(w) } for _, w := range userWrappers { testOrder2.WrapRouter(w) } for _, w := range afterRedirectionWrappers { testOrder2.PrependRouterWrapper(w) } testOrder3 := iris.New() for _, w := range redirectionWrappers { testOrder3.AddRouterWrapper(w) } for _, w := range afterRedirectionWrappers { testOrder3.PrependRouterWrapper(w) } for _, w := range userWrappers { testOrder3.WrapRouter(w) } appTests := []*iris.Application{ testOrder1, testOrder2, testOrder3, } expectedOrderStr := "123456" for _, app := range appTests { app.Get("/", func(ctx iris.Context) {}) // to not append the not found one. e := httptest.New(t, app) e.GET("/").Expect().Status(iris.StatusOK).Body().IsEqual(expectedOrderStr) } } func TestNewSubdomainPartyRedirectHandler(t *testing.T) { app := iris.New() app.Get("/", func(ctx iris.Context) { ctx.WriteString("root index") }) test := app.Subdomain("test") test.OnErrorCode(iris.StatusNotFound, func(ctx iris.Context) { ctx.WriteString("test 404") }) test.Get("/", func(ctx iris.Context) { ctx.WriteString("test index") }) testold := app.Subdomain("testold") // redirects testold.mydomain.com to test.mydomain.com . testold.UseRouter(router.NewSubdomainPartyRedirectHandler(test)) testold.Get("/", func(ctx iris.Context) { ctx.WriteString("test old index (should never be fired)") }) testoldLeveled := testold.Subdomain("leveled") testoldLeveled.Get("/", func(ctx iris.Context) { ctx.WriteString("leveled.testold this can be fired") }) if redirectHandler := router.NewSubdomainPartyRedirectHandler(app.WildcardSubdomain()); redirectHandler != nil { t.Fatal("redirect handler should be nil, we cannot redirect to a wildcard") } e := httptest.New(t, app) e.GET("/").WithURL("http://mydomain.com").Expect().Status(iris.StatusOK).Body().IsEqual("root index") e.GET("/").WithURL("http://test.mydomain.com").Expect().Status(iris.StatusOK).Body().IsEqual("test index") e.GET("/").WithURL("http://testold.mydomain.com").Expect().Status(iris.StatusOK).Body().IsEqual("test index") e.GET("/").WithURL("http://testold.mydomain.com/notfound").Expect().Status(iris.StatusNotFound).Body().IsEqual("test 404") e.GET("/").WithURL("http://leveled.testold.mydomain.com").Expect().Status(iris.StatusOK).Body().IsEqual("leveled.testold this can be fired") } func TestHandleServer(t *testing.T) { otherApp := iris.New() otherApp.Get("/test/me/{first:string}", func(ctx iris.Context) { ctx.HTML(fmt.Sprintf("

      Other App: %s

      ", ctx.Params().Get("first"))) }) otherApp.Build() app := iris.New() app.Get("/", func(ctx iris.Context) { ctx.HTML("

      Main App

      ") }) app.HandleServer("/api/identity/{first:string}/orgs/{second:string}/{p:path}", otherApp) e := httptest.New(t, app) e.GET("/").Expect().Status(iris.StatusOK).Body().IsEqual("

      Main App

      ") e.GET("/api/identity/first/orgs/second/test/me/kataras").Expect().Status(iris.StatusOK).Body().IsEqual("

      Other App: kataras

      ") e.GET("/api/identity/first/orgs/second/test/me").Expect().Status(iris.StatusNotFound) } ================================================ FILE: core/router/router_wildcard_root_test.go ================================================ // black-box testing // // see _examples/routing/main_test.go for the most common router tests that you may want to see, // this is a test for the new feature that I just coded: wildcard "/{p:path}" on root without conflicts package router_test import ( "net/http" "testing" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/httptest" ) const ( same_as_request_path = "same" from_status_code = "from" staticPathPrefixBody = "from the static path: " prefix_static_path_following_by_request_path = "prefix_same" ) type testRouteRequest struct { method string subdomain string path string expectedStatusCode int expectedBody string } type testRoute struct { method string path string handler context.Handler requests []testRouteRequest } var h = func(ctx *context.Context) { ctx.WriteString(ctx.Path()) } var h2 = func(ctx *context.Context) { ctx.StatusCode(iris.StatusForbidden) // ! 200 but send the body as expected, // we need that kind of behavior to determinate which handler is executed for routes that // both having wildcard path but first one is registered on root level. ctx.WriteString(ctx.Path()) } func h3(ctx *context.Context) { ctx.WriteString(staticPathPrefixBody + ctx.Path()) } func TestRouterWildcardDifferentPrefixPath(t *testing.T) { tt := []testRoute{ {"GET", "/s/{p:path}", h, []testRouteRequest{ {"GET", "", "/s/that/is/wildcard", iris.StatusOK, same_as_request_path}, {"GET", "", "/s/ok", iris.StatusOK, same_as_request_path}, }}, {"GET", "/som/{p:path}", h, []testRouteRequest{ {"GET", "", "/som/that/is/wildcard", iris.StatusOK, same_as_request_path}, {"GET", "", "/som/ok", iris.StatusOK, same_as_request_path}, }}, {"GET", "/some/{p:path}", h, []testRouteRequest{ {"GET", "", "/some/that/is/wildcard", iris.StatusOK, same_as_request_path}, {"GET", "", "/some1/that/is/wildcard", iris.StatusNotFound, from_status_code}, }}, } testTheRoutes(t, tt, true) } func TestRouterWildcardAndStatic(t *testing.T) { tt := []testRoute{ {"GET", "/some/{p:path}", h2, []testRouteRequest{ {"GET", "", "/some/that/is/wildcard", iris.StatusForbidden, same_as_request_path}, {"GET", "", "/some/did", iris.StatusForbidden, same_as_request_path}, {"GET", "", "/some1/that/is/wildcard", iris.StatusNotFound, from_status_code}, }}, {"GET", "/some/static", h, []testRouteRequest{ {"GET", "", "/some/static", iris.StatusOK, same_as_request_path}, }}, {"GET", "/s/{p:path}", h2, []testRouteRequest{ {"GET", "", "/s/that/is/wildcard", iris.StatusForbidden, same_as_request_path}, {"GET", "", "/s/did", iris.StatusForbidden, same_as_request_path}, {"GET", "", "/s1/that/is/wildcard", iris.StatusNotFound, from_status_code}, }}, {"GET", "/s/static", h, []testRouteRequest{ {"GET", "", "/s/static", iris.StatusOK, same_as_request_path}, }}, } testTheRoutes(t, tt, false) } func TestRouterWildcardRootMany(t *testing.T) { tt := []testRoute{ // all routes will be handlded by "h" because we added wildcard to root, // this feature is very important and can remove noumerous of previous hacks on our apps. {"GET", "/{p:path}", h, []testRouteRequest{ {"GET", "", "/this/is/wildcard/on/root", iris.StatusOK, same_as_request_path}, }}, // mormally, order matters, root should be registered at last // but we change the front level order algorithm to put last these automatically // see handler.go {"GET", "/some/{p:path}", h2, []testRouteRequest{ {"GET", "", "/some/that/is/wildcard", iris.StatusForbidden, same_as_request_path}, {"GET", "", "/some/did", iris.StatusForbidden, same_as_request_path}, }}, {"GET", "/some/static", h, []testRouteRequest{ {"GET", "", "/some/static", iris.StatusOK, same_as_request_path}, }}, {"GET", "/some1", h, []testRouteRequest{ {"GET", "", "/some1", iris.StatusOK, same_as_request_path}, // this will show up because of the first wildcard, as we wanted to do. {"GET", "", "/some1/that/is/wildcard", iris.StatusOK, same_as_request_path}, }}, } testTheRoutes(t, tt, true) } func TestRouterWildcardRootManyAndRootStatic(t *testing.T) { tt := []testRoute{ // routes that may return 404 will be handled by the below route ("h" handler) because we added wildcard to root, // this feature is very important and can remove noumerous of previous hacks on our apps. // // Static paths and parameters have priority over wildcard, all three types can be registered in the same path prefix. // // Remember, all of those routes are registered don't be tricked by the visual appearance of the below test blocks. {"GET", "/{p:path}", h, []testRouteRequest{ {"GET", "", "/other2almost/some", iris.StatusOK, same_as_request_path}, }}, {"GET", "/static/{p:path}", h, []testRouteRequest{ {"GET", "", "/static", iris.StatusOK, same_as_request_path}, // HERE<- IF NOT FOUND THEN BACKWARDS TO WILDCARD IF THERE IS ONE, HMM. {"GET", "", "/static/something/here", iris.StatusOK, same_as_request_path}, }}, {"GET", "/", h, []testRouteRequest{ {"GET", "", "/", iris.StatusOK, same_as_request_path}, }}, {"GET", "/other/{paramother:path}", h2, []testRouteRequest{ // OK and not h2 because of the root wildcard. {"GET", "", "/other", iris.StatusOK, same_as_request_path}, {"GET", "", "/other/wildcard", iris.StatusForbidden, same_as_request_path}, {"GET", "", "/other/wildcard/here", iris.StatusForbidden, same_as_request_path}, }}, {"GET", "/other2/{paramothersecond:path}", h2, []testRouteRequest{ {"GET", "", "/other2/wildcard", iris.StatusForbidden, same_as_request_path}, {"GET", "", "/other2/more/than/one/path/parts", iris.StatusForbidden, same_as_request_path}, }}, {"GET", "/other2/static", h3, []testRouteRequest{ {"GET", "", "/other2/static", iris.StatusOK, prefix_static_path_following_by_request_path}, // h2(Forbiddenn) instead of h3 OK because it will be handled by the /other2/{paramothersecond:path}'s handler which gives 403. {"GET", "", "/other2/staticed", iris.StatusForbidden, same_as_request_path}, }}, } testTheRoutes(t, tt, false) } func testTheRoutes(t *testing.T, tests []testRoute, debug bool) { // build the api app := iris.New() for _, tt := range tests { app.Handle(tt.method, tt.path, tt.handler) } // setup the test suite e := httptest.New(t, app, httptest.Debug(debug)) // run the tests for _, tt := range tests { for _, req := range tt.requests { // t.Logf("req: %s:%s\n", tt.method, tt.path) method := req.method if method == "" { method = tt.method } ex := e.Request(method, req.path) if req.subdomain != "" { ex.WithURL("http://" + req.subdomain + ".localhost:8080") } expectedBody := req.expectedBody if req.expectedBody == same_as_request_path { expectedBody = req.path } if req.expectedBody == from_status_code { expectedBody = http.StatusText(req.expectedStatusCode) } if req.expectedBody == prefix_static_path_following_by_request_path { expectedBody = staticPathPrefixBody + req.path } ex.Expect().Status(req.expectedStatusCode).Body().IsEqual(expectedBody) } } } ================================================ FILE: core/router/router_wrapper.go ================================================ package router import "net/http" // WrapperFunc is used as an expected input parameter signature // for the WrapRouter. It's a "low-level" signature which is compatible // with the net/http. // It's being used to run or no run the router based on a custom logic. type WrapperFunc func(w http.ResponseWriter, r *http.Request, router http.HandlerFunc) func makeWrapperFunc(original WrapperFunc, wrapperFunc WrapperFunc) WrapperFunc { if wrapperFunc == nil { return original } if original != nil { // wrap into one function, from bottom to top, end to begin. nextWrapper := wrapperFunc prevWrapper := original wrapperFunc = func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { if next != nil { nexthttpFunc := http.HandlerFunc(func(_w http.ResponseWriter, _r *http.Request) { prevWrapper(_w, _r, next) }) nextWrapper(w, r, nexthttpFunc) } } } return wrapperFunc } type wrapper struct { router http.HandlerFunc // http.HandlerFunc to catch the CURRENT state of its .ServeHTTP on case of future change. wrapperFunc WrapperFunc } // newWrapper returns a new http.Handler wrapped by the 'wrapperFunc' // the "next" is the final "wrapped" input parameter. // // Application is responsible to make it to work on more than one wrappers // via composition or func clojure. func newWrapper(wrapperFunc WrapperFunc, wrapped http.HandlerFunc) http.Handler { return &wrapper{ wrapperFunc: wrapperFunc, router: wrapped, } } func (wr *wrapper) ServeHTTP(w http.ResponseWriter, r *http.Request) { wr.wrapperFunc(w, r, wr.router) } ================================================ FILE: core/router/router_wrapper_test.go ================================================ package router import ( "bytes" "net/http" "net/http/httptest" "testing" ) func TestMakeWrapperFunc(t *testing.T) { var ( firstBody = []byte("1") secondBody = []byte("2") mainBody = []byte("3") expectedBody = append(firstBody, append(secondBody, mainBody...)...) ) pre := func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { w.Header().Set("X-Custom", "data") next(w, r) } first := func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { w.Write(firstBody) next(w, r) } second := func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { w.Write(secondBody) next(w, r) } mainHandler := func(w http.ResponseWriter, r *http.Request) { w.Write(mainBody) } wrapper := makeWrapperFunc(second, first) wrapper = makeWrapperFunc(wrapper, pre) w := httptest.NewRecorder() r := httptest.NewRequest("GET", "https://iris-go.com", nil) wrapper(w, r, mainHandler) if got := w.Body.Bytes(); !bytes.Equal(expectedBody, got) { t.Fatalf("expected boy: %s but got: %s", string(expectedBody), string(got)) } if expected, got := "data", w.Header().Get("X-Custom"); expected != got { t.Fatalf("expected x-custom header: %s but got: %s", expected, got) } } ================================================ FILE: core/router/status_test.go ================================================ // black-box testing package router_test import ( "bytes" "net/http" "testing" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/httptest" ) var defaultErrHandler = func(ctx *context.Context) { text := http.StatusText(ctx.GetStatusCode()) ctx.WriteString(text) } func TestOnAnyErrorCode(t *testing.T) { app := iris.New() app.Configure(iris.WithFireMethodNotAllowed) buff := &bytes.Buffer{} expectedPrintBeforeExecuteErr := "printed before error" // with a middleware app.OnAnyErrorCode(func(ctx *context.Context) { buff.WriteString(expectedPrintBeforeExecuteErr) ctx.Next() }, defaultErrHandler) expectedFoundResponse := "found" app.Get("/found", func(ctx *context.Context) { ctx.WriteString(expectedFoundResponse) }) expected407 := "this should be sent, we manage the response response by ourselves" app.Get("/407", func(ctx *context.Context) { ctx.Record() ctx.WriteString(expected407) ctx.StatusCode(iris.StatusProxyAuthRequired) }) e := httptest.New(t, app) e.GET("/found").Expect().Status(iris.StatusOK). Body().IsEqual(expectedFoundResponse) e.GET("/notfound").Expect().Status(iris.StatusNotFound). Body().IsEqual(http.StatusText(iris.StatusNotFound)) checkAndClearBuf(t, buff, expectedPrintBeforeExecuteErr) e.POST("/found").Expect().Status(iris.StatusMethodNotAllowed). Body().IsEqual(http.StatusText(iris.StatusMethodNotAllowed)) checkAndClearBuf(t, buff, expectedPrintBeforeExecuteErr) e.GET("/407").Expect().Status(iris.StatusProxyAuthRequired). Body().IsEqual(expected407) // Test Configuration.ResetOnFireErrorCode. app2 := iris.New() app2.Configure(iris.WithResetOnFireErrorCode) app2.OnAnyErrorCode(func(ctx *context.Context) { buff.WriteString(expectedPrintBeforeExecuteErr) ctx.Next() }, defaultErrHandler) app2.Get("/406", func(ctx *context.Context) { ctx.Record() ctx.WriteString("this should not be sent, only status text will be sent") ctx.WriteString("the handler can handle 'rollback' of the text when error code fired because of the recorder") ctx.StatusCode(iris.StatusNotAcceptable) }) httptest.New(t, app2).GET("/406").Expect().Status(iris.StatusNotAcceptable). Body().IsEqual(http.StatusText(iris.StatusNotAcceptable)) checkAndClearBuf(t, buff, expectedPrintBeforeExecuteErr) } func checkAndClearBuf(t *testing.T, buff *bytes.Buffer, expected string) { t.Helper() if got := buff.String(); got != expected { t.Fatalf("expected middleware to run before the error handler, expected: '%s' but got: '%s'", expected, got) } buff.Reset() } func TestPartyOnErrorCode(t *testing.T) { app := iris.New() app.Configure(iris.WithFireMethodNotAllowed) globalNotFoundResponse := "custom not found" app.OnErrorCode(iris.StatusNotFound, func(ctx iris.Context) { ctx.WriteString(globalNotFoundResponse) }) globalMethodNotAllowedResponse := "global: method not allowed" app.OnErrorCode(iris.StatusMethodNotAllowed, func(ctx iris.Context) { ctx.WriteString(globalMethodNotAllowedResponse) }) app.Get("/path", h) usersResponse := "users: method allowed" users := app.Party("/users") users.OnErrorCode(iris.StatusMethodNotAllowed, func(ctx iris.Context) { ctx.WriteString(usersResponse) }) users.Get("/", h) write400 := func(ctx iris.Context) { ctx.StatusCode(iris.StatusBadRequest) } // test setting the error code from a handler. users.Get("/badrequest", write400) usersuserResponse := "users:user: method allowed" user := users.Party("/{id:int}") user.OnErrorCode(iris.StatusMethodNotAllowed, func(ctx iris.Context) { ctx.WriteString(usersuserResponse) }) usersuserNotFoundResponse := "users:user: not found" user.OnErrorCode(iris.StatusNotFound, func(ctx iris.Context) { ctx.WriteString(usersuserNotFoundResponse) }) user.Get("/", h) user.Get("/ab/badrequest", write400) friends := users.Party("/friends") friends.Get("/{id:int}", h) e := httptest.New(t, app) e.GET("/notfound").Expect().Status(iris.StatusNotFound).Body().IsEqual(globalNotFoundResponse) e.POST("/path").Expect().Status(iris.StatusMethodNotAllowed).Body().IsEqual(globalMethodNotAllowedResponse) e.GET("/path").Expect().Status(iris.StatusOK).Body().IsEqual("/path") e.POST("/users").Expect().Status(iris.StatusMethodNotAllowed). Body().IsEqual(usersResponse) e.POST("/users/42").Expect().Status(iris.StatusMethodNotAllowed). Body().IsEqual(usersuserResponse) e.GET("/users/42").Expect().Status(iris.StatusOK). Body().IsEqual("/users/42") e.GET("/users/ab").Expect().Status(iris.StatusNotFound).Body().IsEqual(usersuserNotFoundResponse) // inherit the parent. e.GET("/users/42/friends/dsa").Expect().Status(iris.StatusNotFound).Body().IsEqual(usersuserNotFoundResponse) // if not registered to the party, then the root is taking action. e.GET("/users/42/ab/badrequest").Expect().Status(iris.StatusBadRequest).Body().IsEqual(http.StatusText(iris.StatusBadRequest)) // if not registered to the party, and not in root, then just write the status text (fallback behavior) e.GET("/users/badrequest").Expect().Status(iris.StatusBadRequest). Body().IsEqual(http.StatusText(iris.StatusBadRequest)) } ================================================ FILE: core/router/trie.go ================================================ package router import ( "strings" "github.com/kataras/iris/v12/context" ) const ( // ParamStart the character in string representation where the underline router starts its dynamic named parameter. ParamStart = ":" // paramStartCharacter is the character as rune of ParamStart. paramStartCharacter = ':' // WildcardParamStart the character in string representation where the underline router starts its dynamic wildcard // path parameter. WildcardParamStart = "*" // wildcardParamStartCharacter is the character as rune of WildcardParamStart. wildcardParamStartCharacter = '*' ) // An iris-specific identical version of the https://github.com/kataras/muxie version 1.0.0 released at 15 Oct 2018 type trieNode struct { parent *trieNode children map[string]*trieNode hasDynamicChild bool // does one of the children contains a parameter or wildcard? childNamedParameter bool // is the child a named parameter (single segmnet) childWildcardParameter bool // or it is a wildcard (can be more than one path segments) ? paramKeys []string // the param keys without : or *. end bool // it is a complete node, here we stop and we can say that the node is valid. key string // if end == true then key is filled with the original value of the insertion's key. // if key != "" && its parent has childWildcardParameter == true, // we need it to track the static part for the closest-wildcard's parameter storage. staticKey string // insert data. Route context.RouteReadOnly Handlers context.Handlers } func newTrieNode() *trieNode { n := new(trieNode) return n } func (tn *trieNode) hasChild(s string) bool { return tn.getChild(s) != nil } func (tn *trieNode) getChild(s string) *trieNode { if tn.children == nil { return nil } return tn.children[s] } func (tn *trieNode) addChild(s string, n *trieNode) { if tn.children == nil { tn.children = make(map[string]*trieNode) } if _, exists := tn.children[s]; exists { return } n.parent = tn tn.children[s] = n } func (tn *trieNode) findClosestParentWildcardNode() *trieNode { tn = tn.parent for tn != nil { if tn.childWildcardParameter { return tn.getChild(WildcardParamStart) } tn = tn.parent } return nil } func (tn *trieNode) String() string { return tn.key } type trie struct { root *trieNode // if true then it will handle any path if not other parent wildcard exists, // so even 404 (on http services) is up to it, see trie#insert. hasRootWildcard bool hasRootSlash bool statusCode int // for error codes only, method is ignored. method string // subdomain is empty for default-hostname routes, // ex: mysubdomain. subdomain string } const ( pathSep = "/" pathSepB = '/' ) func slowPathSplit(path string) []string { if path == "/" { return []string{"/"} } return strings.Split(path, pathSep)[1:] } func (tr *trie) insert(path string, route context.RouteReadOnly, handlers context.Handlers) { input := slowPathSplit(path) if len(input) == 0 { return } n := tr.root if path == pathSep { tr.hasRootSlash = true } var paramKeys []string for _, s := range input { if len(s) == 0 { continue } c := s[0] if len(s) > 1 { // has more than one character. // get the next character, should be the name of the parameter. // E.g: // If /test/:param (or /test/*param) then it's dynamic. // If /test/: (or /test/*) then it's static. if isParam, isWildcard := c == paramStartCharacter, c == wildcardParamStartCharacter; isParam || isWildcard { n.hasDynamicChild = true paramKeys = append(paramKeys, s[1:]) // without : or *. if isParam { n.childNamedParameter = true s = ParamStart } if isWildcard { n.childWildcardParameter = true s = WildcardParamStart if tr.root == n { tr.hasRootWildcard = true } } } } if !n.hasChild(s) { child := newTrieNode() n.addChild(s, child) } n = n.getChild(s) } n.Route = route n.Handlers = handlers n.paramKeys = paramKeys n.key = path n.end = true i := strings.Index(path, ParamStart) if i == -1 { i = strings.Index(path, WildcardParamStart) } if i == -1 { i = len(n.key) } n.staticKey = path[:i] // fmt.Printf("trie.insert: (whole path=%v) Path: %s, Route name: %s, Handlers len: %d\n", n.end, n.key, route.Name(), len(handlers)) } func (tr *trie) search(q string, params *context.RequestParams) *trieNode { end := len(q) if end == 0 || (end == 1 && q[0] == pathSepB) { // fixes only root wildcard but no / registered at. if tr.hasRootSlash { return tr.root.getChild(pathSep) } else if tr.hasRootWildcard { // no need to going through setting parameters, this one has not but it is wildcard. return tr.root.getChild(WildcardParamStart) } return nil } n := tr.root start := 1 i := 1 var paramValues []string for { if i == end || q[i] == pathSepB { segment := q[start:i] if child := n.getChild(segment); child != nil { n = child } else if n.childNamedParameter { n = n.getChild(ParamStart) if ln := len(paramValues); cap(paramValues) > ln { paramValues = paramValues[:ln+1] paramValues[ln] = segment } else { paramValues = append(paramValues, segment) } } else if n.childWildcardParameter { n = n.getChild(WildcardParamStart) if ln := len(paramValues); cap(paramValues) > ln { paramValues = paramValues[:ln+1] paramValues[ln] = q[start:] } else { paramValues = append(paramValues, q[start:]) } break } else { n = n.findClosestParentWildcardNode() if n != nil && len(n.paramKeys) > 0 { // means that it has :param/static and *wildcard, we go trhough the :param // but the next path segment is not the /static, so go back to *wildcard // instead of not found. // // Fixes: // /hello/*p // /hello/:p1/static/:p2 // req: http://localhost:8080/hello/dsadsa/static/dsadsa => found // req: http://localhost:8080/hello/dsadsa => but not found! // and // /second/wild/*p // /second/wild/static/otherstatic/ // req: /second/wild/static/otherstatic/random => but not found! params.Set(n.paramKeys[0], q[len(n.staticKey):]) return n } return nil } if i == end { break } i++ start = i continue } i++ } if n == nil || !n.end { if n != nil { // we need it on both places, on last segment (below) or on the first unnknown (above). if n = n.findClosestParentWildcardNode(); n != nil && len(n.paramKeys) > 0 { params.Set(n.paramKeys[0], q[len(n.staticKey):]) return n } } if tr.hasRootWildcard { // that's the case for root wildcard, tests are passing // even without it but stick with it for reference. // Note ote that something like: // Routes: /other2/*myparam and /other2/static // Reqs: /other2/staticed will be handled // the /other2/*myparam and not the root wildcard, which is what we want. // n = tr.root.getChild(WildcardParamStart) if len(n.paramKeys) == 0 { // fix crashes on /*/*/*. return nil } params.Set(n.paramKeys[0], q[1:]) return n } return nil } for i, paramValue := range paramValues { if len(n.paramKeys) > i { params.Set(n.paramKeys[i], paramValue) } } return n } ================================================ FILE: doc.go ================================================ /* Package iris implements the highest realistic performance, easy to learn Go web framework. Iris provides a beautifully expressive and easy to use foundation for your next website, API, or distributed app. Low-level handlers compatible with `net/http` and high-level fastest MVC implementation and handlers dependency injection. Easy to learn for new gophers and advanced features for experienced, it goes as far as you dive into it! Source code and other details for the project are available at GitHub: https://github.com/kataras/iris # Current Version 12.2.11 # Installation The only requirement is the Go Programming Language, at least version 1.23. $ go get github.com/kataras/iris/v12@latest Wiki: https://www.iris-go.com/#ebookDonateForm Examples: https://github.com/kataras/iris/tree/main/_examples Middleware: https://github.com/kataras/iris/tree/main/middleware https://github.com/iris-contrib/middleware Home Page: https://iris-go.com */ package iris ================================================ FILE: go.mod ================================================ module github.com/kataras/iris/v12 go 1.25 retract [v12.0.0, v12.1.8] // Retract older versions as only latest is to be depended upon. Please update to @latest require ( github.com/BurntSushi/toml v1.6.0 github.com/CloudyKit/jet/v6 v6.3.1 github.com/Joker/jade v1.1.3 github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 github.com/andybalholm/brotli v1.2.0 github.com/blang/semver/v4 v4.0.0 github.com/dgraph-io/badger/v4 v4.9.0 github.com/fatih/structs v1.1.0 github.com/flosch/pongo2/v4 v4.0.2 github.com/golang/snappy v1.0.0 github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a github.com/google/uuid v1.6.0 github.com/gorilla/securecookie v1.1.2 github.com/iris-contrib/httpexpect/v2 v2.15.2 github.com/iris-contrib/schema v0.0.6 github.com/json-iterator/go v1.1.12 github.com/kataras/blocks v0.0.8 github.com/kataras/golog v0.1.12 github.com/kataras/jwt v0.1.17 github.com/kataras/neffos v0.0.24 github.com/kataras/pio v0.0.13 github.com/kataras/sitemap v0.0.6 github.com/kataras/tunnel v0.0.4 github.com/klauspost/compress v1.18.2 github.com/mailgun/raymond/v2 v2.0.48 github.com/mailru/easyjson v0.9.1 github.com/microcosm-cc/bluemonday v1.0.27 github.com/redis/go-redis/v9 v9.17.2 github.com/schollz/closestmatch v2.1.0+incompatible github.com/shirou/gopsutil/v3 v3.24.5 github.com/tdewolff/minify/v2 v2.24.8 github.com/vmihailenco/msgpack/v5 v5.4.1 github.com/yosssi/ace v0.0.5 go.etcd.io/bbolt v1.4.3 golang.org/x/crypto v0.47.0 golang.org/x/exp v0.0.0-20260112195511-716be5621a96 golang.org/x/net v0.49.0 golang.org/x/sys v0.40.0 golang.org/x/text v0.33.0 golang.org/x/time v0.14.0 google.golang.org/protobuf v1.36.11 gopkg.in/ini.v1 v1.67.1 gopkg.in/yaml.v3 v3.0.1 ) require ( github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect github.com/ajg/form v1.5.1 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgraph-io/ristretto/v2 v2.2.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/fatih/color v1.15.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.2.6 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/ws v1.4.0 // indirect github.com/google/flatbuffers v25.2.10+incompatible // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/imkira/go-interpol v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect github.com/mediocregopher/radix/v3 v3.8.1 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/nats-io/nats.go v1.40.1 // indirect github.com/nats-io/nkeys v0.4.9 // indirect github.com/nats-io/nuid v1.0.1 // indirect github.com/nxadm/tail v1.4.11 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sanity-io/litter v1.5.5 // indirect github.com/sergi/go-diff v1.0.0 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/sirupsen/logrus v1.8.1 // indirect github.com/stretchr/testify v1.11.1 // indirect github.com/tdewolff/parse/v2 v2.8.5 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 // indirect github.com/yudai/gojsondiff v1.0.0 // indirect github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/otel v1.37.0 // indirect go.opentelemetry.io/otel/metric v1.37.0 // indirect go.opentelemetry.io/otel/trace v1.37.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect moul.io/http2curl/v2 v2.3.0 // indirect ) ================================================ FILE: go.sum ================================================ github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= github.com/CloudyKit/jet/v6 v6.3.1 h1:6IAo5Cx21xrHVaR8zzXN5gJatKV/wO7Nf6bfCnCSbUw= github.com/CloudyKit/jet/v6 v6.3.1/go.mod h1:lf8ksdNsxZt7/yH/3n4vJQWA9RUq4wpaHtArHhGVMOw= github.com/Joker/hpp v1.0.0 h1:65+iuJYdRXv/XyN62C1uEmmOx3432rNG/rKlX6V7Kkc= github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= github.com/Joker/jade v1.1.3 h1:Qbeh12Vq6BxURXT1qZBRHsDxeURB8ztcL6f3EXSGeHk= github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKduM= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05 h1:dG7/gLroJGht/jSQtHiLvT48Hxn+crbmvyItZC8cWOs= github.com/Shopify/goreferrer v0.0.0-20250617153402-88c1d9a79b05/go.mod h1:NYezi6wtnJtBm5btoprXc5SvAdqH0XTXWnUup0MptAI= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgraph-io/badger/v4 v4.9.0 h1:tpqWb0NewSrCYqTvywbcXOhQdWcqephkVkbBmaaqHzc= github.com/dgraph-io/badger/v4 v4.9.0/go.mod h1:5/MEx97uzdPUHR4KtkNt8asfI2T4JiEiQlV7kWUo8c0= github.com/dgraph-io/ristretto/v2 v2.2.0 h1:bkY3XzJcXoMuELV8F+vS8kzNgicwQFAaGINAEJdWGOM= github.com/dgraph-io/ristretto/v2 v2.2.0/go.mod h1:RZrm63UmcBAaYWC1DotLYBmTvgkrs0+XhBd7Npn7/zI= github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38= github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/flosch/pongo2/v4 v4.0.2 h1:gv+5Pe3vaSVmiJvh/BZa82b7/00YUGm0PIyVVLop0Hw= github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+BWPhwZD8= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a h1:l7A0loSszR5zHd/qK53ZIHMO8b3bBSmENnQ6eKnUT0A= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/iris-contrib/httpexpect/v2 v2.15.2 h1:T9THsdP1woyAqKHwjkEsbCnMefsAFvk8iJJKokcJ3Go= github.com/iris-contrib/httpexpect/v2 v2.15.2/go.mod h1:JLDgIqnFy5loDSUv1OA2j0mb6p/rDhiCqigP22Uq9xE= github.com/iris-contrib/schema v0.0.6 h1:CPSBLyx2e91H2yJzPuhGuifVRnZBBJ3pCOMbOvPZaTw= github.com/iris-contrib/schema v0.0.6/go.mod h1:iYszG0IOsuIsfzjymw1kMzTL8YQcCWlm65f3wX8J5iA= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kataras/blocks v0.0.8 h1:MrpVhoFTCR2v1iOOfGng5VJSILKeZZI+7NGfxEh3SUM= github.com/kataras/blocks v0.0.8/go.mod h1:9Jm5zx6BB+06NwA+OhTbHW1xkMOYxahnqTN5DveZ2Yg= github.com/kataras/golog v0.1.12 h1:Bu7I/G4ilJlbfzjmU39O9N+2uO1pBcMK045fzZ4ytNg= github.com/kataras/golog v0.1.12/go.mod h1:wrGSbOiBqbQSQznleVNX4epWM8rl9SJ/rmEacl0yqy4= github.com/kataras/jwt v0.1.17 h1:dYjemzcdYqA4ylwq9/56MslCr/pNOyVUZ2bl3hYNHgc= github.com/kataras/jwt v0.1.17/go.mod h1:HUnU5HDBCDanVF8zrPVSE2VK8HicospKefZDD4DzOKU= github.com/kataras/neffos v0.0.24 h1:S3lHqJopCfXN285VdlbGeOj+Id83u4xdQKToa+w1vW0= github.com/kataras/neffos v0.0.24/go.mod h1:/3K9zQ0yEC5/xUiSQx46ToWa3xneGfUo/nMit/F5g+U= github.com/kataras/pio v0.0.13 h1:x0rXVX0fviDTXOOLOmr4MUxOabu1InVSTu5itF8CXCM= github.com/kataras/pio v0.0.13/go.mod h1:k3HNuSw+eJ8Pm2lA4lRhg3DiCjVgHlP8hmXApSej3oM= github.com/kataras/sitemap v0.0.6 h1:w71CRMMKYMJh6LR2wTgnk5hSgjVNB9KL60n5e2KHvLY= github.com/kataras/sitemap v0.0.6/go.mod h1:dW4dOCNs896OR1HmG+dMLdT7JjDk7mYBzoIRwuj5jA4= github.com/kataras/tunnel v0.0.4 h1:sCAqWuJV7nPzGrlb0os3j49lk2JhILT0rID38NHNLpA= github.com/kataras/tunnel v0.0.4/go.mod h1:9FkU4LaeifdMWqZu7o20ojmW4B7hdhv2CMLwfnHGpYw= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw= github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18= github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mediocregopher/radix/v3 v3.8.1 h1:rOkHflVuulFKlwsLY01/M2cM2tWCjDoETcMqKbAWu1M= github.com/mediocregopher/radix/v3 v3.8.1/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/nats-io/nats.go v1.40.1 h1:MLjDkdsbGUeCMKFyCFoLnNn/HDTqcgVa3EQm+pMNDPk= github.com/nats-io/nats.go v1.40.1/go.mod h1:wV73x0FSI/orHPSYoyMeJB+KajMDoWyXmFaRrrYaaTo= github.com/nats-io/nkeys v0.4.9 h1:qe9Faq2Gxwi6RZnZMXfmGMZkg3afLLOtrU+gDZJ35b0= github.com/nats-io/nkeys v0.4.9/go.mod h1:jcMqs+FLG+W5YO36OX6wFIFcmpdAns+w1Wm6D3I/evE= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7/go.mod h1:zO8QMzTeZd5cpnIkz/Gn6iK0jDfGicM1nynOkkPIl28= github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI= github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo= github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk= github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8= github.com/tdewolff/minify/v2 v2.24.8 h1:58/VjsbevI4d5FGV0ZSuBrHMSSkH4MCH0sIz/eKIauE= github.com/tdewolff/minify/v2 v2.24.8/go.mod h1:0Ukj0CRpo/sW/nd8uZ4ccXaV1rEVIWA3dj8U7+Shhfw= github.com/tdewolff/parse/v2 v2.8.5 h1:ZmBiA/8Do5Rpk7bDye0jbbDUpXXbCdc3iah4VeUvwYU= github.com/tdewolff/parse/v2 v2.8.5/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo= github.com/tdewolff/test v1.0.11 h1:FdLbwQVHxqG16SlkGveC0JVyrJN62COWTRyUFzfbtBE= github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA= github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0= github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= github.com/yudai/pp v2.0.1+incompatible h1:Q4//iY4pNF6yPLZIigmvcl7k/bPgrcTPIFIcmawg5bI= github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo= go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU= golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.1 h1:tVBILHy0R6e4wkYOn3XmiITt/hEVH4TFMYvAX2Ytz6k= gopkg.in/ini.v1 v1.67.1/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs= moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHcE= ================================================ FILE: hero/binding.go ================================================ package hero import ( "fmt" "reflect" "sort" "strconv" "github.com/kataras/iris/v12/context" ) // binding contains the Dependency and the Input, it's the result of a function or struct + dependencies. type binding struct { Dependency *Dependency Input *Input } // Input contains the input reference of which a dependency is binded to. type Input struct { Index int // for func inputs StructFieldIndex []int // for struct fields in order to support embedded ones. StructFieldName string // the struct field's name. Type reflect.Type selfValue reflect.Value // reflect.ValueOf(*Input) cache. } func newInput(typ reflect.Type, index int, structFieldIndex []int) *Input { in := &Input{ Index: index, StructFieldIndex: structFieldIndex, Type: typ, } in.selfValue = reflect.ValueOf(in) return in } func newStructFieldInput(f reflect.StructField) *Input { input := newInput(f.Type, f.Index[0], f.Index) input.StructFieldName = f.Name return input } // String returns the string representation of a binding. func (b *binding) String() string { var index string if len(b.Input.StructFieldIndex) > 0 { index = strconv.Itoa(b.Input.StructFieldIndex[0]) for _, i := range b.Input.StructFieldIndex[1:] { index += fmt.Sprintf(".%d", i) } } else { index = strconv.Itoa(b.Input.Index) } return fmt.Sprintf("[%s:%s] maps to [%s]", index, b.Input.Type.String(), b.Dependency) } // Equal compares "b" and "other" bindings and reports whether they are referring to the same values. func (b *binding) Equal(other *binding) bool { if b == nil { return other == nil } if other == nil { return false } // if b.String() != other.String() { // return false // } if expected, got := b.Dependency != nil, other.Dependency != nil; expected != got { return false } if expected, got := fmt.Sprintf("%v", b.Dependency.OriginalValue), fmt.Sprintf("%v", other.Dependency.OriginalValue); expected != got { return false } if expected, got := b.Dependency.DestType != nil, other.Dependency.DestType != nil; expected != got { return false } if b.Dependency.DestType != nil { if expected, got := b.Dependency.DestType.String(), other.Dependency.DestType.String(); expected != got { return false } } if expected, got := b.Input != nil, other.Input != nil; expected != got { return false } if b.Input != nil { if expected, got := b.Input.Index, other.Input.Index; expected != got { return false } if expected, got := b.Input.Type.String(), other.Input.Type.String(); expected != got { return false } if expected, got := b.Input.StructFieldIndex, other.Input.StructFieldIndex; !reflect.DeepEqual(expected, got) { return false } } return true } // DependencyMatcher type alias describes a dependency match function. type DependencyMatcher = func(*Dependency, reflect.Type) bool // DefaultDependencyMatcher is the default dependency match function for all DI containers. // It is used to collect dependencies from struct's fields and function's parameters. var DefaultDependencyMatcher = func(dep *Dependency, in reflect.Type) bool { if dep.Explicit { return dep.DestType == in } return dep.DestType == nil || equalTypes(dep.DestType, in) } // ToDependencyMatchFunc converts a DependencyMatcher (generic for all dependencies) // to a dependency-specific input matcher. func ToDependencyMatchFunc(d *Dependency, match DependencyMatcher) DependencyMatchFunc { return func(in reflect.Type) bool { return match(d, in) } } func getBindingsFor(inputs []reflect.Type, deps []*Dependency, disablePayloadAutoBinding bool, paramsCount int) (bindings []*binding) { // Path parameter start index is the result of [total path parameters] - [total func path parameters inputs], // moving from last to first path parameters and first to last (probably) available input args. // // That way the above will work as expected: // 1. mvc.New(app.Party("/path/{firstparam}")).Handle(....Controller.GetBy(secondparam string)) // 2. mvc.New(app.Party("/path/{firstparam}/{secondparam}")).Handle(...Controller.GetBy(firstparam, secondparam string)) // 3. usersRouter := app.Party("/users/{id:uint64}"); usersRouter.ConfigureContainer().Handle(method, "/", handler(id uint64)) // 4. usersRouter.Party("/friends").ConfigureContainer().Handle(method, "/{friendID:uint64}", handler(friendID uint64)) // // Therefore, count the inputs that can be path parameters first. shouldBindParams := make(map[int]struct{}) totalParamsExpected := 0 if paramsCount != -1 { for i, in := range inputs { if _, canBePathParameter := context.ParamResolvers[in]; !canBePathParameter { continue } shouldBindParams[i] = struct{}{} totalParamsExpected++ } } startParamIndex := paramsCount - totalParamsExpected if startParamIndex < 0 { startParamIndex = 0 } lastParamIndex := startParamIndex getParamIndex := func() int { paramIndex := lastParamIndex lastParamIndex++ return paramIndex } bindedInput := make(map[int]struct{}) for i, in := range inputs { //order matters. _, canBePathParameter := shouldBindParams[i] prevN := len(bindings) // to check if a new binding is attached; a dependency was matched (see below). for j := len(deps) - 1; j >= 0; j-- { d := deps[j] // Note: we could use the same slice to return. // // Add all dynamic dependencies (caller-selecting) and the exact typed dependencies. // // A dependency can only be matched to 1 value, and 1 value has a single dependency // (e.g. to avoid conflicting path parameters of the same type). if _, alreadyBinded := bindedInput[j]; alreadyBinded { continue } match := d.Match(in) if !match { continue } if canBePathParameter { // wrap the existing dependency handler. paramHandler := paramDependencyHandler(getParamIndex()) prevHandler := d.Handle d.Handle = func(ctx *context.Context, input *Input) (reflect.Value, error) { v, err := paramHandler(ctx, input) if err != nil { v, err = prevHandler(ctx, input) } return v, err } d.Static = false d.OriginalValue = nil } bindings = append(bindings, &binding{ Dependency: d, Input: newInput(in, i, nil), }) if !d.Explicit { // if explicit then it can be binded to more than one input bindedInput[j] = struct{}{} } break } if prevN == len(bindings) { if canBePathParameter { // Let's keep that option just for "payload": disablePayloadAutoBinding // no new dependency added for this input, // let's check for path parameters. bindings = append(bindings, paramBinding(i, getParamIndex(), in)) continue } // else, if payload binding is not disabled, // add builtin request bindings that // could be registered by end-dev but they didn't if !disablePayloadAutoBinding && isPayloadType(in) { bindings = append(bindings, payloadBinding(i, in)) continue } } } return } func isPayloadType(in reflect.Type) bool { switch indirectType(in).Kind() { case reflect.Struct, reflect.Slice, reflect.Ptr: return true default: return false } } func getBindingsForFunc(fn reflect.Value, dependencies []*Dependency, disablePayloadAutoBinding bool, paramsCount int) []*binding { fnTyp := fn.Type() if !isFunc(fnTyp) { panic(fmt.Sprintf("bindings: unresolved: no a func type: %#+v", fn)) } n := fnTyp.NumIn() inputs := make([]reflect.Type, n) for i := 0; i < n; i++ { inputs[i] = fnTyp.In(i) } bindings := getBindingsFor(inputs, dependencies, disablePayloadAutoBinding, paramsCount) if expected, got := n, len(bindings); expected != got { expectedInputs := "" missingInputs := "" for i, in := range inputs { pos := i + 1 typName := in.String() expectedInputs += fmt.Sprintf("\n - [%d] %s", pos, typName) found := false for _, b := range bindings { if b.Input.Index == i { found = true break } } if !found { missingInputs += fmt.Sprintf("\n - [%d] %s", pos, typName) } } fnName := context.HandlerName(fn) panic(fmt.Sprintf("expected [%d] bindings (input parameters) but got [%d]\nFunction:\n - %s\nExpected:%s\nMissing:%s", expected, got, fnName, expectedInputs, missingInputs)) } return bindings } func getBindingsForStruct(v reflect.Value, dependencies []*Dependency, markExportedFieldsAsRequired bool, disablePayloadAutoBinding, enableStructDependents bool, matchDependency DependencyMatcher, paramsCount int, sorter Sorter) (bindings []*binding) { typ := indirectType(v.Type()) if typ.Kind() != reflect.Struct { panic(fmt.Sprintf("bindings: unresolved: not a struct type: %#+v", v)) } // get bindings from any struct's non zero values first, including unexported. elem := reflect.Indirect(v) nonZero := lookupNonZeroFieldValues(elem) for _, f := range nonZero { // fmt.Printf("Controller [%s] | NonZero | Field Index: %v | Field Type: %s\n", typ, f.Index, f.Type) bindings = append(bindings, &binding{ Dependency: newDependency(elem.FieldByIndex(f.Index).Interface(), disablePayloadAutoBinding, enableStructDependents, nil), Input: newStructFieldInput(f), }) } fields, stateless := lookupFields(elem, true, true, nil) n := len(fields) if n > 1 && sorter != nil { sort.Slice(fields, func(i, j int) bool { return sorter(fields[i].Type, fields[j].Type) }) } inputs := make([]reflect.Type, n) for i := 0; i < n; i++ { // fmt.Printf("Controller [%s] | Field Index: %v | Field Type: %s\n", typ, fields[i].Index, fields[i].Type) inputs[i] = fields[i].Type } exportedBindings := getBindingsFor(inputs, dependencies, disablePayloadAutoBinding, paramsCount) // fmt.Printf("Controller [%s] | Inputs length: %d vs Bindings length: %d | NonZero: %d | Stateless : %d\n", // typ, n, len(exportedBindings), len(nonZero), stateless) // for i, b := range exportedBindings { // fmt.Printf("[%d] [Static=%v] %#+v\n", i, b.Dependency.Static, b.Dependency.OriginalValue) // } if markExportedFieldsAsRequired && len(exportedBindings) != n { panic(fmt.Sprintf("MarkExportedFieldsAsRequired is true and at least one of struct's (%s) field was not binded to a dependency.\nFields length: %d, matched exported bindings length: %d.\nUse the Reporter for further details", typ.String(), n, len(exportedBindings))) } if stateless == 0 && len(nonZero) >= len(exportedBindings) { // if we have not a single stateless and fields are defined then just return. // Note(@kataras): this can accept further improvements. return } // get declared bindings from deps. bindings = append(bindings, exportedBindings...) for _, binding := range bindings { // fmt.Printf(""Controller [%s] | Binding: %s\n", typ, binding.String()) if len(binding.Input.StructFieldIndex) == 0 { // set correctly the input's field index and name. f := fields[binding.Input.Index] binding.Input.StructFieldIndex = f.Index binding.Input.StructFieldName = f.Name } // fmt.Printf("Controller [%s] | binding Index: %v | binding Type: %s\n", typ, binding.Input.StructFieldIndex, binding.Input.Type) // fmt.Printf("Controller [%s] Set [%s] to struct field index: %v\n", typ.String(), binding.Input.Type.String(), binding.Input.StructFieldIndex) } return } func getStaticInputs(bindings []*binding, numIn int) []reflect.Value { inputs := make([]reflect.Value, numIn) for _, b := range bindings { if d := b.Dependency; d != nil && d.Static { inputs[b.Input.Index], _ = d.Handle(nil, nil) } } return inputs } /* Builtin dynamic bindings. */ func paramBinding(index, paramIndex int, typ reflect.Type) *binding { return &binding{ Dependency: &Dependency{Handle: paramDependencyHandler(paramIndex), DestType: typ, Source: getSource()}, Input: newInput(typ, index, nil), } } func paramDependencyHandler(paramIndex int) DependencyHandler { return func(ctx *context.Context, input *Input) (reflect.Value, error) { if ctx.Params().Len() <= paramIndex { return emptyValue, ErrSeeOther } return reflect.ValueOf(ctx.Params().Store[paramIndex].ValueRaw), nil } } // registered if input parameters are more than matched dependencies. // It binds an input to a request body based on the request content-type header // (JSON, Protobuf, Msgpack, XML, YAML, Query, Form). func payloadBinding(index int, typ reflect.Type) *binding { // fmt.Printf("Register payload binding for index: %d and type: %s\n", index, typ.String()) return &binding{ Dependency: &Dependency{ Handle: func(ctx *context.Context, input *Input) (newValue reflect.Value, err error) { wasPtr := input.Type.Kind() == reflect.Ptr if serveDepsV := ctx.Values().Get(context.DependenciesContextKey); serveDepsV != nil { if serveDeps, ok := serveDepsV.(context.DependenciesMap); ok { if newValue, ok = serveDeps[typ]; ok { return } } } if input.Type.Kind() == reflect.Slice { newValue = reflect.New(reflect.SliceOf(indirectType(input.Type))) } else { newValue = reflect.New(indirectType(input.Type)) } ptr := newValue.Interface() err = ctx.ReadBody(ptr) if !wasPtr { newValue = newValue.Elem() } return }, Source: getSource(), }, Input: newInput(typ, index, nil), } } ================================================ FILE: hero/binding_test.go ================================================ package hero import ( stdContext "context" "fmt" "net/http" "reflect" "testing" "time" "github.com/kataras/golog" "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/sessions" ) var ( stdContextTyp = reflect.TypeOf((*stdContext.Context)(nil)).Elem() sessionTyp = reflect.TypeOf((*sessions.Session)(nil)) timeTyp = reflect.TypeOf((*time.Time)(nil)).Elem() mapStringsTyp = reflect.TypeOf(map[string][]string{}) ) func contextBinding(index int) *binding { return &binding{ Dependency: BuiltinDependencies[0], Input: &Input{Type: BuiltinDependencies[0].DestType, Index: index}, } } func TestGetBindingsForFunc(t *testing.T) { type ( testResponse struct { Name string `json:"name"` } testRequest struct { Email string `json:"email"` } testRequest2 struct { // normally a body can't have two requests but let's test it. Age int `json:"age"` } ) var testRequestTyp = reflect.TypeOf(testRequest{}) var deps = []*Dependency{ NewDependency(func(ctx *context.Context) testRequest { return testRequest{Email: "should be ignored"} }), NewDependency(42), NewDependency(func(ctx *context.Context) (v testRequest, err error) { err = ctx.ReadJSON(&v) return }), NewDependency("if two strings requested this should be the last one"), NewDependency("should not be ignored when requested"), // Dependencies like these should always be registered last. NewDependency(func(ctx *context.Context, input *Input) (newValue reflect.Value, err error) { wasPtr := input.Type.Kind() == reflect.Ptr newValue = reflect.New(indirectType(input.Type)) ptr := newValue.Interface() err = ctx.ReadJSON(ptr) if !wasPtr { newValue = newValue.Elem() } return newValue, err }), } var tests = []struct { Func any Expected []*binding }{ { // 0 Func: func(ctx *context.Context) { ctx.WriteString("t1") }, Expected: []*binding{contextBinding(0)}, }, { // 1 Func: func(ctx *context.Context) error { return fmt.Errorf("err1") }, Expected: []*binding{contextBinding(0)}, }, { // 2 Func: func(ctx *context.Context) testResponse { return testResponse{Name: "name"} }, Expected: []*binding{contextBinding(0)}, }, { // 3 Func: func(in testRequest) (testResponse, error) { return testResponse{Name: "email of " + in.Email}, nil }, Expected: []*binding{{Dependency: deps[2], Input: &Input{Index: 0, Type: testRequestTyp}}}, }, { // 4 Func: func(in testRequest) (testResponse, error) { return testResponse{Name: "not valid "}, fmt.Errorf("invalid") }, Expected: []*binding{{Dependency: deps[2], Input: &Input{Index: 0, Type: testRequestTyp}}}, }, { // 5 Func: func(ctx *context.Context, in testRequest) testResponse { return testResponse{Name: "(with ctx) email of " + in.Email} }, Expected: []*binding{contextBinding(0), {Dependency: deps[2], Input: &Input{Index: 1, Type: testRequestTyp}}}, }, { // 6 Func: func(in testRequest, ctx *context.Context) testResponse { // reversed. return testResponse{Name: "(with ctx) email of " + in.Email} }, Expected: []*binding{{Dependency: deps[2], Input: &Input{Index: 0, Type: testRequestTyp}}, contextBinding(1)}, }, { // 7 Func: func(in testRequest, ctx *context.Context, in2 string) testResponse { // reversed. return testResponse{Name: "(with ctx) email of " + in.Email + "and in2: " + in2} }, Expected: []*binding{ { Dependency: deps[2], Input: &Input{Index: 0, Type: testRequestTyp}, }, contextBinding(1), { Dependency: deps[4], Input: &Input{Index: 2, Type: reflect.TypeOf("")}, }, }, }, { // 8 Func: func(in testRequest, ctx *context.Context, in2, in3 string) testResponse { // reversed. return testResponse{Name: "(with ctx) email of " + in.Email + " | in2: " + in2 + " in3: " + in3} }, Expected: []*binding{ { Dependency: deps[2], Input: &Input{Index: 0, Type: testRequestTyp}, }, contextBinding(1), { Dependency: deps[len(deps)-3], Input: &Input{Index: 2, Type: reflect.TypeOf("")}, }, { Dependency: deps[len(deps)-2], Input: &Input{Index: 3, Type: reflect.TypeOf("")}, }, }, }, { // 9 Func: func(ctx *context.Context, in testRequest, in2 testRequest2) testResponse { return testResponse{Name: fmt.Sprintf("(with ctx) email of %s and in2.Age %d", in.Email, in2.Age)} }, Expected: []*binding{ contextBinding(0), { Dependency: deps[2], Input: &Input{Index: 1, Type: testRequestTyp}, }, { Dependency: deps[len(deps)-1], Input: &Input{Index: 2, Type: reflect.TypeOf(testRequest2{})}, }, }, }, { // 10 Func: func() testResponse { return testResponse{Name: "empty in, one out"} }, Expected: nil, }, { // 1 Func: func(userID string, age int) testResponse { return testResponse{Name: "in from path parameters"} }, Expected: []*binding{ paramBinding(0, 0, reflect.TypeOf("")), paramBinding(1, 1, reflect.TypeOf(0)), }, }, // test std context, session, time, request, response writer and headers bindings. { // 12 Func: func(stdContext.Context, *sessions.Session, *golog.Logger, time.Time, *http.Request, http.ResponseWriter, http.Header) testResponse { return testResponse{"builtin deps"} }, Expected: []*binding{ { Dependency: NewDependency(BuiltinDependencies[1]), Input: &Input{Index: 0, Type: stdContextTyp}, }, { Dependency: NewDependency(BuiltinDependencies[2]), Input: &Input{Index: 1, Type: sessionTyp}, }, { Dependency: NewDependency(BuiltinDependencies[3]), Input: &Input{Index: 2, Type: BuiltinDependencies[3].DestType}, }, { Dependency: NewDependency(BuiltinDependencies[4]), Input: &Input{Index: 3, Type: timeTyp}, }, { Dependency: NewDependency(BuiltinDependencies[5]), Input: &Input{Index: 4, Type: BuiltinDependencies[5].DestType}, }, { Dependency: NewDependency(BuiltinDependencies[6]), Input: &Input{Index: 5, Type: BuiltinDependencies[6].DestType}, }, { Dependency: NewDependency(BuiltinDependencies[7]), Input: &Input{Index: 6, Type: BuiltinDependencies[7].DestType}, }, }, }, // test explicitly of http.Header and its underline type map[string][]string which // but shouldn't be binded to request headers because of the (.Explicitly()), instead // the map should be binded to our last of "deps" which is is a dynamic functions reads from request body's JSON // (it's a builtin dependency as well but we declared it to test user dynamic dependencies too). { // 13 Func: func(http.Header) testResponse { return testResponse{"builtin http.Header dep"} }, Expected: []*binding{ { Dependency: NewDependency(BuiltinDependencies[7]), Input: &Input{Index: 0, Type: BuiltinDependencies[7].DestType}, }, }, }, { // 14 Func: func(map[string][]string) testResponse { return testResponse{"not dep registered except the dynamic one"} }, Expected: []*binding{ { Dependency: deps[len(deps)-1], Input: &Input{Index: 0, Type: mapStringsTyp}, }, }, }, { // 15 Func: func(http.Header, map[string][]string) testResponse { return testResponse{} }, Expected: []*binding{ // only http.Header should be binded, we don't have map[string][]string registered. { Dependency: NewDependency(BuiltinDependencies[7]), Input: &Input{Index: 0, Type: BuiltinDependencies[7].DestType}, }, { Dependency: deps[len(deps)-1], Input: &Input{Index: 1, Type: mapStringsTyp}, }, }, }, } c := New() for _, dependency := range deps { c.Register(dependency) } for i, tt := range tests { bindings := getBindingsForFunc(reflect.ValueOf(tt.Func), c.Dependencies, c.DisablePayloadAutoBinding, 0) if expected, got := len(tt.Expected), len(bindings); expected != got { t.Fatalf("[%d] expected bindings length to be: %d but got: %d of: %s", i, expected, got, bindings) } for j, b := range bindings { if b == nil { t.Fatalf("[%d:%d] binding is nil!", i, j) } if tt.Expected[j] == nil { t.Fatalf("[%d:%d] expected dependency was not found!", i, j) } // if expected := tt.Expected[j]; !expected.Equal(b) { // t.Fatalf("[%d:%d] got unexpected binding:\n%s", i, j, spew.Sdump(expected, b)) // } if expected := tt.Expected[j]; !expected.Equal(b) { t.Fatalf("[%d:%d] expected binding:\n%s\nbut got:\n%s", i, j, expected, b) } } } } type ( service interface { String() string } serviceImpl struct{} ) var serviceTyp = reflect.TypeOf((*service)(nil)).Elem() func (s *serviceImpl) String() string { return "service" } func TestBindingsForStruct(t *testing.T) { type ( controller struct { Name string Service service } embedded1 struct { Age int } embedded2 struct { Now time.Time } Embedded3 struct { Age int } Embedded4 struct { Now time.Time } controllerEmbeddingExported struct { Embedded3 Embedded4 } controllerEmbeddingUnexported struct { embedded1 embedded2 } controller2 struct { Emb1 embedded1 Emb2 embedded2 } controller3 struct { Emb1 embedded1 emb2 embedded2 // unused } ) var deps = []*Dependency{ NewDependency("name"), NewDependency(new(serviceImpl)), } var depsForAnonymousEmbedded = []*Dependency{ NewDependency(42), NewDependency(time.Now()), } var depsForFieldsOfStruct = []*Dependency{ NewDependency(embedded1{Age: 42}), NewDependency(embedded2{time.Now()}), } var depsInterfaces = []*Dependency{ NewDependency(func(ctx *context.Context) any { return "name" }), } var autoBindings = []*binding{ payloadBinding(0, reflect.TypeOf(embedded1{})), payloadBinding(1, reflect.TypeOf(embedded2{})), } for _, b := range autoBindings { b.Input.StructFieldIndex = []int{b.Input.Index} } var tests = []struct { Value any Registered []*Dependency Expected []*binding }{ { // 0. Value: &controller{}, Registered: deps, Expected: []*binding{ { Dependency: deps[0], Input: &Input{Index: 0, StructFieldIndex: []int{0}, Type: reflect.TypeOf("")}, }, { Dependency: deps[1], Input: &Input{Index: 1, StructFieldIndex: []int{1}, Type: serviceTyp}, }, }, }, // 1. test controller with pre-defined variables. { Value: &controller{Name: "name_struct", Service: new(serviceImpl)}, Expected: nil, }, // 2. test controller with pre-defined variables and other deps with the exact order and value // (deps from non zero values should be not registerded, if not the Dependency:name_struct will fail for sure). { Value: &controller{Name: "name_struct", Service: new(serviceImpl)}, Registered: deps, Expected: nil, }, // 3. test embedded structs with anonymous and exported. { Value: &controllerEmbeddingExported{}, Registered: depsForAnonymousEmbedded, Expected: []*binding{ { Dependency: depsForAnonymousEmbedded[0], Input: &Input{Index: 0, StructFieldIndex: []int{0, 0}, Type: reflect.TypeOf(0)}, }, { Dependency: depsForAnonymousEmbedded[1], Input: &Input{Index: 1, StructFieldIndex: []int{1, 0}, Type: reflect.TypeOf(time.Time{})}, }, }, }, // 4. test for anonymous but not exported (should still be 2, unexported structs are binded). { Value: &controllerEmbeddingUnexported{}, Registered: depsForAnonymousEmbedded, Expected: []*binding{ { Dependency: depsForAnonymousEmbedded[0], Input: &Input{Index: 0, StructFieldIndex: []int{0, 0}, Type: reflect.TypeOf(0)}, }, { Dependency: depsForAnonymousEmbedded[1], Input: &Input{Index: 1, StructFieldIndex: []int{1, 0}, Type: reflect.TypeOf(time.Time{})}, }, }, }, // 5. test for auto-bindings with zero registered. { Value: &controller2{}, Registered: nil, Expected: autoBindings, }, // 6. test for embedded with named fields which should NOT contain any registered deps // except the two auto-bindings for structs, { Value: &controller2{}, Registered: depsForAnonymousEmbedded, Expected: autoBindings, }, // 7. and only embedded struct's fields are readen, otherwise we expect the struct to be a dependency. { Value: &controller2{}, Registered: depsForFieldsOfStruct, Expected: []*binding{ { Dependency: depsForFieldsOfStruct[0], Input: &Input{Index: 0, StructFieldIndex: []int{0}, Type: reflect.TypeOf(embedded1{})}, }, { Dependency: depsForFieldsOfStruct[1], Input: &Input{Index: 1, StructFieldIndex: []int{1}, Type: reflect.TypeOf(embedded2{})}, }, }, }, // 8. test one exported and other not exported. { Value: &controller3{}, Registered: []*Dependency{depsForFieldsOfStruct[0]}, Expected: []*binding{ { Dependency: depsForFieldsOfStruct[0], Input: &Input{Index: 0, StructFieldIndex: []int{0}, Type: reflect.TypeOf(embedded1{})}, }, }, }, // 9. test same as the above but by registering all dependencies. { Value: &controller3{}, Registered: depsForFieldsOfStruct, Expected: []*binding{ { Dependency: depsForFieldsOfStruct[0], Input: &Input{Index: 0, StructFieldIndex: []int{0}, Type: reflect.TypeOf(embedded1{})}, }, }, }, // 10. test bind an any. { Value: &controller{}, Registered: depsInterfaces, Expected: []*binding{ { Dependency: depsInterfaces[0], Input: &Input{Index: 0, StructFieldIndex: []int{0}, Type: reflect.TypeOf("")}, }, }, }, } for i, tt := range tests { bindings := getBindingsForStruct(reflect.ValueOf(tt.Value), tt.Registered, false, false, false, DefaultDependencyMatcher, 0, nil) if expected, got := len(tt.Expected), len(bindings); expected != got { t.Logf("[%d] expected bindings length to be: %d but got: %d:\n", i, expected, got) for _, b := range bindings { t.Logf("\t%s\n", b) } t.FailNow() } for j, b := range bindings { if tt.Expected[j] == nil { t.Fatalf("[%d:%d] expected dependency was not found!", i, j) } if expected := tt.Expected[j]; !expected.Equal(b) { t.Fatalf("[%d:%d] expected binding:\n%s\nbut got:\n%s", i, j, expected, b) } } } } func TestBindingsForStructMarkExportedFieldsAsRequred(t *testing.T) { type ( Embedded struct { Val string } controller struct { MyService service Embedded *Embedded } ) dependencies := []*Dependency{ NewDependency(&Embedded{"test"}), NewDependency(&serviceImpl{}), } // should panic if fail. _ = getBindingsForStruct(reflect.ValueOf(new(controller)), dependencies, true, true, false, DefaultDependencyMatcher, 0, nil) } ================================================ FILE: hero/container.go ================================================ package hero import ( stdContext "context" "errors" "net" "net/http" "reflect" "strings" "time" "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/sessions" "github.com/kataras/golog" ) // Default is the default container value which can be used for dependencies share. var Default = New().WithLogger(golog.Default) // Container contains and delivers the Dependencies that will be binded // to the controller(s) or handler(s) that can be created // using the Container's `Handler` and `Struct` methods. // // This is not exported for being used by everyone, use it only when you want // to share containers between multi mvc.go#Application // or make custom hero handlers that can be used on the standard // iris' APIBuilder. // // For a more high-level structure please take a look at the "mvc.go#Application". type Container struct { // Optional Logger to report dependencies and matched bindings // per struct, function and method. // By default it is set by the Party creator of this Container. Logger *golog.Logger // Sorter specifies how the inputs should be sorted before binded. // Defaults to sort by "thinnest" target empty interface. Sorter Sorter // The dependencies entries. Dependencies []*Dependency // MarkExportedFieldsAsRequired reports whether all struct's fields // MUST be binded to a dependency from the `Dependencies` list field. // In-short, if it is set to true and if at least one exported field // of a struct is not binded to a dependency then // the entire application will exit with a panic message before server startup. MarkExportedFieldsAsRequired bool // DisablePayloadAutoBinding reports whether // a function's parameter or struct's field of struct type // should not be binded automatically to the request body (e.g. JSON) // if a dependency for that type is missing. // By default the binder will bind structs to request body, // set to true to disable that kind of behavior. DisablePayloadAutoBinding bool // DisableStructDynamicBindings if true panics on struct handler (controller) // if at least one input binding depends on the request and not in a static structure. DisableStructDynamicBindings bool // StructDependents if true then the Container will try to resolve // the fields of a struct value, if any, when it's a dependent struct value // based on the previous registered dependencies. // // Defaults to false. EnableStructDependents bool // this can be renamed to IndirectDependencies?. // DependencyMatcher holds the function that compares equality between // a dependency with an input. Defaults to DefaultMatchDependencyFunc. DependencyMatcher DependencyMatcher // GetErrorHandler should return a valid `ErrorHandler` to handle bindings AND handler dispatch errors. // Defaults to a functon which returns the `DefaultErrorHandler`. GetErrorHandler func(*context.Context) ErrorHandler // cannot be nil. // Reports contains an ordered list of information about bindings for further analysys and testing. Reports []*Report // resultHandlers is a list of functions that serve the return struct value of a function handler. // Defaults to "defaultResultHandler" but it can be overridden. resultHandlers []func(next ResultHandler) ResultHandler } // A Report holds meta information about dependency sources and target values per package, // struct, struct's fields, struct's method, package-level function or closure. // E.g. main -> (*UserController) -> HandleHTTPError. type Report struct { // The name is the last part of the name of a struct or its methods or a function. // Each name is splited by its package.struct.field or package.funcName or package.func.inlineFunc. Name string // If it's a struct or package or function // then it contains children reports of each one of its methods or input parameters // respectfully. Reports []*Report Parent *Report Entries []ReportEntry } // A ReportEntry holds the information about a binding. type ReportEntry struct { InputPosition int // struct field position or parameter position. InputFieldName string // if it's a struct field, then this is its type name (we can't get param names). InputFieldType reflect.Type // the input's type. DependencyValue any // the dependency value binded to that InputPosition of Name. DependencyFile string // the file DependencyLine int // and line number of the dependency's value. Static bool } func (r *Report) fill(bindings []*binding) { for _, b := range bindings { inputFieldName := b.Input.StructFieldName if inputFieldName == "" { // it's not a struct field, then type. inputFieldName = b.Input.Type.String() } // remove only the main one prefix. inputFieldName = strings.TrimPrefix(inputFieldName, "main.") fieldName := inputFieldName switch fieldName { case "*context.Context": inputFieldName = strings.Replace(inputFieldName, "*context", "iris", 1) case "hero.Code", "hero.Result", "hero.View", "hero.Response": inputFieldName = strings.Replace(inputFieldName, "hero", "mvc", 1) } entry := ReportEntry{ InputPosition: b.Input.Index, InputFieldName: inputFieldName, InputFieldType: b.Input.Type, DependencyValue: b.Dependency.OriginalValue, DependencyFile: b.Dependency.Source.File, DependencyLine: b.Dependency.Source.Line, Static: b.Dependency.Static, } r.Entries = append(r.Entries, entry) } } // fillReport adds a report to the Reports field. func (c *Container) fillReport(fullName string, bindings []*binding) { // r := c.getReport(fullName) r := &Report{ Name: fullName, } r.fill(bindings) c.Reports = append(c.Reports, r) } // BuiltinDependencies is a list of builtin dependencies that are added on Container's initilization. // Contains the iris context, standard context, iris sessions and time dependencies. var BuiltinDependencies = []*Dependency{ // iris context dependency. newDependency(func(ctx *context.Context) *context.Context { return ctx }, true, false, nil).Explicitly(), // standard context dependency. newDependency(func(ctx *context.Context) stdContext.Context { return ctx.Request().Context() }, true, false, nil).Explicitly(), // iris session dependency. newDependency(func(ctx *context.Context) *sessions.Session { session := sessions.Get(ctx) if session == nil { ctx.Application().Logger().Debugf("binding: session is nil\nMaybe inside HandleHTTPError? Register it with app.UseRouter(sess.Handler()) to fix it") // let's don't panic here and let the application continue, now we support // not matched routes inside the controller through HandleHTTPError, // so each dependency can check if session was not nil or just use `UseRouter` instead of `Use` // to register the sessions middleware. } return session }, true, false, nil).Explicitly(), // application's logger. newDependency(func(ctx *context.Context) *golog.Logger { return ctx.Application().Logger() }, true, false, nil).Explicitly(), // time.Time to time.Now dependency. newDependency(func(ctx *context.Context) time.Time { return time.Now() }, true, false, nil).Explicitly(), // standard http Request dependency. newDependency(func(ctx *context.Context) *http.Request { return ctx.Request() }, true, false, nil).Explicitly(), // standard http ResponseWriter dependency. newDependency(func(ctx *context.Context) http.ResponseWriter { return ctx.ResponseWriter() }, true, false, nil).Explicitly(), // http headers dependency. newDependency(func(ctx *context.Context) http.Header { return ctx.Request().Header }, true, false, nil).Explicitly(), // Client IP. newDependency(func(ctx *context.Context) net.IP { return net.ParseIP(ctx.RemoteAddr()) }, true, false, nil).Explicitly(), // Status Code (special type for MVC HTTP Error handler to not conflict with path parameters) newDependency(func(ctx *context.Context) Code { return Code(ctx.GetStatusCode()) }, true, false, nil).Explicitly(), // Context Error. May be nil newDependency(func(ctx *context.Context) Err { err := ctx.GetErr() if err == nil { return nil } return err }, true, false, nil).Explicitly(), // Context User, e.g. from basic authentication. newDependency(func(ctx *context.Context) context.User { u := ctx.User() if u == nil { return nil } return u }, true, false, nil), // payload and param bindings are dynamically allocated and declared at the end of the `binding` source file. } // New returns a new Container, a container for dependencies and a factory // for handlers and controllers, this is used internally by the `mvc#Application` structure. // Please take a look at the structure's documentation for more information. func New(dependencies ...any) *Container { deps := make([]*Dependency, len(BuiltinDependencies)) copy(deps, BuiltinDependencies) c := &Container{ Sorter: sortByNumMethods, Dependencies: deps, GetErrorHandler: func(*context.Context) ErrorHandler { return DefaultErrorHandler }, DependencyMatcher: DefaultDependencyMatcher, } for _, dependency := range dependencies { c.Register(dependency) } return c } // WithLogger injects a logger to use to debug dependencies and bindings. func (c *Container) WithLogger(logger *golog.Logger) *Container { c.Logger = logger return c } // Clone returns a new cloned container. // It copies the ErrorHandler, Dependencies and all Options from "c" receiver. func (c *Container) Clone() *Container { cloned := New() cloned.Logger = c.Logger cloned.GetErrorHandler = c.GetErrorHandler cloned.Sorter = c.Sorter cloned.DependencyMatcher = c.DependencyMatcher clonedDeps := make([]*Dependency, len(c.Dependencies)) copy(clonedDeps, c.Dependencies) cloned.Dependencies = clonedDeps cloned.DisablePayloadAutoBinding = c.DisablePayloadAutoBinding cloned.DisableStructDynamicBindings = c.DisableStructDynamicBindings cloned.EnableStructDependents = c.EnableStructDependents cloned.MarkExportedFieldsAsRequired = c.MarkExportedFieldsAsRequired cloned.resultHandlers = c.resultHandlers // Reports are not cloned. return cloned } // Register adds a dependency. // The value can be a single struct value-instance or a function // which has one input and one output, that output type // will be binded to the handler's input argument, if matching. // // Usage: // - Register(loggerService{prefix: "dev"}) // - Register(func(ctx iris.Context) User {...}) // - Register(func(User) OtherResponse {...}) func Register(dependency any) *Dependency { return Default.Register(dependency) } // Register adds a dependency. // The value can be a single struct value or a function. // Follow the rules: // * {structValue} // * func(accepts ) returns or (, error) // * func(accepts iris.Context) returns or (, error) // * func(accepts1 iris.Context, accepts2 *hero.Input) returns or (, error) // // A Dependency can accept a previous registered dependency and return a new one or the same updated. // * func(accepts1 , accepts2 ) returns or (, error) or error // * func(acceptsPathParameter1 string, id uint64) returns or (, error) // // Usage: // // - Register(loggerService{prefix: "dev"}) // - Register(func(ctx iris.Context) User {...}) // - Register(func(User) OtherResponse {...}) func (c *Container) Register(dependency any) *Dependency { d := newDependency(dependency, c.DisablePayloadAutoBinding, c.EnableStructDependents, c.DependencyMatcher, c.Dependencies...) if d.DestType == nil { // prepend the dynamic dependency so it will be tried at the end // (we don't care about performance here, design-time) c.Dependencies = append([]*Dependency{d}, c.Dependencies...) } else { c.Dependencies = append(c.Dependencies, d) } return d } // UseResultHandler adds a result handler to the Container. // A result handler can be used to inject the returned struct value // from a request handler or to replace the default renderer. func (c *Container) UseResultHandler(handler func(next ResultHandler) ResultHandler) *Container { c.resultHandlers = append(c.resultHandlers, handler) return c } // Handler accepts a "handler" function which can accept any input arguments that match // with the Container's `Dependencies` and any output result; like string, int (string,int), // custom structs, Result(View | Response) and anything you can imagine. // It returns a standard `iris/context.Handler` which can be used anywhere in an Iris Application, // as middleware or as simple route handler or subdomain's handler. func Handler(fn any) context.Handler { return Default.Handler(fn) } // Handler accepts a handler "fn" function which can accept any input arguments that match // with the Container's `Dependencies` and any output result; like string, int (string,int), // custom structs, Result(View | Response) and more. // It returns a standard `iris/context.Handler` which can be used anywhere in an Iris Application, // as middleware or as simple route handler or subdomain's handler. // // func(...) iris.Handler // // - if are all static dependencies then // there is no reflection involved at serve-time. // // func(pathParameter string, ...) // // - one or more path parameters (e.g. :uid, :string, :int, :path, :uint64) // are automatically binded to the first input Go standard types (string, int, uint64 and e.t.c.) // // func() error // // - if a function returns an error then this error's text is sent to the client automatically. // // func() // // - The result of the function is a dependency too. // If is a request-scope dependency (dynamic) then // this function will be called at every request. // // func() // // - If is static dependency (e.g. a database or a service) then its result // can be used as a static dependency to the next dependencies or to the controller/function itself. func (c *Container) Handler(fn any) context.Handler { return c.HandlerWithParams(fn, 0) } // HandlerWithParams same as `Handler` but it can receive a total path parameters counts // to resolve coblex path parameters input dependencies. func (c *Container) HandlerWithParams(fn any, paramsCount int) context.Handler { return makeHandler(fn, c, paramsCount) } // Struct accepts a pointer to a struct value and returns a structure which // contains bindings for the struct's fields and a method to // extract a Handler from this struct's method. func (c *Container) Struct(ptrValue any, partyParamsCount int) *Struct { return makeStruct(ptrValue, c, partyParamsCount) } // ErrMissingDependency may returned only from the `Container.Inject` method // when not a matching dependency found for "toPtr". var ErrMissingDependency = errors.New("missing dependency") // Inject SHOULD only be used outside of HTTP handlers (performance is not priority for this method) // as it does not pre-calculate the available list of bindings for the "toPtr" and the registered dependencies. // // It sets a static-only matching dependency to the value of "toPtr". // The parameter "toPtr" SHOULD be a pointer to a value corresponding to a dependency, // like input parameter of a handler or field of a struct. // // If no matching dependency found, the `Inject` method returns an `ErrMissingDependency` and // "toPtr" keeps its original state (e.g. nil). // // Example Code: // c.Register(&LocalDatabase{...}) // [...] // var db Database // err := c.Inject(&db) func (c *Container) Inject(toPtr any) error { val := reflect.Indirect(valueOf(toPtr)) typ := val.Type() for _, d := range c.Dependencies { if d.Static && c.DependencyMatcher(d, typ) { v, err := d.Handle(nil, &Input{Type: typ}) if err != nil { if err == ErrSeeOther { continue } return err } val.Set(v) return nil } } return ErrMissingDependency } ================================================ FILE: hero/container_test.go ================================================ package hero_test import ( "fmt" "reflect" "testing" "github.com/kataras/iris/v12" . "github.com/kataras/iris/v12/hero" "github.com/kataras/iris/v12/httptest" ) var errTyp = reflect.TypeOf((*error)(nil)).Elem() // isError returns true if "typ" is type of `error`. func isError(typ reflect.Type) bool { return typ.Implements(errTyp) } type ( testInput struct { Name string `json:"name"` } testOutput struct { ID int `json:"id"` Name string `json:"name"` } ) var ( fn = func(id int, in testInput) testOutput { return testOutput{ ID: id, Name: in.Name, } } expectedOutput = testOutput{ ID: 42, Name: "makis", } input = testInput{ Name: "makis", } ) func TestContainerHandler(t *testing.T) { app := iris.New() c := New() postHandler := c.Handler(fn) app.Post("/{id:int}", postHandler) e := httptest.New(t, app) path := fmt.Sprintf("/%d", expectedOutput.ID) e.POST(path).WithJSON(input).Expect().Status(httptest.StatusOK).JSON().IsEqual(expectedOutput) } func TestContainerInject(t *testing.T) { c := New() expected := testInput{Name: "test"} c.Register(expected) c.Register(&expected) // struct value. var got1 testInput if err := c.Inject(&got1); err != nil { t.Fatal(err) } if !reflect.DeepEqual(expected, got1) { t.Fatalf("[struct value] expected: %#+v but got: %#+v", expected, got1) } // ptr. var got2 *testInput if err := c.Inject(&got2); err != nil { t.Fatal(err) } if !reflect.DeepEqual(&expected, got2) { t.Fatalf("[ptr] expected: %#+v but got: %#+v", &expected, got2) } // register implementation, expect interface. expected3 := &testServiceImpl{prefix: "prefix: "} c.Register(expected3) var got3 testService if err := c.Inject(&got3); err != nil { t.Fatal(err) } if !reflect.DeepEqual(expected3, got3) { t.Fatalf("[service] expected: %#+v but got: %#+v", expected3, got3) } } func TestContainerUseResultHandler(t *testing.T) { c := New() resultLogger := func(next ResultHandler) ResultHandler { return func(ctx iris.Context, v any) error { t.Logf("%#+v", v) return next(ctx, v) } } c.UseResultHandler(resultLogger) expectedResponse := map[string]any{"injected": true} c.UseResultHandler(func(next ResultHandler) ResultHandler { return func(ctx iris.Context, v any) error { return next(ctx, expectedResponse) } }) c.UseResultHandler(resultLogger) handler := c.Handler(func(id int) testOutput { return testOutput{ ID: id, Name: "kataras", } }) app := iris.New() app.Get("/{id:int}", handler) e := httptest.New(t, app) e.GET("/42").Expect().Status(httptest.StatusOK).JSON().IsEqual(expectedResponse) } ================================================ FILE: hero/dependency.go ================================================ package hero import ( "fmt" "strings" "reflect" "github.com/kataras/iris/v12/context" ) type ( // DependencyHandler is the native function declaration which implementors should return a value match to an input. DependencyHandler = func(ctx *context.Context, input *Input) (reflect.Value, error) // DependencyMatchFunc type alias describes dependency // match function with an input (field or parameter). // // See "DependencyMatcher" too, which can be used on a Container to // change the way dependencies are matched to inputs for all dependencies. DependencyMatchFunc = func(in reflect.Type) bool // Dependency describes the design-time dependency to be injected at serve time. // Contains its source location, the dependency handler (provider) itself and information // such as static for static struct values or explicit to bind a value to its exact DestType and not if just assignable to it (interfaces). Dependency struct { OriginalValue any // Used for debugging and for logging only. Source Source Handle DependencyHandler // It's the exact type of return to bind, if declared to return , otherwise nil. DestType reflect.Type Static bool // If true then input and dependency DestType should be indedical, // not just assiginable to each other. // Example of use case: depenendency like time.Time that we want to be bindable // only to time.Time inputs and not to a service with a `String() string` method that time.Time struct implements too. Explicit bool // Match holds the matcher. Defaults to the Container's one. Match DependencyMatchFunc // StructDependents if true then the Container will try to resolve // the fields of a struct value, if any, when it's a dependent struct value // based on the previous registered dependencies. // // Defaults to false. StructDependents bool } ) // Explicitly sets Explicit option to true. // See `Dependency.Explicit` field godoc for more. // // Returns itself. func (d *Dependency) Explicitly() *Dependency { d.Explicit = true return d } // EnableStructDependents sets StructDependents to true. func (d *Dependency) EnableStructDependents() *Dependency { d.StructDependents = true return d } func (d *Dependency) String() string { sourceLine := d.Source.String() val := d.OriginalValue if val == nil { val = d.Handle } return fmt.Sprintf("%s (%#+v)", sourceLine, val) } // NewDependency converts a function or a function which accepts other dependencies or static struct value to a *Dependency. // // See `Container.Handler` for more. func NewDependency(dependency any, funcDependencies ...*Dependency) *Dependency { // used only on tests. return newDependency(dependency, false, false, nil, funcDependencies...) } func newDependency( dependency any, disablePayloadAutoBinding bool, enableStructDependents bool, matchDependency DependencyMatcher, funcDependencies ...*Dependency, ) *Dependency { if dependency == nil { panic(fmt.Sprintf("bad value: nil: %T", dependency)) } if d, ok := dependency.(*Dependency); ok { // already a *Dependency, do not continue (and most importatly do not call resolveDependency) . return d } v := valueOf(dependency) if !goodVal(v) { panic(fmt.Sprintf("bad value: %#+v", dependency)) } if matchDependency == nil { matchDependency = DefaultDependencyMatcher } dest := &Dependency{ Source: newSource(v), OriginalValue: dependency, StructDependents: enableStructDependents, } dest.Match = ToDependencyMatchFunc(dest, matchDependency) if !resolveDependency(v, disablePayloadAutoBinding, dest, funcDependencies...) { panic(fmt.Sprintf("bad value: could not resolve a dependency from: %#+v", dependency)) } return dest } // DependencyResolver func(v reflect.Value, dest *Dependency) bool // Resolver DependencyResolver func resolveDependency(v reflect.Value, disablePayloadAutoBinding bool, dest *Dependency, prevDependencies ...*Dependency) bool { return fromDependencyHandler(v, dest) || fromBuiltinValue(v, dest) || fromStructValueOrDependentStructValue(v, disablePayloadAutoBinding, dest, prevDependencies) || fromFunc(v, dest) || len(prevDependencies) > 0 && fromDependentFunc(v, disablePayloadAutoBinding, dest, prevDependencies) } func fromDependencyHandler(_ reflect.Value, dest *Dependency) bool { // It's already on the desired form, just return it. dependency := dest.OriginalValue handler, ok := dependency.(DependencyHandler) if !ok { handler, ok = dependency.(func(*context.Context, *Input) (reflect.Value, error)) if !ok { // It's almost a handler, only the second `Input` argument is missing. if h, is := dependency.(func(*context.Context) (reflect.Value, error)); is { handler = func(ctx *context.Context, _ *Input) (reflect.Value, error) { return h(ctx) } ok = is } } } if !ok { return false } dest.Handle = handler return true } func fromBuiltinValue(v reflect.Value, dest *Dependency) bool { if !isBuiltinValue(v) { return false } // It's just a static builtin value. handler := func(*context.Context, *Input) (reflect.Value, error) { return v, nil } dest.DestType = v.Type() dest.Static = true dest.Handle = handler return true } func fromStructValue(v reflect.Value, dest *Dependency) bool { if !isStructValue(v) { return false } // It's just a static struct value. handler := func(*context.Context, *Input) (reflect.Value, error) { return v, nil } dest.DestType = v.Type() dest.Static = true dest.Handle = handler return true } func fromStructValueOrDependentStructValue(v reflect.Value, disablePayloadAutoBinding bool, dest *Dependency, prevDependencies []*Dependency) bool { if !isStructValue(v) { // It's not just a static struct value. return false } if len(prevDependencies) == 0 || !dest.StructDependents { // As a non depedent struct. // We must make this check so we can avoid the auto-filling of // the dependencies from Iris builtin dependencies. return fromStructValue(v, dest) } // Check if it's a builtin dependency (e.g an MVC Application (see mvc.go#newApp)), // if it's and registered without a Dependency wrapper, like the rest builtin dependencies, // then do NOT try to resolve its fields. // // Although EnableStructDependents is false by default, we must check if it's a builtin dependency for any case. if strings.HasPrefix(indirectType(v.Type()).PkgPath(), "github.com/kataras/iris/v12") { return fromStructValue(v, dest) } bindings := getBindingsForStruct(v, prevDependencies, false, disablePayloadAutoBinding, dest.StructDependents, DefaultDependencyMatcher, -1, nil) if len(bindings) == 0 { return fromStructValue(v, dest) // same as above. } // As a depedent struct, however we may need to resolve its dependencies first // so we can decide if it's really a depedent struct or not. var ( handler = func(*context.Context, *Input) (reflect.Value, error) { return v, nil } isStatic = true ) for _, binding := range bindings { if !binding.Dependency.Static { isStatic = false break } } handler = func(ctx *context.Context, _ *Input) (reflect.Value, error) { // Called once per dependency on build-time if the dependency is static. elem := v if elem.Kind() == reflect.Ptr { elem = elem.Elem() } for _, binding := range bindings { field := elem.FieldByIndex(binding.Input.StructFieldIndex) if !field.CanSet() || !field.IsZero() { continue // already set. } // if !binding.Dependency.Match(field.Type()) { A check already happen in getBindingsForStruct. // continue // } input, err := binding.Dependency.Handle(ctx, binding.Input) if err != nil { if err == ErrSeeOther { continue } return emptyValue, err } // fmt.Printf("binding %s to %#+v\n", field.String(), input) field.Set(input) } return v, nil } dest.DestType = v.Type() dest.Static = isStatic dest.Handle = handler return true } func fromFunc(v reflect.Value, dest *Dependency) bool { if !isFunc(v) { return false } typ := v.Type() numIn := typ.NumIn() numOut := typ.NumOut() if numIn == 0 { // it's an empty function, that must return a structure. if numOut != 1 { firstOutType := indirectType(typ.Out(0)) if firstOutType.Kind() != reflect.Struct && firstOutType.Kind() != reflect.Interface { panic(fmt.Sprintf("bad value: function has zero inputs: empty input function must output a single value but got: length=%v, type[0]=%s", numOut, firstOutType.String())) } } // fallback to structure. return fromStructValue(v.Call(nil)[0], dest) } if numOut == 0 { panic("bad value: function has zero outputs") } if numOut == 2 && !isError(typ.Out(1)) { panic("bad value: second output should be an error") } if numOut > 2 { // - at least one output value // - maximum of two output values // - second output value should be a type of error. panic(fmt.Sprintf("bad value: function has invalid number of output arguments: %v", numOut)) } var handler DependencyHandler firstIsContext := isContext(typ.In(0)) secondIsInput := numIn == 2 && typ.In(1) == inputTyp onlyContext := (numIn == 1 && firstIsContext) || (numIn == 2 && firstIsContext && typ.IsVariadic()) if onlyContext || (firstIsContext && secondIsInput) { handler = handlerFromFunc(v, typ) } if handler == nil { return false } dest.DestType = typ.Out(0) dest.Handle = handler return true } func handlerFromFunc(v reflect.Value, typ reflect.Type) DependencyHandler { // * func(Context, *Input) , func(Context) // * func(Context) , func(Context) // * func(Context, *Input) , func(Context) (, error) // * func(Context) , func(Context) (, error) hasErrorOut := typ.NumOut() == 2 // if two, always an error type here. hasInputIn := typ.NumIn() == 2 && typ.In(1) == inputTyp return func(ctx *context.Context, input *Input) (reflect.Value, error) { inputs := ctx.ReflectValue() if hasInputIn { inputs = append(inputs, input.selfValue) } results := v.Call(inputs) if hasErrorOut { return results[0], toError(results[1]) } return results[0], nil } } func fromDependentFunc(v reflect.Value, disablePayloadAutoBinding bool, dest *Dependency, funcDependencies []*Dependency) bool { // * func(...) returns // * func(...) returns error // * func(...) returns , error typ := v.Type() if !isFunc(v) { return false } bindings := getBindingsForFunc(v, funcDependencies, disablePayloadAutoBinding, -1 /* parameter bindings are disabled for depent dependencies */) numIn := typ.NumIn() numOut := typ.NumOut() // d1 = Logger // d2 = func(Logger) S1 // d2 should be static: it accepts dependencies that are static // (note: we don't check the output argument(s) of this dependency). if numIn == len(bindings) { static := true for _, b := range bindings { if !b.Dependency.Static && b.Dependency.Match(typ.In(b.Input.Index)) { static = false break } } if static { dest.Static = static } } firstOutIsError := numOut == 1 && isError(typ.Out(0)) secondOutIsError := numOut == 2 && isError(typ.Out(1)) handler := func(ctx *context.Context, _ *Input) (reflect.Value, error) { inputs := make([]reflect.Value, numIn) for _, binding := range bindings { input, err := binding.Dependency.Handle(ctx, binding.Input) if err != nil { if err == ErrSeeOther { continue } return emptyValue, err } inputs[binding.Input.Index] = input } outputs := v.Call(inputs) if firstOutIsError { return emptyValue, toError(outputs[0]) } else if secondOutIsError { return outputs[0], toError(outputs[1]) } return outputs[0], nil } dest.DestType = typ.Out(0) dest.Handle = handler return true } ================================================ FILE: hero/dependency_source.go ================================================ package hero import ( "fmt" "os" "path/filepath" "reflect" "runtime" "strings" ) // Source describes where a dependency is located at the source code itself. type Source struct { File string Line int Caller string } func newSource(fn reflect.Value) Source { var ( callerFileName string callerLineNumber int callerName string ) switch fn.Kind() { case reflect.Func, reflect.Chan, reflect.Map, reflect.Ptr, reflect.UnsafePointer, reflect.Slice: pc := fn.Pointer() fpc := runtime.FuncForPC(pc) if fpc != nil { callerFileName, callerLineNumber = fpc.FileLine(pc) callerName = fpc.Name() } fallthrough default: if callerFileName == "" { callerFileName, callerLineNumber = GetCaller() } } wd, _ := os.Getwd() if relFile, err := filepath.Rel(wd, callerFileName); err == nil { if !strings.HasPrefix(relFile, "..") { // Only if it's relative to this path, not parent. callerFileName = "./" + relFile } } return Source{ File: filepath.ToSlash(callerFileName), Line: callerLineNumber, Caller: callerName, } } func getSource() Source { filename, line := GetCaller() return Source{ File: filename, Line: line, } } func (s Source) String() string { return fmt.Sprintf("%s:%d", s.File, s.Line) } // https://golang.org/doc/go1.9#callersframes func GetCaller() (string, int) { var pcs [32]uintptr n := runtime.Callers(4, pcs[:]) frames := runtime.CallersFrames(pcs[:n]) for { frame, more := frames.Next() file := frame.File if strings.Contains(file, "go/src/runtime/") { continue } // funcName is something like "github.com/kataras/iris.SomeFunc" funcName := frame.Function if !strings.HasPrefix(funcName, "github.com/kataras/iris/v12") { return file, frame.Line } if !more { break } } return "???", 0 } ================================================ FILE: hero/dependency_test.go ================================================ package hero_test import ( "errors" "fmt" "reflect" "testing" "github.com/kataras/iris/v12/context" . "github.com/kataras/iris/v12/hero" ) type testDependencyTest struct { Dependency any Expected any } func TestDependency(t *testing.T) { var tests = []testDependencyTest{ { Dependency: "myValue", Expected: "myValue", }, { Dependency: struct{ Name string }{"name"}, Expected: struct{ Name string }{"name"}, }, { Dependency: func(*context.Context, *Input) (reflect.Value, error) { return reflect.ValueOf(42), nil }, Expected: 42, }, { Dependency: DependencyHandler(func(*context.Context, *Input) (reflect.Value, error) { return reflect.ValueOf(255), nil }), Expected: 255, }, { Dependency: func(*context.Context) (reflect.Value, error) { return reflect.ValueOf("OK without Input"), nil }, Expected: "OK without Input", }, { Dependency: func(*context.Context, ...string) (reflect.Value, error) { return reflect.ValueOf("OK variadic ignored"), nil }, Expected: "OK variadic ignored", }, { Dependency: func(*context.Context) reflect.Value { return reflect.ValueOf("OK without Input and error") }, Expected: "OK without Input and error", }, { Dependency: func(*context.Context, ...int) reflect.Value { return reflect.ValueOf("OK without error and variadic ignored") }, Expected: "OK without error and variadic ignored", }, { Dependency: func(*context.Context) any { return "1" }, Expected: "1", }, { Dependency: func(*context.Context) any { return false }, Expected: false, }, { Dependency: map[string]string{"test": "value"}, Expected: map[string]string{"test": "value"}, }, } testDependencies(t, tests) } // Test dependencies that depend on previous one(s). func TestDependentDependency(t *testing.T) { msgBody := "prefix: it is a deep dependency" newMsgBody := msgBody + " new" var tests = []testDependencyTest{ // test three level depth and error. { // 0 Dependency: &testServiceImpl{prefix: "prefix:"}, Expected: &testServiceImpl{prefix: "prefix:"}, }, { // 1 Dependency: func(service testService) testMessage { return testMessage{Body: service.Say("it is a deep") + " dependency"} }, Expected: testMessage{Body: msgBody}, }, { // 2 Dependency: func(msg testMessage) string { return msg.Body }, Expected: msgBody, }, { // 3 Dependency: func(msg testMessage) error { return errors.New(msg.Body) }, Expected: errors.New(msgBody), }, // Test depend on more than one previous registered dependencies and require a before-previous one. { // 4 Dependency: func(body string, msg testMessage) string { if body != msg.Body { t.Fatalf("body[%s] != msg.Body[%s]", body, msg.Body) } return body + " new" }, Expected: newMsgBody, }, // Test dependency order by expecting the first returning value and not the later-on registered dependency(#4). // 5 { Dependency: func(body string) string { return body }, Expected: newMsgBody, }, } testDependencies(t, tests) } func testDependencies(t *testing.T, tests []testDependencyTest) { t.Helper() c := New() for i, tt := range tests { d := c.Register(tt.Dependency) if d == nil { t.Fatalf("[%d] expected %#+v to be converted to a valid dependency", i, tt) } val, err := d.Handle(context.NewContext(nil), &Input{}) if expectError := isError(reflect.TypeOf(tt.Expected)); expectError { val = reflect.ValueOf(err) err = nil } if err != nil { t.Fatalf("[%d] expected a nil error but got: %v", i, err) } if !val.CanInterface() { t.Fatalf("[%d] expected output value to be accessible: %T", i, val) } if expected, got := fmt.Sprintf("%#+v", tt.Expected), fmt.Sprintf("%#+v", val.Interface()); expected != got { t.Fatalf("[%d] expected return value to be:\n%s\nbut got:\n%s", i, expected, got) } // t.Logf("[%d] %s", i, d) // t.Logf("[%d] output: %#+v", i, val.Interface()) } } func TestDependentDependencyInheritanceStatic(t *testing.T) { // Tests the following case #1564: // Logger // func(Logger) S1 // ^ Should be static because Logger // is a structure, a static dependency. // // func(Logger) S2 // func(S1, S2) S3 // ^ Should be marked as static dependency // because everything that depends on are static too. type S1 struct { msg string } type S2 struct { msg2 string } serviceDep := NewDependency(&testServiceImpl{prefix: "1"}) d1 := NewDependency(func(t testService) S1 { return S1{t.Say("2")} }, serviceDep) if !d1.Static { t.Fatalf("d1 dependency should be static: %#+v", d1) } d2 := NewDependency(func(t testService, s S1) S2 { return S2{"3"} }, serviceDep, d1) if !d2.Static { t.Fatalf("d2 dependency should be static: %#+v", d2) } } ================================================ FILE: hero/func_result.go ================================================ package hero import ( "reflect" "strings" "github.com/kataras/iris/v12/context" "github.com/fatih/structs" "google.golang.org/protobuf/proto" ) // ResultHandler describes the function type which should serve the "v" struct value. type ResultHandler func(ctx *context.Context, v any) error func defaultResultHandler(ctx *context.Context, v any) error { if p, ok := v.(PreflightResult); ok { if err := p.Preflight(ctx); err != nil { return err } } if d, ok := v.(Result); ok { d.Dispatch(ctx) return nil } switch context.TrimHeaderValue(ctx.GetContentType()) { case context.ContentXMLHeaderValue, context.ContentXMLUnreadableHeaderValue: return ctx.XML(v) case context.ContentYAMLHeaderValue: return ctx.YAML(v) case context.ContentProtobufHeaderValue: msg, ok := v.(proto.Message) if !ok { return context.ErrContentNotSupported } _, err := ctx.Protobuf(msg) return err case context.ContentMsgPackHeaderValue, context.ContentMsgPack2HeaderValue: _, err := ctx.MsgPack(v) return err default: // otherwise default to JSON. return ctx.JSON(v) } } // Result is a response dispatcher. // All types that complete this interface // can be returned as values from the method functions. // // Example at: https://github.com/kataras/iris/tree/main/_examples/dependency-injection/overview. type Result interface { // Dispatch should send a response to the client. Dispatch(*context.Context) } // PreflightResult is an interface which implementers // should be responsible to perform preflight checks of a resource (or Result) before sent to the client. // // If a non-nil error returned from the `Preflight` method then the JSON result // will be not sent to the client and an ErrorHandler will be responsible to render the error. // // Usage: a custom struct value will be a JSON body response (by-default) but it contains // "Code int" and `ID string` fields, the "Code" should be the status code of the response // and the "ID" should be sent as a Header of "X-Request-ID: $ID". // // The caller can manage it at the handler itself. However, // to reduce thoese type of duplications it's preferable to use such a standard interface instead. // // The Preflight method can return `iris.ErrStopExecution` to render // and override any interface that the structure value may implement, e.g. mvc.Result. type PreflightResult interface { Preflight(*context.Context) error } var defaultFailureResponse = Response{Code: DefaultErrStatusCode} // Try will check if "fn" ran without any panics, // using recovery, // and return its result as the final response // otherwise it returns the "failure" response if any, // if not then a 400 bad request is being sent. // // Example usage at: https://github.com/kataras/iris/blob/main/hero/func_result_test.go. func Try(fn func() Result, failure ...Result) Result { var failed bool var actionResponse Result func() { defer func() { if rec := recover(); rec != nil { failed = true } }() actionResponse = fn() }() if failed { if len(failure) > 0 { return failure[0] } return defaultFailureResponse } return actionResponse } const slashB byte = '/' type compatibleErr interface { Error() string } // dispatchErr sets the error status code // and the error value to the context. // The APIBuilder's On(Any)ErrorCode is responsible to render this error code. func dispatchErr(ctx *context.Context, status int, err error) bool { if err == nil { return false } if err != ErrStopExecution { if status == 0 || !context.StatusCodeNotSuccessful(status) { status = DefaultErrStatusCode } ctx.StatusCode(status) } ctx.SetErr(err) return true } // DispatchFuncResult is being used internally to resolve // and send the method function's output values to the // context's response writer using a smart way which // respects status code, content type, content, custom struct // and an error type. // Supports for: // func(c *ExampleController) Get() string | // (string, string) | // (string, int) | // ... // int | // (int, string | // (string, error) | // ... // error | // (int, error) | // (customStruct, error) | // ... // bool | // (int, bool) | // (string, bool) | // (customStruct, bool) | // ... // customStruct | // (customStruct, int) | // (customStruct, string) | // Result or (Result, error) and so on... // // where Get is an HTTP METHOD. func dispatchFuncResult(ctx *context.Context, values []reflect.Value, handler ResultHandler) error { if len(values) == 0 { return nil } var ( // if statusCode > 0 then send this status code. // Except when err != nil then check if status code is < 400 and // if it's set it as DefaultErrStatusCode. // Except when found == false, then the status code is 404. statusCode = ctx.GetStatusCode() // Get the current status code given by any previous middleware. // if not empty then use that as content type, // if empty and custom != nil then set it to application/json. contentType string // if len > 0 then write that to the response writer as raw bytes, // except when found == false or err != nil or custom != nil. content []byte // if not nil then check // for content type (or json default) and send the custom data object // except when found == false or err != nil. custom any // if false then skip everything and fire 404. found = true // defaults to true of course, otherwise will break :) ) for _, v := range values { // order of these checks matters // for example, first we need to check for status code, // secondly the string (for content type and content)... // if !v.IsValid() || !v.CanInterface() { // continue // } if !v.IsValid() { continue } f := v.Interface() /* if b, ok := f.(bool); ok { found = b if !found { // skip everything, we don't care about other return values, // this boolean is the higher in order. break } continue } if i, ok := f.(int); ok { statusCode = i continue } if s, ok := f.(string); ok { // a string is content type when it contains a slash and // content or custom struct is being calculated already; // (string -> content, string-> content type) // (customStruct, string -> content type) if (len(content) > 0 || custom != nil) && strings.IndexByte(s, slashB) > 0 { contentType = s } else { // otherwise is content content = []byte(s) } continue } if b, ok := f.([]byte); ok { // it's raw content, get the latest content = b continue } if e, ok := f.(compatibleErr); ok { if e != nil { // it's always not nil but keep it here. err = e if statusCode < 400 { statusCode = DefaultErrStatusCode } break // break on first error, error should be in the end but we // need to know break the dispatcher if any error. // at the end; we don't want to write anything to the response if error is not nil. } continue } // else it's a custom struct or a dispatcher, we'll decide later // because content type and status code matters // do that check in order to be able to correctly dispatch: // (customStruct, error) -> customStruct filled and error is nil if custom == nil && f != nil { custom = f } } */ switch value := f.(type) { case bool: found = value if !found { // skip everything, skip other values, we don't care about other return values, // this boolean is the higher in order. break } case int: statusCode = value case string: // a string is content type when it contains a slash and // content or custom struct is being calculated already; // (string -> content, string-> content type) // (customStruct, string -> content type) if (len(content) > 0 || custom != nil) && strings.IndexByte(value, slashB) > 0 { contentType = value } else { // otherwise is content contentType = context.ContentTextHeaderValue content = []byte(value) } case []byte: // it's raw content, get the latest content = value case compatibleErr: if value == nil || isNil(v) { continue } if statusCode < 400 && value != ErrStopExecution { statusCode = DefaultErrStatusCode } ctx.StatusCode(statusCode) return value default: // else it's a custom struct or a dispatcher, we'll decide later // because content type and status code matters // do that check in order to be able to correctly dispatch: // (customStruct, error) -> customStruct filled and error is nil if custom == nil { // if it's a pointer to struct/map. if isNil(v) { // if just a ptr to struct with no content type given // then try to get the previous response writer's content type, // and if that is empty too then force-it to application/json // as the default content type we use for structs/maps. if contentType == "" { contentType = ctx.GetContentType() if contentType == "" { contentType = context.ContentJSONHeaderValue } } continue } if value != nil { custom = value // content type will be take care later on. } } } } return dispatchCommon(ctx, statusCode, contentType, content, custom, handler, found) } // dispatchCommon is being used internally to send // commonly used data to the response writer with a smart way. func dispatchCommon(ctx *context.Context, statusCode int, contentType string, content []byte, v any, handler ResultHandler, found bool) error { // if we have a false boolean as a return value // then skip everything and fire a not found, // we even don't care about the given status code or the object or the content. if !found { ctx.NotFound() return nil } status := statusCode if status == 0 { status = 200 } // write the status code, the rest will need that before any write ofc. ctx.StatusCode(status) if contentType == "" { // to respect any ctx.ContentType(...) call // especially if v is not nil. if contentType = ctx.GetContentType(); contentType == "" { // if it's still empty set to JSON. (useful for dynamic middlewares that returns an int status code and the next handler dispatches the JSON, // see dependency-injection/basic/middleware example) contentType = context.ContentJSONHeaderValue } } // write the content type now (internal check for empty value) ctx.ContentType(contentType) if v != nil { return handler(ctx, v) } // .Write even len(content) == 0 , this should be called in order to call the internal tryWriteHeader, // it will not cost anything. _, err := ctx.Write(content) return err } // Response completes the `methodfunc.Result` interface. // It's being used as an alternative return value which // wraps the status code, the content type, a content as bytes or as string // and an error, it's smart enough to complete the request and send the correct response to the client. type Response struct { Code int ContentType string Content []byte // If not empty then content type is the "text/plain" // and content is the text as []byte. If not empty and // the "Lang" field is not empty then this "Text" field // becomes the current locale file's key. Text string // If not empty then "Text" field becomes the locale file's key that should point // to a translation file's unique key. See `Object` for locale template data. // The "Lang" field is the language code // that should render the text inside the locale file's key. Lang string // If not nil then it will fire that as "application/json" or any // previously set "ContentType". If "Lang" and "Text" are not empty // then this "Object" field becomes the template data that the // locale text should use to be rendered. Object any // If Path is not empty then it will redirect // the client to this Path, if Code is >= 300 and < 400 // then it will use that Code to do the redirection, otherwise // StatusFound(302) or StatusSeeOther(303) for post methods will be used. // Except when err != nil. Path string // if not empty then fire a 400 bad request error // unless the Status is > 200, then fire that error code // with the Err.Error() string as its content. // // if Err.Error() is empty then it fires the custom error handler // if any otherwise the framework sends the default http error text based on the status. Err error Try func() int // if true then it skips everything else and it throws a 404 not found error. // Can be named as Failure but NotFound is more precise name in order // to be visible that it's different than the `Err` // because it throws a 404 not found instead of a 400 bad request. // NotFound bool // let's don't add this yet, it has its dangerous of missuse. } var _ Result = Response{} // Dispatch writes the response result to the context's response writer. func (r Response) Dispatch(ctx *context.Context) { if dispatchErr(ctx, r.Code, r.Err) { return } if r.Path != "" { // it's not a redirect valid status if r.Code < 300 || r.Code >= 400 { if ctx.Method() == "POST" { r.Code = 303 // StatusSeeOther } r.Code = 302 // StatusFound } ctx.Redirect(r.Path, r.Code) return } if r.Text != "" { if r.Lang != "" { if r.Code > 0 { ctx.StatusCode(r.Code) } ctx.ContentType(r.ContentType) ctx.SetLanguage(r.Lang) r.Content = []byte(ctx.Tr(r.Text, r.Object)) } else { r.Content = []byte(r.Text) } } err := dispatchCommon(ctx, r.Code, r.ContentType, r.Content, r.Object, defaultResultHandler, true) dispatchErr(ctx, r.Code, err) } // View completes the `hero.Result` interface. // It's being used as an alternative return value which // wraps the template file name, layout, (any) view data, status code and error. // It's smart enough to complete the request and send the correct response to the client. // // Example at: https://github.com/kataras/iris/blob/main/_examples/dependency-injection/overview/web/routes/hello.go. type View struct { Name string Layout string Data any // map or a custom struct. Code int Err error } var _ Result = View{} // Dispatch writes the template filename, template layout and (any) data to the client. // Completes the `Result` interface. func (r View) Dispatch(ctx *context.Context) { // r as Response view. if dispatchErr(ctx, r.Code, r.Err) { return } if r.Code > 0 { ctx.StatusCode(r.Code) } if r.Name != "" { if r.Layout != "" { ctx.ViewLayout(r.Layout) } if r.Data != nil { // In order to respect any c.Ctx.ViewData that may called manually before; dataKey := ctx.Application().ConfigurationReadOnly().GetViewDataContextKey() if ctx.Values().Get(dataKey) == nil { // if no c.Ctx.ViewData set-ed before (the most common scenario) then do a // simple set, it's faster. ctx.Values().Set(dataKey, r.Data) } else { // else check if r.Data is map or struct, if struct convert it to map, // do a range loop and modify the data one by one. // context.Map is actually a map[string]any but we have to make that check: if m, ok := r.Data.(context.Map); ok { setViewData(ctx, m) } else if reflect.Indirect(reflect.ValueOf(r.Data)).Kind() == reflect.Struct { setViewData(ctx, structs.Map(r)) } } } _ = ctx.View(r.Name) } } func setViewData(ctx *context.Context, data map[string]any) { for k, v := range data { ctx.ViewData(k, v) } } ================================================ FILE: hero/func_result_test.go ================================================ package hero_test import ( "errors" "fmt" "net/http" "testing" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/httptest" . "github.com/kataras/iris/v12/hero" ) func GetText() string { return "text" } func GetStatus() int { return iris.StatusBadGateway } func GetTextWithStatusOk() (string, int) { return "OK", iris.StatusOK } // tests should have output arguments mixed func GetStatusWithTextNotOkBy(first string, second string) (int, string) { return iris.StatusForbidden, "NOT_OK_" + first + second } func GetTextAndContentType() (string, string) { return "text", "text/html" } type testCustomResult struct { HTML string } // The only one required function to make that a custom Response dispatcher. func (r testCustomResult) Dispatch(ctx iris.Context) { _, _ = ctx.HTML(r.HTML) } func GetCustomResponse() testCustomResult { return testCustomResult{"text"} } func GetCustomResponseWithStatusOk() (testCustomResult, int) { return testCustomResult{"OK"}, iris.StatusOK } func GetCustomResponseWithStatusNotOk() (testCustomResult, int) { return testCustomResult{"internal server error"}, iris.StatusInternalServerError } type testCustomStruct struct { Name string `json:"name" xml:"name"` Age int `json:"age" xml:"age"` } func GetCustomStruct() testCustomStruct { return testCustomStruct{"Iris", 2} } func GetCustomStructWithStatusNotOk() (testCustomStruct, int) { return testCustomStruct{"Iris", 2}, iris.StatusInternalServerError } func GetCustomStructWithContentType() (testCustomStruct, string) { return testCustomStruct{"Iris", 2}, "text/xml" } func GetCustomStructWithError(ctx iris.Context) (s testCustomStruct, err error) { s = testCustomStruct{"Iris", 2} if ctx.URLParamExists("err") { err = errors.New("omit return of testCustomStruct and fire error") } // it should send the testCustomStruct as JSON if error is nil // otherwise it should fire the default error(BadRequest) with the error's text. return } type err struct { Status int `json:"status_code"` Message string `json:"message"` } func (e err) Dispatch(ctx iris.Context) { // write the status code based on the err's StatusCode. ctx.StatusCode(e.Status) // send to the client the whole object as json _ = ctx.JSON(e) } func GetCustomErrorAsDispatcher() err { return err{iris.StatusBadRequest, "this is my error as json"} } func GetCustomTypedNilEmptyResponse() iris.Map { return nil } func GetCustomTypedPtrNilEmptyResponse() *iris.Map { return nil } func GetCustomMapNilEmptyResponse() map[string]any { return nil } func GetCustomPtrStructNilEmptyResponse() *testCustomStruct { return nil } func TestFuncResult(t *testing.T) { app := iris.New() h := New() // for any 'By', by is not required but we use this suffix here, like controllers // to make it easier for the future to resolve if any bug. // add the binding for path parameters. app.Get("/text", h.Handler(GetText)) app.Get("/status", h.Handler(GetStatus)) app.Get("/text/with/status/ok", h.Handler(GetTextWithStatusOk)) app.Get("/status/with/text/not/ok/{first}/{second}", h.Handler(GetStatusWithTextNotOkBy)) app.Get("/text/and/content/type", h.Handler(GetTextAndContentType)) // app.Get("/custom/response", h.Handler(GetCustomResponse)) app.Get("/custom/response/with/status/ok", h.Handler(GetCustomResponseWithStatusOk)) app.Get("/custom/response/with/status/not/ok", h.Handler(GetCustomResponseWithStatusNotOk)) // app.Get("/custom/struct", h.Handler(GetCustomStruct)) app.Get("/custom/struct/with/status/not/ok", h.Handler(GetCustomStructWithStatusNotOk)) app.Get("/custom/struct/with/content/type", h.Handler(GetCustomStructWithContentType)) app.Get("/custom/struct/with/error", h.Handler(GetCustomStructWithError)) app.Get("/custom/error/as/dispatcher", h.Handler(GetCustomErrorAsDispatcher)) app.Get("/custom/nil/typed", h.Handler(GetCustomTypedNilEmptyResponse)) app.Get("/custom/nil/typed/ptr", h.Handler(GetCustomTypedPtrNilEmptyResponse)) app.Get("/custom/nil/map", h.Handler(GetCustomMapNilEmptyResponse)) app.Get("/custom/nil/struct", h.Handler(GetCustomPtrStructNilEmptyResponse)) e := httptest.New(t, app) e.GET("/text").Expect().Status(iris.StatusOK). Body().IsEqual("text") e.GET("/status").Expect().Status(iris.StatusBadGateway) e.GET("/text/with/status/ok").Expect().Status(iris.StatusOK). Body().IsEqual("OK") e.GET("/status/with/text/not/ok/first/second").Expect().Status(iris.StatusForbidden). Body().IsEqual("NOT_OK_firstsecond") // Author's note: <-- if that fails means that the last binder called for both input args, // see path_param_binder.go e.GET("/text/and/content/type").Expect().Status(iris.StatusOK). ContentType("text/html", "utf-8"). Body().IsEqual("text") e.GET("/custom/response").Expect().Status(iris.StatusOK). ContentType("text/html", "utf-8"). Body().IsEqual("text") e.GET("/custom/response/with/status/ok").Expect().Status(iris.StatusOK). ContentType("text/html", "utf-8"). Body().IsEqual("OK") e.GET("/custom/response/with/status/not/ok").Expect().Status(iris.StatusInternalServerError). ContentType("text/html", "utf-8"). Body().IsEqual("internal server error") expectedResultFromCustomStruct := map[string]any{ "name": "Iris", "age": 2, } e.GET("/custom/struct").Expect().Status(iris.StatusOK). JSON().IsEqual(expectedResultFromCustomStruct) e.GET("/custom/struct/with/status/not/ok").Expect().Status(iris.StatusInternalServerError). JSON().IsEqual(expectedResultFromCustomStruct) e.GET("/custom/struct/with/content/type").Expect().Status(iris.StatusOK). ContentType("text/xml", "utf-8") e.GET("/custom/struct/with/error").Expect().Status(iris.StatusOK). JSON().IsEqual(expectedResultFromCustomStruct) e.GET("/custom/struct/with/error").WithQuery("err", true).Expect(). Status(iris.StatusBadRequest). // the default status code if error is not nil // the content should be not JSON it should be the status code's text // it will fire the error's text Body().IsEqual("omit return of testCustomStruct and fire error") e.GET("/custom/error/as/dispatcher").Expect(). Status(iris.StatusBadRequest). // the default status code if error is not nil // the content should be not JSON it should be the status code's text // it will fire the error's text JSON().IsEqual(err{iris.StatusBadRequest, "this is my error as json"}) // its result is nil should give an empty response but content-type is set correctly. e.GET("/custom/nil/typed").Expect(). Status(iris.StatusOK).ContentType(context.ContentJSONHeaderValue).Body().IsEmpty() e.GET("/custom/nil/typed/ptr").Expect(). Status(iris.StatusOK).ContentType(context.ContentJSONHeaderValue).Body().IsEmpty() e.GET("/custom/nil/map").Expect(). Status(iris.StatusOK).ContentType(context.ContentJSONHeaderValue).Body().IsEmpty() e.GET("/custom/nil/struct").Expect(). Status(iris.StatusOK).ContentType(context.ContentJSONHeaderValue).Body().IsEmpty() } type ( testPreflightRequest struct { FailCode int `json:"fail_code"` // for the sake of the test. } testPreflightResponse struct { Code int Message string } ) func (r testPreflightResponse) Preflight(ctx iris.Context) error { if r.Code == httptest.StatusInternalServerError { return fmt.Errorf("custom error") } if r.Code > 0 { ctx.StatusCode(r.Code) } return nil } func TestPreflightResult(t *testing.T) { app := iris.New() c := New() handler := c.Handler(func(in testPreflightRequest) testPreflightResponse { return testPreflightResponse{Code: in.FailCode, Message: http.StatusText(in.FailCode)} }) app.Post("/", handler) handler2 := c.Handler(func(in testInput) (int, testOutput) { return httptest.StatusAccepted, testOutput{Name: in.Name} }) app.Post("/alternative", handler2) e := httptest.New(t, app) expected1 := testPreflightResponse{Code: httptest.StatusOK, Message: "OK"} e.POST("/").WithJSON(testPreflightRequest{FailCode: expected1.Code}). Expect().Status(httptest.StatusOK).JSON().IsEqual(expected1) expected2 := testPreflightResponse{Code: httptest.StatusBadRequest, Message: "Bad Request"} e.POST("/").WithJSON(testPreflightRequest{FailCode: expected2.Code}). Expect().Status(httptest.StatusBadRequest).JSON().IsEqual(expected2) // Test error returned from Preflight. e.POST("/").WithJSON(testPreflightRequest{FailCode: httptest.StatusInternalServerError}). Expect().Status(httptest.StatusBadRequest).Body().IsEqual("custom error") // Can be done without Preflight as the second output argument can be a status code. expected4 := testOutput{Name: "my_name"} e.POST("/alternative").WithJSON(testInput{expected4.Name}). Expect().Status(httptest.StatusAccepted).JSON().IsEqual(expected4) } func TestResponseErr(t *testing.T) { app := iris.New() var expectedErr = errors.New("response error") app.OnAnyErrorCode(func(ctx iris.Context) { err := ctx.GetErr() if err != expectedErr { t.Fatalf("expected error value does not match") } ctx.WriteString(err.Error()) }) app.ConfigureContainer().Get("/", func() Response { return Response{Code: iris.StatusBadGateway, Err: expectedErr} }) e := httptest.New(t, app) e.GET("/").Expect().Status(iris.StatusBadGateway).Body().IsEqual("response error") } ================================================ FILE: hero/handler.go ================================================ package hero import ( "fmt" "reflect" "github.com/kataras/iris/v12/context" ) type ( // ErrorHandler describes an interface to handle errors per hero handler and its dependencies. // // Handles non-nil errors return from a hero handler or a controller's method (see `getBindingsFor` and `Handler`) // the error may return from a request-scoped dependency too (see `Handler`). ErrorHandler interface { HandleError(*context.Context, error) } // ErrorHandlerFunc implements the `ErrorHandler`. // It describes the type defnition for an error function handler. ErrorHandlerFunc func(*context.Context, error) // Code is a special type for status code. // It's used for a builtin dependency to map the status code given by a previous // method or middleware. // Use a type like that in order to not conflict with any developer-registered // dependencies. // Alternatively: ctx.GetStatusCode(). Code int // Err is a special type for error stored in mvc responses or context. // It's used for a builtin dependency to map the error given by a previous // method or middleware. // Use a type like that in order to not conflict with any developer-registered // dependencies. // Alternatively: ctx.GetErr(). Err error ) // HandleError fires when a non-nil error returns from a request-scoped dependency at serve-time or the handler itself. func (fn ErrorHandlerFunc) HandleError(ctx *context.Context, err error) { fn(ctx, err) } // String implements the fmt.Stringer interface. // Returns the text corresponding to this status code, e.g. "Not Found". // Same as iris.StatusText(int(code)). func (code Code) String() string { return context.StatusText(int(code)) } // Value returns the underline int value. // Same as int(code). func (code Code) Value() int { return int(code) } var ( // ErrSeeOther may be returned from a dependency handler to skip a specific dependency // based on custom logic. ErrSeeOther = fmt.Errorf("see other") // ErrStopExecution may be returned from a dependency handler to stop // and return the execution of the function without error (it calls ctx.StopExecution() too). // It may be occurred from request-scoped dependencies as well. ErrStopExecution = fmt.Errorf("stop execution") ) var ( // DefaultErrStatusCode is the default error status code (400) // when the response contains a non-nil error or a request-scoped binding error occur. DefaultErrStatusCode = 400 // DefaultErrorHandler is the default error handler which is fired // when a function returns a non-nil error or a request-scoped dependency failed to binded. DefaultErrorHandler = ErrorHandlerFunc(func(ctx *context.Context, err error) { if err != ErrStopExecution { if status := ctx.GetStatusCode(); status == 0 || !context.StatusCodeNotSuccessful(status) { ctx.StatusCode(DefaultErrStatusCode) } _, _ = ctx.WriteString(err.Error()) } ctx.StopExecution() }) ) var ( irisHandlerType = reflect.TypeOf((*context.Handler)(nil)).Elem() irisHandlerFuncType = reflect.TypeOf(func(*context.Context) {}) ) func isIrisHandlerType(typ reflect.Type) bool { return typ == irisHandlerType || typ == irisHandlerFuncType } func makeHandler(fn any, c *Container, paramsCount int) context.Handler { if fn == nil { panic("makeHandler: function is nil") } // 0. A normal handler. if handler, ok := isHandler(fn); ok { return handler } // 1. A handler which returns just an error, handle it faster. if handlerWithErr, ok := isHandlerWithError(fn); ok { return func(ctx *context.Context) { if err := handlerWithErr(ctx); err != nil { c.GetErrorHandler(ctx).HandleError(ctx, err) } } } v := valueOf(fn) typ := v.Type() numIn := typ.NumIn() bindings := getBindingsForFunc(v, c.Dependencies, c.DisablePayloadAutoBinding, paramsCount) c.fillReport(context.HandlerName(fn), bindings) // Check if it's a function that accept zero or more dependencies // and returns an Iris Handler. if paramsCount <= 0 { // println(irisHandlerType.String()) if typ.NumOut() == 1 && isIrisHandlerType(typ.Out(0)) { inputs := getStaticInputs(bindings, numIn) if len(inputs) != numIn { panic(fmt.Sprintf("makeHandler: func(...) iris.Handler: expected %d function input parameters but fewer static dependencies matched (%d)", numIn, len(inputs))) } handler := v.Call(inputs)[0].Interface().(context.Handler) return handler } } resultHandler := defaultResultHandler for i, lidx := 0, len(c.resultHandlers)-1; i <= lidx; i++ { resultHandler = c.resultHandlers[lidx-i](resultHandler) } return func(ctx *context.Context) { inputs := make([]reflect.Value, numIn) for _, binding := range bindings { input, err := binding.Dependency.Handle(ctx, binding.Input) if err != nil { if err == ErrSeeOther { continue } // handled inside ErrorHandler. // else if err == ErrStopExecution { // ctx.StopExecution() // return // return without error. // } c.GetErrorHandler(ctx).HandleError(ctx, err) // return [13 Sep 2020, commented that in order to be able to // give end-developer the option not only to handle the error // but to skip it if necessary, example: // read form, unknown field, continue without StopWith, // the binder should bind the method's input argument and continue // without errors. See `mvc.TestErrorHandlerContinue` test.] } // If ~an error status code is set or~ execution has stopped // from within the dependency (something went wrong while validating the request), // then stop everything and let handler fire that status code. if ctx.IsStopped() /* || context.StatusCodeNotSuccessful(ctx.GetStatusCode())*/ { return } inputs[binding.Input.Index] = input } // fmt.Printf("For func: %s | valid input deps length(%d)\n", typ.String(), len(inputs)) // for idx, in := range inputs { // fmt.Printf("[%d] (%s) %#+v\n", idx, in.Type().String(), in.Interface()) // } outputs := v.Call(inputs) if err := dispatchFuncResult(ctx, outputs, resultHandler); err != nil { c.GetErrorHandler(ctx).HandleError(ctx, err) } } } func isHandler(fn any) (context.Handler, bool) { if handler, ok := fn.(context.Handler); ok { return handler, ok } if handler, ok := fn.(func(*context.Context)); ok { return handler, ok } return nil, false } func isHandlerWithError(fn any) (func(*context.Context) error, bool) { if handlerWithErr, ok := fn.(func(*context.Context) error); ok { return handlerWithErr, true } return nil, false } ================================================ FILE: hero/handler_test.go ================================================ package hero_test import ( "fmt" "testing" "github.com/kataras/iris/v12" . "github.com/kataras/iris/v12/hero" "github.com/kataras/iris/v12/httptest" ) // dynamic func type testUserStruct struct { ID int64 `json:"id" form:"id" url:"id"` Username string `json:"username" form:"username" url:"username"` } func testBinderFunc(ctx iris.Context) testUserStruct { id, _ := ctx.Params().GetInt64("id") username := ctx.Params().Get("username") return testUserStruct{ ID: id, Username: username, } } // service type ( // these testService and testServiceImpl could be in lowercase, unexported // but the `Say` method should be exported however we have those exported // because of the controller handler test. testService interface { Say(string) string } testServiceImpl struct { prefix string } ) func (s *testServiceImpl) Say(message string) string { return s.prefix + " " + message } var ( // binders, as user-defined testBinderFuncUserStruct = testBinderFunc testBinderService = &testServiceImpl{prefix: "say"} testBinderFuncParam = func(ctx iris.Context) string { return ctx.Params().Get("param") } // consumers // a context as first input arg, which is not needed to be binded manually, // and a user struct which is binded to the input arg by the #1 func(ctx) any binder. testConsumeUserHandler = func(ctx iris.Context, user testUserStruct) { ctx.JSON(user) } // just one input arg, the service which is binded by the #2 service binder. testConsumeServiceHandler = func(service testService) string { return service.Say("something") } // just one input arg, a standar string which is binded by the #3 func(ctx) any binder. testConsumeParamHandler = func(myParam string) string { return "param is: " + myParam } ) func TestHandler(t *testing.T) { Register(testBinderFuncUserStruct) Register(testBinderService) Register(testBinderFuncParam) var ( h1 = Handler(testConsumeUserHandler) h2 = Handler(testConsumeServiceHandler) h3 = Handler(testConsumeParamHandler) ) testAppWithHeroHandlers(t, h1, h2, h3) } func testAppWithHeroHandlers(t *testing.T, h1, h2, h3 iris.Handler) { app := iris.New() app.Get("/{id:int64}/{username:string}", h1) app.Get("/service", h2) app.Get("/param/{param:string}", h3) expectedUser := testUserStruct{ ID: 42, Username: "kataras", } e := httptest.New(t, app) // 1 e.GET(fmt.Sprintf("/%d/%s", expectedUser.ID, expectedUser.Username)).Expect().Status(httptest.StatusOK). JSON().IsEqual(expectedUser) // 2 e.GET("/service").Expect().Status(httptest.StatusOK). Body().IsEqual("say something") // 3 e.GET("/param/the_param_value").Expect().Status(httptest.StatusOK). Body().IsEqual("param is: the_param_value") } // TestBindFunctionAsFunctionInputArgument tests to bind // a whole dynamic function based on the current context // as an input argument in the hero handler's function. func TestBindFunctionAsFunctionInputArgument(t *testing.T) { app := iris.New() postsBinder := func(ctx iris.Context) func(string) string { return ctx.PostValue // or FormValue, the same here. } h := New(postsBinder).Handler(func(get func(string) string) string { // send the `ctx.PostValue/FormValue("username")` value // to the client. return get("username") }) app.Post("/", h) e := httptest.New(t, app) expectedUsername := "kataras" e.POST("/").WithFormField("username", expectedUsername). Expect().Status(iris.StatusOK).Body().IsEqual(expectedUsername) } func TestPayloadBinding(t *testing.T) { h := New() ptrHandler := h.Handler(func(input *testUserStruct /* ptr */) string { return input.Username }) valHandler := h.Handler(func(input testUserStruct) string { return input.Username }) h.GetErrorHandler = func(iris.Context) ErrorHandler { return ErrorHandlerFunc(func(ctx iris.Context, err error) { if iris.IsErrPath(err) { return // continue. } ctx.StopWithError(iris.StatusBadRequest, err) }) } app := iris.New() app.Get("/", ptrHandler) app.Post("/", ptrHandler) app.Post("/2", valHandler) e := httptest.New(t, app) // JSON e.POST("/").WithJSON(iris.Map{"username": "makis"}).Expect().Status(httptest.StatusOK).Body().IsEqual("makis") e.POST("/2").WithJSON(iris.Map{"username": "kataras"}).Expect().Status(httptest.StatusOK).Body().IsEqual("kataras") // FORM (url-encoded) e.POST("/").WithFormField("username", "makis").Expect().Status(httptest.StatusOK).Body().IsEqual("makis") // FORM (multipart) e.POST("/").WithMultipart().WithFormField("username", "makis").Expect().Status(httptest.StatusOK).Body().IsEqual("makis") // FORM: test ErrorHandler skip the ErrPath. e.POST("/").WithMultipart().WithFormField("username", "makis").WithFormField("unknown", "continue"). Expect().Status(httptest.StatusOK).Body().IsEqual("makis") // POST URL query. e.POST("/").WithQuery("username", "makis").Expect().Status(httptest.StatusOK).Body().IsEqual("makis") // GET URL query. e.GET("/").WithQuery("username", "makis").Expect().Status(httptest.StatusOK).Body().IsEqual("makis") } /* Author's notes: If aksed or required by my company, make the following test to pass but think downsides of code complexity and performance-cost before begin the implementation of it. - Dependencies without depending on other values can be named "root-level dependencies" - Dependencies could be linked (a new .DependsOn?) to a "root-level dependency"(or by theirs same-level deps too?) with much more control if "root-level dependencies" are named, e.g.: b.Register("db", &myDBImpl{}) b.Register("user_dep", func(db myDB) User{...}).DependsOn("db") b.Handler(func(user User) error{...}) b.Handler(func(ctx iris.Context, reuseDB myDB) {...}) Why linked over automatically? Because more than one dependency can implement the same input and end-user does not care about ordering the registered ones. Link with `DependsOn` SHOULD be optional, if exists then limit the available dependencies, `DependsOn` SHOULD accept comma-separated values, e.g. "db, otherdep" and SHOULD also work by calling it multiple times i.e `Depends("db").DependsOn("otherdep")`. Handlers should also be able to explicitly limit the list of their available dependencies per-handler, a `.DependsOn` feature SHOULD exist there too. Also, note that with the new implementation a `*hero.Input` value can be accepted on dynamic dependencies, that value contains an `Options.Dependencies` field which lists all the registered dependencies, so, in theory, end-developers could achieve same results by hand-code(inside the dependency's function body). 26 Feb 2020. Gerasimos Maropoulos ______________________________________________ 29 Feb 2020. It's done. */ type testMessage struct { Body string } type myMap map[string]*testMessage func TestDependentDependencies(t *testing.T) { b := New() b.Register(&testServiceImpl{prefix: "prefix:"}) b.Register(func(service testService) testMessage { return testMessage{Body: service.Say("it is a deep") + " dependency"} }) b.Register(myMap{"test": &testMessage{Body: "value"}}) var ( h1 = b.Handler(func(msg testMessage) string { return msg.Body }) h2 = b.Handler(func(reuse testService) string { return reuse.Say("message") }) h3 = b.Handler(func(m myMap) string { return m["test"].Body }) ) app := iris.New() app.Get("/h1", h1) app.Get("/h2", h2) app.Get("/h3", h3) e := httptest.New(t, app) e.GET("/h1").Expect().Status(httptest.StatusOK).Body().IsEqual("prefix: it is a deep dependency") e.GET("/h2").Expect().Status(httptest.StatusOK).Body().IsEqual("prefix: message") e.GET("/h3").Expect().Status(httptest.StatusOK).Body().IsEqual("value") } func TestHandlerPathParams(t *testing.T) { // See white box `TestPathParams` test too. // All cases should pass. app := iris.New() handler := func(id uint64) string { return fmt.Sprintf("%d", id) } app.Party("/users").ConfigureContainer(func(api *iris.APIContainer) { api.Get("/{id:uint64}", handler) }) app.Party("/editors/{id:uint64}").ConfigureContainer(func(api *iris.APIContainer) { api.Get("/", handler) }) // should receive the last one, as we expected only one useful for MVC (there is a similar test there too). app.ConfigureContainer().Get("/{ownerID:uint64}/book/{booKID:uint64}", handler) e := httptest.New(t, app) for _, testReq := range []*httptest.Request{ e.GET("/users/42"), e.GET("/editors/42"), e.GET("/1/book/42"), } { testReq.Expect().Status(httptest.StatusOK).Body().IsEqual("42") } } func TestRegisterDependenciesFromContext(t *testing.T) { // Tests serve-time struct dependencies through a common Iris middleware. app := iris.New() app.Use(func(ctx iris.Context) { ctx.RegisterDependency(testUserStruct{Username: "kataras"}) ctx.Next() }) app.Use(func(ctx iris.Context) { ctx.RegisterDependency(&testServiceImpl{prefix: "say"}) ctx.Next() }) app.ConfigureContainer(func(api *iris.APIContainer) { api.Get("/", func(u testUserStruct) string { return u.Username }) api.Get("/service", func(s *testServiceImpl) string { return s.Say("hello") }) // Note: we are not allowed to pass the service as an interface here // because the container will, correctly, panic because it will expect // a dependency to be registered before server ran. api.Get("/both", func(s *testServiceImpl, u testUserStruct) string { return s.Say(u.Username) }) api.Get("/non", func() string { return "nothing" }) }) e := httptest.New(t, app) e.GET("/").Expect().Status(httptest.StatusOK).Body().IsEqual("kataras") e.GET("/service").Expect().Status(httptest.StatusOK).Body().IsEqual("say hello") e.GET("/both").Expect().Status(httptest.StatusOK).Body().IsEqual("say kataras") e.GET("/non").Expect().Status(httptest.StatusOK).Body().IsEqual("nothing") } ================================================ FILE: hero/param_test.go ================================================ package hero import ( "testing" "github.com/kataras/iris/v12/context" ) func TestPathParams(t *testing.T) { got := "" h := New() handler := h.Handler(func(firstname string, lastname string) { got = firstname + lastname }) h.Register(func(ctx *context.Context) func() string { return func() string { return "" } }) handlerWithOther := h.Handler(func(f func() string, firstname string, lastname string) { got = f() + firstname + lastname }) handlerWithOtherBetweenThem := h.Handler(func(firstname string, f func() string, lastname string) { got = firstname + lastname }) ctx := context.NewContext(nil) ctx.Params().Set("firstname", "Gerasimos") ctx.Params().Set("lastname", "Maropoulos") handler(ctx) expected := "GerasimosMaropoulos" if got != expected { t.Fatalf("[0] expected the params 'firstname' + 'lastname' to be '%s' but got '%s'", expected, got) } got = "" handlerWithOther(ctx) expected = "GerasimosMaropoulos" if got != expected { t.Fatalf("[1] expected the params 'firstname' + 'lastname' to be '%s' but got '%s'", expected, got) } got = "" handlerWithOtherBetweenThem(ctx) expected = "GerasimosMaropoulos" if got != expected { t.Fatalf("[2] expected the params 'firstname' + 'lastname' to be '%s' but got '%s'", expected, got) } } ================================================ FILE: hero/reflect.go ================================================ package hero import ( "net" "reflect" "github.com/kataras/iris/v12/context" ) func valueOf(v any) reflect.Value { if val, ok := v.(reflect.Value); ok { // check if it's already a reflect.Value. return val } return reflect.ValueOf(v) } // indirectType returns the value of a pointer-type "typ". // If "typ" is a pointer, array, chan, map or slice it returns its Elem, // otherwise returns the "typ" as it is. func indirectType(typ reflect.Type) reflect.Type { switch typ.Kind() { case reflect.Ptr, reflect.Array, reflect.Chan, reflect.Map, reflect.Slice: return typ.Elem() } return typ } func goodVal(v reflect.Value) bool { switch v.Kind() { case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr, reflect.Interface, reflect.Slice: if v.IsNil() { return false } } return v.IsValid() } func isFunc(kindable interface{ Kind() reflect.Kind }) bool { return kindable.Kind() == reflect.Func } func isStructValue(v reflect.Value) bool { return indirectType(v.Type()).Kind() == reflect.Struct } // isBuiltin reports whether a reflect.Value is a builtin type func isBuiltinValue(v reflect.Value) bool { switch v.Type().Kind() { case reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128, reflect.Array, reflect.Chan, reflect.Map, reflect.Slice, reflect.String: return true default: return false } } var ( inputTyp = reflect.TypeOf((*Input)(nil)) errTyp = reflect.TypeOf((*error)(nil)).Elem() ipTyp = reflect.TypeOf(net.IP{}) ) // isError returns true if "typ" is type of `error`. func isError(typ reflect.Type) bool { return typ.Implements(errTyp) } func toError(v reflect.Value) error { if v.IsNil() { return nil } return v.Interface().(error) } var contextType = reflect.TypeOf((*context.Context)(nil)) // isContext returns true if the "typ" is a type of Context. func isContext(typ reflect.Type) bool { return typ == contextType } var errorHandlerTyp = reflect.TypeOf((*ErrorHandler)(nil)).Elem() func isErrorHandler(typ reflect.Type) bool { return typ.Implements(errorHandlerTyp) } var emptyValue reflect.Value func equalTypes(binding reflect.Type, input reflect.Type) bool { if binding == input { return true } // fmt.Printf("got: %s expected: %s\n", got.String(), expected.String()) // if accepts an interface, check if the given "got" type does // implement this "expected" user handler's input argument. if input.Kind() == reflect.Interface { // fmt.Printf("expected interface = %s and got to set on the arg is: %s\n", binding.String(), input.String()) // return input.Implements(binding) return binding.AssignableTo(input) } // dependency: func(...) any { return "string" } // expected input: string. if binding.Kind() == reflect.Interface { return input.AssignableTo(binding) } return false } func structFieldIgnored(f reflect.StructField) bool { if !f.Anonymous { return true // if not anonymous(embedded), ignore it. } if s := f.Tag.Get("ignore"); s == "true" { return true } if s := f.Tag.Get("stateless"); s == "true" { return true } return false } // all except non-zero. func lookupFields(elem reflect.Value, skipUnexported bool, onlyZeros bool, parentIndex []int) (fields []reflect.StructField, stateless int) { // Note: embedded pointers are not supported. // elem = reflect.Indirect(elem) elemTyp := elem.Type() if elemTyp.Kind() == reflect.Pointer { return } for i, n := 0, elem.NumField(); i < n; i++ { field := elemTyp.Field(i) fieldValue := elem.Field(i) // embed any fields from other structs. if indirectType(field.Type).Kind() == reflect.Struct { if structFieldIgnored(field) { stateless++ // don't skip the loop yet, e.g. iris.Context. } else { structFields, statelessN := lookupFields(fieldValue, skipUnexported, onlyZeros, append(parentIndex, i)) stateless += statelessN fields = append(fields, structFields...) continue } } if onlyZeros && !isZero(fieldValue) { continue } // skip unexported fields here. if isExported := field.PkgPath == ""; skipUnexported && !isExported { continue } index := []int{i} if len(parentIndex) > 0 { index = append(parentIndex, i) } tmp := make([]int, len(index)) copy(tmp, index) field.Index = tmp fields = append(fields, field) } return } func lookupNonZeroFieldValues(elem reflect.Value) (nonZeroFields []reflect.StructField) { fields, _ := lookupFields(elem, true, false, nil) for _, f := range fields { if structFieldIgnored(f) { continue // re-check here for ignored struct fields so we don't include them on dependencies. Non-zeroes fields can be static, even if they are functions. } if fieldVal := elem.FieldByIndex(f.Index); goodVal(fieldVal) && !isZero(fieldVal) { /* && f.Type.Kind() == reflect.Ptr &&*/ nonZeroFields = append(nonZeroFields, f) } } return } // isZero returns true if a value is nil. // Remember; fields to be checked should be exported otherwise it returns false. // Notes for users: // Boolean's zero value is false, even if not set-ed. // UintXX are not zero on 0 because they are pointers to. func isZero(v reflect.Value) bool { // switch v.Kind() { // case reflect.Struct: // zero := true // for i := 0; i < v.NumField(); i++ { // f := v.Field(i) // if f.Type().PkgPath() != "" { // continue // unexported. // } // zero = zero && isZero(f) // } // if typ := v.Type(); typ != nil && v.IsValid() { // f, ok := typ.MethodByName("IsZero") // // if not found // // if has input arguments (1 is for the value receiver, so > 1 for the actual input args) // // if output argument is not boolean // // then skip this IsZero user-defined function. // if !ok || f.Type.NumIn() > 1 || f.Type.NumOut() != 1 && f.Type.Out(0).Kind() != reflect.Bool { // return zero // } // method := v.Method(f.Index) // // no needed check but: // if method.IsValid() && !method.IsNil() { // // it shouldn't panic here. // zero = method.Call([]reflect.Value{})[0].Interface().(bool) // } // } // return zero // case reflect.Func, reflect.Map, reflect.Slice: // return v.IsNil() // case reflect.Array: // zero := true // for i := 0; i < v.Len(); i++ { // zero = zero && isZero(v.Index(i)) // } // return zero // } // if not any special type then use the reflect's .Zero // usually for fields, but remember if it's boolean and it's false // then it's zero, even if set-ed. if !v.CanInterface() { // if can't interface, i.e return value from unexported field or method then return false return false } if v.Type() == ipTyp { return len(v.Interface().(net.IP)) == 0 } // zero := reflect.Zero(v.Type()) // return v.Interface() == zero.Interface() return v.IsZero() } // IsNil same as `reflect.IsNil` but a bit safer to use, returns false if not a correct type. func isNil(v reflect.Value) bool { k := v.Kind() switch k { case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr, reflect.UnsafePointer, reflect.Interface, reflect.Slice: return v.IsNil() default: return false } } ================================================ FILE: hero/reflect_test.go ================================================ package hero import ( "reflect" "testing" ) type testInterface interface { Get() string } var testInterfaceTyp = reflect.TypeOf((*testInterface)(nil)).Elem() type testImplPtr struct{} func (*testImplPtr) Get() string { return "get_ptr" } type testImpl struct{} func (testImpl) Get() string { return "get" } func TestEqualTypes(t *testing.T) { of := reflect.TypeOf var tests = map[reflect.Type]reflect.Type{ of("string"): of("input"), of(42): of(10), testInterfaceTyp: testInterfaceTyp, of(new(testImplPtr)): testInterfaceTyp, of(new(testImpl)): testInterfaceTyp, of(testImpl{}): testInterfaceTyp, } for binding, input := range tests { if !equalTypes(binding, input) { t.Fatalf("expected type of: %s to be equal to the binded one of: %s", input, binding) } } } ================================================ FILE: hero/struct.go ================================================ package hero import ( "fmt" "reflect" "github.com/kataras/iris/v12/context" ) // Sorter is the type for sort customization of a struct's fields // and its available bindable values. // // Sorting applies only when a field can accept more than one registered value. type Sorter func(t1 reflect.Type, t2 reflect.Type) bool // sortByNumMethods is a builtin sorter to sort fields and values // based on their type and its number of methods, highest number of methods goes first. // // It is the default sorter on struct injector of `hero.Struct` method. var sortByNumMethods Sorter = func(t1 reflect.Type, t2 reflect.Type) bool { if t1.Kind() != t2.Kind() { return true } if k := t1.Kind(); k == reflect.Interface || k == reflect.Struct { return t1.NumMethod() > t2.NumMethod() } else if k != reflect.Struct { return false // non-structs goes last. } return true } // Struct keeps a record of a particular struct value injection. // See `Container.Struct` and `mvc#Application.Handle` methods. type Struct struct { ptrType reflect.Type ptrValue reflect.Value // the original ptr struct value. elementType reflect.Type // the original struct type. bindings []*binding // struct field bindings. Container *Container Singleton bool } type singletonStruct interface { Singleton() bool } func isMarkedAsSingleton(structPtr any) bool { if sing, ok := structPtr.(singletonStruct); ok && sing.Singleton() { return true } return false } func makeStruct(structPtr any, c *Container, partyParamsCount int) *Struct { v := valueOf(structPtr) typ := v.Type() if typ.Kind() != reflect.Ptr || indirectType(typ).Kind() != reflect.Struct { panic("binder: struct: should be a pointer to a struct value") } isSingleton := isMarkedAsSingleton(structPtr) disablePayloadAutoBinding := c.DisablePayloadAutoBinding enableStructDependents := c.EnableStructDependents disableStructDynamicBindings := c.DisableStructDynamicBindings if isSingleton { disablePayloadAutoBinding = true enableStructDependents = false disableStructDynamicBindings = true } // get struct's fields bindings. bindings := getBindingsForStruct(v, c.Dependencies, c.MarkExportedFieldsAsRequired, disablePayloadAutoBinding, enableStructDependents, c.DependencyMatcher, partyParamsCount, c.Sorter) // length bindings of 0, means that it has no fields or all mapped deps are static. // If static then Struct.Acquire will return the same "value" instance, otherwise it will create a new one. singleton := true elem := v.Elem() // fmt.Printf("Service: %s, Bindings(%d):\n", typ, len(bindings)) for _, b := range bindings { // fmt.Printf("* " + b.String() + "\n") if b.Dependency.Static { // Fill now. input, err := b.Dependency.Handle(nil, b.Input) if err != nil { if err == ErrSeeOther { continue } panic(err) } elem.FieldByIndex(b.Input.StructFieldIndex).Set(input) } else if !b.Dependency.Static { if disableStructDynamicBindings { panic(fmt.Sprintf("binder: DisableStructDynamicBindings setting is set to true: dynamic binding found: %s", b.String())) } singleton = false } } if isSingleton && !singleton { panic(fmt.Sprintf("binder: Singleton setting is set to true but struct has dynamic bindings: %s", typ)) } s := &Struct{ ptrValue: v, ptrType: typ, elementType: elem.Type(), bindings: bindings, Singleton: singleton, } isErrHandler := isErrorHandler(typ) newContainer := c.Clone() newContainer.fillReport(typ.String(), bindings) // Add the controller dependency itself as func dependency but with a known type which should be explicit binding // in order to keep its maximum priority. newContainer.Register(s.Acquire).Explicitly().DestType = typ newContainer.GetErrorHandler = func(ctx *context.Context) ErrorHandler { if isErrHandler { return ctx.Controller().Interface().(ErrorHandler) } return c.GetErrorHandler(ctx) } s.Container = newContainer return s } // Acquire returns a struct value based on the request. // If the dependencies are all static then these are already set-ed at the initialization of this Struct // and the same struct value instance will be returned, ignoring the Context. Otherwise // a new struct value with filled fields by its pre-calculated bindings will be returned instead. func (s *Struct) Acquire(ctx *context.Context) (reflect.Value, error) { if s.Singleton { ctx.Values().Set(context.ControllerContextKey, s.ptrValue) return s.ptrValue, nil } ctrl := ctx.Controller() if ctrl.Kind() == reflect.Invalid || ctrl.Type() != s.ptrType /* in case of changing controller in the same request (see RouteOverlap feature) */ { ctrl = reflect.New(s.elementType) ctx.Values().Set(context.ControllerContextKey, ctrl) elem := ctrl.Elem() for _, b := range s.bindings { input, err := b.Dependency.Handle(ctx, b.Input) if err != nil { if err == ErrSeeOther { continue } s.Container.GetErrorHandler(ctx).HandleError(ctx, err) if ctx.IsStopped() { // return emptyValue, err return ctrl, err } // #1629 } elem.FieldByIndex(b.Input.StructFieldIndex).Set(input) } } return ctrl, nil } // MethodHandler accepts a "methodName" that should be a valid an exported // method of the struct and returns its converted Handler. // // Second input is optional, // even zero is a valid value and can resolve path parameters correctly if from root party. func (s *Struct) MethodHandler(methodName string, paramsCount int) context.Handler { m, ok := s.ptrValue.Type().MethodByName(methodName) if !ok { panic(fmt.Sprintf("struct: method: %s does not exist", methodName)) } return makeHandler(m.Func, s.Container, paramsCount) } ================================================ FILE: hero/struct_test.go ================================================ package hero_test import ( "errors" "fmt" "testing" "github.com/kataras/iris/v12" . "github.com/kataras/iris/v12/hero" "github.com/kataras/iris/v12/httptest" ) type testStruct struct { Ctx iris.Context } func (c *testStruct) MyHandler(name string) testOutput { return fn(42, testInput{Name: name}) } func (c *testStruct) MyHandler2(id int, in testInput) testOutput { return fn(id, in) } func (c *testStruct) MyHandler3(in testInput) testOutput { return fn(42, in) } func (c *testStruct) MyHandler4() { c.Ctx.WriteString("MyHandler4") } func TestStruct(t *testing.T) { app := iris.New() b := New() s := b.Struct(&testStruct{}, 0) postHandler := s.MethodHandler("MyHandler", 0) // fallbacks such as {path} and {string} should registered first when same path. app.Post("/{name:string}", postHandler) postHandler2 := s.MethodHandler("MyHandler2", 0) app.Post("/{id:int}", postHandler2) postHandler3 := s.MethodHandler("MyHandler3", 0) app.Post("/myHandler3", postHandler3) getHandler := s.MethodHandler("MyHandler4", 0) app.Get("/myHandler4", getHandler) e := httptest.New(t, app) e.POST("/" + input.Name).Expect().Status(httptest.StatusOK).JSON().IsEqual(expectedOutput) path := fmt.Sprintf("/%d", expectedOutput.ID) e.POST(path).WithJSON(input).Expect().Status(httptest.StatusOK).JSON().IsEqual(expectedOutput) e.POST("/myHandler3").WithJSON(input).Expect().Status(httptest.StatusOK).JSON().IsEqual(expectedOutput) e.GET("/myHandler4").Expect().Status(httptest.StatusOK).Body().IsEqual("MyHandler4") } type testStructErrorHandler struct{} func (s *testStructErrorHandler) HandleError(ctx iris.Context, err error) { ctx.StopWithError(httptest.StatusConflict, err) } func (s *testStructErrorHandler) Handle(errText string) error { return errors.New(errText) } func TestStructErrorHandler(t *testing.T) { b := New() s := b.Struct(&testStructErrorHandler{}, 0) app := iris.New() app.Get("/{errText:string}", s.MethodHandler("Handle", 0)) expectedErrText := "an error" e := httptest.New(t, app) e.GET("/" + expectedErrText).Expect().Status(httptest.StatusConflict).Body().IsEqual(expectedErrText) } type ( testServiceInterface1 interface { Parse() string } testServiceImpl1 struct { inner string } testServiceInterface2 interface { } testServiceImpl2 struct { tf int } testControllerDependenciesSorter struct { Service2 testServiceInterface2 Service1 testServiceInterface1 } ) func (s *testServiceImpl1) Parse() string { return s.inner } func (c *testControllerDependenciesSorter) Index() string { return fmt.Sprintf("%#+v | %#+v", c.Service1, c.Service2) } func TestStructFieldsSorter(t *testing.T) { // see https://github.com/kataras/iris/issues/1343 b := New() b.Register(&testServiceImpl1{"parser"}) b.Register(&testServiceImpl2{24}) s := b.Struct(&testControllerDependenciesSorter{}, 0) app := iris.New() app.Get("/", s.MethodHandler("Index", 0)) e := httptest.New(t, app) expectedBody := `&hero_test.testServiceImpl1{inner:"parser"} | &hero_test.testServiceImpl2{tf:24}` e.GET("/").Expect().Status(httptest.StatusOK).Body().IsEqual(expectedBody) } ================================================ FILE: httptest/aliases.go ================================================ package httptest import "github.com/iris-contrib/httpexpect/v2" type ( // Request type alias. Request = httpexpect.Request // Expect type alias. Expect = httpexpect.Expect ) ================================================ FILE: httptest/httptest.go ================================================ package httptest import ( "crypto/tls" "net/http" "net/http/httptest" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/core/router" "github.com/kataras/iris/v12/i18n" "github.com/iris-contrib/httpexpect/v2" ) type ( // OptionSetter sets a configuration field to the configuration OptionSetter interface { // Set receives a pointer to the Configuration type and does the job of filling it Set(c *Configuration) } // OptionSet implements the OptionSetter OptionSet func(c *Configuration) ) // Set is the func which makes the OptionSet an OptionSetter, this is used mostly func (o OptionSet) Set(c *Configuration) { o(c) } // Configuration httptest configuration type Configuration struct { // URL the base url. // Defaults to empty string "". URL string // Debug if true then debug messages from the httpexpect will be shown when a test runs // Defaults to false. Debug bool // LogLevel sets the application's log level. // Defaults to "disable" when testing. LogLevel string // If true then the underline httpexpect report will be acquired by the NewRequireReporter // call instead of the default NewAssertReporter. // Defaults to false. Strict bool // Note: if more reports are available in the future then add a Reporter interface as a field. } // Set implements the OptionSetter for the Configuration itself func (c Configuration) Set(main *Configuration) { main.URL = c.URL main.Debug = c.Debug if c.LogLevel != "" { main.LogLevel = c.LogLevel } main.Strict = c.Strict } var ( // URL if set then it sets the httptest's BaseURL. // Defaults to empty string "". URL = func(schemeAndHost string) OptionSet { return func(c *Configuration) { c.URL = schemeAndHost } } // Debug if true then debug messages from the httpexpect will be shown when a test runs // Defaults to false. Debug = func(val bool) OptionSet { return func(c *Configuration) { c.Debug = val } } // LogLevel sets the application's log level. // Defaults to disabled when testing. LogLevel = func(level string) OptionSet { return func(c *Configuration) { c.LogLevel = level } } // Strict sets the Strict configuration field to "val". // Applies the NewRequireReporter instead of the default one. // Use this if you want the test to fail on first error, before all checks have been done. Strict = func(val bool) OptionSet { return func(c *Configuration) { c.Strict = val } } ) // DefaultConfiguration returns the default configuration for the httptest. func DefaultConfiguration() *Configuration { return &Configuration{URL: "", Debug: false, LogLevel: "disable"} } // New Prepares and returns a new test framework based on the "app". // Usage: // // httptest.New(t, app) // // With options: // // httptest.New(t, app, httptest.URL(...), httptest.Debug(true), httptest.LogLevel("debug"), httptest.Strict(true)) // // Examples at: https://github.com/kataras/iris/tree/main/_examples/testing/httptest and // https://github.com/kataras/iris/tree/main/_examples/testing/ginkgotest. func New(t IrisTesty, app *iris.Application, setters ...OptionSetter) *httpexpect.Expect { conf := DefaultConfiguration() for _, setter := range setters { setter.Set(conf) } // set the logger or disable it (default). app.Logger().SetLevel(conf.LogLevel) if err := app.Build(); err != nil { if conf.LogLevel != "disable" { app.Logger().Println(err.Error()) return nil } } var reporter httpexpect.Reporter if conf.Strict { reporter = httpexpect.NewRequireReporter(t) } else { reporter = httpexpect.NewAssertReporter(t) } testConfiguration := httpexpect.Config{ BaseURL: conf.URL, Client: &http.Client{ Transport: httpexpect.NewBinder(app), Jar: httpexpect.NewCookieJar(), }, Reporter: reporter, } if conf.Debug { testConfiguration.Printers = []httpexpect.Printer{ httpexpect.NewDebugPrinter(t, true), } } return httpexpect.WithConfig(testConfiguration) } // NewInsecure same as New but receives a single host instead of the whole framework. // Useful for testing running TLS servers. func NewInsecure(t IrisTesty, setters ...OptionSetter) *httpexpect.Expect { conf := DefaultConfiguration() for _, setter := range setters { setter.Set(conf) } transport := &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true, MinVersion: tls.VersionTLS13}, // lint:ignore } testConfiguration := httpexpect.Config{ BaseURL: conf.URL, Client: &http.Client{ Transport: transport, Jar: httpexpect.NewCookieJar(), }, Reporter: httpexpect.NewAssertReporter(t), } if conf.Debug { testConfiguration.Printers = []httpexpect.Printer{ httpexpect.NewDebugPrinter(t, true), } } return httpexpect.WithConfig(testConfiguration) } // Aliases for "net/http/httptest" package. See `Do` package-level function. var ( NewRecorder = httptest.NewRecorder NewRequest = httptest.NewRequest ) // Do is a simple helper which can be used to test handlers individually // with the "net/http/httptest" package. // This package contains aliases for `NewRequest` and `NewRecorder` too. // // For a more efficient testing please use the `New` function instead. func Do(w http.ResponseWriter, r *http.Request, handler iris.Handler, irisConfigurators ...iris.Configurator) { app := new(iris.Application) app.I18n = i18n.New() app.Configure(iris.WithConfiguration(iris.DefaultConfiguration()), iris.WithLogLevel("disable")) app.Configure(irisConfigurators...) app.HTTPErrorHandler = router.NewDefaultHandler(app.ConfigurationReadOnly(), app.Logger()) app.ContextPool = context.New(func() any { return context.NewContext(app) }) ctx := app.ContextPool.Acquire(w, r) handler(ctx) app.ContextPool.Release(ctx) } // IrisTesty is an interface which all testing package should implement. // The `httptest` standard package and `ginkgo` third-party module do implement this interface indeed. // // See the `New` package-level function for more. type IrisTesty interface { Cleanup(func()) Error(args ...any) Errorf(format string, args ...any) Fail() FailNow() Failed() bool Fatal(args ...any) Fatalf(format string, args ...any) Helper() Log(args ...any) Logf(format string, args ...any) Name() string Setenv(key, value string) Skip(args ...any) SkipNow() Skipf(format string, args ...any) Skipped() bool TempDir() string } ================================================ FILE: httptest/netutils.go ================================================ package httptest import ( "crypto/tls" "net" ) // copied from net/http/httptest/internal // LocalhostCert is a PEM-encoded TLS cert with SAN IPs // "127.0.0.1" and "[::1]", expiring at Jan 29 16:00:00 2084 GMT. // generated from src/crypto/tls: // go run generate_cert.go --rsa-bits 1024 --host 127.0.0.1,::1,example.com --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h // note: these are not the net/http/httptest/internal contents but doesn't matter. var LocalhostCert = []byte(`-----BEGIN CERTIFICATE----- MIIDAzCCAeugAwIBAgIJAP0pWSuIYyQCMA0GCSqGSIb3DQEBBQUAMBgxFjAUBgNV BAMMDWxvY2FsaG9zdDozMzEwHhcNMTYxMjI1MDk1OTI3WhcNMjYxMjIzMDk1OTI3 WjAYMRYwFAYDVQQDDA1sb2NhbGhvc3Q6MzMxMIIBIjANBgkqhkiG9w0BAQEFAAOC AQ8AMIIBCgKCAQEA5vETjLa+8W856rWXO1xMF/CLss9vn5xZhPXKhgz+D7ogSAXm mWP53eeBUGC2r26J++CYfVqwOmfJEu9kkGUVi8cGMY9dHeIFPfxD31MYX175jJQe tu0WeUII7ciNsSUDyBMqsl7yi1IgN7iLONM++1+QfbbmNiEbghRV6icEH6M+bWlz 3YSAMEdpK3mg2gsugfLKMwJkaBKEehUNMySRlIhyLITqt1exYGaggRd1zjqUpqpD sL2sRVHJ3qHGkSh8nVy8MvG8BXiFdYQJP3mCQDZzruCyMWj5/19KAyu7Cto3Bcvu PgujnwRtU+itt8WhZUVtU1n7Ivf6lMJTBcc4OQIDAQABo1AwTjAdBgNVHQ4EFgQU MXrBvbILQmiwjUj19aecF2N+6IkwHwYDVR0jBBgwFoAUMXrBvbILQmiwjUj19aec F2N+6IkwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEA4zbFml1t9KXJ OijAV8gALePR8v04DQwJP+jsRxXw5zzhc8Wqzdd2hjUd07mfRWAvmyywrmhCV6zq OHznR+aqIqHtm0vV8OpKxLoIQXavfBd6axEXt3859RDM4xJNwIlxs3+LWGPgINud wjJqjyzSlhJpQpx4YZ5Da+VMiqAp8N1UeaZ5lBvmSDvoGh6HLODSqtPlWMrldRW9 AfsXVxenq81MIMeKW2fSOoPnWZ4Vjf1+dSlbJE/DD4zzcfbyfgY6Ep/RrUltJ3ag FQbuNTQlgKabe21dSL9zJ2PengVKXl4Trl+4t/Kina9N9Jw535IRCSwinD6a/2Ca m7DnVXFiVA== -----END CERTIFICATE----- `) // LocalhostKey is the private key for localhostCert. var LocalhostKey = []byte(`-----BEGIN RSA PRIVATE KEY----- MIIEpAIBAAKCAQEA5vETjLa+8W856rWXO1xMF/CLss9vn5xZhPXKhgz+D7ogSAXm mWP53eeBUGC2r26J++CYfVqwOmfJEu9kkGUVi8cGMY9dHeIFPfxD31MYX175jJQe tu0WeUII7ciNsSUDyBMqsl7yi1IgN7iLONM++1+QfbbmNiEbghRV6icEH6M+bWlz 3YSAMEdpK3mg2gsugfLKMwJkaBKEehUNMySRlIhyLITqt1exYGaggRd1zjqUpqpD sL2sRVHJ3qHGkSh8nVy8MvG8BXiFdYQJP3mCQDZzruCyMWj5/19KAyu7Cto3Bcvu PgujnwRtU+itt8WhZUVtU1n7Ivf6lMJTBcc4OQIDAQABAoIBAQCTLE0eHpPevtg0 +FaRUMd5diVA5asoF3aBIjZXaU47bY0G+SO02x6wSMmDFK83a4Vpy/7B3Bp0jhF5 DLCUyKaLdmE/EjLwSUq37ty+JHFizd7QtNBCGSN6URfpmSabHpCjX3uVQqblHIhF mki3BQCdJ5CoXPemxUCHjDgYSZb6JVNIPJExjekc0+4A2MYWMXV6Wr86C7AY3659 KmveZpC3gOkLA/g/IqDQL/QgTq7/3eloHaO+uPBihdF56do4eaOO0jgFYpl8V7ek PZhHfhuPZV3oq15+8Vt77ngtjUWVI6qX0E3ilh+V5cof+03q0FzHPVe3zBUNXcm0 OGz19u/FAoGBAPSm4Aa4xs/ybyjQakMNix9rak66ehzGkmlfeK5yuQ/fHmTg8Ac+ ahGs6A3lFWQiyU6hqm6Qp0iKuxuDh35DJGCWAw5OUS/7WLJtu8fNFch6iIG29rFs s+Uz2YLxJPebpBsKymZUp7NyDRgEElkiqsREmbYjLrc8uNKkDy+k14YnAoGBAPGn ZlN0Mo5iNgQStulYEP5pI7WOOax9KOYVnBNguqgY9c7fXVXBxChoxt5ebQJWG45y KPG0hB0bkA4YPu4bTRf5acIMpjFwcxNlmwdc4oCkT4xqAFs9B/AKYZgkf4IfKHqW P9PD7TbUpkaxv25bPYwUSEB7lPa+hBtRyN9Wo6qfAoGAPBkeISiU1hJE0i7YW55h FZfKZoqSYq043B+ywo+1/Dsf+UH0VKM1ZSAnZPpoVc/hyaoW9tAb98r0iZ620wJl VkCjgYklknbY5APmw/8SIcxP6iVq1kzQqDYjcXIRVa3rEyWEcLzM8VzL8KFXbIQC lPIRHFfqKuMEt+HLRTXmJ7MCgYAHGvv4QjdmVl7uObqlG9DMGj1RjlAF0VxNf58q NrLmVG2N2qV86wigg4wtZ6te4TdINfUcPkmQLYpLz8yx5Z2bsdq5OPP+CidoD5nC WqnSTIKGR2uhQycjmLqL5a7WHaJsEFTqHh2wego1k+5kCUzC/KmvM7MKmkl6ICp+ 3qZLUwKBgQCDOhKDwYo1hdiXoOOQqg/LZmpWOqjO3b4p99B9iJqhmXN0GKXIPSBh 5nqqmGsG8asSQhchs7EPMh8B80KbrDTeidWskZuUoQV27Al1UEmL6Zcl83qXD6sf k9X9TwWyZtp5IL1CAEd/Il9ZTXFzr3lNaN8LCFnU+EIsz1YgUW8LTg== -----END RSA PRIVATE KEY----- `) // NewLocalListener returns a new ipv4 "127.0.0.1:0" // or tcp6 "[::1]:0" tcp listener. func NewLocalListener() net.Listener { l, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { if l, err = net.Listen("tcp6", "[::1]:0"); err != nil { panic(err) } } return l } // NewLocalTLSListener returns a new tls listener // based on the "tcpListener", if "tcpListener" is nil // it make use of the `NewLocalListener`. // Cert and Key are `LocalhostCert` and `LocalhostKey` respectfully. func NewLocalTLSListener(tcpListener net.Listener) net.Listener { if tcpListener == nil { tcpListener = NewLocalListener() } cert, err := tls.X509KeyPair(LocalhostCert, LocalhostKey) if err != nil { panic(err) } cfg := new(tls.Config) cfg.NextProtos = []string{"http/1.1"} cfg.Certificates = []tls.Certificate{cert} cfg.InsecureSkipVerify = true return tls.NewListener(tcpListener, cfg) } ================================================ FILE: httptest/server.go ================================================ package httptest import ( "net/http/httptest" "github.com/kataras/iris/v12" ) // NewServer is just a helper to create a new standard // httptest.Server instance. func NewServer(t IrisTesty, app *iris.Application) *httptest.Server { if err := app.Build(); err != nil { t.Fatal(err) return nil } return httptest.NewServer(app) } ================================================ FILE: httptest/status.go ================================================ package httptest // HTTP status codes as registered with IANA. // See: http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml // Raw Copy from the future(tip) net/http std package in order to recude the import path of "net/http" for the users. const ( StatusContinue = 100 // RFC 7231, 6.2.1 StatusSwitchingProtocols = 101 // RFC 7231, 6.2.2 StatusProcessing = 102 // RFC 2518, 10.1 StatusEarlyHints = 103 // RFC 8297 StatusOK = 200 // RFC 7231, 6.3.1 StatusCreated = 201 // RFC 7231, 6.3.2 StatusAccepted = 202 // RFC 7231, 6.3.3 StatusNonAuthoritativeInfo = 203 // RFC 7231, 6.3.4 StatusNoContent = 204 // RFC 7231, 6.3.5 StatusResetContent = 205 // RFC 7231, 6.3.6 StatusPartialContent = 206 // RFC 7233, 4.1 StatusMultiStatus = 207 // RFC 4918, 11.1 StatusAlreadyReported = 208 // RFC 5842, 7.1 StatusIMUsed = 226 // RFC 3229, 10.4.1 StatusMultipleChoices = 300 // RFC 7231, 6.4.1 StatusMovedPermanently = 301 // RFC 7231, 6.4.2 StatusFound = 302 // RFC 7231, 6.4.3 StatusSeeOther = 303 // RFC 7231, 6.4.4 StatusNotModified = 304 // RFC 7232, 4.1 StatusUseProxy = 305 // RFC 7231, 6.4.5 _ = 306 // RFC 7231, 6.4.6 (Unused) StatusTemporaryRedirect = 307 // RFC 7231, 6.4.7 StatusPermanentRedirect = 308 // RFC 7538, 3 StatusBadRequest = 400 // RFC 7231, 6.5.1 StatusUnauthorized = 401 // RFC 7235, 3.1 StatusPaymentRequired = 402 // RFC 7231, 6.5.2 StatusForbidden = 403 // RFC 7231, 6.5.3 StatusNotFound = 404 // RFC 7231, 6.5.4 StatusMethodNotAllowed = 405 // RFC 7231, 6.5.5 StatusNotAcceptable = 406 // RFC 7231, 6.5.6 StatusProxyAuthRequired = 407 // RFC 7235, 3.2 StatusRequestTimeout = 408 // RFC 7231, 6.5.7 StatusConflict = 409 // RFC 7231, 6.5.8 StatusGone = 410 // RFC 7231, 6.5.9 StatusLengthRequired = 411 // RFC 7231, 6.5.10 StatusPreconditionFailed = 412 // RFC 7232, 4.2 StatusRequestEntityTooLarge = 413 // RFC 7231, 6.5.11 StatusRequestURITooLong = 414 // RFC 7231, 6.5.12 StatusUnsupportedMediaType = 415 // RFC 7231, 6.5.13 StatusRequestedRangeNotSatisfiable = 416 // RFC 7233, 4.4 StatusExpectationFailed = 417 // RFC 7231, 6.5.14 StatusTeapot = 418 // RFC 7168, 2.3.3 StatusUnprocessableEntity = 422 // RFC 4918, 11.2 StatusLocked = 423 // RFC 4918, 11.3 StatusFailedDependency = 424 // RFC 4918, 11.4 StatusUpgradeRequired = 426 // RFC 7231, 6.5.15 StatusPreconditionRequired = 428 // RFC 6585, 3 StatusTooManyRequests = 429 // RFC 6585, 4 StatusRequestHeaderFieldsTooLarge = 431 // RFC 6585, 5 StatusUnavailableForLegalReasons = 451 // RFC 7725, 3 StatusInternalServerError = 500 // RFC 7231, 6.6.1 StatusNotImplemented = 501 // RFC 7231, 6.6.2 StatusBadGateway = 502 // RFC 7231, 6.6.3 StatusServiceUnavailable = 503 // RFC 7231, 6.6.4 StatusGatewayTimeout = 504 // RFC 7231, 6.6.5 StatusHTTPVersionNotSupported = 505 // RFC 7231, 6.6.6 StatusVariantAlsoNegotiates = 506 // RFC 2295, 8.1 StatusInsufficientStorage = 507 // RFC 4918, 11.5 StatusLoopDetected = 508 // RFC 5842, 7.2 StatusNotExtended = 510 // RFC 2774, 7 StatusNetworkAuthenticationRequired = 511 // RFC 6585, 6 ) ================================================ FILE: i18n/i18n.go ================================================ // Package i18n provides internalization and localization features for Iris. // To use with net/http see https://github.com/kataras/i18n instead. package i18n import ( "fmt" "io/fs" "net/http" "os" "strings" "sync" "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/core/router" "github.com/kataras/iris/v12/i18n/internal" "golang.org/x/text/language" ) type ( // MessageFunc is the function type to modify the behavior when a key or language was not found. // All language inputs fallback to the default locale if not matched. // This is why this signature accepts both input and matched languages, so caller // can provide better messages. // // The first parameter is set to the client real input of the language, // the second one is set to the matched language (default one if input wasn't matched) // and the third and forth are the translation format/key and its optional arguments. // // Note: we don't accept the Context here because Tr method and template func {{ tr }} // have no direct access to it. MessageFunc = internal.MessageFunc // Loader accepts a `Matcher` and should return a `Localizer`. // Functions that implement this type should load locale files. Loader func(m *Matcher) (Localizer, error) // Localizer is the interface which returned from a `Loader`. // Types that implement this interface should be able to retrieve a `Locale` // based on the language index. Localizer interface { // GetLocale should return a valid `Locale` based on the language index. // It will always match the Loader.Matcher.Languages[index]. // It may return the default language if nothing else matches based on custom localizer's criteria. GetLocale(index int) context.Locale } ) // I18n is the structure which keeps the i18n configuration and implements localization and internationalization features. type I18n struct { localizer Localizer matcher *Matcher Loader LoaderConfig loader Loader mu sync.Mutex // ExtractFunc is the type signature for declaring custom logic // to extract the language tag name. // Defaults to nil. ExtractFunc func(ctx *context.Context) string // DefaultMessageFunc is the field which can be used // to modify the behavior when a key or language was not found. // All language inputs fallback to the default locale if not matched. // This is why this one accepts both input and matched languages, // so the caller can be more expressful knowing those. // // Defaults to nil. DefaultMessageFunc MessageFunc // If not empty, it is language identifier by url query. // // Defaults to "lang". URLParameter string // If not empty, it is language identifier by cookie of this name. // // Defaults to empty. Cookie string // If true then a subdomain can be a language identifier. // // Defaults to true. Subdomain bool // If a DefaultMessageFunc is NOT set: // If true then it will return empty string when translation for a // specific language's key was not found. // Defaults to false, fallback defaultLang:key will be used. // Otherwise, DefaultMessageFunc is called in either case. Strict bool // If true then Iris will wrap its router with the i18n router wrapper on its Build state. // It will (local) redirect requests like: // 1. /$lang_prefix/$path to /$path with the language set to $lang_prefix part. // 2. $lang_subdomain.$domain/$path to $domain/$path with the language set to $lang_subdomain part. // // Defaults to true. PathRedirect bool } var _ context.I18nReadOnly = (*I18n)(nil) // makeTags converts language codes to language Tags. func makeTags(languages ...string) (tags []language.Tag) { languages = removeDuplicates(languages) for _, lang := range languages { tag, err := language.Parse(lang) if err == nil && tag != language.Und { tags = append(tags, tag) } } return } // New returns a new `I18n` instance. Use its `Load` or `LoadAssets` to load languages. // Examples at: https://github.com/kataras/iris/tree/main/_examples/i18n. func New() *I18n { i := &I18n{ Loader: DefaultLoaderConfig, URLParameter: "lang", Subdomain: true, PathRedirect: true, } return i } // Load is a method shortcut to load files using a filepath.Glob pattern. // It returns a non-nil error on failure. // // See `New` and `Glob` package-level functions for more. func (i *I18n) Load(globPattern string, languages ...string) error { return i.Reset(Glob(globPattern, i.Loader), languages...) } // LoadAssets is a method shortcut to load files using go-bindata. // It returns a non-nil error on failure. // // See `New` and `Asset` package-level functions for more. func (i *I18n) LoadAssets(assetNames func() []string, asset func(string) ([]byte, error), languages ...string) error { return i.Reset(Assets(assetNames, asset, i.Loader), languages...) } // LoadFS is a method shortcut to load files using // an `embed.FS` or `fs.FS` or `http.FileSystem` value. // The "pattern" is a classic glob pattern. // // See `New` and `FS` package-level functions for more. // Example: https://github.com/kataras/iris/blob/main/_examples/i18n/template-embedded/main.go. func (i *I18n) LoadFS(fileSystem fs.FS, pattern string, languages ...string) error { loader, err := FS(fileSystem, pattern, i.Loader) if err != nil { return err } return i.Reset(loader, languages...) } // LoadKV is a method shortcut to load locales from a map of specified languages. // See `KV` package-level function for more. func (i *I18n) LoadKV(langMap LangMap, languages ...string) error { loader := KV(langMap, i.Loader) return i.Reset(loader, languages...) } // Reset sets the locales loader and languages. // It is not meant to be used by users unless // a custom `Loader` must be used instead of the default one. func (i *I18n) Reset(loader Loader, languages ...string) error { tags := makeTags(languages...) i.loader = loader i.matcher = &Matcher{ strict: len(tags) > 0, Languages: tags, matcher: language.NewMatcher(tags), defaultMessageFunc: i.DefaultMessageFunc, } return i.reload() } // reload loads the language files from the provided Loader, // the `New` package-level function preloads those files already. func (i *I18n) reload() error { // May be an exported function, if requested. i.mu.Lock() defer i.mu.Unlock() if i.loader == nil { return fmt.Errorf("nil loader") } localizer, err := i.loader(i.matcher) if err != nil { return err } i.localizer = localizer return nil } // Loaded reports whether `New` or `Load/LoadAssets` called. func (i *I18n) Loaded() bool { return i != nil && i.loader != nil && i.localizer != nil && i.matcher != nil } // Tags returns the registered languages or dynamically resolved by files. // Use `Load` or `LoadAssets` first. func (i *I18n) Tags() []language.Tag { if !i.Loaded() { return nil } return i.matcher.Languages } // SetDefault changes the default language. // Please avoid using this method; the default behavior will accept // the first language of the registered tags as the default one. func (i *I18n) SetDefault(langCode string) bool { t, err := language.Parse(langCode) if err != nil { return false } if tag, index, conf := i.matcher.Match(t); conf > language.Low { if l, ok := i.localizer.(interface { SetDefault(int) bool }); ok { if l.SetDefault(index) { tags := i.matcher.Languages // set the order tags[index] = tags[0] tags[0] = tag i.matcher.Languages = tags i.matcher.matcher = language.NewMatcher(tags) return true } } } return false } // Matcher implements the languae.Matcher. // It contains the original language Matcher and keeps an ordered // list of the registered languages for further use (see `Loader` implementation). type Matcher struct { strict bool Languages []language.Tag matcher language.Matcher // defaultMessageFunc passed by the i18n structure. defaultMessageFunc MessageFunc } var _ language.Matcher = (*Matcher)(nil) // Match returns the best match for any of the given tags, along with // a unique index associated with the returned tag and a confidence // score. func (m *Matcher) Match(t ...language.Tag) (language.Tag, int, language.Confidence) { return m.matcher.Match(t...) } // MatchOrAdd acts like Match but it checks and adds a language tag, if not found, // when the `Matcher.strict` field is true (when no tags are provided by the caller) // and they should be dynamically added to the list. func (m *Matcher) MatchOrAdd(t language.Tag) (tag language.Tag, index int, conf language.Confidence) { tag, index, conf = m.Match(t) if conf <= language.Low && !m.strict { // not found, add it now. m.Languages = append(m.Languages, t) tag = t index = len(m.Languages) - 1 conf = language.Exact m.matcher = language.NewMatcher(m.Languages) // reset matcher to include the new language. } return } // ParseLanguageFiles returns a map of language indexes and // their associated files based on the "fileNames". func (m *Matcher) ParseLanguageFiles(fileNames []string) (map[int][]string, error) { languageFiles := make(map[int][]string) for _, fileName := range fileNames { index := parsePath(m, fileName) if index == -1 { continue } languageFiles[index] = append(languageFiles[index], fileName) } return languageFiles, nil } func parsePath(m *Matcher, path string) int { if t, ok := parseLanguage(path); ok { if _, index, conf := m.MatchOrAdd(t); conf > language.Low { return index } } return -1 } func parseLanguageName(m *Matcher, name string) int { if t, err := language.Parse(name); err == nil { if _, index, conf := m.MatchOrAdd(t); conf > language.Low { return index } } return -1 } func reverseStrings(s []string) []string { for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 { s[i], s[j] = s[j], s[i] } return s } func parseLanguage(path string) (language.Tag, bool) { if idx := strings.LastIndexByte(path, '.'); idx > 0 { path = path[0:idx] } // path = strings.ReplaceAll(path, "..", "") names := strings.FieldsFunc(path, func(r rune) bool { return r == '_' || r == os.PathSeparator || r == '/' || r == '.' }) names = reverseStrings(names) // see https://github.com/kataras/i18n/issues/1 for _, s := range names { t, err := language.Parse(s) if err != nil { continue } return t, true } return language.Und, false } // TryMatchString will try to match the "s" with a registered language tag. // It returns -1 as the language index and false if not found. func (i *I18n) TryMatchString(s string) (language.Tag, int, bool) { if tag, err := language.Parse(s); err == nil { if tag, index, conf := i.matcher.Match(tag); conf > language.Low { return tag, index, true } } return language.Und, -1, false } // Tr returns a translated message based on the "lang" language code // and its key with any optional arguments attached to it. // // It returns an empty string if "lang" not matched, unless DefaultMessageFunc. // It returns the default language's translation if "key" not matched, unless DefaultMessageFunc. func (i *I18n) Tr(lang, key string, args ...any) string { _, index, ok := i.TryMatchString(lang) if !ok { index = 0 } loc := i.localizer.GetLocale(index) return i.getLocaleMessage(loc, lang, key, args...) } // TrContext returns the localized text message for this Context. // It returns an empty string if context's locale not matched, unless DefaultMessageFunc. // It returns the default language's translation if "key" not matched, unless DefaultMessageFunc. func (i *I18n) TrContext(ctx *context.Context, key string, args ...any) string { loc := ctx.GetLocale() langInput := ctx.Values().GetString(ctx.Application().ConfigurationReadOnly().GetLanguageInputContextKey()) return i.getLocaleMessage(loc, langInput, key, args...) } func (i *I18n) getLocaleMessage(loc context.Locale, langInput string, key string, args ...any) (msg string) { langMatched := "" if loc != nil { langMatched = loc.Language() msg = loc.GetMessage(key, args...) if msg == "" && i.DefaultMessageFunc == nil && !i.Strict && loc.Index() > 0 { // it's not the default/fallback language and not message found for that lang:key. msg = i.localizer.GetLocale(0).GetMessage(key, args...) } } if msg == "" && i.DefaultMessageFunc != nil { msg = i.DefaultMessageFunc(langInput, langMatched, key, args...) } return } const acceptLanguageHeaderKey = "Accept-Language" // GetLocale returns the found locale of a request. // It will return the first registered language if nothing else matched. func (i *I18n) GetLocale(ctx *context.Context) context.Locale { var ( index int ok bool extractedLang string ) languageInputKey := ctx.Application().ConfigurationReadOnly().GetLanguageInputContextKey() if contextKey := ctx.Application().ConfigurationReadOnly().GetLanguageContextKey(); contextKey != "" { if v := ctx.Values().GetString(contextKey); v != "" { if languageInputKey != "" { ctx.Values().Set(languageInputKey, v) } if v == "default" { index = 0 // no need to call `TryMatchString` and spend time. } else { _, index, _ = i.TryMatchString(v) } locale := i.localizer.GetLocale(index) if locale == nil { return nil } return locale } } if !ok && i.ExtractFunc != nil { if v := i.ExtractFunc(ctx); v != "" { extractedLang = v _, index, ok = i.TryMatchString(v) } } if !ok && i.URLParameter != "" { if v := ctx.URLParam(i.URLParameter); v != "" { extractedLang = v _, index, ok = i.TryMatchString(v) } } if !ok && i.Cookie != "" { if v := ctx.GetCookie(i.Cookie); v != "" { extractedLang = v _, index, ok = i.TryMatchString(v) // url.QueryUnescape(cookie.Value) } } if !ok && i.Subdomain { if v := ctx.Subdomain(); v != "" { extractedLang = v _, index, ok = i.TryMatchString(v) } } if !ok { if v := ctx.GetHeader(acceptLanguageHeaderKey); v != "" { extractedLang = v // note. desired, _, err := language.ParseAcceptLanguage(v) if err == nil { if _, idx, conf := i.matcher.Match(desired...); conf > language.Low { index = idx } } } } // locale := i.localizer.GetLocale(index) // ctx.Values().Set(ctx.Application().ConfigurationReadOnly().GetLocaleContextKey(), locale) if languageInputKey != "" { // Set the user input we wanna use it on DefaultMessageFunc. // Even if matched because it may be en-gb or en but if there is a language registered // as en-us it will be successfully matched ( see TrymatchString and Low conf). ctx.Values().Set(languageInputKey, extractedLang) } // if index == 0 then it defaults to the first language. locale := i.localizer.GetLocale(index) if locale == nil { return nil } return locale } func (i *I18n) setLangWithoutContext(w http.ResponseWriter, r *http.Request, lang string) { if i.Cookie != "" { http.SetCookie(w, &http.Cookie{ Name: i.Cookie, Value: lang, // allow subdomain sharing. Domain: context.GetDomain(context.GetHost(r)), SameSite: http.SameSiteLaxMode, }) } else if i.URLParameter != "" { q := r.URL.Query() q.Set(i.URLParameter, lang) r.URL.RawQuery = q.Encode() } r.Header.Set(acceptLanguageHeaderKey, lang) } // Wrapper returns a new router wrapper. // The result function can be passed on `Application.WrapRouter/AddRouterWrapper`. // It compares the path prefix for translated language and // local redirects the requested path with the selected (from the path) language to the router. // // You do NOT have to call it manually, just set the `I18n.PathRedirect` field to true. func (i *I18n) Wrapper() router.WrapperFunc { if !i.PathRedirect { return nil } return func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { found := false path := r.URL.Path if len(path) > 0 && path[0] == '/' { path = path[1:] } if idx := strings.IndexByte(path, '/'); idx > 0 { path = path[:idx] } if path != "" { if tag, _, ok := i.TryMatchString(path); ok { lang := tag.String() path = r.URL.Path[len(path)+1:] if path == "" { path = "/" } r.RequestURI = path r.URL.Path = path i.setLangWithoutContext(w, r, lang) found = true } } if !found && i.Subdomain { host := context.GetHost(r) if dotIdx := strings.IndexByte(host, '.'); dotIdx > 0 { if subdomain := host[0:dotIdx]; subdomain != "" { if tag, _, ok := i.TryMatchString(subdomain); ok { host = host[dotIdx+1:] r.URL.Host = host r.Host = host i.setLangWithoutContext(w, r, tag.String()) } } } } next(w, r) } } func removeDuplicates(elements []string) (result []string) { seen := make(map[string]struct{}) for v := range elements { val := elements[v] if _, ok := seen[val]; !ok { seen[val] = struct{}{} result = append(result, val) } } return result } ================================================ FILE: i18n/internal/aliases.go ================================================ package internal // Map is just an alias of the map[string]any type. // Just like the iris.Map one. type Map = map[string]any ================================================ FILE: i18n/internal/catalog.go ================================================ package internal import ( "fmt" "text/template" "github.com/kataras/iris/v12/context" "golang.org/x/text/language" "golang.org/x/text/message" "golang.org/x/text/message/catalog" ) // MessageFunc is the function type to modify the behavior when a key or language was not found. // All language inputs fallback to the default locale if not matched. // This is why this signature accepts both input and matched languages, so caller // can provide better messages. // // The first parameter is set to the client real input of the language, // the second one is set to the matched language (default one if input wasn't matched) // and the third and forth are the translation format/key and its optional arguments. // // Note: we don't accept the Context here because Tr method and template func {{ tr }} // have no direct access to it. type MessageFunc func(langInput, langMatched, key string, args ...any) string // Catalog holds the locales and the variables message storage. type Catalog struct { builder *catalog.Builder Locales []*Locale } // The Options of the Catalog and its Locales. type Options struct { // Left delimiter for template messages. Left string // Right delimeter for template messages. Right string // Enable strict mode. Strict bool // Optional functions for template messages per locale. Funcs func(context.Locale) template.FuncMap // Optional function to be called when no message was found. DefaultMessageFunc MessageFunc // Customize the overall behavior of the plurazation feature. PluralFormDecoder PluralFormDecoder } // NewCatalog returns a new Catalog based on the registered languages and the loader options. func NewCatalog(languages []language.Tag, opts Options) (*Catalog, error) { // ordered languages, the first should be the default one. if len(languages) == 0 { return nil, fmt.Errorf("catalog: empty languages") } if opts.Left == "" { opts.Left = "{{" } if opts.Right == "" { opts.Right = "}}" } if opts.PluralFormDecoder == nil { opts.PluralFormDecoder = DefaultPluralFormDecoder } builder := catalog.NewBuilder(catalog.Fallback(languages[0])) locales := make([]*Locale, 0, len(languages)) for idx, tag := range languages { locale := &Locale{ tag: tag, index: idx, ID: tag.String(), Options: opts, Printer: message.NewPrinter(tag, message.Catalog(builder)), Messages: make(map[string]Renderer), } locale.FuncMap = getFuncs(locale) locales = append(locales, locale) } c := &Catalog{ builder: builder, Locales: locales, } return c, nil } // Set sets a simple translation message. func (c *Catalog) Set(tag language.Tag, key string, msgs ...catalog.Message) error { // fmt.Printf("Catalog.Set[%s] %s:\n", tag.String(), key) // for _, msg := range msgs { // fmt.Printf("%#+v\n", msg) // } return c.builder.Set(tag, key, msgs...) } // Store stores the a map of values to the locale derives from the given "langIndex". func (c *Catalog) Store(langIndex int, kv Map) error { loc := c.getLocale(langIndex) if loc == nil { return fmt.Errorf("expected language index to be lower or equal than %d but got %d", len(c.Locales), langIndex) } return loc.Load(c, kv) } /* Localizer interface. */ // SetDefault changes the default language based on the "index". // See `I18n#SetDefault` method for more. func (c *Catalog) SetDefault(index int) bool { if index < 0 { index = 0 } if maxIdx := len(c.Locales) - 1; index > maxIdx { return false } // callers should protect with mutex if called at serve-time. loc := c.Locales[index] loc.index = 0 f := c.Locales[0] c.Locales[0] = loc f.index = index c.Locales[index] = f return true } // GetLocale returns a valid `Locale` based on the "index". func (c *Catalog) GetLocale(index int) context.Locale { return c.getLocale(index) } func (c *Catalog) getLocale(index int) *Locale { if index < 0 { index = 0 } if maxIdx := len(c.Locales) - 1; index > maxIdx { // panic("expected language index to be lower or equal than %d but got %d", maxIdx, langIndex) return nil } loc := c.Locales[index] return loc } ================================================ FILE: i18n/internal/locale.go ================================================ package internal import ( "fmt" "text/template" "github.com/kataras/iris/v12/context" "golang.org/x/text/language" "golang.org/x/text/message" "golang.org/x/text/message/catalog" ) // Locale is the default Locale. // Created by Catalog. // One Locale maps to one registered and loaded language. // Stores the translation variables and most importantly, the Messages (keys and their renderers). type Locale struct { // The index of the language registered by the user, starting from zero. index int tag language.Tag // ID is the tag.String(). ID string // Options given by the Catalog Options Options // Fields set by Catalog. FuncMap template.FuncMap Printer *message.Printer // // Fields set by this Load method. Messages map[string]Renderer Vars []Var // shared per-locale variables. } // Ensures that the Locale completes the context.Locale interface. var _ context.Locale = (*Locale)(nil) // Load sets the translation messages based on the Catalog's key values. func (loc *Locale) Load(c *Catalog, keyValues Map) error { return loc.setMap(c, "", keyValues) } func (loc *Locale) setMap(c *Catalog, key string, keyValues Map) error { // unique locals or the shared ones. isRoot := key == "" vars := getVars(loc, VarsKey, keyValues) if isRoot { loc.Vars = vars } else { vars = removeVarsDuplicates(append(vars, loc.Vars...)) } for k, v := range keyValues { form, isPlural := loc.Options.PluralFormDecoder(loc, k) if isPlural { k = key } else if !isRoot { k = key + "." + k } switch value := v.(type) { case string: if err := loc.setString(c, k, value, vars, form); err != nil { return fmt.Errorf("%s:%s parse string: %w", loc.ID, key, err) } case Map: // fmt.Printf("%s is map\n", fullKey) if err := loc.setMap(c, k, value); err != nil { return fmt.Errorf("%s:%s parse map: %w", loc.ID, key, err) } default: return fmt.Errorf("%s:%s unexpected type of %T as value", loc.ID, key, value) } } return nil } func (loc *Locale) setString(c *Catalog, key string, value string, vars []Var, form PluralForm) (err error) { isPlural := form != nil // fmt.Printf("setStringVars: %s=%s\n", key, value) msgs, vars := makeSelectfVars(value, vars, isPlural) msgs = append(msgs, catalog.String(value)) m := &Message{ Locale: loc, Key: key, Value: value, Vars: vars, Plural: isPlural, } var ( renderer, pluralRenderer Renderer = m, m ) if stringIsTemplateValue(value, loc.Options.Left, loc.Options.Right) { t, err := NewTemplate(c, m) if err != nil { return err } pluralRenderer = t if !isPlural { renderer = t } } else { if isPlural { pluralRenderer, err = newIndependentPluralRenderer(c, loc, key, msgs...) if err != nil { return fmt.Errorf("<%s = %s>: %w", key, value, err) } } else if err = c.Set(loc.tag, key, msgs...); err != nil { // let's make normal keys direct fire: // renderer = &simpleRenderer{key, loc.Printer} return fmt.Errorf("<%s = %s>: %w", key, value, err) } } if isPlural { if existingMsg, ok := loc.Messages[key]; ok { if msg, ok := existingMsg.(*Message); ok { msg.AddPlural(form, pluralRenderer) return } } m.AddPlural(form, pluralRenderer) } loc.Messages[key] = renderer return } /* context.Locale interface */ // Index returns the current locale index from the languages list. func (loc *Locale) Index() int { return loc.index } // Tag returns the full language Tag attached to this Locale, // it should be unique across different Locales. func (loc *Locale) Tag() *language.Tag { return &loc.tag } // Language should return the exact languagecode of this `Locale` // that the user provided on `New` function. // // Same as `Tag().String()` but it's static. func (loc *Locale) Language() string { return loc.ID } // GetMessage should return translated text based on the given "key". func (loc *Locale) GetMessage(key string, args ...any) string { if msg, ok := loc.Messages[key]; ok { result, err := msg.Render(args...) if err != nil { result = err.Error() } return result } return "" } ================================================ FILE: i18n/internal/message.go ================================================ package internal import ( "fmt" "sort" ) // Renderer is responsible to render a translation based // on the given "args". type Renderer interface { Render(args ...any) (string, error) } // Message is the default Renderer for translation messages. // Holds the variables and the plurals of this key. // Each Locale has its own list of messages. type Message struct { Locale *Locale Key string Value string Plural bool Plurals []*PluralMessage // plural forms by order. Vars []Var } // AddPlural adds a plural message to the Plurals list. func (m *Message) AddPlural(form PluralForm, r Renderer) { msg := &PluralMessage{ Form: form, Renderer: r, } if len(m.Plurals) == 0 { m.Plural = true m.Plurals = append(m.Plurals, msg) return } for i, p := range m.Plurals { if p.Form.String() == form.String() { // replace m.Plurals[i] = msg return } } m.Plurals = append(m.Plurals, msg) sort.SliceStable(m.Plurals, func(i, j int) bool { return m.Plurals[i].Form.Less(m.Plurals[j].Form) }) } // Render completes the Renderer interface. // It accepts arguments, which can resolve the pluralization type of the message // and its variables. If the Message is wrapped by a Template then the // first argument should be a map. The map key resolves to the pluralization // of the message is the "PluralCount". And for variables the user // should set a message key which looks like: %VAR_NAME%Count, e.g. "DogsCount" // to set plural count for the "Dogs" variable, case-sensitive. func (m *Message) Render(args ...any) (string, error) { if m.Plural { if len(args) > 0 { if pluralCount, ok := findPluralCount(args[0]); ok { for _, plural := range m.Plurals { if plural.Form.MatchPlural(pluralCount) { return plural.Renderer.Render(args...) } } return "", fmt.Errorf("key: %q: no registered plurals for <%d>", m.Key, pluralCount) } } return "", fmt.Errorf("key: %q: missing plural count argument", m.Key) } return m.Locale.Printer.Sprintf(m.Key, args...), nil } ================================================ FILE: i18n/internal/plural.go ================================================ package internal import ( "strconv" "github.com/kataras/iris/v12/context" "golang.org/x/text/feature/plural" "golang.org/x/text/message" "golang.org/x/text/message/catalog" ) // PluralCounter if completes by an input argument of a message to render, // then the plural renderer will resolve the plural count // and any variables' counts. This is useful when the data is not a type of Map or integers. type PluralCounter interface { // PluralCount returns the plural count of the message. // If returns -1 then this is not a valid plural message. PluralCount() int // VarCount should return the variable count, based on the variable name. VarCount(name string) int } // PluralMessage holds the registered Form and the corresponding Renderer. // It is used on the `Message.AddPlural` method. type PluralMessage struct { Form PluralForm Renderer Renderer } type independentPluralRenderer struct { key string printer *message.Printer } func newIndependentPluralRenderer(c *Catalog, loc *Locale, key string, msgs ...catalog.Message) (Renderer, error) { builder := catalog.NewBuilder(catalog.Fallback(c.Locales[0].tag)) if err := builder.Set(loc.tag, key, msgs...); err != nil { return nil, err } printer := message.NewPrinter(loc.tag, message.Catalog(builder)) return &independentPluralRenderer{key, printer}, nil } func (m *independentPluralRenderer) Render(args ...any) (string, error) { return m.printer.Sprintf(m.key, args...), nil } // A PluralFormDecoder should report and return whether // a specific "key" is a plural one. This function // can be implemented and set on the `Options` to customize // the plural forms and their behavior in general. // // See the `DefaultPluralFormDecoder` package-level // variable for the default implementation one. type PluralFormDecoder func(loc context.Locale, key string) (PluralForm, bool) // DefaultPluralFormDecoder is the default `PluralFormDecoder`. // Supprots "zero", "one", "two", "other", "=x", "x". var DefaultPluralFormDecoder = func(_ context.Locale, key string) (PluralForm, bool) { if isDefaultPluralForm(key) { return pluralForm(key), true } return nil, false } func isDefaultPluralForm(s string) bool { switch s { case "zero", "one", "two", "other": return true default: if len(s) > 1 { ch := s[0] if ch == '=' || ch == '<' || ch == '>' { if isDigit(s[1]) { return true } } } return false } } // A PluralForm is responsible to decode // locale keys to plural forms and match plural forms // based on the given pluralCount. // // See `pluralForm` package-level type for a default implementation. type PluralForm interface { String() string // the string is a verified plural case's raw string value. // Field for priority on which order to register the plural cases. Less(next PluralForm) bool MatchPlural(pluralCount int) bool } type pluralForm string func (f pluralForm) String() string { return string(f) } func (f pluralForm) Less(next PluralForm) bool { form1 := f.String() form2 := next.String() // Order by // - equals, // - less than // - greater than // - "zero", "one", "two" // - rest is last "other". dig1, typ1, hasDig1 := formAtoi(form1) if typ1 == eq { return true } dig2, typ2, hasDig2 := formAtoi(form2) if typ2 == eq { return false } // digits smaller, number. if hasDig1 { return !hasDig2 || dig1 < dig2 } if hasDig2 { return false } if form1 == "other" { return false // other go to last. } if form2 == "other" { return true } if form1 == "zero" { return true } if form2 == "zero" { return false } if form1 == "one" { return true } if form2 == "one" { return false } if form1 == "two" { return true } if form2 == "two" { return false } return false } func (f pluralForm) MatchPlural(pluralCount int) bool { switch f { case "other": return true case "=0", "zero": return pluralCount == 0 case "=1", "one": return pluralCount == 1 case "=2", "two": return pluralCount == 2 default: // <5 or =5 n, typ, ok := formAtoi(string(f)) if !ok { return false } switch typ { case eq: return n == pluralCount case lt: return pluralCount < n case gt: return pluralCount > n default: return false } } } func makeSelectfVars(text string, vars []Var, insidePlural bool) ([]catalog.Message, []Var) { newVars := sortVars(text, vars) newVars = removeVarsDuplicates(newVars) msgs := selectfVars(newVars, insidePlural) return msgs, newVars } func selectfVars(vars []Var, insidePlural bool) []catalog.Message { msgs := make([]catalog.Message, 0, len(vars)) for _, variable := range vars { argth := variable.Argth if insidePlural { argth++ } msg := catalog.Var(variable.Name, plural.Selectf(argth, variable.Format, variable.Cases...)) // fmt.Printf("%s:%d | cases | %#+v\n", variable.Name, variable.Argth, variable.Cases) msgs = append(msgs, msg) } return msgs } const ( eq uint8 = iota + 1 lt gt ) func formType(ch byte) uint8 { switch ch { case '=': return eq case '<': return lt case '>': return gt } return 0 } func formAtoi(form string) (int, uint8, bool) { if len(form) < 2 { return -1, 0, false } typ := formType(form[0]) if typ == 0 { return -1, 0, false } dig, err := strconv.Atoi(form[1:]) if err != nil { return -1, 0, false } return dig, typ, true } func isDigit(ch byte) bool { return '0' <= ch && ch <= '9' } ================================================ FILE: i18n/internal/template.go ================================================ package internal import ( "bytes" "fmt" "strconv" "strings" "sync" "text/template" "golang.org/x/text/message/catalog" ) const ( // VarsKey is the key for the message's variables, per locale(global) or per key (local). VarsKey = "Vars" // PluralCountKey is the key for the template's message pluralization. PluralCountKey = "PluralCount" // VarCountKeySuffix is the key suffix for the template's variable's pluralization, // e.g. HousesCount for ${Houses}. VarCountKeySuffix = "Count" // VarsKeySuffix is the key which the template message's variables // are stored with, // e.g. welcome.human.other_vars VarsKeySuffix = "_vars" ) // Template is a Renderer which renders template messages. type Template struct { *Message tmpl *template.Template bufPool *sync.Pool } // NewTemplate returns a new Template message based on the // catalog and the base translation Message. See `Locale.Load` method. func NewTemplate(c *Catalog, m *Message) (*Template, error) { tmpl, err := template.New(m.Key). Delims(m.Locale.Options.Left, m.Locale.Options.Right). Funcs(m.Locale.FuncMap). Parse(m.Value) if err != nil { return nil, err } if err := registerTemplateVars(c, m); err != nil { return nil, fmt.Errorf("template vars: <%s = %s>: %w", m.Key, m.Value, err) } bufPool := &sync.Pool{ New: func() any { return new(bytes.Buffer) }, } t := &Template{ Message: m, tmpl: tmpl, bufPool: bufPool, } return t, nil } func registerTemplateVars(c *Catalog, m *Message) error { if len(m.Vars) == 0 { return nil } msgs := selectfVars(m.Vars, false) variableText := "" for _, variable := range m.Vars { variableText += variable.Literal + " " } variableText = variableText[0 : len(variableText)-1] fullKey := m.Key + "." + VarsKeySuffix return c.Set(m.Locale.tag, fullKey, append(msgs, catalog.String(variableText))...) } // Render completes the Renderer interface. // It renders a template message. // Each key has its own Template, plurals too. func (t *Template) Render(args ...any) (string, error) { var ( data any result string ) argsLength := len(args) if argsLength > 0 { data = args[0] } buf := t.bufPool.Get().(*bytes.Buffer) buf.Reset() if err := t.tmpl.Execute(buf, data); err != nil { t.bufPool.Put(buf) return "", err } result = buf.String() t.bufPool.Put(buf) if len(t.Vars) > 0 { // get the variables plurals. if argsLength > 1 { // if has more than the map/struct // then let's assume the user passes variable counts by raw integer arguments. args = args[1:] } else if data != nil { // otherwise try to resolve them by the map(%var_name%Count)/struct(PlrualCounter). args = findVarsCount(data, t.Vars) } result = t.replaceTmplVars(result, args...) } return result, nil } func findVarsCount(data any, vars []Var) (args []any) { if data == nil { return nil } switch dataValue := data.(type) { case PluralCounter: for _, v := range vars { if count := dataValue.VarCount(v.Name); count >= 0 { args = append(args, count) } } case Map: for _, v := range vars { varCountKey := v.Name + VarCountKeySuffix if value, ok := dataValue[varCountKey]; ok { args = append(args, value) } } case map[string]string: for _, v := range vars { varCountKey := v.Name + VarCountKeySuffix if value, ok := dataValue[varCountKey]; ok { if count, err := strconv.Atoi(value); err == nil { args = append(args, count) } } } case map[string]int: for _, v := range vars { varCountKey := v.Name + VarCountKeySuffix if value, ok := dataValue[varCountKey]; ok { args = append(args, value) } } default: return nil } return } func findPluralCount(data any) (int, bool) { if data == nil { return -1, false } switch dataValue := data.(type) { case PluralCounter: if count := dataValue.PluralCount(); count >= 0 { return count, true } case Map: if v, ok := dataValue[PluralCountKey]; ok { if count, ok := v.(int); ok { return count, true } } case map[string]string: if v, ok := dataValue[PluralCountKey]; ok { count, err := strconv.Atoi(v) if err != nil { return -1, false } return count, true } case map[string]int: if count, ok := dataValue[PluralCountKey]; ok { return count, true } case int: return dataValue, true // when this is not a template data, the caller's argument should be args[1:] now. case int64: count := int(dataValue) return count, true } return -1, false } func (t *Template) replaceTmplVars(result string, args ...any) string { varsKey := t.Key + "." + VarsKeySuffix translationVarsText := t.Locale.Printer.Sprintf(varsKey, args...) if translationVarsText != "" { translatioVars := strings.Split(translationVarsText, " ") for i, variable := range t.Vars { result = strings.Replace(result, variable.Literal, translatioVars[i], 1) } } return result } func stringIsTemplateValue(value, left, right string) bool { leftIdx, rightIdx := strings.Index(value, left), strings.Index(value, right) return leftIdx != -1 && rightIdx > leftIdx } func getFuncs(loc *Locale) template.FuncMap { // set the template funcs for this locale. funcs := template.FuncMap{ "tr": loc.GetMessage, } if getFuncs := loc.Options.Funcs; getFuncs != nil { // set current locale's template's funcs. for k, v := range getFuncs(loc) { funcs[k] = v } } return funcs } ================================================ FILE: i18n/internal/var.go ================================================ package internal import ( "regexp" "sort" ) // Var represents a message variable. // The variables, like the sub messages are sorted. // First: plurals (which again, are sorted) // and then any custom keys. // In variables, the sorting depends on the exact // order the associated message uses the variables. // This is extremely handy. // This package requires the golang.org/x/text/message capabilities // only for the variables feature, the message itself's pluralization is managed by the package. type Var struct { Name string // Variable name, e.g. Name Literal string // Its literal is ${Name} Cases []any // one:...,few:...,... Format string // defaults to "%d". Argth int // 1, 2, 3... } func getVars(loc *Locale, key string, src map[string]any) []Var { if len(src) == 0 { return nil } varsKey, ok := src[key] if !ok { return nil } varValue, ok := varsKey.([]any) if !ok { return nil } vars := make([]Var, 0, len(varValue)) for _, v := range varValue { m, ok := v.(map[string]any) if !ok { continue } for k, inner := range m { varFormat := "%d" innerMap, ok := inner.(map[string]any) if !ok { continue } for kk, vv := range innerMap { if kk == "format" { if format, ok := vv.(string); ok { varFormat = format } break } } cases := getCases(loc, innerMap) if len(cases) > 0 { // cases = sortCases(cases) vars = append(vars, Var{ Name: k, Literal: "${" + k + "}", Cases: cases, Format: varFormat, Argth: 1, }) } } } delete(src, key) // delete the key after. return vars } var unescapeVariableRegex = regexp.MustCompile(`\$\{(.*?)}`) func sortVars(text string, vars []Var) (newVars []Var) { argth := 1 for _, submatches := range unescapeVariableRegex.FindAllStringSubmatch(text, -1) { name := submatches[1] for _, variable := range vars { if variable.Name == name { variable.Argth = argth newVars = append(newVars, variable) argth++ break } } } sort.SliceStable(newVars, func(i, j int) bool { return newVars[i].Argth < newVars[j].Argth }) return } // it will panic if the incoming "elements" are not catmsg.Var (internal text package). func removeVarsDuplicates(elements []Var) (result []Var) { seen := make(map[string]struct{}) for v := range elements { variable := elements[v] name := variable.Name if _, ok := seen[name]; !ok { seen[name] = struct{}{} result = append(result, variable) } } return result } /* func removeMsgVarsDuplicates(elements []catalog.Message) (result []catalog.Message) { seen := make(map[string]struct{}) for _, elem := range elements { val := reflect.Indirect(reflect.ValueOf(elem)) if val.Type().String() != "catmsg.Var" { // keep. result = append(result, elem) continue // it's not a var. } name := val.FieldByName("Name").Interface().(string) if _, ok := seen[name]; !ok { seen[name] = struct{}{} result = append(result, elem) } } return } */ func getCases(loc *Locale, src map[string]any) []any { type PluralCase struct { Form PluralForm Value any } pluralCases := make([]PluralCase, 0, len(src)) for key, value := range src { form, ok := loc.Options.PluralFormDecoder(loc, key) if !ok { continue } pluralCases = append(pluralCases, PluralCase{ Form: form, Value: value, }) } if len(pluralCases) == 0 { return nil } sort.SliceStable(pluralCases, func(i, j int) bool { left, right := pluralCases[i].Form, pluralCases[j].Form return left.Less(right) }) cases := make([]any, 0, len(pluralCases)*2) for _, pluralCase := range pluralCases { // fmt.Printf("%s=%v\n", pluralCase.Form, pluralCase.Value) cases = append(cases, pluralCase.Form.String(), pluralCase.Value) } return cases } ================================================ FILE: i18n/loader.go ================================================ package i18n import ( "encoding/json" "fmt" "io" "io/fs" "os" "path/filepath" "strings" "github.com/kataras/iris/v12/i18n/internal" "github.com/BurntSushi/toml" "gopkg.in/ini.v1" "gopkg.in/yaml.v3" ) // LoaderConfig the configuration structure which contains // some options about how the template loader should act. // // See `Glob` and `Assets` package-level functions. type LoaderConfig = internal.Options // Glob accepts a glob pattern (see: https://golang.org/pkg/path/filepath/#Glob) // and loads the locale files based on any "options". // // The "globPattern" input parameter is a glob pattern which the default loader should // search and load for locale files. // // See `New` and `LoaderConfig` too. func Glob(globPattern string, options LoaderConfig) Loader { assetNames, err := filepath.Glob(globPattern) if err != nil { panic(err) } return load(assetNames, os.ReadFile, options) } // Assets accepts a function that returns a list of filenames (physical or virtual), // another a function that should return the contents of a specific file // and any Loader options. Go-bindata usage. // It returns a valid `Loader` which loads and maps the locale files. // // See `Glob`, `FS`, `New` and `LoaderConfig` too. func Assets(assetNames func() []string, asset func(string) ([]byte, error), options LoaderConfig) Loader { return load(assetNames(), asset, options) } // LoadFS loads the files using embed.FS or fs.FS or // http.FileSystem or string (local directory). // The "pattern" is a classic glob pattern. // // See `Glob`, `Assets`, `New` and `LoaderConfig` too. func FS(fileSystem fs.FS, pattern string, options LoaderConfig) (Loader, error) { pattern = strings.TrimPrefix(pattern, "./") assetNames, err := fs.Glob(fileSystem, pattern) if err != nil { return nil, err } assetFunc := func(name string) ([]byte, error) { f, err := fileSystem.Open(name) if err != nil { return nil, err } return io.ReadAll(f) } return load(assetNames, assetFunc, options), nil } // LangMap key as language (e.g. "el-GR") and value as a map of key-value pairs (e.g. "hello": "Γειά"). type LangMap = map[string]map[string]any // KV is a loader which accepts a map of language(key) and the available key-value pairs. // Example Code: // // m := i18n.LangMap{ // "en-US": map[string]any{ // "hello": "Hello", // }, // "el-GR": map[string]any{ // "hello": "Γειά", // }, // } // // app := iris.New() // [...] // app.I18N.LoadKV(m) // app.I18N.SetDefault("en-US") func KV(langMap LangMap, opts ...LoaderConfig) Loader { return func(m *Matcher) (Localizer, error) { options := DefaultLoaderConfig if len(opts) > 0 { options = opts[0] } languageIndexes := make([]int, 0, len(langMap)) keyValuesMulti := make([]map[string]any, 0, len(langMap)) for languageName, pairs := range langMap { langIndex := parseLanguageName(m, languageName) // matches and adds the language tag to m.Languages. languageIndexes = append(languageIndexes, langIndex) keyValuesMulti = append(keyValuesMulti, pairs) } cat, err := internal.NewCatalog(m.Languages, options) if err != nil { return nil, err } for _, langIndex := range languageIndexes { if langIndex == -1 { // If loader has more languages than defined for use in New function, // e.g. when New(KV(m), "en-US") contains el-GR and en-US but only "en-US" passed. continue } kv := keyValuesMulti[langIndex] err := cat.Store(langIndex, kv) if err != nil { return nil, err } } if n := len(cat.Locales); n == 0 { return nil, fmt.Errorf("locales not found in map") } else if options.Strict && n < len(m.Languages) { return nil, fmt.Errorf("locales expected to be %d but %d parsed", len(m.Languages), n) } return cat, nil } } // DefaultLoaderConfig represents the default loader configuration. var DefaultLoaderConfig = LoaderConfig{ Left: "{{", Right: "}}", Strict: false, DefaultMessageFunc: nil, PluralFormDecoder: internal.DefaultPluralFormDecoder, Funcs: nil, } // load accepts a list of filenames (physical or virtual), // a function that should return the contents of a specific file // and any Loader options. // It returns a valid `Loader` which loads and maps the locale files. // // See `FS`, `Glob`, `Assets` and `LoaderConfig` too. func load(assetNames []string, asset func(string) ([]byte, error), options LoaderConfig) Loader { return func(m *Matcher) (Localizer, error) { languageFiles, err := m.ParseLanguageFiles(assetNames) if err != nil { return nil, err } if options.DefaultMessageFunc == nil { options.DefaultMessageFunc = m.defaultMessageFunc } cat, err := internal.NewCatalog(m.Languages, options) if err != nil { return nil, err } for langIndex, langFiles := range languageFiles { keyValues := make(map[string]any) for _, fileName := range langFiles { unmarshal := yaml.Unmarshal if idx := strings.LastIndexByte(fileName, '.'); idx > 1 { switch fileName[idx:] { case ".toml", ".tml": unmarshal = toml.Unmarshal case ".json": unmarshal = json.Unmarshal case ".ini": unmarshal = unmarshalINI } } b, err := asset(fileName) if err != nil { return nil, err } if err = unmarshal(b, &keyValues); err != nil { return nil, err } } err = cat.Store(langIndex, keyValues) if err != nil { return nil, err } } if n := len(cat.Locales); n == 0 { return nil, fmt.Errorf("locales not found in %s", strings.Join(assetNames, ", ")) } else if options.Strict && n < len(m.Languages) { return nil, fmt.Errorf("locales expected to be %d but %d parsed", len(m.Languages), n) } return cat, nil } } func unmarshalINI(data []byte, v any) error { f, err := ini.Load(data) if err != nil { return err } m := *v.(*map[string]any) // Includes the ini.DefaultSection which has the root keys too. // We don't have to iterate to each section to find the subsection, // the Sections() returns all sections, sub-sections are separated by dot '.' // and we match the dot with a section on the translate function, so we just save the values as they are, // so we don't have to do section lookup on every translate call. for _, section := range f.Sections() { keyPrefix := "" if name := section.Name(); name != ini.DefaultSection { keyPrefix = name + "." } for _, key := range section.Keys() { m[keyPrefix+key.Name()] = key.Value() } } return nil } ================================================ FILE: iris.go ================================================ package iris import ( "bytes" stdContext "context" "errors" "fmt" "io" "log" "math" "net" "net/http" "os" "regexp" "strings" "sync" "time" "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/core/host" "github.com/kataras/iris/v12/core/netutil" "github.com/kataras/iris/v12/core/router" "github.com/kataras/iris/v12/i18n" "github.com/kataras/iris/v12/middleware/cors" "github.com/kataras/iris/v12/middleware/recover" "github.com/kataras/iris/v12/middleware/requestid" "github.com/kataras/iris/v12/view" "github.com/kataras/golog" "github.com/kataras/tunnel" "github.com/tdewolff/minify/v2" "github.com/tdewolff/minify/v2/css" "github.com/tdewolff/minify/v2/html" "github.com/tdewolff/minify/v2/js" "github.com/tdewolff/minify/v2/json" "github.com/tdewolff/minify/v2/svg" "github.com/tdewolff/minify/v2/xml" ) // Version is the current version of the Iris Web Framework. const Version = "12.2.11" // Byte unit helpers. const ( B = 1 << (10 * iota) KB MB GB TB PB EB ) // Application is responsible to manage the state of the application. // It contains and handles all the necessary parts to create a fast web server. type Application struct { // routing embedded | exposing APIBuilder's and Router's public API. *router.APIBuilder *router.Router router.HTTPErrorHandler // if Router is Downgraded this is nil. ContextPool *context.Pool // See SetContextErrorHandler, defaults to nil. contextErrorHandler context.ErrorHandler // config contains the configuration fields // all fields defaults to something that is working, developers don't have to set it. config *Configuration // the golog logger instance, defaults to "Info" level messages (all except "Debug") logger *golog.Logger // I18n contains localization and internationalization support. // Use the `Load` or `LoadAssets` to locale language files. // // See `Context#Tr` method for request-based translations. I18n *i18n.I18n // Validator is the request body validator, defaults to nil. Validator context.Validator // Minifier to minify responses. minifier *minify.M // view engine view *view.View // used for build builded bool defaultMode bool // OnBuild is a single function which // is fired on the first `Build` method call. // If reports an error then the execution // is stopped and the error is logged. // It's nil by default except when `Switch` instead of `New` or `Default` // is used to initialize the Application. // Users can wrap it to accept more events. OnBuild func() error mu sync.RWMutex // name is the application name and the log prefix for // that Application instance's Logger. See `SetName` and `String`. // Defaults to IRIS_APP_NAME envrinoment variable otherwise empty. name string // Hosts contains a list of all servers (Host Supervisors) that this app is running on. // // Hosts may be empty only if application ran(`app.Run`) with `iris.Raw` option runner, // otherwise it contains a single host (`app.Hosts[0]`). // // Additional Host Supervisors can be added to that list by calling the `app.NewHost` manually. // // Hosts field is available after `Run` or `NewHost`. Hosts []*host.Supervisor hostConfigurators []host.Configurator runError error runErrorMu sync.RWMutex } // New creates and returns a fresh empty iris *Application instance. func New() *Application { config := DefaultConfiguration() app := &Application{ config: &config, Router: router.NewRouter(), I18n: i18n.New(), minifier: newMinifier(), view: new(view.View), } logger := newLogger(app) app.logger = logger app.APIBuilder = router.NewAPIBuilder(logger) app.ContextPool = context.New(func() any { return context.NewContext(app) }) context.RegisterApplication(app) return app } // Default returns a new Application. // Default with "debug" Logger Level. // Localization enabled on "./locales" directory // and HTML templates on "./views" or "./templates" directory. // CORS (allow all), Recovery and // Request ID middleware already registered. func Default() *Application { app := New() // Set default log level. app.logger.SetLevel("debug") app.logger.Debugf(`Log level set to "debug"`) /* #2046. // Register the accesslog middleware. logFile, err := os.OpenFile("./access.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600) if err == nil { // Close the file on shutdown. app.ConfigureHost(func(su *Supervisor) { su.RegisterOnShutdown(func() { logFile.Close() }) }) ac := accesslog.New(logFile) ac.AddOutput(app.logger.Printer) app.UseRouter(ac.Handler) app.logger.Debugf("Using <%s> to log requests", logFile.Name()) } */ // Register the requestid middleware // before recover so current Context.GetID() contains the info on panic logs. app.UseRouter(requestid.New()) app.logger.Debugf("Using to identify requests") // Register the recovery, after accesslog and recover, // before end-developer's middleware. app.UseRouter(recover.New()) // Register CORS (allow any origin to pass through) middleware. app.UseRouter(cors.New(). ExtractOriginFunc(cors.DefaultOriginExtractor). ReferrerPolicy(cors.NoReferrerWhenDowngrade). AllowOriginFunc(cors.AllowAnyOrigin). Handler()) app.defaultMode = true return app } func newLogger(app *Application) *golog.Logger { logger := golog.Default.Child(app) if name := os.Getenv("IRIS_APP_NAME"); name != "" { app.name = name logger.SetChildPrefix(name) } return logger } // SetName sets a unique name to this Iris Application. // It sets a child prefix for the current Application's Logger. // Look `String` method too. // // It returns this Application. func (app *Application) SetName(appName string) *Application { app.mu.Lock() defer app.mu.Unlock() if app.name == "" { app.logger.SetChildPrefix(appName) } app.name = appName return app } // String completes the fmt.Stringer interface and it returns // the application's name. // If name was not set by `SetName` or `IRIS_APP_NAME` environment variable // then this will return an empty string. func (app *Application) String() string { return app.name } // WWW creates and returns a "www." subdomain. // The difference from `app.Subdomain("www")` or `app.Party("www.")` is that the `app.WWW()` method // wraps the router so all http(s)://mydomain.com will be redirect to http(s)://www.mydomain.com. // Other subdomains can be registered using the app: `sub := app.Subdomain("mysubdomain")`, // child subdomains can be registered using the www := app.WWW(); www.Subdomain("wwwchildSubdomain"). func (app *Application) WWW() router.Party { return app.SubdomainRedirect(app, app.Subdomain("www")) } // SubdomainRedirect registers a router wrapper which // redirects(StatusMovedPermanently) a (sub)domain to another subdomain or to the root domain as fast as possible, // before the router's try to execute route's handler(s). // // It receives two arguments, they are the from and to/target locations, // 'from' can be a wildcard subdomain as well (app.WildcardSubdomain()) // 'to' is not allowed to be a wildcard for obvious reasons, // 'from' can be the root domain(app) when the 'to' is not the root domain and visa-versa. // // Usage: // www := app.Subdomain("www") <- same as app.Party("www.") // app.SubdomainRedirect(app, www) // This will redirect all http(s)://mydomain.com/%anypath% to http(s)://www.mydomain.com/%anypath%. // // One or more subdomain redirects can be used to the same app instance. // // If you need more information about this implementation then you have to navigate through // the `core/router#NewSubdomainRedirectWrapper` function instead. // // Example: https://github.com/kataras/iris/tree/main/_examples/routing/subdomains/redirect func (app *Application) SubdomainRedirect(from, to router.Party) router.Party { sd := router.NewSubdomainRedirectWrapper(app.ConfigurationReadOnly().GetVHost, from.GetRelPath(), to.GetRelPath()) app.Router.AddRouterWrapper(sd) return to } // Configure can called when modifications to the framework instance needed. // It accepts the framework instance // and returns an error which if it's not nil it's printed to the logger. // See configuration.go for more. // // Returns itself in order to be used like `app:= New().Configure(...)` func (app *Application) Configure(configurators ...Configurator) *Application { for _, cfg := range configurators { if cfg != nil { cfg(app) } } return app } // ConfigurationReadOnly returns an object which doesn't allow field writing. func (app *Application) ConfigurationReadOnly() context.ConfigurationReadOnly { return app.config } // Logger returns the golog logger instance(pointer) that is being used inside the "app". // // Available levels: // - "disable" // - "fatal" // - "error" // - "warn" // - "info" // - "debug" // Usage: app.Logger().SetLevel("error") // Or set the level through Configurartion's LogLevel or WithLogLevel functional option. // Defaults to "info" level. // // Callers can use the application's logger which is // the same `golog.Default.LastChild()` logger, // to print custom logs too. // Usage: // app.Logger().Error/Errorf("...") // app.Logger().Warn/Warnf("...") // app.Logger().Info/Infof("...") // app.Logger().Debug/Debugf("...") // // Setting one or more outputs: app.Logger().SetOutput(io.Writer...) // Adding one or more outputs : app.Logger().AddOutput(io.Writer...) // // Adding custom levels requires import of the `github.com/kataras/golog` package: // // First we create our level to a golog.Level // in order to be used in the Log functions. // var SuccessLevel golog.Level = 6 // Register our level, just three fields. // golog.Levels[SuccessLevel] = &golog.LevelMetadata{ // Name: "success", // RawText: "[SUCC]", // // ColorfulText (Green Color[SUCC]) // ColorfulText: "\x1b[32m[SUCC]\x1b[0m", // } // // Usage: // app.Logger().SetLevel("success") // app.Logger().Logf(SuccessLevel, "a custom leveled log message") func (app *Application) Logger() *golog.Logger { return app.logger } // IsDebug reports whether the application is running // under debug/development mode. // It's just a shortcut of Logger().Level >= golog.DebugLevel. // The same method existss as Context.IsDebug() too. func (app *Application) IsDebug() bool { return app.logger.Level >= golog.DebugLevel } // I18nReadOnly returns the i18n's read-only features. // See `I18n` method for more. func (app *Application) I18nReadOnly() context.I18nReadOnly { return app.I18n } // Validate validates a value and returns nil if passed or // the failure reason if does not. func (app *Application) Validate(v any) error { if app.Validator == nil { return nil } // val := reflect.ValueOf(v) // if val.Kind() == reflect.Ptr && !val.IsNil() { // val = val.Elem() // } // if val.Kind() == reflect.Struct && val.Type() != timeType { // return app.Validator.Struct(v) // } // no need to check the kind, underline lib does it but in the future this may change (look above). err := app.Validator.Struct(v) if err != nil { if !strings.HasPrefix(err.Error(), "validator: ") { return err } } return nil } func newMinifier() *minify.M { m := minify.New() m.AddFunc("text/css", css.Minify) m.AddFunc("text/html", html.Minify) m.AddFunc("image/svg+xml", svg.Minify) m.AddFuncRegexp(regexp.MustCompile("^(application|text)/(x-)?(java|ecma)script$"), js.Minify) m.AddFuncRegexp(regexp.MustCompile("[/+]json$"), json.Minify) m.AddFuncRegexp(regexp.MustCompile("[/+]xml$"), xml.Minify) return m } // Minify is a middleware which minifies the responses // based on the response content type. // Note that minification might be slower, caching is advised. // Customize the minifier through `Application.Minifier()`. // Usage: // app.Use(iris.Minify) func Minify(ctx Context) { w := ctx.Application().Minifier().ResponseWriter(ctx.ResponseWriter().Naive(), ctx.Request()) // Note(@kataras): // We don't use defer w.Close() // because this response writer holds a sync.WaitGroup under the hoods // and we MUST be sure that its wg.Wait is called on request cancelation // and not in the end of handlers chain execution // (which if running a time-consuming task it will delay its resource release). ctx.OnCloseErr(w.Close) ctx.ResponseWriter().SetWriter(w) ctx.Next() } // Minifier returns the minifier instance. // By default it can minifies: // - text/html // - text/css // - image/svg+xml // - application/text(javascript, ecmascript, json, xml). // Use that instance to add custom Minifiers before server ran. func (app *Application) Minifier() *minify.M { return app.minifier } // RegisterView registers a view engine for the application. // Children can register their own too. If no Party view Engine is registered // then this one will be used to render the templates instead. func (app *Application) RegisterView(viewEngine view.Engine) { app.view.Register(viewEngine) } // View executes and writes the result of a template file to the writer. // // First parameter is the writer to write the parsed template. // Second parameter is the relative, to templates directory, template filename, including extension. // Third parameter is the layout, can be empty string. // Forth parameter is the bindable data to the template, can be nil. // // Use context.View to render templates to the client instead. // Returns an error on failure, otherwise nil. func (app *Application) View(writer io.Writer, filename string, layout string, bindingData any) error { if !app.view.Registered() { err := errors.New("view engine is missing, use `RegisterView`") app.logger.Error(err) return err } return app.view.ExecuteWriter(writer, filename, layout, bindingData) } // GetContextPool returns the Iris sync.Pool which holds the contexts values. // Iris automatically releases the request context, so you don't have to use it. // It's only useful to manually release the context on cases that connection // is hijacked by a third-party middleware and the http handler return too fast. func (app *Application) GetContextPool() *context.Pool { return app.ContextPool } // SetContextErrorHandler can optionally register a handler to handle // and fire a customized error body to the client on JSON write failures. // // ExampleCode: // // type contextErrorHandler struct{} // func (e *contextErrorHandler) HandleContextError(ctx iris.Context, err error) { // errors.HandleError(ctx, err) // } // ... // app.SetContextErrorHandler(new(contextErrorHandler)) func (app *Application) SetContextErrorHandler(errHandler context.ErrorHandler) *Application { app.contextErrorHandler = errHandler return app } // GetContextErrorHandler returns the handler which handles errors // on JSON write failures. func (app *Application) GetContextErrorHandler() context.ErrorHandler { return app.contextErrorHandler } // ConfigureHost accepts one or more `host#Configuration`, these configurators functions // can access the host created by `app.Run` or `app.Listen`, // they're being executed when application is ready to being served to the public. // // It's an alternative way to interact with a host that is automatically created by // `app.Run`. // // These "configurators" can work side-by-side with the `iris#Addr, iris#Server, iris#TLS, iris#AutoTLS, iris#Listener` // final arguments("hostConfigs") too. // // Note that these application's host "configurators" will be shared with the rest of // the hosts that this app will may create (using `app.NewHost`), meaning that // `app.NewHost` will execute these "configurators" everytime that is being called as well. // // These "configurators" should be registered before the `app.Run` or `host.Serve/Listen` functions. func (app *Application) ConfigureHost(configurators ...host.Configurator) *Application { app.mu.Lock() app.hostConfigurators = append(app.hostConfigurators, configurators...) app.mu.Unlock() return app } const serverLoggerPrefix = "[HTTP Server] " type customHostServerLogger struct { // see #1875 parent io.Writer ignoreLogs [][]byte } var newLineBytes = []byte("\n") func newCustomHostServerLogger(w io.Writer, ignoreLogs []string) *customHostServerLogger { prefixAsByteSlice := []byte(serverLoggerPrefix) // build the ignore lines. ignoreLogsAsByteSlice := make([][]byte, 0, len(ignoreLogs)) for _, s := range ignoreLogs { ignoreLogsAsByteSlice = append(ignoreLogsAsByteSlice, append(prefixAsByteSlice, []byte(s)...)) // append([]byte(s), newLineBytes...) } return &customHostServerLogger{ parent: w, ignoreLogs: ignoreLogsAsByteSlice, } } func (l *customHostServerLogger) Write(p []byte) (int, error) { for _, ignoredLogBytes := range l.ignoreLogs { if bytes.Equal(bytes.TrimSuffix(p, newLineBytes), ignoredLogBytes) { return 0, nil } } return l.parent.Write(p) } // this may change during parallel jobs (see Application.NonBlocking & Wait). func (app *Application) getVHost() string { app.mu.RLock() vhost := app.config.VHost app.mu.RUnlock() return vhost } func (app *Application) setVHost(vhost string) { app.mu.Lock() app.config.VHost = vhost app.mu.Unlock() } // NewHost accepts a standard *http.Server object, // completes the necessary missing parts of that "srv" // and returns a new, ready-to-use, host (supervisor). func (app *Application) NewHost(srv *http.Server) *host.Supervisor { if app.getVHost() == "" { // vhost now is useful for router subdomain on wildcard subdomains, // in order to correct decide what to do on: // mydomain.com -> invalid // localhost -> invalid // sub.mydomain.com -> valid // sub.localhost -> valid // we need the host (without port if 80 or 443) in order to validate these, so: app.setVHost(netutil.ResolveVHost(srv.Addr)) } else { context.GetDomain = func(_ string) string { // #1886 return app.config.VHost // GetVHost: here we don't need mutex protection as it's request-time and all modifications are already made. } } // before lock. app.mu.Lock() defer app.mu.Unlock() // set the server's handler to the framework's router if srv.Handler == nil { srv.Handler = app.Router } // check if different ErrorLog provided, if not bind it with the framework's logger. if srv.ErrorLog == nil { serverLogger := newCustomHostServerLogger(app.logger.Printer.Output, app.config.IgnoreServerErrors) srv.ErrorLog = log.New(serverLogger, serverLoggerPrefix, 0) } if addr := srv.Addr; addr == "" { addr = ":8080" if len(app.Hosts) > 0 { if v := app.Hosts[0].Server.Addr; v != "" { addr = v } } srv.Addr = addr } // app.logger.Debugf("Host: addr is %s", srv.Addr) // create the new host supervisor // bind the constructed server and return it su := host.New(srv) // app.logger.Debugf("Host: virtual host is %s", app.config.VHost) // the below schedules some tasks that will run among the server if !app.config.DisableStartupLog { printer := app.logger.Printer.Output hostPrinter := host.WriteStartupLogOnServe(printer) if len(app.Hosts) == 0 { // print the version info on the first running host. su.RegisterOnServe(func(h host.TaskHost) { hasBuildInfo := BuildTime != "" && BuildRevision != "" tab := " " if hasBuildInfo { tab = " " } fmt.Fprintf(printer, "Iris Version:%s%s\n", tab, Version) if hasBuildInfo { fmt.Fprintf(printer, "Build Time: %s\nBuild Revision: %s\n", BuildTime, BuildRevision) } fmt.Fprintln(printer) hostPrinter(h) }) } else { su.RegisterOnServe(hostPrinter) } // app.logger.Debugf("Host: register startup notifier") } if !app.config.DisableInterruptHandler { // when CTRL/CMD+C pressed. shutdownTimeout := 10 * time.Second RegisterOnInterrupt(host.ShutdownOnInterrupt(su, shutdownTimeout)) // app.logger.Debugf("Host: register server shutdown on interrupt(CTRL+C/CMD+C)") } su.IgnoredErrors = append(su.IgnoredErrors, app.config.IgnoreServerErrors...) if len(su.IgnoredErrors) > 0 { app.logger.Debugf("Host: server will ignore the following errors: %s", su.IgnoredErrors) } su.Configure(app.hostConfigurators...) app.Hosts = append(app.Hosts, su) return su } // func (app *Application) OnShutdown(closers ...func()) { // for _,cb := range closers { // if cb == nil { // continue // } // RegisterOnInterrupt(cb) // } // } // Shutdown gracefully terminates all the application's server hosts and any tunnels. // Returns an error on the first failure, otherwise nil. func (app *Application) Shutdown(ctx stdContext.Context) error { app.mu.Lock() defer app.mu.Unlock() defer app.setRunError(ErrServerClosed) // make sure to set the error so any .Wait calls return. for i, su := range app.Hosts { app.logger.Debugf("Host[%d]: Shutdown now", i) if err := su.Shutdown(ctx); err != nil { app.logger.Debugf("Host[%d]: Error while trying to shutdown", i) return err } } for _, t := range app.config.Tunneling.Tunnels { if t.Name == "" { continue } if err := app.config.Tunneling.StopTunnel(t); err != nil { return err } } return nil } // Build sets up, once, the framework. // It builds the default router with its default macros // and the template functions that are very-closed to iris. // // If error occurred while building the Application, the returns type of error will be an *errgroup.Group // which let the callers to inspect the errors and cause, usage: // // import "github.com/kataras/iris/v12/core/errgroup" // // errgroup.Walk(app.Build(), func(typ any, err error) { // app.Logger().Errorf("%s: %s", typ, err) // }) func (app *Application) Build() error { if app.builded { return nil } if cb := app.OnBuild; cb != nil { if err := cb(); err != nil { return fmt.Errorf("build: %w", err) } } // start := time.Now() app.builded = true // even if fails. // check if a prior app.Logger().SetLevel called and if not // then set the defined configuration's log level. if app.logger.Level == golog.InfoLevel /* the default level */ { app.logger.SetLevel(app.config.LogLevel) } if app.defaultMode { // the app.I18n and app.View will be not available until Build. if !app.I18n.Loaded() { for _, s := range []string{"./locales/*/*", "./locales/*", "./translations"} { if _, err := os.Stat(s); err != nil { continue } if err := app.I18n.Load(s); err != nil { continue } app.I18n.SetDefault("en-US") break } } if !app.view.Registered() { for _, s := range []string{"./views", "./templates", "./web/views"} { if _, err := os.Stat(s); err != nil { continue } app.RegisterView(HTML(s, ".html")) break } } } if app.I18n.Loaded() { // {{ tr "lang" "key" arg1 arg2 }} app.view.AddFunc("tr", app.I18n.Tr) app.Router.PrependRouterWrapper(app.I18n.Wrapper()) } if app.view.Registered() { app.logger.Debugf("Application: view engine %q is registered", app.view.Name()) // view engine // here is where we declare the closed-relative framework functions. // Each engine has their defaults, i.e yield,render,render_r,partial, params... rv := router.NewRoutePathReverser(app.APIBuilder) app.view.AddFunc("urlpath", rv.Path) // app.view.AddFunc("url", rv.URL) if err := app.view.Load(); err != nil { return fmt.Errorf("build: view engine: %v", err) } } if !app.Router.Downgraded() { // router if _, err := injectLiveReload(app); err != nil { return fmt.Errorf("build: inject live reload: failed: %v", err) } if app.config.ForceLowercaseRouting { // This should always be executed first. app.Router.PrependRouterWrapper(func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { r.Host = strings.ToLower(r.Host) r.URL.Host = strings.ToLower(r.URL.Host) r.URL.Path = strings.ToLower(r.URL.Path) next(w, r) }) } // create the request handler, the default routing handler routerHandler := router.NewDefaultHandler(app.config, app.logger) err := app.Router.BuildRouter(app.ContextPool, routerHandler, app.APIBuilder, false) if err != nil { return fmt.Errorf("build: router: %w", err) } app.HTTPErrorHandler = routerHandler if app.config.Timeout > 0 { app.Router.SetTimeoutHandler(app.config.Timeout, app.config.TimeoutMessage) app.ConfigureHost(func(su *Supervisor) { if su.Server.ReadHeaderTimeout == 0 { su.Server.ReadHeaderTimeout = app.config.Timeout + 5*time.Second } if su.Server.ReadTimeout == 0 { su.Server.ReadTimeout = app.config.Timeout + 10*time.Second } if su.Server.WriteTimeout == 0 { su.Server.WriteTimeout = app.config.Timeout + 15*time.Second } if su.Server.IdleTimeout == 0 { su.Server.IdleTimeout = app.config.Timeout + 25*time.Second } }) } // re-build of the router from outside can be done with // app.RefreshRouter() } // if end := time.Since(start); end.Seconds() > 5 { // app.logger.Debugf("Application: build took %s", time.Since(start)) return nil } // Runner is just an interface which accepts the framework instance // and returns an error. // // It can be used to register a custom runner with `Run` in order // to set the framework's server listen action. // // Currently `Runner` is being used to declare the builtin server listeners. // // See `Run` for more. type Runner func(*Application) error // Listener can be used as an argument for the `Run` method. // It can start a server with a custom net.Listener via server's `Serve`. // // Second argument is optional, it accepts one or more // `func(*host.Configurator)` that are being executed // on that specific host that this function will create to start the server. // Via host configurators you can configure the back-end host supervisor, // i.e to add events for shutdown, serve or error. // An example of this use case can be found at: // https://github.com/kataras/iris/blob/main/_examples/http-server/notify-on-shutdown/main.go // Look at the `ConfigureHost` too. // // See `Run` for more. func Listener(l net.Listener, hostConfigs ...host.Configurator) Runner { return func(app *Application) error { app.config.SetVHost(netutil.ResolveVHost(l.Addr().String())) return app.NewHost(&http.Server{Addr: l.Addr().String()}). Configure(hostConfigs...). Serve(l) } } // Server can be used as an argument for the `Run` method. // It can start a server with a *http.Server. // // Second argument is optional, it accepts one or more // `func(*host.Configurator)` that are being executed // on that specific host that this function will create to start the server. // Via host configurators you can configure the back-end host supervisor, // i.e to add events for shutdown, serve or error. // An example of this use case can be found at: // https://github.com/kataras/iris/blob/main/_examples/http-server/notify-on-shutdown/main.go // Look at the `ConfigureHost` too. // // See `Run` for more. func Server(srv *http.Server, hostConfigs ...host.Configurator) Runner { return func(app *Application) error { return app.NewHost(srv). Configure(hostConfigs...). ListenAndServe() } } // Addr can be used as an argument for the `Run` method. // It accepts a host address which is used to build a server // and a listener which listens on that host and port. // // Addr should have the form of [host]:port, i.e localhost:8080 or :8080. // // Second argument is optional, it accepts one or more // `func(*host.Configurator)` that are being executed // on that specific host that this function will create to start the server. // Via host configurators you can configure the back-end host supervisor, // i.e to add events for shutdown, serve or error. // An example of this use case can be found at: // https://github.com/kataras/iris/blob/main/_examples/http-server/notify-on-shutdown/main.go // Look at the `ConfigureHost` too. // // See `Run` for more. func Addr(addr string, hostConfigs ...host.Configurator) Runner { return func(app *Application) error { return app.NewHost(&http.Server{Addr: addr}). Configure(hostConfigs...). ListenAndServe() } } var ( // TLSNoRedirect is a `host.Configurator` which can be passed as last argument // to the `TLS` runner function. It disables the automatic // registration of redirection from "http://" to "https://" requests. // Applies only to the `TLS` runner. // See `AutoTLSNoRedirect` to register a custom fallback server for `AutoTLS` runner. TLSNoRedirect = func(su *host.Supervisor) { su.NoRedirect() } // AutoTLSNoRedirect is a `host.Configurator`. // It registers a fallback HTTP/1.1 server for the `AutoTLS` one. // The function accepts the letsencrypt wrapper and it // should return a valid instance of http.Server which its handler should be the result // of the "acmeHandler" wrapper. // Usage: // getServer := func(acme func(http.Handler) http.Handler) *http.Server { // srv := &http.Server{Handler: acme(yourCustomHandler), ...otherOptions} // go srv.ListenAndServe() // return srv // } // app.Run(iris.AutoTLS(":443", "example.com example2.com", "mail@example.com", getServer)) // // Note that if Server.Handler is nil then the server is automatically ran // by the framework and the handler set to automatic redirection, it's still // a valid option when the caller wants just to customize the server's fields (except Addr). // With this host configurator the caller can customize the server // that letsencrypt relies to perform the challenge. // LetsEncrypt Certification Manager relies on http://example.com/.well-known/acme-challenge/. AutoTLSNoRedirect = func(getFallbackServer func(acmeHandler func(fallback http.Handler) http.Handler) *http.Server) host.Configurator { return func(su *host.Supervisor) { su.NoRedirect() su.Fallback = getFallbackServer } } ) // TLS can be used as an argument for the `Run` method. // It will start the Application's secure server. // // Use it like you used to use the http.ListenAndServeTLS function. // // Addr should have the form of [host]:port, i.e localhost:443 or :443. // "certFileOrContents" & "keyFileOrContents" should be filenames with their extensions // or raw contents of the certificate and the private key. // // Last argument is optional, it accepts one or more // `func(*host.Configurator)` that are being executed // on that specific host that this function will create to start the server. // Via host configurators you can configure the back-end host supervisor, // i.e to add events for shutdown, serve or error. // An example of this use case can be found at: // https://github.com/kataras/iris/blob/main/_examples/http-server/notify-on-shutdown/main.go // Look at the `ConfigureHost` too. // // See `Run` for more. func TLS(addr string, certFileOrContents, keyFileOrContents string, hostConfigs ...host.Configurator) Runner { return func(app *Application) error { return app.NewHost(&http.Server{Addr: addr}). Configure(hostConfigs...). ListenAndServeTLS(certFileOrContents, keyFileOrContents) } } // AutoTLS can be used as an argument for the `Run` method. // It will start the Application's secure server using // certifications created on the fly by the "autocert" golang/x package, // so localhost may not be working, use it at "production" machine. // // Addr should have the form of [host]:port, i.e mydomain.com:443. // // The whitelisted domains are separated by whitespace in "domain" argument, // i.e "iris-go.com", can be different than "addr". // If empty, all hosts are currently allowed. This is not recommended, // as it opens a potential attack where clients connect to a server // by IP address and pretend to be asking for an incorrect host name. // Manager will attempt to obtain a certificate for that host, incorrectly, // eventually reaching the CA's rate limit for certificate requests // and making it impossible to obtain actual certificates. // // For an "e-mail" use a non-public one, letsencrypt needs that for your own security. // // Note: `AutoTLS` will start a new server for you // which will redirect all http versions to their https, including subdomains as well. // // Last argument is optional, it accepts one or more // `func(*host.Configurator)` that are being executed // on that specific host that this function will create to start the server. // Via host configurators you can configure the back-end host supervisor, // i.e to add events for shutdown, serve or error. // An example of this use case can be found at: // https://github.com/kataras/iris/blob/main/_examples/http-server/notify-on-shutdown/main.go // Look at the `ConfigureHost` too. // // Usage: // app.Run(iris.AutoTLS("iris-go.com:443", "iris-go.com www.iris-go.com", "mail@example.com")) // // See `Run` and `core/host/Supervisor#ListenAndServeAutoTLS` for more. func AutoTLS( addr string, domain string, email string, hostConfigs ...host.Configurator, ) Runner { return func(app *Application) error { return app.NewHost(&http.Server{Addr: addr}). Configure(hostConfigs...). ListenAndServeAutoTLS(domain, email, "letscache") } } // Raw can be used as an argument for the `Run` method. // It accepts any (listen) function that returns an error, // this function should be block and return an error // only when the server exited or a fatal error caused. // // With this option you're not limited to the servers // that iris can run by-default. // // See `Run` for more. func Raw(f func() error) Runner { return func(app *Application) error { app.logger.Debugf("HTTP Server will start from unknown, external function") return f() } } var ( // ErrServerClosed is logged by the standard net/http server when the server is terminated. // Ignore it by passing this error to the `iris.WithoutServerError` configurator // on `Application.Run/Listen` method. // // An alias of the `http#ErrServerClosed`. ErrServerClosed = http.ErrServerClosed // ErrURLQuerySemicolon is logged by the standard net/http server when // the request contains a semicolon (;) wihch, after go1.17 it's not used as a key-value separator character. // // Ignore it by passing this error to the `iris.WithoutServerError` configurator // on `Application.Run/Listen` method. // // An alias of the `http#ErrServerClosed`. ErrURLQuerySemicolon = errors.New("http: URL query contains semicolon, which is no longer a supported separator; parts of the query may be stripped when parsed; see golang.org/issue/25192") ) // Listen builds the application and starts the server // on the TCP network address "host:port" which // handles requests on incoming connections. // // Listen always returns a non-nil error except // when NonBlocking option is being passed, so the error goes to the Wait method. // Ignore specific errors by using an `iris.WithoutServerError(iris.ErrServerClosed)` // as a second input argument. // // Listen is a shortcut of `app.Run(iris.Addr(hostPort, withOrWithout...))`. // See `Run` for details. func (app *Application) Listen(hostPort string, withOrWithout ...Configurator) error { return app.Run(Addr(hostPort), withOrWithout...) } // Run builds the framework and starts the desired `Runner` with or without configuration edits. // // Run should be called only once per Application instance, it blocks like http.Server. // // If more than one server needed to run on the same iris instance // then create a new host and run it manually by `go NewHost(*http.Server).Serve/ListenAndServe` etc... // or use an already created host: // h := NewHost(*http.Server) // Run(Raw(h.ListenAndServe), WithCharset("utf-8"), WithRemoteAddrHeader("CF-Connecting-IP")) // // The Application can go online with any type of server or iris's host with the help of // the following runners: // `Listener`, `Server`, `Addr`, `TLS`, `AutoTLS` and `Raw`. func (app *Application) Run(serve Runner, withOrWithout ...Configurator) error { app.Configure(withOrWithout...) if err := app.Build(); err != nil { app.logger.Error(err) return err } app.ConfigureHost(func(host *Supervisor) { host.SocketSharding = app.config.SocketSharding host.KeepAlive = app.config.KeepAlive }) app.tryStartTunneling() if len(app.Hosts) > 0 { app.logger.Debugf("Application: running using %d host(s)", len(app.Hosts)+1 /* +1 the current */) } if app.config.NonBlocking { go func() { err := app.serve(serve) if err != nil { app.setRunError(err) } }() return nil } // this will block until an error(unless supervisor's DeferFlow called from a Task) // or NonBlocking was passed (see above). return app.serve(serve) } func (app *Application) serve(serve Runner) error { err := serve(app) if err != nil { app.logger.Error(err) } return err } func (app *Application) setRunError(err error) { app.runErrorMu.Lock() app.runError = err app.runErrorMu.Unlock() } func (app *Application) getRunError() error { app.runErrorMu.RLock() err := app.runError app.runErrorMu.RUnlock() return err } // Wait blocks the main goroutine until the server application is up and running. // Useful only when `Run` is called with `iris.NonBlocking()` option. func (app *Application) Wait(ctx stdContext.Context) error { if !app.config.NonBlocking { return nil } // First check if there is an error already from the app.Run. if err := app.getRunError(); err != nil { return err } // Set the base for exponential backoff. base := 2.0 // Get the maximum number of retries by context or force to 7 retries. var maxRetries int // Get the deadline of the context. if deadline, ok := ctx.Deadline(); ok { now := time.Now() timeout := deadline.Sub(now) maxRetries = getMaxRetries(timeout, base) } else { maxRetries = 7 // 256 seconds max. } // Set the initial retry interval. retryInterval := time.Second return app.tryConnect(ctx, maxRetries, retryInterval, base) } // getMaxRetries calculates the maximum number of retries from the retry interval and the base. func getMaxRetries(retryInterval time.Duration, base float64) int { // Convert the retry interval to seconds. seconds := retryInterval.Seconds() // Apply the inverse formula. retries := math.Log(seconds)/math.Log(base) - 1 return int(math.Round(retries)) } // tryConnect tries to connect to the server with the given context and retry parameters. func (app *Application) tryConnect(ctx stdContext.Context, maxRetries int, retryInterval time.Duration, base float64) error { // Try to connect to the server in a loop. for i := 0; i < maxRetries; i++ { // Check the context before each attempt. select { case <-ctx.Done(): // Context is canceled, return the context error. return ctx.Err() default: address := app.getVHost() // Get this server's listening address. if address == "" { i-- // Note that this may be modified at another go routine of the serve method. So it may be empty at first chance. So retry fetching the VHost every 1 second. time.Sleep(time.Second) continue } // Context is not canceled, proceed with the attempt. conn, err := net.Dial("tcp", address) if err == nil { // Connection successful, close the connection and return nil. conn.Close() return nil // exit. } // ignore error. // Connection failed, wait for the retry interval and try again. time.Sleep(retryInterval) // After each failed attempt, check the server Run's error again. if err := app.getRunError(); err != nil { return err } // Increase the retry interval by the base raised to the power of the number of attempts. /* 0 2 seconds 1 4 seconds 2 8 seconds 3 ~16 seconds 4 ~32 seconds 5 ~64 seconds 6 ~128 seconds 7 ~256 seconds 8 ~512 seconds ... */ retryInterval = time.Duration(math.Pow(base, float64(i+1))) * time.Second } } // All attempts failed, return an error. return fmt.Errorf("failed to connect to the server after %d retries", maxRetries) } // https://ngrok.com/docs func (app *Application) tryStartTunneling() { if len(app.config.Tunneling.Tunnels) == 0 { return } app.ConfigureHost(func(su *host.Supervisor) { su.RegisterOnServe(func(h host.TaskHost) { publicAddrs, err := tunnel.Start(app.config.Tunneling) if err != nil { app.logger.Errorf("Host: tunneling error: %v", err) return } publicAddr := publicAddrs[0] // to make subdomains resolution still based on this new remote, public addresses. app.setVHost(publicAddr[strings.Index(publicAddr, "://")+3:]) directLog := []byte(fmt.Sprintf("• Public Address: %s\n", publicAddr)) app.logger.Printer.Write(directLog) // nolint:errcheck }) }) } ================================================ FILE: iris_guide.go ================================================ package iris import ( "strings" "time" "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/core/router" "github.com/kataras/iris/v12/middleware/cors" "github.com/kataras/iris/v12/middleware/modrevision" "github.com/kataras/iris/v12/middleware/recover" "github.com/kataras/iris/v12/x/errors" ) // NewGuide returns a simple Iris API builder. // // Example Code: /* package main import ( "context" "database/sql" "time" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/x/errors" ) func main() { iris.NewGuide(). AllowOrigin("*"). Compression(true). Health(true, "development", "kataras"). Timeout(0, 20*time.Second, 20*time.Second). Middlewares(). Services( // openDatabase(), // NewSQLRepoRegistry, NewMemRepoRegistry, NewTestService, ). API("/tests", new(TestAPI)). Listen(":80") } // Recommendation: move it to /api/tests/api.go file. type TestAPI struct { TestService *TestService } func (api *TestAPI) Configure(r iris.Party) { r.Get("/", api.listTests) } func (api *TestAPI) listTests(ctx iris.Context) { tests, err := api.TestService.ListTests(ctx) if err != nil { errors.Internal.LogErr(ctx, err) return } ctx.JSON(tests) } // Recommendation: move it to /pkg/storage/sql/db.go file. type DB struct { *sql.DB } func openDatabase( your database configuration... ) *DB { conn, err := sql.Open(...) // handle error. return &DB{DB: conn} } func (db *DB) Close() error { return nil } // Recommendation: move it to /pkg/repository/registry.go file. type RepoRegistry interface { Tests() TestRepository InTransaction(ctx context.Context, fn func(RepoRegistry) error) error } // Recommendation: move it to /pkg/repository/registry/memory.go file. type repoRegistryMem struct { tests TestRepository } func NewMemRepoRegistry() RepoRegistry { return &repoRegistryMem{ tests: NewMemTestRepository(), } } func (r *repoRegistryMem) Tests() TestRepository { return r.tests } func (r *repoRegistryMem) InTransaction(ctx context.Context, fn func(RepoRegistry) error) error { return nil } // Recommendation: move it to /pkg/repository/registry/sql.go file. type repoRegistrySQL struct { db *DB tests TestRepository } func NewSQLRepoRegistry(db *DB) RepoRegistry { return &repoRegistrySQL{ db: db, tests: NewSQLTestRepository(db), } } func (r *repoRegistrySQL) Tests() TestRepository { return r.tests } func (r *repoRegistrySQL) InTransaction(ctx context.Context, fn func(RepoRegistry) error) error { return nil // your own database transaction code, may look something like that: // tx, err := r.db.BeginTx(ctx, nil) // if err != nil { // return err // } // defer tx.Rollback() // newRegistry := NewSQLRepoRegistry(tx) // if err := fn(newRegistry);err!=nil{ // return err // } // return tx.Commit() } // Recommendation: move it to /pkg/test/test.go type Test struct { Name string `db:"name"` } // Recommendation: move it to /pkg/test/repository.go type TestRepository interface { ListTests(ctx context.Context) ([]Test, error) } type testRepositoryMem struct { tests []Test } func NewMemTestRepository() TestRepository { list := []Test{ {Name: "test1"}, {Name: "test2"}, {Name: "test3"}, } return &testRepositoryMem{ tests: list, } } func (r *testRepositoryMem) ListTests(ctx context.Context) ([]Test, error) { return r.tests, nil } type testRepositorySQL struct { db *DB } func NewSQLTestRepository(db *DB) TestRepository { return &testRepositorySQL{db: db} } func (r *testRepositorySQL) ListTests(ctx context.Context) ([]Test, error) { query := `SELECT * FROM tests ORDER BY created_at;` rows, err := r.db.QueryContext(ctx, query) if err != nil { return nil, err } defer rows.Close() tests := make([]Test, 0) for rows.Next() { var t Test if err := rows.Scan(&t.Name); err != nil { return nil, err } tests = append(tests, t) } if err := rows.Err(); err != nil { return nil, err } return tests, nil } // Recommendation: move it to /pkg/service/test_service.go file. type TestService struct { repos RepoRegistry } func NewTestService(registry RepoRegistry) *TestService { return &TestService{ repos: registry, } } func (s *TestService) ListTests(ctx context.Context) ([]Test, error) { return s.repos.Tests().ListTests(ctx) } */ func NewGuide() Guide { return &step1{} } type ( // Guide is the simplify API builder. // It's a step-by-step builder which can be used to build an Iris Application // with the most common features. Guide interface { // AllowOrigin defines the CORS allowed domains. // Many can be splitted by comma. // If "*" is provided then all origins are accepted (use it for public APIs). AllowOrigin(originLine string) CompressionGuide } // CompressionGuide is the 2nd step of the Guide. // Compression (gzip or any other client requested) can be enabled or disabled. CompressionGuide interface { // Compression enables or disables the gzip (or any other client-preferred) compression algorithm // for response writes. Compression(b bool) HealthGuide } // HealthGuide is the 3rd step of the Guide. // Health enables the /health route. HealthGuide interface { // Health enables the /health route. // If "env" and "developer" are given, these fields will be populated to the client // through headers and environment on health route. Health(b bool, env, developer string) TimeoutGuide } // TimeoutGuide is the 4th step of the Guide. // Timeout defines the http timeout, server read & write timeouts. TimeoutGuide interface { // Timeout defines the http timeout, server read & write timeouts. Timeout(requestResponseLife, read time.Duration, write time.Duration) MiddlewareGuide } // MiddlewareGuide is the 5th step of the Guide. // It registers one or more handlers to run before everything else (RouterMiddlewares) or // before registered routes (Middlewares). MiddlewareGuide interface { // RouterMiddlewares registers one or more handlers to run before everything else. RouterMiddlewares(handlers ...Handler) MiddlewareGuide // Middlewares registers one or more handlers to run before the requested route's handler. Middlewares(handlers ...Handler) ServiceGuide } // ServiceGuide is the 6th step of the Guide. // It is used to register deferrable functions and, most importantly, dependencies that APIs can use. ServiceGuide interface { // Deferrables registers one or more functions to be ran when the server is terminated. Deferrables(closers ...func()) ServiceGuide // Prefix sets the API Party prefix path. // Usage: WithPrefix("/api"). WithPrefix(prefixPath string) ServiceGuide // WithoutPrefix disables the API Party prefix path. // Usage: WithoutPrefix(), same as WithPrefix(""). WithoutPrefix() ServiceGuide // Services registers one or more dependencies that APIs can use. Services(deps ...any) ApplicationBuilder } // ApplicationBuilder is the final step of the Guide. // It is used to register APIs controllers (PartyConfigurators) and // its Build, Listen and Run methods configure and build the actual Iris application // based on the previous steps. ApplicationBuilder interface { // Handle registers a simple route on specific method and (dynamic) path. // It simply calls the Iris Application's Handle method. // Use the "API" method instead to keep the app organized. Handle(method, path string, handlers ...Handler) ApplicationBuilder // API registers a router which is responsible to serve the /api group. API(pathPrefix string, c ...router.PartyConfigurator) ApplicationBuilder // Build builds the application with the prior configuration and returns the // Iris Application instance for further customizations. // // Use "Build" before "Listen" or "Run" to apply further modifications // to the framework before starting the server. Calling "Build" is optional. Build() *Application // optional call. // Listen calls the Application's Listen method which is a shortcut of Run(iris.Addr("hostPort")). // Use "Run" instead if you need to customize the HTTP/2 server itself. Listen(hostPort string, configurators ...Configurator) error // Listen OR Run. // Run calls the Application's Run method. // The 1st argument is a Runner (iris.Listener, iris.Server, iris.Addr, iris.TLS, iris.AutoTLS and iris.Raw). // The 2nd argument can be used to add custom configuration right before the server is up and running. Run(runner Runner, configurators ...Configurator) error } ) type step1 struct { originLine string } func (s *step1) AllowOrigin(originLine string) CompressionGuide { s.originLine = originLine return &step2{ step1: s, } } type step2 struct { step1 *step1 enableCompression bool } func (s *step2) Compression(b bool) HealthGuide { s.enableCompression = b return &step3{ step2: s, } } type step3 struct { step2 *step2 enableHealth bool env, developer string } func (s *step3) Health(b bool, env, developer string) TimeoutGuide { s.enableHealth = b s.env, s.developer = env, developer return &step4{ step3: s, } } type step4 struct { step3 *step3 handlerTimeout time.Duration serverTimeoutRead time.Duration serverTimeoutWrite time.Duration } func (s *step4) Timeout(requestResponseLife, read, write time.Duration) MiddlewareGuide { s.handlerTimeout = requestResponseLife s.serverTimeoutRead = read s.serverTimeoutWrite = write return &step5{ step4: s, } } type step5 struct { step4 *step4 routerMiddlewares []Handler // top-level router middlewares, fire even on 404s. middlewares []Handler } func (s *step5) RouterMiddlewares(handlers ...Handler) MiddlewareGuide { s.routerMiddlewares = append(s.routerMiddlewares, handlers...) return s } func (s *step5) Middlewares(handlers ...Handler) ServiceGuide { s.middlewares = handlers return &step6{ step5: s, prefix: getDefaultAPIPrefix(), } } type step6 struct { step5 *step5 deps []any // derives from "deps". closers []func() // derives from "deps". configuratorsAsDeps []Configurator // API Party optional prefix path. // If this is nil then it defaults to "/api" in order to keep backwards compatibility, // otherwise can be set to empty or a custom one. prefix *string } func (s *step6) Deferrables(closers ...func()) ServiceGuide { s.closers = append(s.closers, closers...) return s } var defaultAPIPrefix = "/api" func getDefaultAPIPrefix() *string { return &defaultAPIPrefix } // WithPrefix sets the API Party prefix path. // Usage: WithPrefix("/api"). func (s *step6) WithPrefix(prefixPath string) ServiceGuide { if prefixPath == "" { return s.WithoutPrefix() } *s.prefix = prefixPath return s } // WithoutPrefix disables the API Party prefix path, same as WithPrefix(""). // Usage: WithoutPrefix() func (s *step6) WithoutPrefix() ServiceGuide { s.prefix = nil return s } func (s *step6) getPrefix() string { if s.prefix == nil { // if WithoutPrefix called then API has no prefix. return "" } apiPrefix := *s.prefix if apiPrefix == "" { // if not nil but empty (this shouldn't happen) then it defaults to "/api". apiPrefix = defaultAPIPrefix } return apiPrefix } func (s *step6) Services(deps ...any) ApplicationBuilder { s.deps = deps for _, d := range deps { if d == nil { continue } switch cb := d.(type) { case func(): s.closers = append(s.closers, cb) case func() error: s.closers = append(s.closers, func() { cb() }) case interface{ Close() }: s.closers = append(s.closers, cb.Close) case interface{ Close() error }: s.closers = append(s.closers, func() { cb.Close() }) case Configurator: s.configuratorsAsDeps = append(s.configuratorsAsDeps, cb) } } return &step7{ step6: s, } } type step7 struct { step6 *step6 app *Application m map[string][]router.PartyConfigurator handlers []step7SimpleRoute } type step7SimpleRoute struct { method, path string handlers []Handler } func (s *step7) Handle(method, path string, handlers ...Handler) ApplicationBuilder { s.handlers = append(s.handlers, step7SimpleRoute{method: method, path: path, handlers: handlers}) return s } func (s *step7) API(prefix string, c ...router.PartyConfigurator) ApplicationBuilder { if s.m == nil { s.m = make(map[string][]router.PartyConfigurator) } s.m[prefix] = append(s.m[prefix], c...) return s } func (s *step7) Build() *Application { if s.app != nil { return s.app } app := New() app.SetContextErrorHandler(errors.DefaultContextErrorHandler) app.Macros().SetErrorHandler(errors.DefaultPathParameterTypeErrorHandler) routeFilters := s.step6.step5.routerMiddlewares if !context.HandlerExists(routeFilters, errors.RecoveryHandler) { // If not errors.RecoveryHandler registered, then use the default one. app.UseRouter(recover.New()) } app.UseRouter(routeFilters...) app.UseRouter(func(ctx Context) { ctx.Header("Server", "Iris") if dev := s.step6.step5.step4.step3.developer; dev != "" { ctx.Header("X-Developer", dev) } ctx.Next() }) if allowOrigin := s.step6.step5.step4.step3.step2.step1.originLine; strings.TrimSpace(allowOrigin) != "" && allowOrigin != "none" { corsMiddleware := cors.New().HandleErrorFunc(errors.FailedPrecondition.Err).AllowOrigin(allowOrigin).Handler() app.UseRouter(corsMiddleware) } if s.step6.step5.step4.step3.step2.enableCompression { app.Use(Compression) } for _, middleware := range s.step6.step5.middlewares { if middleware == nil { continue } app.Use(middleware) } if configAsDeps := s.step6.configuratorsAsDeps; len(configAsDeps) > 0 { app.Configure(configAsDeps...) } if s.step6.step5.step4.step3.enableHealth { app.Get("/health", modrevision.New(modrevision.Options{ ServerName: "Iris Server", Env: s.step6.step5.step4.step3.env, Developer: s.step6.step5.step4.step3.developer, })) } if deps := s.step6.deps; len(deps) > 0 { app.EnsureStaticBindings().RegisterDependency(deps...) } apiPrefix := s.step6.getPrefix() for prefix, c := range s.m { app.PartyConfigure(apiPrefix+prefix, c...) } for _, route := range s.handlers { app.Handle(route.method, route.path, route.handlers...) } if readTimeout := s.step6.step5.step4.serverTimeoutRead; readTimeout > 0 { app.ConfigureHost(func(su *Supervisor) { su.Server.ReadTimeout = readTimeout su.Server.IdleTimeout = readTimeout if v, recommended := readTimeout/4, 5*time.Second; v > recommended { su.Server.ReadHeaderTimeout = v } else { su.Server.ReadHeaderTimeout = recommended } }) } if writeTimeout := s.step6.step5.step4.serverTimeoutWrite; writeTimeout > 0 { app.ConfigureHost(func(su *Supervisor) { su.Server.WriteTimeout = writeTimeout }) } var defaultConfigurators = []Configurator{ WithoutServerError(ErrServerClosed, ErrURLQuerySemicolon), WithOptimizations, WithRemoteAddrHeader( "X-Real-Ip", "X-Forwarded-For", "CF-Connecting-IP", "True-Client-Ip", "X-Appengine-Remote-Addr", ), WithTimeout(s.step6.step5.step4.handlerTimeout), } app.Configure(defaultConfigurators...) s.app = app return app } func (s *step7) Listen(hostPort string, configurators ...Configurator) error { return s.Run(Addr(hostPort), configurators...) } func (s *step7) Run(runner Runner, configurators ...Configurator) error { app := s.Build() defer func() { // they will be called on interrupt signals too, // because Iris has a builtin mechanism to call server's shutdown on interrupt. for _, cb := range s.step6.closers { if cb == nil { continue } cb() } }() return app.Run(runner, configurators...) } ================================================ FILE: macro/handler/handler.go ================================================ // Package handler is the highest level module of the macro package which makes use the rest of the macro package, // it is mainly used, internally, by the router package. package handler import ( "fmt" "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/core/memstore" "github.com/kataras/iris/v12/macro" ) // ParamErrorHandler is a special type of Iris handler which receives // any error produced by a path type parameter evaluator and let developers // customize the output instead of the // provided error code 404 or anyother status code given on the `else` literal. // // Note that the builtin macros return error too, but they're handled // by the `else` literal (error code). To change this behavior // and send a custom error response you have to register it: // // app.Macros().Get("uuid").HandleError(func(ctx iris.Context, paramIndex int, err error)). // // You can also set custom macros by `app.Macros().Register`. // // See macro.HandleError to set it. type ParamErrorHandler = func(*context.Context, int, error) // alias. // CanMakeHandler reports whether a macro template needs a special macro's evaluator handler to be validated // before procceed to the next handler(s). // If the template does not contain any dynamic attributes and a special handler is NOT required // then it returns false. func CanMakeHandler(tmpl macro.Template) (needsMacroHandler bool) { if len(tmpl.Params) == 0 { return } // check if we have params like: {name:string} or {name} or {anything:path} without else keyword or any functions used inside these params. // 1. if we don't have, then we don't need to add a handler before the main route's handler (as I said, no performance if macro is not really used) // 2. if we don't have any named params then we don't need a handler too. for i := range tmpl.Params { p := tmpl.Params[i] if p.CanEval() { // if at least one needs it, then create the handler. needsMacroHandler = true if p.HandleError != nil { // Check for its type. if _, ok := p.HandleError.(ParamErrorHandler); !ok { panic(fmt.Sprintf("HandleError input argument must be a type of func(iris.Context, int, error) but got: %T", p.HandleError)) } } break } } return } // MakeHandler creates and returns a handler from a macro template, the handler evaluates each of the parameters if necessary at all. // If the template does not contain any dynamic attributes and a special handler is NOT required // then it returns a nil handler. func MakeHandler(tmpl macro.Template) context.Handler { filter := MakeFilter(tmpl) return func(ctx *context.Context) { if !filter(ctx) { if ctx.GetCurrentRoute().StatusErrorCode() == ctx.GetStatusCode() { ctx.Next() } else { ctx.StopExecution() } return } // if all passed or the next is the registered error handler to handle this status code, // just continue. ctx.Next() } } // MakeFilter returns a Filter which reports whether a specific macro template // and its parameters pass the serve-time validation. func MakeFilter(tmpl macro.Template) context.Filter { if !CanMakeHandler(tmpl) { return nil } return func(ctx *context.Context) bool { for i := range tmpl.Params { p := tmpl.Params[i] if !p.CanEval() { continue // allow. } // 07-29-2019 // changed to retrieve by param index in order to support // different parameter names for routes with // different param types (and probably different param names i.e {name:string}, {id:uint64}) // in the exact same path pattern. // // Same parameter names are not allowed, different param types in the same path // should have different name e.g. {name} {id:uint64}; // something like {name} and {name:uint64} // is bad API design and we do NOT allow it by-design. entry, found := ctx.Params().Store.GetEntryAt(p.Index) if !found { // should never happen. ctx.StatusCode(p.ErrCode) // status code can change from an error handler, set it here. return false } value, passed := p.Eval(entry.String()) if !passed { ctx.StatusCode(p.ErrCode) // status code can change from an error handler, set it here. if value != nil && p.HandleError != nil { // The "value" is an error here, always (see template.Eval). // This is always a type of ParamErrorHandler at this state (see CanMakeHandler). p.HandleError.(ParamErrorHandler)(ctx, p.Index, value.(error)) } return false } // Fixes binding different path parameters names, // // app.Get("/{fullname:string}", strHandler) // app.Get("/{id:int}", idHandler) // // before that user didn't see anything // but under the hoods the set-ed value was a type of string instead of type of int, // because store contained both "fullname" (which set-ed by the router itself on its string representation) // and "id" by the param evaluator (see core/router/handler.go and bindMultiParamTypesHandler->MakeFilter) // and the MVC get by index (e.g. 0) therefore // it got the "fullname" of type string instead of "id" int if /{int} requested. // which is critical for faster type assertion in the upcoming, new iris dependency injection (20 Feb 2020). ctx.Params().Store[p.Index] = memstore.Entry{ Key: p.Name, ValueRaw: value, } // for i, v := range ctx.Params().Store { // fmt.Printf("[%d:%s] macro/handler/handler.go: param passed: %s(%v of type: %T)\n", i, v.Key, // p.Src, v.ValueRaw, v.ValueRaw) // } } return true } } ================================================ FILE: macro/handler/handler_test.go ================================================ package handler import ( "testing" "github.com/kataras/iris/v12/macro" ) func TestCanMakeHandler(t *testing.T) { tests := []struct { src string needsHandler bool }{ {"/static/static", false}, {"/{myparam}", false}, {"/{myparam min(1)}", true}, {"/{myparam else 500}", true}, {"/{myparam else 404}", false}, {"/{myparam:string}/static", false}, {"/{myparam:int}", true}, {"/static/{myparam:int}/static", true}, {"/{myparam:path}", false}, {"/{myparam:path min(1) else 404}", true}, } availableMacros := *macro.Defaults for i, tt := range tests { tmpl, err := macro.Parse(tt.src, availableMacros) if err != nil { t.Fatalf("[%d] '%s' failed to be parsed: %v", i, tt.src, err) } if got := CanMakeHandler(tmpl); got != tt.needsHandler { if tt.needsHandler { t.Fatalf("[%d] '%s' expected to be able to generate an evaluator handler instead of a nil one", i, tt.src) } else { t.Fatalf("[%d] '%s' should not need an evaluator handler", i, tt.src) } } } } ================================================ FILE: macro/interpreter/ast/ast.go ================================================ package ast type ( // ParamType holds the necessary information about a parameter type for the parser to lookup for. ParamType interface { // The name of the parameter type. // Indent should contain the characters for the parser. Indent() string } // MasterParamType if implemented and its `Master()` returns true then empty type param will be translated to this param type. // Also its functions will be available to the rest of the macro param type's funcs. // // Only one Master is allowed. MasterParamType interface { ParamType Master() bool } // TrailingParamType if implemented and its `Trailing()` returns true // then it should be declared at the end of a route path and can accept any trailing path segment as one parameter. TrailingParamType interface { ParamType Trailing() bool } // AliasParamType if implemeneted nad its `Alias()` returns a non-empty string // then the param type can be written with that string literal too. AliasParamType interface { ParamType Alias() string } ) // IsMaster returns true if the "pt" param type is a master one. func IsMaster(pt ParamType) bool { p, ok := pt.(MasterParamType) return ok && p.Master() } // IsTrailing returns true if the "pt" param type is a marked as trailing, // which should accept more than one path segment when in the end. func IsTrailing(pt ParamType) bool { p, ok := pt.(TrailingParamType) return ok && p.Trailing() } // HasAlias returns any alias of the "pt" param type. // If alias is empty or not found then it returns false as its second output argument. func HasAlias(pt ParamType) (string, bool) { if p, ok := pt.(AliasParamType); ok { alias := p.Alias() return alias, len(alias) > 0 } return "", false } // GetMasterParamType accepts a list of ParamType and returns its master. // If no `Master` specified: // and len(paramTypes) > 0 then it will return the first one, // otherwise it returns nil. func GetMasterParamType(paramTypes ...ParamType) ParamType { for _, pt := range paramTypes { if IsMaster(pt) { return pt } } if len(paramTypes) > 0 { return paramTypes[0] } return nil } // LookupParamType accepts the string // representation of a parameter type. // Example: // "string" // "number" or "int" // "long" or "int64" // "uint8" // "uint64" // "boolean" or "bool" // "alphabetical" // "file" // "path" func LookupParamType(indentOrAlias string, paramTypes ...ParamType) (ParamType, bool) { for _, pt := range paramTypes { if pt.Indent() == indentOrAlias { return pt, true } if alias, has := HasAlias(pt); has { if alias == indentOrAlias { return pt, true } } } return nil, false } // ParamStatement is a struct // which holds all the necessary information about a macro parameter. // It holds its type (string, int, alphabetical, file, path), // its source ({param:type}), // its name ("param"), // its attached functions by the user (min, max...) // and the http error code if that parameter // failed to be evaluated. type ParamStatement struct { Src string // the original unparsed source, i.e: {id:int range(1,5) else 404} Name string // id Type ParamType // int Funcs []ParamFunc // range ErrorCode int // 404 } // ParamFunc holds the name of a parameter's function // and its arguments (values) // A param func is declared with: // {param:int range(1,5)}, // the range is the // param function name // the 1 and 5 are the two param function arguments // range(1,5) type ParamFunc struct { Name string // range Args []string // ["1","5"] } ================================================ FILE: macro/interpreter/lexer/lexer.go ================================================ package lexer import ( "github.com/kataras/iris/v12/macro/interpreter/token" ) // Lexer helps us to read/scan characters of a source and resolve their token types. type Lexer struct { input string pos int // current pos in input, current char readPos int // current reading pos in input, after current char ch byte // current char under examination } // New takes a source, series of chars, and returns // a new, ready to read from the first letter, lexer. func New(src string) *Lexer { l := &Lexer{ input: src, } // step to the first character in order to be ready l.readChar() return l } func (l *Lexer) readChar() { if l.readPos >= len(l.input) { l.ch = 0 } else { l.ch = l.input[l.readPos] } l.pos = l.readPos l.readPos++ } const ( // Begin is the symbol which lexer should scan forward to. Begin = '{' // token.LBRACE // End is the symbol which lexer should stop scanning. End = '}' // token.RBRACE ) func resolveTokenType(ch byte) token.Type { switch ch { case Begin: return token.LBRACE case End: return token.RBRACE // Let's keep it simple, no evaluation for logical operators, we are not making a new programming language, keep it simple makis. // || // case '|': // if l.peekChar() == '|' { // ch := l.ch // l.readChar() // t = token.Token{Type: token.OR, Literal: string(ch) + string(l.ch)} // } // == case ':': return token.COLON case '(': return token.LPAREN case ')': return token.RPAREN case ',': return token.COMMA // literals case 0: return token.EOF default: return token.IDENT // } } // NextToken returns the next token in the series of characters. // It can be a single symbol, a token type or a literal. // It's able to return an EOF token too. // // It moves the cursor forward. func (l *Lexer) NextToken() (t token.Token) { l.skipWhitespace() typ := resolveTokenType(l.ch) t.Type = typ switch typ { case token.EOF: t.Literal = "" case token.IDENT: if isLetter(l.ch) { // letters lit := l.readIdentifier() typ = token.LookupIdent(lit) t = l.newToken(typ, lit) return } if isDigit(l.ch) { // numbers lit := l.readNumber() t = l.newToken(token.INT, lit) return } t = l.newTokenRune(token.ILLEGAL, l.ch) default: t = l.newTokenRune(typ, l.ch) } l.readChar() // set the pos to the next return } // NextDynamicToken doesn't cares about the grammar. // It reads numbers or any unknown symbol, // it's being used by parser to skip all characters // between parameter function's arguments inside parenthesis, // in order to allow custom regexp on the end-language too. // // It moves the cursor forward. func (l *Lexer) NextDynamicToken() (t token.Token) { // calculate anything, even spaces. // numbers lit := l.readNumber() if lit != "" { return l.newToken(token.INT, lit) } lit = l.readIdentifierFuncArgument() return l.newToken(token.IDENT, lit) } // used to skip any illegal token if inside parenthesis, used to be able to set custom regexp inside a func. func (l *Lexer) readIdentifierFuncArgument() string { pos := l.pos for resolveTokenType(l.ch) != token.RPAREN && l.ch != 0 { l.readChar() } return l.input[pos:l.pos] } // PeekNextTokenType returns only the token type // of the next character and it does not move forward the cursor. // It's being used by parser to recognise empty functions, i.e `even()` // as valid functions with zero input arguments. func (l *Lexer) PeekNextTokenType() token.Type { if len(l.input)-1 > l.pos { ch := l.input[l.pos] return resolveTokenType(ch) } return resolveTokenType(0) // EOF } func (l *Lexer) newToken(tokenType token.Type, lit string) token.Token { t := token.Token{ Type: tokenType, Literal: lit, Start: l.pos, End: l.pos, } // remember, l.pos is the last char // and we want to include both start and end // in order to be easy to the user to see by just marking the expression if l.pos > 1 && len(lit) > 1 { t.End = l.pos - 1 t.Start = t.End - len(lit) + 1 } return t } func (l *Lexer) newTokenRune(tokenType token.Type, ch byte) token.Token { return l.newToken(tokenType, string(ch)) } func (l *Lexer) skipWhitespace() { for l.ch == ' ' || l.ch == '\t' || l.ch == '\n' || l.ch == '\r' { l.readChar() } } func (l *Lexer) readIdentifier() string { pos := l.pos for isLetter(l.ch) || isDigit(l.ch) { l.readChar() } return l.input[pos:l.pos] } func isLetter(ch byte) bool { return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || ch == '_' } func (l *Lexer) readNumber() string { pos := l.pos for isDigit(l.ch) { l.readChar() } return l.input[pos:l.pos] } func isDigit(ch byte) bool { return '0' <= ch && ch <= '9' } ================================================ FILE: macro/interpreter/lexer/lexer_test.go ================================================ package lexer import ( "testing" "github.com/kataras/iris/v12/macro/interpreter/token" ) func TestNextToken(t *testing.T) { input := `{id:int min(1) max(5) else 404}` tests := []struct { expectedType token.Type expectedLiteral string }{ {token.LBRACE, "{"}, // 0 {token.IDENT, "id"}, // 1 {token.COLON, ":"}, // 2 {token.IDENT, "int"}, // 3 {token.IDENT, "min"}, // 4 {token.LPAREN, "("}, // 5 {token.INT, "1"}, // 6 {token.RPAREN, ")"}, // 7 {token.IDENT, "max"}, // 8 {token.LPAREN, "("}, // 9 {token.INT, "5"}, // 10 {token.RPAREN, ")"}, // 11 {token.ELSE, "else"}, // 12 {token.INT, "404"}, // 13 {token.RBRACE, "}"}, // 14 } l := New(input) for i, tt := range tests { tok := l.NextToken() if tok.Type != tt.expectedType { t.Fatalf("tests[%d] - tokentype wrong. expected=%q, got=%q", i, tt.expectedType, tok.Type) } if tok.Literal != tt.expectedLiteral { t.Fatalf("tests[%d] - literal wrong. expected=%q, got=%q", i, tt.expectedLiteral, tok.Literal) } } } // EMEINA STO: // 30/232 selida apto making a interpeter in Go. // den ekana to skipWhitespaces giati skeftomai // an borei na to xreiastw 9a dw aurio. ================================================ FILE: macro/interpreter/parser/parser.go ================================================ package parser import ( "errors" "fmt" "strconv" "strings" "github.com/kataras/iris/v12/macro/interpreter/ast" "github.com/kataras/iris/v12/macro/interpreter/lexer" "github.com/kataras/iris/v12/macro/interpreter/token" ) // Parse takes a route "fullpath" // and returns its param statements // or an error if failed. func Parse(fullpath string, paramTypes []ast.ParamType) ([]*ast.ParamStatement, error) { if len(paramTypes) == 0 { return nil, fmt.Errorf("empty parameter types") } pathParts := strings.Split(fullpath, "/") p := new(ParamParser) statements := make([]*ast.ParamStatement, 0) for i, s := range pathParts { if s == "" { // if starts with / continue } // if it's not a named path parameter of the new syntax then continue to the next // if s[0] != lexer.Begin || s[len(s)-1] != lexer.End { // continue // } // Modified to show an error on a certain invalid action. if s[0] != lexer.Begin { continue } if s[len(s)-1] != lexer.End { if idx := strings.LastIndexByte(s, lexer.End); idx > 2 && idx < len(s)-1 /* at least {x}*/ { // Do NOT allow something more than a dynamic path parameter in the same path segment, // e.g. /{param}-other-static-part/. See #2024. // this allows it but NO (see trie insert): s = s[0 : idx+1] return nil, fmt.Errorf("%s: invalid path part: dynamic path parameter and other parameters or static parts are not allowed in the same exact request path part, use the {regexp} function alone instead", s) } else { continue } } p.Reset(s) stmt, err := p.Parse(paramTypes) if err != nil { // exit on first error return nil, err } // if we have param type path but it's not the last path part if ast.IsTrailing(stmt.Type) && i < len(pathParts)-1 { return nil, fmt.Errorf("%s: parameter type \"%s\" should be registered to the very end of a path once", s, stmt.Type.Indent()) } statements = append(statements, stmt) } return statements, nil } // ParamParser is the parser // which is being used by the Parse function // to parse path segments one by one // and return their parsed parameter statements (param name, param type its functions and the inline route's functions). type ParamParser struct { src string errors []string } // NewParamParser receives a "src" of a single parameter // and returns a new ParamParser, ready to Parse. func NewParamParser(src string) *ParamParser { p := new(ParamParser) p.Reset(src) return p } // Reset resets this ParamParser, // reset the errors and set the source to the input "src". func (p *ParamParser) Reset(src string) { p.src = src p.errors = []string{} } func (p *ParamParser) appendErr(format string, a ...any) { p.errors = append(p.errors, fmt.Sprintf(format, a...)) } const ( // DefaultParamErrorCode is the default http error code, 404 not found, // per-parameter. An error code can be set via // the "else" keyword inside a route's path. DefaultParamErrorCode = 404 ) func (p ParamParser) Error() error { if len(p.errors) > 0 { return errors.New(strings.Join(p.errors, "\n")) } return nil } // Parse parses the p.src based on the given param types and returns its param statement // and an error on failure. func (p *ParamParser) Parse(paramTypes []ast.ParamType) (*ast.ParamStatement, error) { l := lexer.New(p.src) stmt := &ast.ParamStatement{ ErrorCode: DefaultParamErrorCode, Type: ast.GetMasterParamType(paramTypes...), Src: p.src, } lastParamFunc := ast.ParamFunc{} for { t := l.NextToken() if t.Type == token.EOF { if stmt.Name == "" { p.appendErr("[1:] parameter name is missing") } break } switch t.Type { case token.LBRACE: // can accept only letter or number only. nextTok := l.NextToken() stmt.Name = nextTok.Literal case token.COLON: // type can accept both letters and numbers but not symbols ofc. nextTok := l.NextToken() paramType, found := ast.LookupParamType(nextTok.Literal, paramTypes...) if !found { p.appendErr("[%d:%d] unexpected parameter type: %s", t.Start, t.End, nextTok.Literal) } stmt.Type = paramType // param func case token.IDENT: lastParamFunc.Name = t.Literal case token.LPAREN: // param function without arguments () if l.PeekNextTokenType() == token.RPAREN { // do nothing, just continue to the RPAREN continue } argValTok := l.NextDynamicToken() // catch anything from "(" and forward, until ")", because we need to // be able to use regex expression as a macro type's func argument too. // fmt.Printf("argValTok: %#v\n", argValTok) // fmt.Printf("argVal: %#v\n", argVal) lastParamFunc.Args = append(lastParamFunc.Args, argValTok.Literal) case token.COMMA: argValTok := l.NextToken() lastParamFunc.Args = append(lastParamFunc.Args, argValTok.Literal) case token.RPAREN: stmt.Funcs = append(stmt.Funcs, lastParamFunc) lastParamFunc = ast.ParamFunc{} // reset case token.ELSE: errCodeTok := l.NextToken() if errCodeTok.Type != token.INT { p.appendErr("[%d:%d] expected error code to be an integer but got %s", t.Start, t.End, errCodeTok.Literal) continue } errCode, err := strconv.Atoi(errCodeTok.Literal) if err != nil { // this is a bug on lexer if throws because we already check for token.INT p.appendErr("[%d:%d] unexpected lexer error while trying to convert error code to an integer, %s", t.Start, t.End, err.Error()) continue } stmt.ErrorCode = errCode case token.RBRACE: // check if } but not { if stmt.Name == "" { p.appendErr("[%d:%d] illegal token: }, forgot '{' ?", t.Start, t.End) } case token.ILLEGAL: p.appendErr("[%d:%d] illegal token: %s", t.Start, t.End, t.Literal) default: p.appendErr("[%d:%d] unexpected token type: %q with value %s", t.Start, t.End, t.Type, t.Literal) } } return stmt, p.Error() } ================================================ FILE: macro/interpreter/parser/parser_test.go ================================================ package parser import ( "fmt" "reflect" "strings" "testing" "github.com/kataras/iris/v12/macro/interpreter/ast" ) type simpleParamType string func (pt simpleParamType) Indent() string { return string(pt) } type masterParamType simpleParamType func (pt masterParamType) Indent() string { return string(pt) } func (pt masterParamType) Master() bool { return true } type wildcardParamType string func (pt wildcardParamType) Indent() string { return string(pt) } func (pt wildcardParamType) Trailing() bool { return true } type aliasedParamType []string func (pt aliasedParamType) Indent() string { return string(pt[0]) } func (pt aliasedParamType) Alias() string { return pt[1] } var ( paramTypeString = masterParamType("string") paramTypeNumber = aliasedParamType{"number", "int"} paramTypeInt64 = aliasedParamType{"int64", "long"} paramTypeUint8 = simpleParamType("uint8") paramTypeUint64 = simpleParamType("uint64") paramTypeBool = aliasedParamType{"bool", "boolean"} paramTypeAlphabetical = simpleParamType("alphabetical") paramTypeFile = simpleParamType("file") paramTypePath = wildcardParamType("path") ) var testParamTypes = []ast.ParamType{ paramTypeString, paramTypeNumber, paramTypeInt64, paramTypeUint8, paramTypeUint64, paramTypeBool, paramTypeAlphabetical, paramTypeFile, paramTypePath, } func TestParseParamError(t *testing.T) { // fail illegalChar := '$' input := "{id" + string(illegalChar) + "int range(1,5) else 404}" p := NewParamParser(input) _, err := p.Parse(testParamTypes) if err == nil { t.Fatalf("expecting not empty error on input '%s'", input) } illIdx := strings.IndexRune(input, illegalChar) expectedErr := fmt.Sprintf("[%d:%d] illegal token: %s", illIdx, illIdx, "$") if got := err.Error(); got != expectedErr { t.Fatalf("expecting error to be '%s' but got: %s", expectedErr, got) } // // success input2 := "{id:uint64 range(1,5) else 404}" p.Reset(input2) _, err = p.Parse(testParamTypes) if err != nil { t.Fatalf("expecting empty error on input '%s', but got: %s", input2, err.Error()) } // } // mustLookupParamType same as `ast.LookupParamType` but it panics if "indent" does not match with a valid Param Type. func mustLookupParamType(indent string) ast.ParamType { pt, found := ast.LookupParamType(indent, testParamTypes...) if !found { panic("param type '" + indent + "' is not part of the provided param types") } return pt } func TestParseParam(t *testing.T) { tests := []struct { valid bool expectedStatement ast.ParamStatement }{ { true, ast.ParamStatement{ Src: "{id:int min(1) max(5) else 404}", Name: "id", Type: mustLookupParamType("number"), Funcs: []ast.ParamFunc{ { Name: "min", Args: []string{"1"}, }, { Name: "max", Args: []string{"5"}, }, }, ErrorCode: 404, }, }, // 0 { true, ast.ParamStatement{ // test alias of int. Src: "{id:number range(1,5)}", Name: "id", Type: mustLookupParamType("number"), Funcs: []ast.ParamFunc{ { Name: "range", Args: []string{"1", "5"}, }, }, ErrorCode: 404, }, }, // 1 { true, ast.ParamStatement{ Src: "{file:path contains(.)}", Name: "file", Type: mustLookupParamType("path"), Funcs: []ast.ParamFunc{ { Name: "contains", Args: []string{"."}, }, }, ErrorCode: 404, }, }, // 2 { true, ast.ParamStatement{ Src: "{username:alphabetical}", Name: "username", Type: mustLookupParamType("alphabetical"), ErrorCode: 404, }, }, // 3 { true, ast.ParamStatement{ Src: "{myparam}", Name: "myparam", Type: mustLookupParamType("string"), ErrorCode: 404, }, }, // 4 { false, ast.ParamStatement{ Src: "{myparam_:thisianunexpected}", Name: "myparam_", Type: nil, ErrorCode: 404, }, }, // 5 { true, ast.ParamStatement{ Src: "{myparam2}", Name: "myparam2", // we now allow integers to the parameter names. Type: ast.GetMasterParamType(testParamTypes...), ErrorCode: 404, }, }, // 6 { true, ast.ParamStatement{ Src: "{id:int even()}", // test param funcs without any arguments (LPAREN peek for RPAREN) Name: "id", Type: mustLookupParamType("number"), Funcs: []ast.ParamFunc{ { Name: "even", }, }, ErrorCode: 404, }, }, // 7 { true, ast.ParamStatement{ Src: "{id:int64 else 404}", Name: "id", Type: mustLookupParamType("int64"), ErrorCode: 404, }, }, // 8 { true, ast.ParamStatement{ Src: "{id:long else 404}", // backwards-compatible test. Name: "id", Type: mustLookupParamType("int64"), ErrorCode: 404, }, }, // 9 { true, ast.ParamStatement{ Src: "{id:long else 404}", Name: "id", Type: mustLookupParamType("int64"), // backwards-compatible test of LookupParamType. ErrorCode: 404, }, }, // 10 { true, ast.ParamStatement{ Src: "{has:bool else 404}", Name: "has", Type: mustLookupParamType("bool"), ErrorCode: 404, }, }, // 11 { true, ast.ParamStatement{ Src: "{has:boolean else 404}", // backwards-compatible test. Name: "has", Type: mustLookupParamType("bool"), ErrorCode: 404, }, }, // 12 } p := new(ParamParser) for i, tt := range tests { p.Reset(tt.expectedStatement.Src) resultStmt, err := p.Parse(testParamTypes) if tt.valid && err != nil { t.Fatalf("tests[%d] - error %s", i, err.Error()) } else if !tt.valid && err == nil { t.Fatalf("tests[%d] - expected to be a failure", i) } if resultStmt != nil { // is valid here if !reflect.DeepEqual(tt.expectedStatement, *resultStmt) { t.Fatalf("tests[%d] - wrong statement, expected and result differs. Details:\n%#v\n%#v", i, tt.expectedStatement, *resultStmt) } } } } func TestParse(t *testing.T) { tests := []struct { path string valid bool expectedStatements []ast.ParamStatement }{ { "/api/users/{id:int min(1) max(5) else 404}", true, []ast.ParamStatement{ { Src: "{id:int min(1) max(5) else 404}", Name: "id", Type: paramTypeNumber, Funcs: []ast.ParamFunc{ { Name: "min", Args: []string{"1"}, }, { Name: "max", Args: []string{"5"}, }, }, ErrorCode: 404, }, }, }, // 0 { "/admin/{id:uint64 range(1,5)}", true, []ast.ParamStatement{ { Src: "{id:uint64 range(1,5)}", Name: "id", Type: paramTypeUint64, Funcs: []ast.ParamFunc{ { Name: "range", Args: []string{"1", "5"}, }, }, ErrorCode: 404, }, }, }, // 1 { "/files/{file:path contains(.)}", true, []ast.ParamStatement{ { Src: "{file:path contains(.)}", Name: "file", Type: paramTypePath, Funcs: []ast.ParamFunc{ { Name: "contains", Args: []string{"."}, }, }, ErrorCode: 404, }, }, }, // 2 { "/profile/{username:alphabetical}", true, []ast.ParamStatement{ { Src: "{username:alphabetical}", Name: "username", Type: paramTypeAlphabetical, ErrorCode: 404, }, }, }, // 3 { "/something/here/{myparam}", true, []ast.ParamStatement{ { Src: "{myparam}", Name: "myparam", Type: paramTypeString, ErrorCode: 404, }, }, }, // 4 { "/unexpected/{myparam_:thisianunexpected}", false, []ast.ParamStatement{ { Src: "{myparam_:thisianunexpected}", Name: "myparam_", Type: nil, ErrorCode: 404, }, }, }, // 5 { "/p2/{myparam2}", true, []ast.ParamStatement{ { Src: "{myparam2}", Name: "myparam2", // we now allow integers to the parameter names. Type: paramTypeString, ErrorCode: 404, }, }, }, // 6 { "/assets/{file:path}/invalid", false, // path should be in the end segment []ast.ParamStatement{ { Src: "{file:path}", Name: "file", Type: paramTypePath, ErrorCode: 404, }, }, }, // 7 } for i, tt := range tests { statements, err := Parse(tt.path, testParamTypes) if tt.valid && err != nil { t.Fatalf("tests[%d] - error %s", i, err.Error()) } else if !tt.valid && err == nil { t.Fatalf("tests[%d] - expected to be a failure", i) } for j := range statements { for l := range tt.expectedStatements { if !reflect.DeepEqual(tt.expectedStatements[l], *statements[j]) { t.Fatalf("tests[%d] - wrong statements, expected and result differs. Details:\n%#v\n%#v", i, tt.expectedStatements[l], *statements[j]) } } } } } ================================================ FILE: macro/interpreter/token/token.go ================================================ package token // Type is a specific type of int which describes the symbols. type Type int // Token describes the letter(s) or symbol, is a result of the lexer. type Token struct { Type Type Literal string Start int // including the first char End int // including the last char } // /about/{fullname:alphabetical} // /profile/{anySpecialName:string} // {id:uint64 range(1,5) else 404} // /admin/{id:int eq(1) else 402} // /file/{filepath:file else 405} const ( EOF = iota // 0 ILLEGAL // Identifiers + literals LBRACE // { RBRACE // } // PARAM_IDENTIFIER // id COLON // : LPAREN // ( RPAREN // ) // PARAM_FUNC_ARG // 1 COMMA IDENT // string or keyword // Keywords // keywords_start ELSE // else // keywords_end INT // 42 ) var keywords = map[string]Type{ "else": ELSE, } // LookupIdent receives a series of chars // and tries to resolves the token type. func LookupIdent(ident string) Type { if tok, ok := keywords[ident]; ok { return tok } return IDENT } ================================================ FILE: macro/macro.go ================================================ package macro import ( "fmt" "reflect" "regexp" "strconv" "strings" "unicode" ) type ( // ParamEvaluator is the signature for param type evaluator. // It accepts the param's value as string and returns // the value (which its type is used for the input argument of the parameter functions, if any) // and a true value for passed, otherwise nil and false should be returned. ParamEvaluator func(paramValue string) (any, bool) ) var goodEvaluatorFuncs = []reflect.Type{ reflect.TypeOf(func(string) (any, bool) { return nil, false }), reflect.TypeOf(ParamEvaluator(func(string) (any, bool) { return nil, false })), } func goodParamFunc(typ reflect.Type) bool { if typ.Kind() == reflect.Func { // it should be a func which returns a func (see below check). if typ.NumOut() == 1 { typOut := typ.Out(0) if typOut.Kind() != reflect.Func { return false } if typOut.NumOut() == 2 { // if it's a type of EvaluatorFunc, used for param evaluator. for _, fType := range goodEvaluatorFuncs { if typOut == fType { return true } } return false } if typOut.NumIn() == 1 && typOut.NumOut() == 1 { // if it's a type of func(paramValue [int,string...]) bool, used for param funcs. return typOut.Out(0).Kind() == reflect.Bool } } } return false } // Regexp accepts a regexp "expr" expression // and returns its MatchString. // The regexp is compiled before return. // // Returns a not-nil error on regexp compile failure. func Regexp(expr string) (func(string) bool, error) { if expr == "" { return nil, fmt.Errorf("empty regex expression") } // add the last $ if missing (and not wildcard(?)) if i := expr[len(expr)-1]; i != '$' && i != '*' { expr += "$" } r, err := regexp.Compile(expr) if err != nil { return nil, err } return r.MatchString, nil } // MustRegexp same as Regexp // but it panics on the "expr" parse failure. func MustRegexp(expr string) func(string) bool { r, err := Regexp(expr) if err != nil { panic(err) } return r } // goodParamFuncName reports whether the function name is a valid identifier. func goodParamFuncName(name string) bool { if name == "" { return false } // valid names are only letters and _ for _, r := range name { switch { case r == '_': case !unicode.IsLetter(r): return false } } return true } // the convertBuilderFunc return value is generating at boot time. // convertFunc converts an interface to a valid full param function. func convertBuilderFunc(fn any) ParamFuncBuilder { typFn := reflect.TypeOf(fn) if !goodParamFunc(typFn) { // it's not a function which returns a function, // it's not a a func(compileArgs) func(requestDynamicParamValue) bool // but it's a func(requestDynamicParamValue) bool, such as regexp.Compile.MatchString if typFn.NumIn() == 1 && typFn.In(0).Kind() == reflect.String && typFn.NumOut() == 1 && typFn.Out(0).Kind() == reflect.Bool { fnV := reflect.ValueOf(fn) // let's convert it to a ParamFuncBuilder which its combile route arguments are empty and not used at all. // the below return function runs on each route that this param type function is used in order to validate the function, // if that param type function is used wrongly it will be panic like the rest, // indeed the only check is the len of arguments not > 0, no types of values or conversions, // so we return it as soon as possible. return func(args []string) reflect.Value { if n := len(args); n > 0 { panic(fmt.Sprintf("%T does not allow any input arguments from route but got [len=%d,values=%s]", fn, n, strings.Join(args, ", "))) } return fnV } } return nil } numFields := typFn.NumIn() panicIfErr := func(i int, err error) { if err != nil { panic(fmt.Sprintf("on field index: %d: %v", i, err)) } } return func(args []string) reflect.Value { if len(args) != numFields { // no variadics support, for now. panic(fmt.Sprintf("args(len=%d) should be the same len as numFields(%d) for: %s", len(args), numFields, typFn)) } var argValues []reflect.Value for i := 0; i < numFields; i++ { field := typFn.In(i) arg := args[i] // try to convert the string literal as we get it from the parser. var ( val any ) // try to get the value based on the expected type. switch field.Kind() { case reflect.Int: v, err := strconv.Atoi(arg) panicIfErr(i, err) val = v case reflect.Int8: v, err := strconv.ParseInt(arg, 10, 8) panicIfErr(i, err) val = int8(v) case reflect.Int16: v, err := strconv.ParseInt(arg, 10, 16) panicIfErr(i, err) val = int16(v) case reflect.Int32: v, err := strconv.ParseInt(arg, 10, 32) panicIfErr(i, err) val = int32(v) case reflect.Int64: v, err := strconv.ParseInt(arg, 10, 64) panicIfErr(i, err) val = v case reflect.Uint: v, err := strconv.ParseUint(arg, 10, strconv.IntSize) panicIfErr(i, err) val = uint(v) case reflect.Uint8: v, err := strconv.ParseUint(arg, 10, 8) panicIfErr(i, err) val = uint8(v) case reflect.Uint16: v, err := strconv.ParseUint(arg, 10, 16) panicIfErr(i, err) val = uint16(v) case reflect.Uint32: v, err := strconv.ParseUint(arg, 10, 32) panicIfErr(i, err) val = uint32(v) case reflect.Uint64: v, err := strconv.ParseUint(arg, 10, 64) panicIfErr(i, err) val = v case reflect.Float32: v, err := strconv.ParseFloat(arg, 32) panicIfErr(i, err) val = float32(v) case reflect.Float64: v, err := strconv.ParseFloat(arg, 64) panicIfErr(i, err) val = v case reflect.Bool: v, err := strconv.ParseBool(arg) panicIfErr(i, err) val = v case reflect.Slice: if len(arg) > 1 { if arg[0] == '[' && arg[len(arg)-1] == ']' { // it is a single argument but as slice. val = strings.Split(arg[1:len(arg)-1], ",") // only string slices. } } default: val = arg } argValue := reflect.ValueOf(val) if expected, got := field.Kind(), argValue.Kind(); expected != got { panic(fmt.Sprintf("func's input arguments should have the same type: [%d] expected %s but got %s", i, expected, got)) } argValues = append(argValues, argValue) } evalFn := reflect.ValueOf(fn).Call(argValues)[0] // var evaluator EvaluatorFunc // // check for typed and not typed // if _v, ok := evalFn.(EvaluatorFunc); ok { // evaluator = _v // } else if _v, ok = evalFn.(func(string) bool); ok { // evaluator = _v // } // return func(paramValue any) bool { // return evaluator(paramValue) // } return evalFn } } type ( // Macro represents the parsed macro, // which holds // the evaluator (param type's evaluator + param functions evaluators) // and its param functions. // // Any type contains its own macro // instance, so an String type // contains its type evaluator // which is the "Evaluator" field // and it can register param functions // to that macro which maps to a parameter type. Macro struct { indent string alias string master bool trailing bool Evaluator ParamEvaluator handleError any funcs []ParamFunc goType reflect.Type } // ParamFuncBuilder is a func // which accepts a param function's arguments (values) // and returns a function as value, its job // is to make the macros to be registered // by user at the most generic possible way. ParamFuncBuilder func([]string) reflect.Value // the func() bool // ParamFunc represents the parsed // parameter function, it holds // the parameter's name // and the function which will build // the evaluator func. ParamFunc struct { Name string Func ParamFuncBuilder } ) // NewMacro creates and returns a Macro that can be used as a registry for // a new customized parameter type and its functions. func NewMacro(indent, alias string, valueType any, master, trailing bool, evaluator ParamEvaluator) *Macro { return &Macro{ indent: indent, alias: alias, master: master, trailing: trailing, goType: reflect.TypeOf(valueType), Evaluator: evaluator, } } // Indent returns the name of the parameter type. func (m *Macro) Indent() string { return m.indent } // Alias returns the alias of the parameter type, if any. func (m *Macro) Alias() string { return m.alias } // Master returns true if that macro's parameter type is the // default one if not :type is followed by a parameter type inside the route path. func (m *Macro) Master() bool { return m.master } // Trailing returns true if that macro's parameter type // is wildcard and can accept one or more path segments as one parameter value. // A wildcard should be registered in the last path segment only. func (m *Macro) Trailing() bool { return m.trailing } // GoType returns the type of the parameter type's evaluator. // string if it's a string evaluator, int if it's an int evaluator etc. func (m *Macro) GoType() reflect.Type { return m.goType } // HandleError registers a handler which will be executed // when a parameter evaluator returns false and a non nil value which is a type of `error`. // The "fnHandler" value MUST BE a type of `func(iris.Context, paramIndex int, err error)`, // otherwise the program will receive a panic before server startup. // The status code of the ErrCode (`else` literal) is set // before the error handler but it can be modified inside the handler itself. func (m *Macro) HandleError(fnHandler any) *Macro { // See handler.MakeFilter. m.handleError = fnHandler return m } // func (m *Macro) SetParamResolver(fn func(memstore.Entry) any) *Macro { // m.ParamResolver = fn // return m // } // RegisterFunc registers a parameter function // to that macro. // Accepts the func name ("range") // and the function body, which should return an EvaluatorFunc // a bool (it will be converted to EvaluatorFunc later on), // i.e RegisterFunc("min", func(minValue int) func(paramValue string) bool){}) func (m *Macro) RegisterFunc(funcName string, fn any) *Macro { fullFn := convertBuilderFunc(fn) // if it's not valid then not register it at all. if fullFn != nil { m.registerFunc(funcName, fullFn) } return m } func (m *Macro) registerFunc(funcName string, fullFn ParamFuncBuilder) { if !goodParamFuncName(funcName) { return } for _, fn := range m.funcs { if fn.Name == funcName { fn.Func = fullFn return } } m.funcs = append(m.funcs, ParamFunc{ Name: funcName, Func: fullFn, }) } func (m *Macro) getFunc(funcName string) ParamFuncBuilder { for _, fn := range m.funcs { if fn.Name == funcName { if fn.Func == nil { continue } return fn.Func } } return nil } ================================================ FILE: macro/macro_test.go ================================================ package macro import ( "reflect" "strconv" "testing" "time" ) // Most important tests to look: // ../parser/parser_test.go // ../lexer/lexer_test.go func TestGoodParamFunc(t *testing.T) { good1 := func(min int, max int) func(string) bool { return func(paramValue string) bool { return true } } good2 := func(min uint64, max uint64) func(string) bool { return func(paramValue string) bool { return true } } notgood1 := func(min int, max int) bool { return false } if !goodParamFunc(reflect.TypeOf(good1)) { t.Fatalf("expected good1 func to be good but it's not") } if !goodParamFunc(reflect.TypeOf(good2)) { t.Fatalf("expected good2 func to be good but it's not") } if goodParamFunc(reflect.TypeOf(notgood1)) { t.Fatalf("expected notgood1 func to be the worst") } } func TestGoodParamFuncName(t *testing.T) { tests := []struct { name string good bool }{ {"range", true}, {"_range", true}, {"range_", true}, {"r_ange", true}, // numbers or other symbols are invalid. {"range1", false}, {"2range", false}, {"r@nge", false}, {"rang3", false}, } for i, tt := range tests { isGood := goodParamFuncName(tt.name) if tt.good && !isGood { t.Fatalf("tests[%d] - expecting valid name but got invalid for name %s", i, tt.name) } else if !tt.good && isGood { t.Fatalf("tests[%d] - expecting invalid name but got valid for name %s", i, tt.name) } } } func testEvaluatorRaw(t *testing.T, macroEvaluator *Macro, input string, expectedType reflect.Kind, pass bool, i int) { t.Helper() if macroEvaluator.Evaluator == nil && pass { return // if not evaluator defined then it should allow everything. } value, passed := macroEvaluator.Evaluator(input) if pass != passed { t.Fatalf("%s - tests[%d] - expecting[pass] %v but got %v", t.Name(), i, pass, passed) } if !passed { return } if value == nil && expectedType != reflect.Invalid { t.Fatalf("%s - tests[%d] - expecting[value] to not be nil", t.Name(), i) } if v := reflect.ValueOf(value); v.Kind() != expectedType { t.Fatalf("%s - tests[%d] - expecting[value.Kind] %v but got %v", t.Name(), i, expectedType, v.Kind()) } } func TestStringEvaluatorRaw(t *testing.T) { tests := []struct { pass bool input string }{ {true, "astring"}, // 0 {true, "astringwith_numb3rS_and_symbol$"}, // 1 {true, "32321"}, // 2 {true, "main.css"}, // 3 {true, "/assets/main.css"}, // 4 // false never } // 0 for i, tt := range tests { testEvaluatorRaw(t, String, tt.input, reflect.String, tt.pass, i) } } func TestIntEvaluatorRaw(t *testing.T) { x64 := strconv.IntSize == 64 tests := []struct { pass bool input string }{ {false, "astring"}, // 0 {false, "astringwith_numb3rS_and_symbol$"}, // 1 {true, "32321"}, // 2 {x64, "9223372036854775807" /* max int64 */}, // 3 {x64, "-9223372036854775808" /* min int64 */}, // 4 {false, "-18446744073709553213213213213213121615"}, // 5 {false, "42 18446744073709551615"}, // 6 {false, "--42"}, // 7 {true, "+42"}, // 8 {false, "main.css"}, // 9 {false, "/assets/main.css"}, // 10 } for i, tt := range tests { testEvaluatorRaw(t, Int, tt.input, reflect.Int, tt.pass, i) } } func BenchmarkIntEvaluatorRaw(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { Int.Evaluator("1234568320") Int.Evaluator("-12345678999321") } } func TestInt8EvaluatorRaw(t *testing.T) { tests := []struct { pass bool input string }{ {false, "astring"}, // 0 {false, "astringwith_numb3rS_and_symbol$"}, // 1 {false, "32321"}, // 2 {true, "127" /* max int8 */}, // 3 {true, "-128" /* min int8 */}, // 4 {false, "128"}, // 5 {false, "-129"}, // 6 {false, "-18446744073709553213213213213213121615"}, // 7 {false, "42 18446744073709551615"}, // 8 {false, "--42"}, // 9 {true, "+42"}, // 10 {false, "main.css"}, // 11 {false, "/assets/main.css"}, // 12 } for i, tt := range tests { testEvaluatorRaw(t, Int8, tt.input, reflect.Int8, tt.pass, i) } } func TestInt16EvaluatorRaw(t *testing.T) { tests := []struct { pass bool input string }{ {false, "astring"}, // 0 {false, "astringwith_numb3rS_and_symbol$"}, // 1 {true, "32321"}, // 2 {true, "32767" /* max int16 */}, // 3 {true, "-32768" /* min int16 */}, // 4 {false, "-32769"}, // 5 {false, "32768"}, // 6 {false, "-18446744073709553213213213213213121615"}, // 7 {false, "42 18446744073709551615"}, // 8 {false, "--42"}, // 9 {true, "+42"}, // 10 {false, "main.css"}, // 11 {false, "/assets/main.css"}, // 12 } for i, tt := range tests { testEvaluatorRaw(t, Int16, tt.input, reflect.Int16, tt.pass, i) } } func TestInt32EvaluatorRaw(t *testing.T) { tests := []struct { pass bool input string }{ {false, "astring"}, // 0 {false, "astringwith_numb3rS_and_symbol$"}, // 1 {true, "32321"}, // 2 {true, "1"}, // 3 {true, "42"}, // 4 {true, "2147483647" /* max int32 */}, // 5 {true, "-2147483648" /* min int32 */}, // 6 {false, "-2147483649"}, // 7 {false, "2147483648"}, // 8 {false, "-18446744073709553213213213213213121615"}, // 9 {false, "42 18446744073709551615"}, // 10 {false, "--42"}, // 11 {true, "+42"}, // 12 {false, "main.css"}, // 13 {false, "/assets/main.css"}, // 14 } for i, tt := range tests { testEvaluatorRaw(t, Int32, tt.input, reflect.Int32, tt.pass, i) } } func TestInt64EvaluatorRaw(t *testing.T) { tests := []struct { pass bool input string }{ {false, "astring"}, // 0 {false, "astringwith_numb3rS_and_symbol$"}, // 1 {false, "18446744073709551615"}, // 2 {false, "92233720368547758079223372036854775807"}, // 3 {false, "9223372036854775808 9223372036854775808"}, // 4 {false, "main.css"}, // 5 {false, "/assets/main.css"}, // 6 {true, "9223372036854775807"}, // 7 {true, "-9223372036854775808"}, // 8 {true, "-0"}, // 9 {true, "1"}, // 10 {true, "-042"}, // 11 {true, "142"}, // 12 } for i, tt := range tests { testEvaluatorRaw(t, Int64, tt.input, reflect.Int64, tt.pass, i) } } func TestUintEvaluatorRaw(t *testing.T) { x64 := strconv.IntSize == 64 tests := []struct { pass bool input string }{ {false, "astring"}, // 0 {false, "astringwith_numb3rS_and_symbol$"}, // 1 {true, "32321"}, // 2 {true, "1"}, // 3 {true, "42"}, // 4 {x64, "18446744073709551615" /* max uint64 */}, // 5 {true, "4294967295" /* max uint32 */}, // 6 {false, "-2147483649"}, // 7 {true, "2147483648"}, // 8 {false, "-18446744073709553213213213213213121615"}, // 9 {false, "42 18446744073709551615"}, // 10 {false, "--42"}, // 11 {false, "+42"}, // 12 {false, "main.css"}, // 13 {false, "/assets/main.css"}, // 14 } for i, tt := range tests { testEvaluatorRaw(t, Uint, tt.input, reflect.Uint, tt.pass, i) } } func TestUint8EvaluatorRaw(t *testing.T) { tests := []struct { pass bool input string }{ {false, "astring"}, // 0 {false, "astringwith_numb3rS_and_symbol$"}, // 1 {false, "-9223372036854775808"}, // 2 {false, "main.css"}, // 3 {false, "/assets/main.css"}, // 4 {false, "92233720368547758079223372036854775807"}, // 5 {false, "9223372036854775808 9223372036854775808"}, // 6 {false, "-1"}, // 7 {false, "-0"}, // 8 {false, "+1"}, // 9 {false, "18446744073709551615"}, // 10 {false, "9223372036854775807"}, // 11 {true, "021"}, // 12 - leading zeroes are allowed. {false, "300"}, // 13 {true, "0"}, // 14 {true, "255"}, // 15 {true, "21"}, // 16 } for i, tt := range tests { testEvaluatorRaw(t, Uint8, tt.input, reflect.Uint8, tt.pass, i) } } func TestUint16EvaluatorRaw(t *testing.T) { tests := []struct { pass bool input string }{ {false, "astring"}, // 0 {false, "astringwith_numb3rS_and_symbol$"}, // 1 {true, "32321"}, // 2 {true, "65535" /* max uint16 */}, // 3 {true, "0" /* min uint16 */}, // 4 {false, "-32769"}, // 5 {true, "32768"}, // 6 {false, "-18446744073709553213213213213213121615"}, // 7 {false, "42 18446744073709551615"}, // 8 {false, "--42"}, // 9 {false, "+42"}, // 10 {false, "main.css"}, // 11 {false, "/assets/main.css"}, // 12 } for i, tt := range tests { testEvaluatorRaw(t, Uint16, tt.input, reflect.Uint16, tt.pass, i) } } func TestUint32EvaluatorRaw(t *testing.T) { tests := []struct { pass bool input string }{ {false, "astring"}, // 0 {false, "astringwith_numb3rS_and_symbol$"}, // 1 {true, "32321"}, // 2 {true, "1"}, // 3 {true, "42"}, // 4 {true, "4294967295" /* max uint32*/}, // 5 {true, "0" /* min uint32 */}, // 6 {false, "-2147483649"}, // 7 {true, "2147483648"}, // 8 {false, "-18446744073709553213213213213213121615"}, // 9 {false, "42 18446744073709551615"}, // 10 {false, "--42"}, // 11 {false, "+42"}, // 12 {false, "main.css"}, // 13 {false, "/assets/main.css"}, // 14 } for i, tt := range tests { testEvaluatorRaw(t, Uint32, tt.input, reflect.Uint32, tt.pass, i) } } func TestUint64EvaluatorRaw(t *testing.T) { tests := []struct { pass bool input string }{ {false, "astring"}, // 0 {false, "astringwith_numb3rS_and_symbol$"}, // 1 {false, "-9223372036854775808"}, // 2 {false, "main.css"}, // 3 {false, "/assets/main.css"}, // 4 {false, "92233720368547758079223372036854775807"}, // 5 {false, "9223372036854775808 9223372036854775808"}, // 6 {false, "-1"}, // 7 {false, "-0"}, // 8 {false, "+1"}, // 9 {true, "18446744073709551615"}, // 10 {true, "9223372036854775807"}, // 11 {true, "0"}, // 12 } for i, tt := range tests { testEvaluatorRaw(t, Uint64, tt.input, reflect.Uint64, tt.pass, i) } } func TestAlphabeticalEvaluatorRaw(t *testing.T) { tests := []struct { pass bool input string }{ {true, "astring"}, // 0 {false, "astringwith_numb3rS_and_symbol$"}, // 1 {false, "32321"}, // 2 {false, "main.css"}, // 3 {false, "/assets/main.css"}, // 4 } for i, tt := range tests { testEvaluatorRaw(t, Alphabetical, tt.input, reflect.String, tt.pass, i) } } func TestFileEvaluatorRaw(t *testing.T) { tests := []struct { pass bool input string }{ {true, "astring"}, // 0 {false, "astringwith_numb3rS_and_symbol$"}, // 1 {true, "32321"}, // 2 {true, "main.css"}, // 3 {false, "/assets/main.css"}, // 4 } for i, tt := range tests { testEvaluatorRaw(t, File, tt.input, reflect.String, tt.pass, i) } } func TestPathEvaluatorRaw(t *testing.T) { pathTests := []struct { pass bool input string }{ {true, "astring"}, // 0 {true, "astringwith_numb3rS_and_symbol$"}, // 1 {true, "32321"}, // 2 {true, "main.css"}, // 3 {true, "/assets/main.css"}, // 4 {true, "disk/assets/main.css"}, // 5 } for i, tt := range pathTests { testEvaluatorRaw(t, Path, tt.input, reflect.String, tt.pass, i) } } func TestUUIDEvaluatorRaw(t *testing.T) { tests := []struct { pass bool input string }{ {true, "978ad967-5fad-4c82-af99-580097ace662"}, // v4 {true, "c7067f9c-6d43-11eb-9439-0242ac130002"}, // v1 {false, "astring"}, // 2 {false, "astringwith_numb3rS_and_symbol$"}, // 3 {false, "32321"}, // 4 {false, "main.css"}, // 5 {false, "/assets/main.css"}, // 6 } for i, tt := range tests { testEvaluatorRaw(t, UUID, tt.input, reflect.String, tt.pass, i) } } func TestMailEvaluatorRaw(t *testing.T) { tests := []struct { pass bool input string }{ {true, "kataras2006@hotmail.com"}, // 0 {true, "iris-go@outlook.com"}, // 1 {true, "iris-go@mail"}, // 2 {true, "kataras@k.c"}, // 3 {false, "www.kataras@"}, // 4 {false, "name"}, // 5 {false, "b-c@"}, // 6 } for i, tt := range tests { testEvaluatorRaw(t, Mail, tt.input, reflect.String, tt.pass, i) } } func TestEmailEvaluatorRaw(t *testing.T) { tests := []struct { pass bool input string }{ {true, "kataras2006@hotmail.com"}, // 0 {true, "iris-go@outlook.com"}, // 1 {false, "iris-go@mail"}, // 2 {false, "kataras@k.c"}, // 3 {false, "www.kataras@"}, // 4 {false, "name"}, // 5 {false, "b-c@"}, // 6 } for i, tt := range tests { testEvaluatorRaw(t, Email, tt.input, reflect.String, tt.pass, i) } } func TestDateEvaluatorRaw(t *testing.T) { tests := []struct { pass bool input string timeStringValue string }{ {true, "2022/04/21", "2022-04-21 00:00:00 +0000 UTC"}, // 0 {true, "2022/12/05", "2022-12-05 00:00:00 +0000 UTC"}, // 1 {false, "2022/4", ""}, // 2 {false, "1/4/1", ""}, // 3 {false, "2022/4/", ""}, // 4 {false, "2022/4/21/0", ""}, // 5 {false, "1993", ""}, // 6 } for i, tt := range tests { testEvaluatorRaw(t, Date, tt.input, reflect.TypeOf(time.Time{}).Kind(), tt.pass, i) if v, ok := Date.Evaluator(tt.input); ok { if value, ok := v.(time.Time); ok { if expected, got := tt.timeStringValue, value.String(); expected != got { t.Fatalf("[%d] expected: %s but got: %s", i, expected, got) } } else { t.Fatalf("[%d] expected to be able to cast as time.Time directly", i) } } } } func TestWeekdayEvaluatorRaw(t *testing.T) { tests := []struct { pass bool input string expected time.Weekday }{ {true, "Monday", time.Monday}, // 0 {true, "monday", time.Monday}, // 1 {false, "Sundays", time.Weekday(-1)}, // 2 {false, "sundays", time.Weekday(-1)}, // 3 {false, "-1", time.Weekday(-1)}, // 4 {true, "0000002", time.Tuesday}, // 5 {true, "3", time.Wednesday}, // 6 {true, "6", time.Saturday}, // 7 } for i, tt := range tests { testEvaluatorRaw(t, Weekday, tt.input, reflect.TypeOf(time.Weekday(0)).Kind(), tt.pass, i) if v, ok := Weekday.Evaluator(tt.input); ok { if value, ok := v.(time.Weekday); ok { if expected, got := tt.expected, value; expected != got { t.Fatalf("[%d] expected: %s but got: %s", i, expected, got) } } else { t.Fatalf("[%d] expected to be able to cast as time.Weekday directly", i) } } } } func TestConvertBuilderFunc(t *testing.T) { fn := func(min uint64, slice []string) func(string) bool { return func(paramValue string) bool { if expected, got := "ok", paramValue; expected != got { t.Fatalf("paramValue is not the expected one: %s vs %s", expected, got) } if expected, got := uint64(1), min; expected != got { t.Fatalf("min argument is not the expected one: %d vs %d", expected, got) } if expected, got := []string{"name1", "name2"}, slice; len(expected) == len(got) { if expected, got := "name1", slice[0]; expected != got { t.Fatalf("slice argument[%d] does not contain the expected value: %s vs %s", 0, expected, got) } if expected, got := "name2", slice[1]; expected != got { t.Fatalf("slice argument[%d] does not contain the expected value: %s vs %s", 1, expected, got) } } else { t.Fatalf("slice argument is not the expected one, the length is difference: %d vs %d", len(expected), len(got)) } return true } } evalFunc := convertBuilderFunc(fn) if !evalFunc([]string{"1", "[name1,name2]"}).Call([]reflect.Value{reflect.ValueOf("ok")})[0].Interface().(bool) { t.Fatalf("failed, it should fail already") } fnSimplify := func(requestParamValue string) bool { return requestParamValue == "kataras" } evalFunc = convertBuilderFunc(fnSimplify) if !evalFunc([]string{}).Call([]reflect.Value{reflect.ValueOf("kataras")})[0].Interface().(bool) { t.Fatalf("it should pass, the combile arguments are empty and the given request value is the expected one") } defer func() { if r := recover(); r == nil { t.Fatalf("it should panic, the combile arguments are more than one") } }() // should panic. evalFunc([]string{"1"}).Call([]reflect.Value{reflect.ValueOf("kataras")}) } ================================================ FILE: macro/macros.go ================================================ package macro import ( "errors" "fmt" "net" "net/mail" "strconv" "strings" "time" "github.com/kataras/iris/v12/macro/interpreter/ast" "github.com/google/uuid" ) var ( // String type // Allows anything (single path segment, as everything except the `Path`). // Its functions can be used by the rest of the macros and param types whenever not available function by name is used. // Because of its "master" boolean value to true (third parameter). String = NewMacro("string", "", "", true, false, nil). RegisterFunc("regexp", MustRegexp). // checks if param value starts with the 'prefix' arg RegisterFunc("prefix", func(prefix string) func(string) bool { return func(paramValue string) bool { return strings.HasPrefix(paramValue, prefix) } }). // checks if param value ends with the 'suffix' arg RegisterFunc("suffix", func(suffix string) func(string) bool { return func(paramValue string) bool { return strings.HasSuffix(paramValue, suffix) } }). // checks if param value contains the 's' arg RegisterFunc("contains", func(s string) func(string) bool { return func(paramValue string) bool { return strings.Contains(paramValue, s) } }). // checks if param value's length is at least 'min' RegisterFunc("min", func(min int) func(string) bool { return func(paramValue string) bool { return len(paramValue) >= min } }). // checks if param value's length is not bigger than 'max' RegisterFunc("max", func(max int) func(string) bool { return func(paramValue string) bool { return max >= len(paramValue) } }). // checks if param value's matches the given input RegisterFunc("eq", func(s string) func(string) bool { return func(paramValue string) bool { return paramValue == s } }). // checks if param value's matches at least one of the inputs RegisterFunc("eqor", func(texts []string) func(string) bool { if len(texts) == 1 { text := texts[0] return func(paramValue string) bool { return paramValue == text } } return func(paramValue string) bool { for _, s := range texts { if paramValue == s { return true } } return false } }) // Int or number type // both positive and negative numbers, actual value can be min-max int64 or min-max int32 depends on the arch. // If x64: -9223372036854775808 to 9223372036854775807. // If x32: -2147483648 to 2147483647 and etc.. Int = NewMacro("int", "number", 0, false, false, func(paramValue string) (any, bool) { v, err := strconv.Atoi(paramValue) if err != nil { return err, false } return v, true }). // checks if the param value's int representation is // bigger or equal than 'min' RegisterFunc("min", func(min int) func(int) bool { return func(paramValue int) bool { return paramValue >= min } }). // checks if the param value's int representation is // smaller or equal than 'max'. RegisterFunc("max", func(max int) func(int) bool { return func(paramValue int) bool { return paramValue <= max } }). // checks if the param value's int representation is // between min and max, including 'min' and 'max'. RegisterFunc("range", func(min, max int) func(int) bool { return func(paramValue int) bool { return !(paramValue < min || paramValue > max) } }) // Int8 type // -128 to 127. Int8 = NewMacro("int8", "", int8(0), false, false, func(paramValue string) (any, bool) { v, err := strconv.ParseInt(paramValue, 10, 8) if err != nil { return err, false } return int8(v), true }). RegisterFunc("min", func(min int8) func(int8) bool { return func(paramValue int8) bool { return paramValue >= min } }). RegisterFunc("max", func(max int8) func(int8) bool { return func(paramValue int8) bool { return paramValue <= max } }). RegisterFunc("range", func(min, max int8) func(int8) bool { return func(paramValue int8) bool { return !(paramValue < min || paramValue > max) } }) // Int16 type // -32768 to 32767. Int16 = NewMacro("int16", "", int16(0), false, false, func(paramValue string) (any, bool) { v, err := strconv.ParseInt(paramValue, 10, 16) if err != nil { return err, false } return int16(v), true }). RegisterFunc("min", func(min int16) func(int16) bool { return func(paramValue int16) bool { return paramValue >= min } }). RegisterFunc("max", func(max int16) func(int16) bool { return func(paramValue int16) bool { return paramValue <= max } }). RegisterFunc("range", func(min, max int16) func(int16) bool { return func(paramValue int16) bool { return !(paramValue < min || paramValue > max) } }) // Int32 type // -2147483648 to 2147483647. Int32 = NewMacro("int32", "", int32(0), false, false, func(paramValue string) (any, bool) { v, err := strconv.ParseInt(paramValue, 10, 32) if err != nil { return err, false } return int32(v), true }). RegisterFunc("min", func(min int32) func(int32) bool { return func(paramValue int32) bool { return paramValue >= min } }). RegisterFunc("max", func(max int32) func(int32) bool { return func(paramValue int32) bool { return paramValue <= max } }). RegisterFunc("range", func(min, max int32) func(int32) bool { return func(paramValue int32) bool { return !(paramValue < min || paramValue > max) } }) // Int64 as int64 type // -9223372036854775808 to 9223372036854775807. Int64 = NewMacro("int64", "long", int64(0), false, false, func(paramValue string) (any, bool) { v, err := strconv.ParseInt(paramValue, 10, 64) if err != nil { // if err == strconv.ErrRange... return err, false } return v, true }). // checks if the param value's int64 representation is // bigger or equal than 'min'. RegisterFunc("min", func(min int64) func(int64) bool { return func(paramValue int64) bool { return paramValue >= min } }). // checks if the param value's int64 representation is // smaller or equal than 'max'. RegisterFunc("max", func(max int64) func(int64) bool { return func(paramValue int64) bool { return paramValue <= max } }). // checks if the param value's int64 representation is // between min and max, including 'min' and 'max'. RegisterFunc("range", func(min, max int64) func(int64) bool { return func(paramValue int64) bool { return !(paramValue < min || paramValue > max) } }) // Uint as uint type // actual value can be min-max uint64 or min-max uint32 depends on the arch. // If x64: 0 to 18446744073709551615. // If x32: 0 to 4294967295 and etc. Uint = NewMacro("uint", "", uint(0), false, false, func(paramValue string) (any, bool) { v, err := strconv.ParseUint(paramValue, 10, strconv.IntSize) // 32,64... if err != nil { return err, false } return uint(v), true }). // checks if the param value's int representation is // bigger or equal than 'min' RegisterFunc("min", func(min uint) func(uint) bool { return func(paramValue uint) bool { return paramValue >= min } }). // checks if the param value's int representation is // smaller or equal than 'max'. RegisterFunc("max", func(max uint) func(uint) bool { return func(paramValue uint) bool { return paramValue <= max } }). // checks if the param value's int representation is // between min and max, including 'min' and 'max'. RegisterFunc("range", func(min, max uint) func(uint) bool { return func(paramValue uint) bool { return !(paramValue < min || paramValue > max) } }) // Uint8 as uint8 type // 0 to 255. Uint8 = NewMacro("uint8", "", uint8(0), false, false, func(paramValue string) (any, bool) { v, err := strconv.ParseUint(paramValue, 10, 8) if err != nil { return err, false } return uint8(v), true }). // checks if the param value's uint8 representation is // bigger or equal than 'min'. RegisterFunc("min", func(min uint8) func(uint8) bool { return func(paramValue uint8) bool { return paramValue >= min } }). // checks if the param value's uint8 representation is // smaller or equal than 'max'. RegisterFunc("max", func(max uint8) func(uint8) bool { return func(paramValue uint8) bool { return paramValue <= max } }). // checks if the param value's uint8 representation is // between min and max, including 'min' and 'max'. RegisterFunc("range", func(min, max uint8) func(uint8) bool { return func(paramValue uint8) bool { return !(paramValue < min || paramValue > max) } }) // Uint16 as uint16 type // 0 to 65535. Uint16 = NewMacro("uint16", "", uint16(0), false, false, func(paramValue string) (any, bool) { v, err := strconv.ParseUint(paramValue, 10, 16) if err != nil { return err, false } return uint16(v), true }). RegisterFunc("min", func(min uint16) func(uint16) bool { return func(paramValue uint16) bool { return paramValue >= min } }). RegisterFunc("max", func(max uint16) func(uint16) bool { return func(paramValue uint16) bool { return paramValue <= max } }). RegisterFunc("range", func(min, max uint16) func(uint16) bool { return func(paramValue uint16) bool { return !(paramValue < min || paramValue > max) } }) // Uint32 as uint32 type // 0 to 4294967295. Uint32 = NewMacro("uint32", "", uint32(0), false, false, func(paramValue string) (any, bool) { v, err := strconv.ParseUint(paramValue, 10, 32) if err != nil { return err, false } return uint32(v), true }). RegisterFunc("min", func(min uint32) func(uint32) bool { return func(paramValue uint32) bool { return paramValue >= min } }). RegisterFunc("max", func(max uint32) func(uint32) bool { return func(paramValue uint32) bool { return paramValue <= max } }). RegisterFunc("range", func(min, max uint32) func(uint32) bool { return func(paramValue uint32) bool { return !(paramValue < min || paramValue > max) } }) // Uint64 as uint64 type // 0 to 18446744073709551615. Uint64 = NewMacro("uint64", "", uint64(0), false, false, func(paramValue string) (any, bool) { v, err := strconv.ParseUint(paramValue, 10, 64) if err != nil { return err, false } return v, true }). // checks if the param value's uint64 representation is // bigger or equal than 'min'. RegisterFunc("min", func(min uint64) func(uint64) bool { return func(paramValue uint64) bool { return paramValue >= min } }). // checks if the param value's uint64 representation is // smaller or equal than 'max'. RegisterFunc("max", func(max uint64) func(uint64) bool { return func(paramValue uint64) bool { return paramValue <= max } }). // checks if the param value's uint64 representation is // between min and max, including 'min' and 'max'. RegisterFunc("range", func(min, max uint64) func(uint64) bool { return func(paramValue uint64) bool { return !(paramValue < min || paramValue > max) } }) // Bool or boolean as bool type // a string which is "1" or "t" or "T" or "TRUE" or "true" or "True" // or "0" or "f" or "F" or "FALSE" or "false" or "False". Bool = NewMacro("bool", "boolean", false, false, false, func(paramValue string) (any, bool) { // a simple if statement is faster than regex ^(true|false|True|False|t|0|f|FALSE|TRUE)$ // in this case. v, err := strconv.ParseBool(paramValue) if err != nil { return err, false } return v, true }) // ErrParamNotAlphabetical is fired when the parameter value is not an alphabetical text. ErrParamNotAlphabetical = errors.New("parameter is not alphabetical") alphabeticalEval = MustRegexp("^[a-zA-Z ]+$") // Alphabetical letter type // letters only (upper or lowercase) Alphabetical = NewMacro("alphabetical", "", "", false, false, func(paramValue string) (any, bool) { if !alphabeticalEval(paramValue) { return fmt.Errorf("%s: %w", paramValue, ErrParamNotAlphabetical), false } return paramValue, true }) // ErrParamNotFile is fired when the parameter value is not a form of a file. ErrParamNotFile = errors.New("parameter is not a file") fileEval = MustRegexp("^[a-zA-Z0-9_.-]*$") // File type // letters (upper or lowercase) // numbers (0-9) // underscore (_) // dash (-) // point (.) // no spaces! or other character File = NewMacro("file", "", "", false, false, func(paramValue string) (any, bool) { if !fileEval(paramValue) { return fmt.Errorf("%s: %w", paramValue, ErrParamNotFile), false } return paramValue, true }) // Path type // anything, should be the last part // // It allows everything, we have String and Path as different // types because I want to give the opportunity to the user // to organise the macro functions based on wildcard or single dynamic named path parameter. // Should be living in the latest path segment of a route path. Path = NewMacro("path", "", "", false, true, nil) // UUID string type for validating a uuidv4 (and v1) path parameter. // Read more at: https://tools.ietf.org/html/rfc4122. UUID = NewMacro("uuid", "uuidv4", "", false, false, func(paramValue string) (any, bool) { _, err := uuid.Parse(paramValue) // this is x10+ times faster than regexp. if err != nil { return err, false } return paramValue, true }) // Email string type for validating an e-mail path parameter. It returns the address as string, instead of an *mail.Address. // Read more at go std mail.ParseAddress method. See the ':email' path parameter for a more strictly version of validation. Mail = NewMacro("mail", "", "", false, false, func(paramValue string) (any, bool) { _, err := mail.ParseAddress(paramValue) if err != nil { return fmt.Errorf("%s: %w", paramValue, err), false } return paramValue, true }) // Email string type for validating an e-mail path parameter. It returns the address as string, instead of an *mail.Address. // It is a combined validation using mail.ParseAddress and net.LookupMX so only valid domains can be passed. // It's a more strictly version of the ':mail' path parameter. Email = NewMacro("email", "", "", false, false, func(paramValue string) (any, bool) { _, err := mail.ParseAddress(paramValue) if err != nil { return fmt.Errorf("%s: %w", paramValue, err), false } domainPart := strings.Split(paramValue, "@")[1] mx, err := net.LookupMX(domainPart) if err != nil { return fmt.Errorf("%s: %w", paramValue, err), false } if len(mx) == 0 { return fmt.Errorf("%s: mx is empty", paramValue), false } return paramValue, true }) simpleDateLayout = "2006/01/02" // Date type. Date = NewMacro("date", "", time.Time{}, false, true, func(paramValue string) (any, bool) { tt, err := time.Parse(simpleDateLayout, paramValue) if err != nil { return fmt.Errorf("%s: %w", paramValue, err), false } return tt, true }) // ErrParamNotWeekday is fired when the parameter value is not a form of a time.Weekday. ErrParamNotWeekday = errors.New("parameter is not a valid weekday") longDayNames = map[string]time.Weekday{ "Sunday": time.Sunday, "Monday": time.Monday, "Tuesday": time.Tuesday, "Wednesday": time.Wednesday, "Thursday": time.Thursday, "Friday": time.Friday, "Saturday": time.Saturday, // lowercase. "sunday": time.Sunday, "monday": time.Monday, "tuesday": time.Tuesday, "wednesday": time.Wednesday, "thursday": time.Thursday, "friday": time.Friday, "saturday": time.Saturday, } // Weekday type, returns a type of time.Weekday. // Valid values: // 0 to 7 (leading zeros don't matter) or "Sunday" to "Monday" or "sunday" to "monday". Weekday = NewMacro("weekday", "", time.Weekday(0), false, false, func(paramValue string) (any, bool) { d, ok := longDayNames[paramValue] if !ok { // try parse from integer. n, err := strconv.Atoi(paramValue) if err != nil { return fmt.Errorf("%s: %w", paramValue, err), false } if n < 0 || n > 6 { return fmt.Errorf("%s: %w", paramValue, ErrParamNotWeekday), false } return time.Weekday(n), true } return d, true }) // Defaults contains the defaults macro and parameters types for the router. // // Read https://github.com/kataras/iris/tree/main/_examples/routing/macros for more details. Defaults = &Macros{ String, Int, Int8, Int16, Int32, Int64, Uint, Uint8, Uint16, Uint32, Uint64, Bool, Alphabetical, File, Path, UUID, Mail, Email, Date, Weekday, } ) // Macros is just a type of a slice of *Macro // which is responsible to register and search for macros based on the indent(parameter type). type Macros []*Macro // Register registers a custom Macro. // The "indent" should not be empty and should be unique, it is the parameter type's name, i.e "string". // The "alias" is optionally and it should be unique, it is the alias of the parameter type. // The "valueType" should be the zero value of the parameter type, i.e "" for string, 0 for int and etc. // "isMaster" and "isTrailing" is for default parameter type and wildcard respectfully. // The "evaluator" is the function that is converted to an Iris handler which is executed every time // before the main chain of a route's handlers that contains this macro of the specific parameter type. // // Read https://github.com/kataras/iris/tree/main/_examples/routing/macros for more details. func (ms *Macros) Register(indent, alias string, valueType any, isMaster, isTrailing bool, evaluator ParamEvaluator) *Macro { macro := NewMacro(indent, alias, valueType, isMaster, isTrailing, evaluator) if ms.register(macro) { return macro } return nil } func (ms *Macros) register(macro *Macro) bool { if macro.Indent() == "" { return false } cp := *ms for _, m := range cp { // can't add more than one with the same ast characteristics. if macro.Indent() == m.Indent() { return false } if alias := macro.Alias(); alias != "" { if alias == m.Alias() || alias == m.Indent() { return false } } if macro.Master() && m.Master() { return false } } cp = append(cp, macro) *ms = cp return true } // Unregister removes a macro and its parameter type from the list. func (ms *Macros) Unregister(indent string) bool { cp := *ms for i, m := range cp { if m.Indent() == indent { copy(cp[i:], cp[i+1:]) cp[len(cp)-1] = nil cp = cp[:len(cp)-1] *ms = cp return true } } return false } // Lookup returns the responsible macro for a parameter type, it can return nil. func (ms *Macros) Lookup(pt ast.ParamType) *Macro { if m := ms.Get(pt.Indent()); m != nil { return m } if alias, has := ast.HasAlias(pt); has { if m := ms.Get(alias); m != nil { return m } } return nil } // Get returns the responsible macro for a parameter type, it can return nil. func (ms *Macros) Get(indentOrAlias string) *Macro { if indentOrAlias == "" { return nil } for _, m := range *ms { if m.Indent() == indentOrAlias { return m } if m.Alias() == indentOrAlias { return m } } return nil } // GetMaster returns the default macro and its parameter type, // by default it will return the `String` macro which is responsible for the "string" parameter type. func (ms *Macros) GetMaster() *Macro { for _, m := range *ms { if m.Master() { return m } } return nil } // GetTrailings returns the macros that have support for wildcards parameter types. // By default it will return the `Path` macro which is responsible for the "path" parameter type. func (ms *Macros) GetTrailings() (macros []*Macro) { for _, m := range *ms { if m.Trailing() { macros = append(macros, m) } } return } // SetErrorHandler registers a common type path parameter error handler. // The "fnHandler" MUST be a type of handler.ParamErrorHandler: // func(ctx iris.Context, paramIndex int, err error). It calls // the Macro.HandleError method for each of the "ms" entries. func (ms *Macros) SetErrorHandler(fnHandler any) { for _, m := range *ms { if m == nil { continue } m.HandleError(fnHandler) } } ================================================ FILE: macro/template.go ================================================ package macro import ( "reflect" "github.com/kataras/iris/v12/macro/interpreter/ast" "github.com/kataras/iris/v12/macro/interpreter/parser" ) // Template contains a route's path full parsed template. // // Fields: // Src is the raw source of the path, i.e /users/{id:int min(1)} // Params is the list of the Params that are being used to the // path, i.e the min as param name and 1 as the param argument. type Template struct { // Src is the original template given by the client Src string `json:"src"` Params []TemplateParam `json:"params"` } // IsTrailing reports whether this Template is a traling one. func (t *Template) IsTrailing() bool { return len(t.Params) > 0 && ast.IsTrailing(t.Params[len(t.Params)-1].Type) } // TemplateParam is the parsed macro parameter's template // they are being used to describe the param's syntax result. type TemplateParam struct { macro *Macro // keep for reference. Src string `json:"src"` // the unparsed param'false source // Type is not useful anywhere here but maybe // it's useful on host to decide how to convert the path template to specific router's syntax Type ast.ParamType `json:"type"` Name string `json:"name"` Index int `json:"index"` ErrCode int `json:"errCode"` // Note that, the value MUST BE a type of `handler.ParamErrorHandler`. HandleError any `json:"-"` /* It's not an typed value because of import-cycle, // neither a special struct required, see `handler.MakeFilter`. */ TypeEvaluator ParamEvaluator `json:"-"` Funcs []reflect.Value `json:"-"` stringInFuncs []func(string) bool canEval bool } func (p TemplateParam) preComputed() TemplateParam { for _, pfn := range p.Funcs { if fn, ok := pfn.Interface().(func(string) bool); ok { p.stringInFuncs = append(p.stringInFuncs, fn) } } // if true then it should be execute the type parameter or its functions // else it can be ignored, // i.e {myparam} or {myparam:string} or {myparam:path} -> // their type evaluator is nil because they don't do any checks and they don't change // the default parameter value's type (string) so no need for any work). p.canEval = p.TypeEvaluator != nil || len(p.Funcs) > 0 || p.ErrCode != parser.DefaultParamErrorCode || p.HandleError != nil return p } // CanEval returns true if this "p" TemplateParam should be evaluated in serve time. // It is computed before server ran and it is used to determinate if a route needs to build a macro handler (middleware). func (p *TemplateParam) CanEval() bool { return p.canEval } type errorInterface interface { Error() string } // Eval is the most critical part of the TemplateParam. // It is responsible to return the type-based value if passed otherwise nil. // If the "paramValue" is the correct type of the registered parameter type // and all functions, if any, are passed. // // It is called from the converted macro handler (middleware) // from the higher-level component of "kataras/iris/macro/handler#MakeHandler". func (p *TemplateParam) Eval(paramValue string) (any, bool) { if p.TypeEvaluator == nil { for _, fn := range p.stringInFuncs { if !fn(paramValue) { return nil, false } } return paramValue, true } // fmt.Printf("macro/template.go#L88: Eval for param value: %s and p.Src: %s\n", paramValue, p.Src) newValue, passed := p.TypeEvaluator(paramValue) if !passed { if newValue != nil && p.HandleError != nil { // return this error only when a HandleError was registered. if _, ok := newValue.(errorInterface); ok { return newValue, false // this is an error, see `HandleError` and `MakeFilter`. } } return nil, false } if len(p.Funcs) > 0 { paramIn := []reflect.Value{reflect.ValueOf(newValue)} for _, evalFunc := range p.Funcs { // or make it as func(any) bool and pass directly the "newValue" // but that would not be as easy for end-developer, so keep that "slower": if !evalFunc.Call(paramIn)[0].Interface().(bool) { // i.e func(paramValue int) bool return nil, false } } } // fmt.Printf("macro/template.go: passed with value: %v and type: %T\n", newValue, newValue) return newValue, true } // IsMacro reports whether this TemplateParam's underline macro matches the given one. func (p *TemplateParam) IsMacro(macro *Macro) bool { return p.macro == macro } // Parse takes a full route path and a macro map (macro map contains the macro types with their registered param functions) // and returns a new Template. // It builds all the parameter functions for that template // and their evaluators, it's the api call that makes use the interpeter's parser -> lexer. func Parse(src string, macros Macros) (Template, error) { types := make([]ast.ParamType, len(macros)) for i, m := range macros { types[i] = m } tmpl := Template{Src: src} params, err := parser.Parse(src, types) if err != nil { return tmpl, err } for idx, p := range params { m := macros.Lookup(p.Type) typEval := m.Evaluator tmplParam := TemplateParam{ macro: m, Src: p.Src, Type: p.Type, Name: p.Name, Index: idx, ErrCode: p.ErrorCode, HandleError: m.handleError, TypeEvaluator: typEval, } for _, paramfn := range p.Funcs { tmplFn := m.getFunc(paramfn.Name) if tmplFn == nil { // if not find on this type, check for Master's which is for global funcs too. if m := macros.GetMaster(); m != nil { tmplFn = m.getFunc(paramfn.Name) } if tmplFn == nil { // if not found then just skip this param. continue } } evalFn := tmplFn(paramfn.Args) if evalFn.IsNil() || !evalFn.IsValid() || evalFn.Kind() != reflect.Func { continue } tmplParam.Funcs = append(tmplParam.Funcs, evalFn) } tmpl.Params = append(tmpl.Params, tmplParam.preComputed()) } return tmpl, nil } // CountParams returns the length of the dynamic path's input parameters. func CountParams(fullpath string, macros Macros) int { tmpl, _ := Parse(fullpath, macros) return len(tmpl.Params) } ================================================ FILE: middleware/README.md ================================================ Builtin Handlers ------------ | Middleware | Example | | -----------|-------------| | [rewrite](rewrite) | [iris/_examples/routing/rewrite](https://github.com/kataras/iris/tree/main/_examples/routing/rewrite) | | [basic authentication](basicauth) | [iris/_examples/auth/basicauth](https://github.com/kataras/iris/tree/main/_examples/auth/basicauth) | | [request logger](logger) | [iris/_examples/logging/request-logger](https://github.com/kataras/iris/tree/main/_examples/logging/request-logger) | | [HTTP method override](methodoverride) | [iris/middleware/methodoverride/methodoverride_test.go](https://github.com/kataras/iris/blob/main/middleware/methodoverride/methodoverride_test.go) | | [profiling (pprof)](pprof) | [iris/_examples/pprof](https://github.com/kataras/iris/tree/main/_examples/pprof) | | [Google reCAPTCHA](recaptcha) | [iris/_examples/auth/recaptcha](https://github.com/kataras/iris/tree/main/_examples/auth/recaptcha) | | [hCaptcha](hcaptcha) | [iris/_examples/auth/recaptcha](https://github.com/kataras/iris/tree/main/_examples/auth/hcaptcha) | | [recovery](recover) | [iris/_examples/recover](https://github.com/kataras/iris/tree/main/_examples/recover) | | [rate](rate) | [iris/_examples/request-ratelimit](https://github.com/kataras/iris/tree/main/_examples/request-ratelimit) | | [jwt](jwt) | [iris/_examples/auth/jwt](https://github.com/kataras/iris/tree/main/_examples/auth/jwt) | | [requestid](requestid) | [iris/middleware/requestid/requestid_test.go](https://github.com/kataras/iris/blob/main/_examples/middleware/requestid/requestid_test.go) | Community made ------------ Most of the experimental handlers are ported to work with _iris_'s handler form, from third-party sources. | Middleware | Description | Example | | -----------|--------|-------------| | [pg](https://github.com/iris-contrib/middleware/tree/master/pg) | Middleware that provides easy and type-safe access to PostgreSQL database | [iris-contrib/middleware/pg/_examples](https://github.com/iris-contrib/middleware/tree/master/pg/_examples) | | [jwt](https://github.com/iris-contrib/middleware/tree/master/jwt) | Middleware checks for a JWT on the `Authorization` header on incoming requests and decodes it | [iris-contrib/middleware/jwt/_example](https://github.com/iris-contrib/middleware/tree/master/jwt/_example) | | [cors](https://github.com/iris-contrib/middleware/tree/master/cors) | HTTP Access Control | [iris-contrib/middleware/cors/_example](https://github.com/iris-contrib/middleware/tree/master/cors/_example) | | [secure](https://github.com/iris-contrib/middleware/tree/master/secure) | Middleware that implements a few quick security wins | [iris-contrib/middleware/secure/_example](https://github.com/iris-contrib/middleware/tree/master/secure/_example/main.go) | | [tollbooth](https://github.com/iris-contrib/middleware/tree/master/tollboothic) | Generic middleware to rate-limit HTTP requests | [iris-contrib/middleware/tollboothic/_examples/limit-handler](https://github.com/iris-contrib/middleware/tree/master/tollboothic/_examples/limit-handler) | | [cloudwatch](https://github.com/iris-contrib/middleware/tree/master/cloudwatch) | AWS cloudwatch metrics middleware |[iris-contrib/middleware/cloudwatch/_example](https://github.com/iris-contrib/middleware/tree/master/cloudwatch/_example) | | [new relic](https://github.com/iris-contrib/middleware/tree/master/newrelic) | Official [New Relic Go Agent](https://github.com/newrelic/go-agent) | [iris-contrib/middleware/newrelic/_example](https://github.com/iris-contrib/middleware/tree/master/newrelic/_example) | | [prometheus](https://github.com/iris-contrib/middleware/tree/master/prometheus)| Easily create metrics endpoint for the [prometheus](http://prometheus.io) instrumentation tool | [iris-contrib/middleware/prometheus/_example](https://github.com/iris-contrib/middleware/tree/master/prometheus/_example) | | [casbin](https://github.com/iris-contrib/middleware/tree/master/casbin)| An authorization library that supports access control models like ACL, RBAC, ABAC | [iris-contrib/middleware/casbin/_examples](https://github.com/iris-contrib/middleware/tree/master/casbin/_examples) | | [sentry-go (ex. raven)](https://github.com/getsentry/sentry-go/tree/master/iris)| Sentry client in Go | [sentry-go/example/iris](https://github.com/getsentry/sentry-go/blob/master/example/iris/main.go) | | [csrf](https://github.com/iris-contrib/middleware/tree/master/csrf)| Cross-Site Request Forgery Protection | [iris-contrib/middleware/csrf/_example](https://github.com/iris-contrib/middleware/blob/master/csrf/_example/main.go) | | [throttler](https://github.com/iris-contrib/middleware/tree/master/throttler)| Rate limiting access to HTTP endpoints | [iris-contrib/middleware/throttler/_example](https://github.com/iris-contrib/middleware/blob/master/throttler/_example/main.go) | Third-Party Handlers ------------ Iris has its own middleware form of `func(ctx iris.Context)` but it's also compatible with all `net/http` middleware forms. See [here](https://github.com/kataras/iris/tree/main/_examples/convert-handlers). Here's a small list of useful third-party handlers: | Middleware | Description | | -----------|-------------| | [goth](https://github.com/markbates/goth) | OAuth, OAuth2 authentication. [Example](https://github.com/kataras/iris/tree/main/_examples/auth/goth) | | [permissions2](https://github.com/xyproto/permissions2) | Cookies, users and permissions. [Example](https://github.com/kataras/iris/tree/main/_examples/auth/permissions) | | [csp](https://github.com/awakenetworks/csp) | [Content Security Policy](https://www.w3.org/TR/CSP2/) (CSP) support | | [delay](https://github.com/jeffbmartinez/delay) | Add delays/latency to endpoints. Useful when testing effects of high latency | | [onthefly](https://github.com/xyproto/onthefly) | Generate TinySVG, HTML and CSS on the fly | | [RestGate](https://github.com/pjebs/restgate) | Secure authentication for REST API endpoints | | [stats](https://github.com/thoas/stats) | Store information about your web application (response time, etc.) | | [VanGoH](https://github.com/auroratechnologies/vangoh) | Configurable [AWS-Style](http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html) HMAC authentication middleware | | [digits](https://github.com/bamarni/digits) | Middleware that handles [Twitter Digits](https://get.digits.com/) authentication | > Feel free to put up your own middleware in this list! ================================================ FILE: middleware/accesslog/accesslog.go ================================================ package accesslog import ( "bufio" "bytes" stdContext "context" "fmt" "io" "net/http" "net/http/httputil" "os" "strconv" "strings" "sync" "sync/atomic" "time" "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/core/host" "github.com/kataras/iris/v12/core/memstore" ) func init() { context.SetHandlerName("iris/middleware/accesslog.*", "iris.accesslog") } const ( fieldsContextKey = "iris.accesslog.request.fields" skipLogContextKey = "iris.accesslog.request.skip" ) // GetFields returns the accesslog fields for this request. // Returns a store which the caller can use to // set/get/remove custom log fields. Use its `Set` method. // // To use with MVC: Register(accesslog.GetFields). // DI Handlers: ConfigureContainer().RegisterDependency(accesslog.GetFields). func GetFields(ctx *context.Context) (fields *Fields) { if v := ctx.Values().Get(fieldsContextKey); v != nil { fields = v.(*Fields) } else { fields = new(Fields) ctx.Values().Set(fieldsContextKey, fields) } return } // Skip called when a specific route should be skipped from the logging process. // It's an easy to use alternative for iris.NewConditionalHandler. func Skip(ctx *context.Context) { ctx.Values().Set(skipLogContextKey, struct{}{}) } // SkipHandler same as `Skip` but it can be used // as a middleware, it executes ctx.Next(). func SkipHandler(ctx *context.Context) { Skip(ctx) ctx.Next() } func shouldSkip(ctx *context.Context) bool { return ctx.Values().Get(skipLogContextKey) != nil } type ( // Fields is a type alias for memstore.Store, used to set // more than one field at serve-time. Same as FieldExtractor. Fields = memstore.Store // FieldSetter sets one or more fields at once. FieldSetter func(*context.Context, *Fields) ) type ( // Clock is an interface which contains a single `Now` method. // It can be used to set a static timer on end to end testing. // See `AccessLog.Clock` field. Clock interface{ Now() time.Time } clockFunc func() time.Time ) // Now completes the `Clock` interface. func (c clockFunc) Now() time.Time { return c() } var ( // UTC returns time with UTC based location. UTC = clockFunc(func() time.Time { return time.Now().UTC() }) // TClock accepts a static time.Time to use as // accesslog's Now method on current log fired timestamp. // Useful for testing. TClock = func(t time.Time) clockFunc { return func() time.Time { return t } } ) // AccessLog is a middleware which prints information // incoming HTTP requests. // // Default log format: // Time|Latency|Code|Method|Path|IP|Path Params Query Fields|Bytes Received|Bytes Sent|Request|Response| // // Look `New`, `File` package-level functions // and its `Handler` method to learn more. // If the given writer is a buffered one, // its contents are flushed automatically on Close call. // // A new AccessLog middleware MUST // be created after a `New` function call. type AccessLog struct { mu sync.RWMutex // ensures atomic writes. // The destination writer. // If multiple output required, then define an `io.MultiWriter`. // See `SetOutput` and `AddOutput` methods too. Writer io.Writer // If not empty then each one of them is called on `Close` method. // File type destinations are automatically added. Flushers []Flusher Closers []io.Closer // Outputs that support the Truncate method. BufferTruncaters []BufferTruncater FileTruncaters []FileTruncater // If not empty then overrides the time.Now to this custom clocker's `Now` method, // useful for testing (see `TClock`) and // on platforms that its internal clock is not compatible by default (advanced case) and // to change the time location (e.g. `UTC`). // // This field is used to set the time the log fired. // By default the middleware is using the local time, however // can be changed to `UTC` too. // // Do NOT touch this field if you don't know what you're doing. Clock Clock // If true then the middleware will fire the logs in a separate // go routine, making the request to finish first. // The log will be printed based on a copy of the Request's Context instead. // // Defaults to false. Async bool // The delimiter between fields when logging with the default format. // See `SetFormatter` to customize the log even further. // // Defaults to '|'. Delim byte // The time format for current time on log print. // Set it to empty to inherit the Iris Application's TimeFormat. // // Defaults to "2006-01-02 15:04:05" TimeFormat string // A text that will appear in a blank value. // Applies to the default formatter on // IP, RequestBody and ResponseBody fields, if enabled, so far. // // Defaults to nil. Blank []byte // Round the latency based on the given duration, e.g. time.Second. // // Defaults to 0. LatencyRound time.Duration // IP displays the remote address. // // Defaults to true. IP bool // The number of bytes for the request body only. // Applied when BytesReceived is false. // // Defaults to true. BytesReceivedBody bool // The number of bytes for the response body only. // Applied when BytesSent is false. // // Defaults to true. BytesSentBody bool // The actual number of bytes received and sent on the network (headers + body). // It is kind of "slow" operation as it uses the httputil to dumb request // and response to get the total amount of bytes (headers + body). // // They override the BytesReceivedBody and BytesSentBody fields. // These two fields provide a more a acquirate measurement // than BytesReceivedBody and BytesSentBody however, // they are expensive operations, expect a slower execution. // // They both default to false. BytesReceived bool BytesSent bool // Enable request body logging. // Note that, if this is true then it modifies the underline request's body type. // // Defaults to true. RequestBody bool // Enable response body logging. // Note that, if this is true then it uses a response recorder. // // Defaults to false. ResponseBody bool // Force minify request and response contents. // // Defaults to true. BodyMinify bool // KeepMultiLineError displays the Context's error as it's. // If set to false then it replaces all line characters with spaces. // // See `PanicLog` to customize recovered-from-panic errors even further. // // Defaults to true. KeepMultiLineError bool // What the logger should write to the output destination // when recovered from a panic. // Available options: // * LogHandler (default, logs the handler's file:line only) // * LogCallers (logs callers separated by line breaker) // * LogStack (logs the debug stack) PanicLog PanicLog // Map log fields with custom request values. // See `AddFields` method. FieldSetters []FieldSetter // Note: We could use a map but that way we lose the // order of registration so use a slice and // take the field key from the extractor itself. formatter Formatter broker *Broker // the log instance for custom formatters. logsPool *sync.Pool // the builder for the default format. bufPool *sync.Pool // remaining logs when Close is called, we wait for timeout (see CloseContext). remaining uint32 // reports whether the logger is already closed, see `Close` & `CloseContext` methods. isClosed uint32 } // PanicLog holds the type for the available panic log levels. type PanicLog uint8 const ( // LogHandler logs the handler's file:line that recovered from. LogHandler PanicLog = iota // LogCallers logs all callers separated by new lines. LogCallers // LogStack logs the whole debug stack. LogStack ) const ( defaultDelim = '|' defaultTimeFormat = "2006-01-02 15:04:05" newLine = '\n' ) // New returns a new AccessLog value with the default values. // Writes to the "w". Output can be further modified through its `Set/AddOutput` methods. // // Register by its `Handler` method. // See `File` package-level function too. // // Examples: // https://github.com/kataras/iris/tree/main/_examples/logging/request-logger/accesslog // https://github.com/kataras/iris/tree/main/_examples/logging/request-logger/accesslog-template // https://github.com/kataras/iris/tree/main/_examples/logging/request-logger/accesslog-broker func New(w io.Writer) *AccessLog { ac := &AccessLog{ Clock: clockFunc(time.Now), Delim: defaultDelim, TimeFormat: defaultTimeFormat, Blank: nil, LatencyRound: 0, Async: false, IP: true, BytesReceived: false, BytesSent: false, BytesReceivedBody: true, BytesSentBody: true, RequestBody: true, ResponseBody: false, BodyMinify: true, KeepMultiLineError: true, PanicLog: LogHandler, logsPool: &sync.Pool{New: func() any { return new(Log) }}, bufPool: &sync.Pool{New: func() any { return new(bytes.Buffer) }}, } if w == nil { w = os.Stdout } ac.SetOutput(w) // workers := 20 // listener := ac.Broker().NewListener() // for i := 0; i < workers; i++ { // go func() { // for log := range listener { // atomic.AddUint32(&ac.remaining, 1) // ac.handleLog(log) // atomic.AddUint32(&ac.remaining, ^uint32(0)) // } // }() // } host.RegisterOnInterrupt(func() { ac.Close() }) return ac } // File returns a new AccessLog value with the given "path" // as the log's output file destination. // The Writer is now a buffered file writer & reader. // Register by its `Handler` method. // // A call of its `Close` method to unlock the underline // file is required on program termination. // // It panics on error. func File(path string) *AccessLog { f := mustOpenFile(path) return New(bufio.NewReadWriter(bufio.NewReader(f), bufio.NewWriter(f))) } // FileUnbuffered same as File but it does not buffer the data, // it flushes the loggers contents as soon as possible. func FileUnbuffered(path string) *AccessLog { f := mustOpenFile(path) return New(f) } func mustOpenFile(path string) *os.File { // Note: we add os.RDWR in order to be able to read from it, // some formatters (e.g. CSV) needs that. f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0600) if err != nil { panic(err) } return f } // Broker creates or returns the broker. // Use its `NewListener` and `CloseListener` // to listen and unlisten for incoming logs. // // Should be called before serve-time. func (ac *AccessLog) Broker() *Broker { ac.mu.Lock() if ac.broker == nil { ac.broker = newBroker() } ac.mu.Unlock() return ac.broker } // SetOutput sets the log's output destination. Accepts one or more io.Writer values. // Also, if a writer is a Closer, then it is automatically appended to the Closers. // It's safe to used concurrently (experimental). func (ac *AccessLog) SetOutput(writers ...io.Writer) *AccessLog { ac.setOutput(true, writers...) return ac } // AddOutput appends an io.Writer value to the existing writer. // Call it before `SetFormatter` and `Handler` methods. func (ac *AccessLog) AddOutput(writers ...io.Writer) *AccessLog { ac.setOutput(false, writers...) return ac } func (ac *AccessLog) setOutput(reset bool, writers ...io.Writer) { /* Initial idea was to wait for remaining logs to be written in the existing writer before resetting to the new one. But, a faster approach would be to just write the logs to the new writers instead. This can be done by: 1. copy all existing closers and flushers, 2. change the writer immediately 3. fire a goroutine which flushes and closes the old writers, no locks required there because they are not used for concurrent writing anymore. Errors there are ignored (we could collect them with sync errgroup and wait for them before exit this Reset method, but we don't). */ if !reset { // prepend if one exists. ac.mu.Lock() if ac.Writer != nil { writers = append([]io.Writer{ac.Writer}, writers...) } ac.mu.Unlock() } switch len(writers) { case 0: return case 1: ac.mu.Lock() ac.Writer = writers[0] ac.mu.Unlock() default: multi := io.MultiWriter(writers...) ac.mu.Lock() ac.Writer = multi ac.mu.Unlock() } // NO need to check for a "hadWriter", // because it will always have a previous writer // on serve-time (the spot we care about performance), // so if it set by New, on build-time, we don't rly care about some read locks slowdown. ac.mu.RLock() n := len(ac.Flushers) ac.mu.RUnlock() flushers := make([]Flusher, n) if n > 0 { ac.mu.Lock() copy(flushers, ac.Flushers) ac.mu.Unlock() } ac.mu.RLock() n = len(ac.Closers) ac.mu.RUnlock() closers := make([]io.Closer, n) if n > 0 { ac.mu.Lock() copy(closers, ac.Closers) ac.mu.Unlock() } if reset { // Reset previous flushers and closers, // so any middle request can't flush to the old ones. // Note that, because we don't lock the whole operation, // there is a chance of Flush while we are doing this, // not by the middleware (unless panic, but again, the data are written // to the new writer, they are not lost, just not flushed immediately), // an outsider may call it, and if it does // then it is its responsibility to lock between manual Flush calls and // SetOutput ones. This is done to be able // to serve requests fast even on Async == false // while SetOutput is called at serve-time, if we didn't care about it // we could lock the whole operation which would make the // log writers to wait and be done with this. ac.mu.Lock() ac.Flushers = ac.Flushers[0:0] ac.Closers = ac.Closers[0:0] ac.BufferTruncaters = ac.BufferTruncaters[0:0] ac.FileTruncaters = ac.FileTruncaters[0:0] ac.mu.Unlock() } // Store the new flushers, closers and truncaters... for _, w := range writers { if flusher, ok := w.(Flusher); ok { ac.mu.Lock() ac.Flushers = append(ac.Flushers, flusher) ac.mu.Unlock() } if closer, ok := w.(io.Closer); ok { ac.mu.Lock() ac.Closers = append(ac.Closers, closer) ac.mu.Unlock() } if truncater, ok := w.(BufferTruncater); ok { ac.mu.Lock() ac.BufferTruncaters = append(ac.BufferTruncaters, truncater) ac.mu.Unlock() } if truncater, ok := w.(FileTruncater); ok { ac.mu.Lock() ac.FileTruncaters = append(ac.FileTruncaters, truncater) ac.mu.Unlock() } } if reset { // And finally, wait before exit this method // until previous writer's closers and flush finish. for _, flusher := range flushers { if flusher != nil { flusher.Flush() } } for _, closer := range closers { if closer != nil { // cannot close os.Stdout/os.Stderr if closer == os.Stdout || closer == os.Stderr { continue } closer.Close() } } } } // Close terminates any broker listeners, // waits for any remaining logs up to 10 seconds // (see `CloseContext` to set custom deadline), // flushes any formatter and any buffered data to the underline writer // and finally closes any registered closers (files are automatically added as Closer). // // After Close is called the AccessLog is not accessible. func (ac *AccessLog) Close() (err error) { ctx, cancelFunc := stdContext.WithTimeout(stdContext.Background(), 10*time.Second) defer cancelFunc() return ac.CloseContext(ctx) } // CloseContext same as `Close` but waits until given "ctx" is done. func (ac *AccessLog) CloseContext(ctx stdContext.Context) (err error) { if !atomic.CompareAndSwapUint32(&ac.isClosed, 0, 1) { return } if ac.broker != nil { ac.broker.close <- struct{}{} } if ac.Async { ac.waitRemaining(ctx) } if fErr := ac.Flush(); fErr != nil { if err == nil { err = fErr } else { err = fmt.Errorf("%v, %v", err, fErr) } } for _, closer := range ac.Closers { cErr := closer.Close() if cErr != nil { if err == nil { err = cErr } else { err = fmt.Errorf("%v, %v", err, cErr) } } } return } func (ac *AccessLog) waitRemaining(ctx stdContext.Context) { if n := atomic.LoadUint32(&ac.remaining); n == 0 { return } t := time.NewTicker(2 * time.Second) defer t.Stop() for { select { case <-ctx.Done(): return case <-t.C: if atomic.LoadUint32(&ac.remaining) == 0 { return } } } } // func (ac *AccessLog) isBrokerActive() bool { // see `Print` method. // return atomic.LoadUint32(&ac.brokerActive) > 0 // } // ^ No need, we declare that the Broker should be called // before serve-time. Let's respect our comment // and don't try to make it safe for write and read concurrent access. // Write writes to the log destination. // It completes the io.Writer interface. // Safe for concurrent use. func (ac *AccessLog) Write(p []byte) (n int, err error) { if ac.Async { if atomic.LoadUint32(&ac.isClosed) > 0 { return 0, io.ErrClosedPipe } } ac.mu.Lock() n, err = ac.Writer.Write(p) ac.mu.Unlock() return } // Flush writes any buffered data to the underlying Fluser Writer. // Flush is called automatically on Close. func (ac *AccessLog) Flush() (err error) { ac.mu.Lock() for _, f := range ac.Flushers { fErr := f.Flush() if fErr != nil { if err == nil { err = fErr } else { err = fmt.Errorf("%v, %v", err, fErr) } } } ac.mu.Unlock() return } // Truncate if the output is a buffer, then // it discards all but the first n unread bytes. // See `TruncateFile` for a file size. // // It panics if n is negative or greater than the length of the buffer. func (ac *AccessLog) Truncate(n int) { ac.mu.Lock() // Lock as we do with all write operations. for _, truncater := range ac.BufferTruncaters { truncater.Truncate(n) } ac.mu.Unlock() } // TruncateFile flushes any buffered contents // and changes the size of the internal file destination, directly. // It does not change the I/O offset. // // Note that `TruncateFile` calls the `Truncate(int(size))` automatically // in order to clear any buffered contents (if the file was wrapped by a buffer) // before truncating the file itself. // // Usage, clear a file: // err := TruncateFile(0) func (ac *AccessLog) TruncateFile(size int64) (err error) { ac.Truncate(int(size)) ac.mu.Lock() for _, truncater := range ac.FileTruncaters { tErr := truncater.Truncate(size) if tErr != nil { if err == nil { err = tErr } else { err = fmt.Errorf("%v, %v", err, tErr) } } } ac.mu.Unlock() return err } // SetFormatter sets a custom formatter to print the logs. // Any custom output writers should be // already registered before calling this method. // Returns this AccessLog instance. // // Usage: // ac.SetFormatter(&accesslog.JSON{Indent: " "}) func (ac *AccessLog) SetFormatter(f Formatter) *AccessLog { if ac.Writer == nil { panic("accesslog: SetFormatter called with nil Writer") } if f == nil { return ac } if flusher, ok := ac.formatter.(Flusher); ok { // PREPEND formatter flushes, they should run before destination's ones. ac.Flushers = append([]Flusher{flusher}, ac.Flushers...) } // Inject the writer (AccessLog) here, the writer // is protected with mutex. f.SetOutput(ac) ac.formatter = f return ac } // AddFields maps one or more log entries with values extracted by the Request Context. // You can also add fields per request handler, look the `GetFields` package-level function. // Note that this method can override a key stored by a handler's fields. func (ac *AccessLog) AddFields(setters ...FieldSetter) *AccessLog { ac.FieldSetters = append(ac.FieldSetters, setters...) return ac } func (ac *AccessLog) shouldReadRequestBody() bool { return ac.RequestBody || ac.BytesReceived || ac.BytesReceivedBody } func (ac *AccessLog) shouldReadResponseBody() bool { return ac.ResponseBody || ac.BytesSent /* || ac.BytesSentBody this can be measured by the default writer's Written() */ } // Handler prints request information to the output destination. // It is the main method of the AccessLog middleware. // // Usage: // ac := New(io.Writer) or File("access.log") // defer ac.Close() // app.UseRouter(ac.Handler) func (ac *AccessLog) Handler(ctx *context.Context) { if shouldSkip(ctx) { // usage: another middleware before that one disables logging. ctx.Next() return } var ( startTime = time.Now() // Store some values, as future handler chain // can modify those (note: we could clone the request or context object too). method = ctx.Method() path = ctx.Path() ) // Enable response recording. if ac.shouldReadResponseBody() { ctx.Record() } // Enable reading the request body // multiple times (route handler and this middleware). if ac.shouldReadRequestBody() { ctx.RecordRequestBody(true) } // Set the fields context value so they can be modified // on the following handlers chain. Same as `AddFields` but per-request. // ctx.Values().Set(fieldsContextKey, new(Fields)) // No need ^ The GetFields will set it if it's missing. // So we initialize them whenever, and if, asked. // Proceed to the handlers chain. currentIndex := ctx.HandlerIndex(-1) ctx.Next() if context.StatusCodeNotSuccessful(ctx.GetStatusCode()) { _, wasRecovered := ctx.IsRecovered() // The ctx.HandlerName is still accesslog because // on end of router filters the router resets // the handler index, same for errors. // So, as a special case, if it's a failure status code // call FireErorrCode manually instead of wait // to be called on EndRequest (which is, correctly, called on end of everything // so we don't have chance to record its body by default). // // Note: this however will call the error handler twice // if the end-developer registered that using `UseError` instead of `UseRouter`, // there is a way to fix that too: by checking the handler index diff: if currentIndex == ctx.HandlerIndex(-1) || wasRecovered { // if handler index before and after ctx.Next // is the same, then it means we are in `UseRouter` // and on error handler. ctx.Application().FireErrorCode(ctx) } } if shouldSkip(ctx) { // normal flow, we can get the context by executing the handler first. return } latency := time.Since(startTime).Round(ac.LatencyRound) if ac.Async { ctxCopy := ctx.Clone() go ac.after(ctxCopy, latency, method, path) } else { // wait to finish before proceed with response end. ac.after(ctx, latency, method, path) } } func (ac *AccessLog) after(ctx *context.Context, lat time.Duration, method, path string) { var ( // request and response data or error reading them. requestBody string responseBody string bytesReceived int // total or body, depends on the configuration. bytesSent int ) if ac.shouldReadRequestBody() { // any error handler stored ( ctx.SetErr or StopWith(Plain)Error ) if ctxErr := ctx.GetErr(); ctxErr != nil { // If there is an error here // we may need to NOT read the body for security reasons, e.g. // unauthorized user tries to send a malicious body. requestBody = ac.getErrorText(ctxErr) } else { requestData, err := ctx.GetBody() requestBodyLength := len(requestData) if ac.BytesReceivedBody { bytesReceived = requestBodyLength // store it, if the total is enabled then this will be overridden. } if err != nil && ac.RequestBody { if err != http.ErrBodyReadAfterClose { // if body was already closed, don't send it as error. requestBody = ac.getErrorText(err) } } else if requestBodyLength > 0 { if ac.RequestBody { if ac.BodyMinify { if minified, err := ctx.Application().Minifier().Bytes(ctx.GetContentTypeRequested(), requestData); err == nil { requestBody = string(minified) } } /* Some content types, like the text/plain, no need minifier. Should be printed with spaces and \n. */ if requestBody == "" { requestBody = string(requestData) } } } if ac.BytesReceived { // Unfortunally the DumpRequest cannot read the body // length as expected (see postman's i/o values) // so we had to read the data length manually even if RequestBody/ResponseBody // are false, extra operation if they are enabled is to minify their log entry representation. b, _ := httputil.DumpRequest(ctx.Request(), false) bytesReceived = len(b) + requestBodyLength } } } if ac.shouldReadResponseBody() { actualResponseData := ctx.Recorder().Body() responseBodyLength := len(actualResponseData) if ac.BytesSentBody { bytesSent = responseBodyLength } if ac.ResponseBody && responseBodyLength > 0 { if ac.BodyMinify { // Copy response data as minifier now can change the back slice, // fixes: https://github.com/kataras/iris-premium/issues/17. responseData := make([]byte, len(actualResponseData)) copy(responseData, actualResponseData) if minified, err := ctx.Application().Minifier().Bytes(ctx.GetContentType(), responseData); err == nil { responseBody = string(minified) responseBodyLength = len(responseBody) } } if responseBody == "" { responseBody = string(actualResponseData) } } if ac.BytesSent { resp := ctx.Recorder().Result() b, _ := httputil.DumpResponse(resp, false) dateLengthProx := 38 /* it's actually ~37 */ if resp.Header.Get("Date") != "" { dateLengthProx = 0 // dump response calculated it. } bytesSent = len(b) + responseBodyLength + dateLengthProx } } else if ac.BytesSentBody { bytesSent = ctx.ResponseWriter().Written() } // Grab any custom fields. fields := GetFields(ctx) for _, setter := range ac.FieldSetters { setter(ctx, fields) } timeFormat := ac.TimeFormat if timeFormat == "" { timeFormat = ctx.Application().ConfigurationReadOnly().GetTimeFormat() } ip := "" if ac.IP { ip = ctx.RemoteAddr() } if err := ac.Print(ctx, // latency between begin and finish of the handlers chain. lat, timeFormat, // response code. ctx.GetStatusCode(), // original request's method and path. method, path, // remote ip. ip, requestBody, responseBody, bytesReceived, bytesSent, ctx.Params().Store, ctx.URLParamsSorted(), *fields, ); err != nil { ctx.Application().Logger().Errorf("accesslog: %v", err) } } // Print writes a log manually. // The `Handler` method calls it. func (ac *AccessLog) Print(ctx *context.Context, latency time.Duration, timeFormat string, code int, method, path, ip, reqBody, respBody string, bytesReceived, bytesSent int, params memstore.Store, query []memstore.StringEntry, fields []memstore.Entry) (err error) { if ac.Async { // atomic.AddUint32(&ac.remaining, 1) // This could work ^ // but to make sure we have the correct number of increments. // CAS loop: for { cur := atomic.LoadUint32(&ac.remaining) if atomic.CompareAndSwapUint32(&ac.remaining, cur, cur+1) { break } } defer atomic.AddUint32(&ac.remaining, ^uint32(0)) } now := ac.Clock.Now() if hasFormatter, hasBroker := ac.formatter != nil, ac.broker != nil; hasFormatter || hasBroker { log := ac.logsPool.Get().(*Log) log.Logger = ac log.Now = now log.TimeFormat = timeFormat log.Timestamp = now.UnixNano() / 1000000 log.Latency = latency log.Code = code log.Method = method log.Path = path log.IP = ip log.Query = query log.PathParams = params log.Fields = fields log.Request = reqBody log.Response = respBody log.BytesReceived = bytesReceived log.BytesSent = bytesSent log.Ctx = ctx var handled bool if hasFormatter { handled, err = ac.formatter.Format(log) // formatter can alter this, we wait until it's finished. if err != nil { ac.logsPool.Put(log) return } } if hasBroker { // after Format, it may want to customize the log's fields. ac.broker.notify(log.Clone()) // a listener cannot edit the log as we use object pooling. } ac.logsPool.Put(log) // we don't need it anymore. if handled { return // OK, it's handled, exit now. } } // the number of separators is the same, in order to be easier // for 3rd-party programs to read the result log file. builder := ac.bufPool.Get().(*bytes.Buffer) builder.WriteString(now.Format(timeFormat)) builder.WriteByte(ac.Delim) builder.WriteString(latency.String()) builder.WriteByte(ac.Delim) builder.WriteString(strconv.Itoa(code)) builder.WriteByte(ac.Delim) builder.WriteString(method) builder.WriteByte(ac.Delim) builder.WriteString(path) builder.WriteByte(ac.Delim) if ac.IP { ac.writeText(builder, ip) builder.WriteByte(ac.Delim) } // url parameters, path parameters and custom fields separated by space, // key=value key2=value2. if n, all := parseRequestValues(builder, code, params, query, fields); n > 0 { builder.Truncate(all - 1) // remove the last space. builder.WriteByte(ac.Delim) } if ac.BytesReceived || ac.BytesReceivedBody { builder.WriteString(formatBytes(bytesReceived)) builder.WriteByte(ac.Delim) } if ac.BytesSent || ac.BytesSentBody { builder.WriteString(formatBytes(bytesSent)) builder.WriteByte(ac.Delim) } if ac.RequestBody { ac.writeText(builder, reqBody) builder.WriteByte(ac.Delim) } if ac.ResponseBody { ac.writeText(builder, respBody) builder.WriteByte(ac.Delim) } builder.WriteByte(newLine) _, err = ac.Write(builder.Bytes()) builder.Reset() ac.bufPool.Put(builder) return } // We could have a map of blanks per field, // but let's don't coplicate things so much // as the end-developer can use a custom template. func (ac *AccessLog) writeText(buf *bytes.Buffer, s string) { if s == "" { if len(ac.Blank) == 0 { return } buf.Write(ac.Blank) } else { buf.WriteString(s) } } var lineBreaksReplacer = strings.NewReplacer("\n\r", " ", "\n", " ") func (ac *AccessLog) getErrorText(err error) (text string) { // caller checks for nil. if errPanic, ok := context.IsErrPanicRecovery(err); ok { ac.Flush() // flush any buffered contents to be written to the output. switch ac.PanicLog { case LogHandler: text = errPanic.CurrentHandlerFileLine case LogCallers: text = strings.Join(errPanic.Callers, "\n") case LogStack: text = string(errPanic.Stack) } text = fmt.Sprintf("error(%v %s)", errPanic.Cause, text) } else { text = fmt.Sprintf("error(%s)", err.Error()) } if !ac.KeepMultiLineError { return lineBreaksReplacer.Replace(text) } return text } ================================================ FILE: middleware/accesslog/accesslog_test.go ================================================ package accesslog import ( "bytes" "fmt" "io" "net/http" "net/http/httptest" "strings" "sync" "sync/atomic" "testing" "time" "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/core/memstore" ) func TestAccessLogPrint_Simple(t *testing.T) { const goroutinesN = 42 w := new(bytes.Buffer) ac := New(w) ac.Async = true ac.ResponseBody = true ac.Clock = TClock(time.Time{}) var ( expected string expectedLines int mu sync.Mutex ) now := time.Now() for i := 0; i < goroutinesN; i++ { go func() { err := ac.Print( nil, 1*time.Second, ac.TimeFormat, 200, "GET", "/path_value?url_query=url_query_value", "::1", "Incoming", "Outcoming", 0, 0, memstore.Store{ {Key: "path_param", ValueRaw: "path_param_value"}, }, []memstore.StringEntry{ {Key: "url_query", Value: "url_query_value"}, }, []memstore.Entry{ {Key: "custom", ValueRaw: "custom_value"}, }) if err == nil { mu.Lock() expected += "0001-01-01 00:00:00|1s|200|GET|/path_value?url_query=url_query_value|::1|path_param=path_param_value url_query=url_query_value custom=custom_value|0 B|0 B|Incoming|Outcoming|\n" expectedLines++ mu.Unlock() } }() } // give some time to write at least some messages or all // (depends on the machine the test is running). time.Sleep(42 * time.Millisecond) ac.Close() end := time.Since(now) if got := atomic.LoadUint32(&ac.remaining); got > 0 { // test wait. t.Fatalf("expected remaining: %d but got: %d", 0, got) } mu.Lock() expectedSoFoar := expected expectedLinesSoFar := expectedLines mu.Unlock() if got := w.String(); expectedSoFoar != got { gotLines := strings.Count(got, "\n") t.Logf("expected printed result to be[%d]:\n'%s'\n\nbut got[%d]:\n'%s'", expectedLinesSoFar, expectedSoFoar, gotLines, got) t.Fatalf("expected: %d | got: %d lines", expectedLinesSoFar, gotLines) } else { t.Logf("We've got [%d/%d] lines of logs in %s", expectedLinesSoFar, goroutinesN, end.String()) } } func TestAccessLogBroker(t *testing.T) { w := new(bytes.Buffer) ac := New(w) ac.Clock = TClock(time.Time{}) broker := ac.Broker() wg := new(sync.WaitGroup) n := 4 wg.Add(n) go func() { i := 0 ln := broker.NewListener() for log := range ln { lat := log.Latency t.Log(lat.String()) wg.Done() if expected := time.Duration(i) * time.Second; expected != lat { panic(fmt.Sprintf("expected latency: %s but got: %s", expected, lat)) } time.Sleep(1350 * time.Millisecond) if log.Latency != lat { panic("expected logger to wait for notifier before release the log") } i++ } if i != n { for i < n { wg.Done() i++ } } t.Log("Log Listener Closed: interrupted") }() time.Sleep(time.Second) printLog := func(lat time.Duration) { err := ac.Print( nil, lat, "", 0, "", "", "", "", "", 0, 0, nil, nil, nil, ) if err != nil { t.Fatal(err) } } for i := 0; i < n; i++ { printLog(time.Duration(i) * time.Second) } // wait for all listeners to finish. wg.Wait() ac.Close() } func TestAccessLogBlank(t *testing.T) { w := new(bytes.Buffer) ac := New(w) clockTime, _ := time.Parse(defaultTimeFormat, "1993-01-01 05:00:00") ac.Clock = TClock(clockTime) ac.Blank = []byte("") ac.Print( nil, time.Second, defaultTimeFormat, 200, "GET", "/", "127.0.0.1", "", "", 0, 0, nil, nil, nil, ) ac.Close() // the request and bodies length are enabled by-default, zero bytes length // are written with 0 B and this cannot changed, so the request field // should be written as "". expected := "1993-01-01 05:00:00|1s|200|GET|/|127.0.0.1|0 B|0 B||\n" if got := w.String(); expected != got { t.Fatalf("expected:\n'%s'\n\nbut got:\n'%s'", expected, got) } } type slowClose struct{ *bytes.Buffer } func (c *slowClose) Close() error { time.Sleep(1 * time.Second) return nil } func TestAccessLogSetOutput(t *testing.T) { var ( w1 = &bytes.Buffer{} w2 = &bytes.Buffer{} w3 = &slowClose{&bytes.Buffer{}} w4 = &bytes.Buffer{} ) ac := New(w1) ac.Clock = TClock(time.Time{}) n := 40 expected := strings.Repeat("0001-01-01 00:00:00|1s|200|GET|/|127.0.0.1|0 B|0 B||\n", n) printLog := func() { err := ac.Print( nil, time.Second, defaultTimeFormat, 200, "GET", "/", "127.0.0.1", "", "", 0, 0, nil, nil, nil, ) if err != nil { t.Fatal(err) } } testSetOutput := func(name string, w io.Writer, withSlowClose bool) { wg := new(sync.WaitGroup) wg.Add(n / 4) for i := 0; i < n/4; i++ { go func(i int) { defer wg.Done() if i%2 == 0 { time.Sleep(10 * time.Millisecond) } if i == 5 { if w != nil { now := time.Now() ac.SetOutput(w) if withSlowClose { end := time.Since(now) if end < time.Second { panic(fmt.Sprintf("[%s] [%d]: SetOutput should wait for previous Close. Expected to return a bit after %s but %s", name, i, time.Second, end)) } } } } printLog() }(i) } // wait to finish. wg.Wait() } go testSetOutput("w1", nil, false) // write at least one line and then time.Sleep(100 * time.Millisecond) // concurrently testSetOutput("w2", w2, false) // change the writer testSetOutput("w3", w3, false) testSetOutput("w4", w4, true) gotAll := w1.String() + w2.String() + w3.String() + w4.String() // test if all content written and we have no loses. if expected != gotAll { t.Fatalf("expected total written result to be:\n'%s'\n\nbut got:\n'%s'", expected, gotAll) } // now, check if all have contents, they should because we wait between them, // contents spread. checkLines := func(name, s string, minimumLines int) { if got := strings.Count(s, "\n"); got < minimumLines { t.Logf("[%s] expected minimum lines of: %d but got %d", name, minimumLines, got) } } checkLines("w1", w1.String(), 1) checkLines("w2", w2.String(), 5) checkLines("w3", w3.String(), 5) checkLines("w4", w4.String(), 5) if err := ac.Close(); err != nil { t.Fatalf("On close: %v", err) } } type noOpFormatter struct{} func (*noOpFormatter) SetOutput(io.Writer) {} // Format prints the logs in text/template format. func (*noOpFormatter) Format(*Log) (bool, error) { return true, nil } // go test -run=^$ -bench=BenchmarkAccessLogAfter -benchmem func BenchmarkAccessLogAfter(b *testing.B) { benchmarkAccessLogAfter(b, true, false) } func BenchmarkAccessLogAfterPrint(b *testing.B) { benchmarkAccessLogAfter(b, false, false) } func benchmarkAccessLogAfter(b *testing.B, withLogStruct, async bool) { ac := New(io.Discard) ac.Clock = TClock(time.Time{}) ac.BytesReceived = false ac.BytesReceivedBody = false ac.BytesSent = false ac.BytesSentBody = false ac.BodyMinify = false ac.RequestBody = false ac.ResponseBody = false ac.Async = false ac.IP = false if withLogStruct { ac.SetFormatter(new(noOpFormatter)) // just to create the log structure, here we test the log creation time only. } ctx := new(context.Context) req, err := http.NewRequest("GET", "/", nil) if err != nil { b.Fatal(err) } ctx.ResetRequest(req) recorder := httptest.NewRecorder() w := context.AcquireResponseWriter() w.BeginResponse(recorder) ctx.ResetResponseWriter(w) wg := new(sync.WaitGroup) if async { wg.Add(b.N) } b.ResetTimer() for n := 0; n < b.N; n++ { if async { go func() { ac.after(ctx, time.Millisecond, "GET", "/") wg.Done() }() } else { ac.after(ctx, time.Millisecond, "GET", "/") } } b.StopTimer() if async { wg.Wait() } w.EndResponse() } ================================================ FILE: middleware/accesslog/broker.go ================================================ package accesslog // LogChan describes the log channel. // See `Broker` for details. type LogChan chan Log // A Broker holds the active listeners, // incoming logs on its Notifier channel // and broadcast event data to all registered listeners. // // Exports the `NewListener` and `CloseListener` methods. type Broker struct { // Logs are pushed to this channel // by the main events-gathering `run` routine. Notifier LogChan // NewListener action. newListeners chan LogChan // CloseListener action. closingListeners chan LogChan // listeners store. listeners map[LogChan]bool // force-terminate all listeners. close chan struct{} } // newBroker returns a new broker factory. func newBroker() *Broker { b := &Broker{ Notifier: make(LogChan, 1), newListeners: make(chan LogChan), closingListeners: make(chan LogChan), listeners: make(map[LogChan]bool), close: make(chan struct{}), } // Listens and Broadcasts events. go b.run() return b } // run listens on different channels and act accordingly. func (b *Broker) run() { for { select { case s := <-b.newListeners: // A new channel has started to listen. b.listeners[s] = true case s := <-b.closingListeners: // A listener has dettached. // Stop sending them the logs. delete(b.listeners, s) case log := <-b.Notifier: // A new log sent by the logger. // Send it to all active listeners. for clientMessageChan := range b.listeners { clientMessageChan <- log } case <-b.close: for clientMessageChan := range b.listeners { delete(b.listeners, clientMessageChan) close(clientMessageChan) } } } } // notify sends the "log" to all active listeners. func (b *Broker) notify(log Log) { b.Notifier <- log } // NewListener returns a new log channel listener. // The caller SHALL NOT use this to write logs. func (b *Broker) NewListener() LogChan { // Each listener registers its own message channel with the Broker's connections registry. logs := make(LogChan) // Signal the broker that we have a new listener. b.newListeners <- logs return logs } // CloseListener removes the "ln" listener from the active listeners. func (b *Broker) CloseListener(ln LogChan) { b.closingListeners <- ln } // As we cant export a read-only and pass it as closing client // we will return a read-write channel on NewListener and add a note that the user // should NOT send data back to the channel, its use is read-only. // func (b *Broker) CloseListener(ln <-chan *Log) { // b.closingListeners <- ln // } ================================================ FILE: middleware/accesslog/csv.go ================================================ package accesslog import ( "encoding/csv" "io" "strconv" "sync" ) // CSV is a Formatter type for csv encoded logs. type CSV struct { writerPool *sync.Pool ac *AccessLog // Add header fields to the first line if it's not exist. // Note that the destination should be a compatible io.Reader // with access to write. Header bool // Google Spreadsheet's Script to wrap the Timestamp field // in order to convert it into a readable date. // Example: "FROM_UNIX" when // function FROM_UNIX(epoch_in_millis) { // return new Date(epoch_in_millis); // } DateScript string // TODO: Fields []string // field name, position? } // SetOutput initializes the csv writer. // It uses the "dest" as AccessLog to // write the first csv record which // contains the names of the future log values. func (f *CSV) SetOutput(dest io.Writer) { f.ac, _ = dest.(*AccessLog) f.writerPool = &sync.Pool{ New: func() any { return csv.NewWriter(dest) }, } if !f.Header { return } { // If the destination is not a reader // we can't detect if the header already inserted // so we exit, we dont want to malform the contents. destReader, ok := f.ac.Writer.(io.Reader) if !ok { return } r := csv.NewReader(destReader) if header, err := r.Read(); err == nil && len(header) > 0 && header[0] == "Timestamp" { // we assume header already exists, exit. return } } // Write the header. w := csv.NewWriter(dest) keys := []string{"Timestamp", "Latency", "Code", "Method", "Path"} if f.ac.IP { keys = append(keys, "IP") } // keys = append(keys, []string{"Params", "Query"}...) keys = append(keys, "Req Values") if f.ac.BytesReceived || f.ac.BytesReceivedBody { keys = append(keys, "In") } if f.ac.BytesSent || f.ac.BytesSentBody { keys = append(keys, "Out") } if f.ac.RequestBody { keys = append(keys, "Request") } if f.ac.ResponseBody { keys = append(keys, "Response") } w.Write(keys) w.Flush() } // Format writes an incoming log using CSV encoding. func (f *CSV) Format(log *Log) (bool, error) { // Timestamp, Latency, Code, Method, Path, IP, Path Params Query Fields //|Bytes Received|Bytes Sent|Request|Response| timestamp := strconv.FormatInt(log.Timestamp, 10) if f.DateScript != "" { timestamp = "=" + f.DateScript + "(" + timestamp + ")" } values := []string{ timestamp, log.Latency.String(), strconv.Itoa(log.Code), log.Method, log.Path, } if f.ac.IP { values = append(values, log.IP) } if s := log.RequestValuesLine(); s != "" || f.Header { // even if it's empty, if Header was set, then add it. values = append(values, s) } if f.ac.BytesReceived || f.ac.BytesReceivedBody { values = append(values, strconv.Itoa(log.BytesReceived)) } if f.ac.BytesSent || f.ac.BytesSentBody { values = append(values, strconv.Itoa(log.BytesSent)) } if f.ac.RequestBody && (log.Request != "" || f.Header) { values = append(values, log.Request) } if f.ac.ResponseBody && (log.Response != "" || f.Header) { values = append(values, log.Response) } w := f.writerPool.Get().(*csv.Writer) err := w.Write(values) w.Flush() // it works as "reset" too. f.writerPool.Put(w) return true, err } ================================================ FILE: middleware/accesslog/csv_test.go ================================================ package accesslog import ( "bytes" "testing" "time" "github.com/kataras/iris/v12/core/memstore" ) func TestCSV(t *testing.T) { buf := new(bytes.Buffer) ac := New(buf) ac.RequestBody = false staticNow, _ := time.Parse(defaultTimeFormat, "1993-01-01 05:00:00") ac.Clock = TClock(staticNow) ac.SetFormatter(&CSV{ Header: true, }) lat, _ := time.ParseDuration("1s") printFunc := func() { ac.Print( nil, lat, "", 200, "GET", "/", "::1", "", "Index", 573, 81, nil, []memstore.StringEntry{{Key: "sleep", Value: "1s"}}, nil) } // print twice, the header should only be written once. printFunc() printFunc() expected := `Timestamp,Latency,Code,Method,Path,IP,Req Values,In,Out 725864400000,1s,200,GET,/,::1,sleep=1s,573,81 725864400000,1s,200,GET,/,::1,sleep=1s,573,81 ` ac.Close() if got := buf.String(); expected != got { t.Fatalf("expected:\n%s\n\nbut got:\n%s", expected, got) } } ================================================ FILE: middleware/accesslog/json.go ================================================ package accesslog import ( "bytes" "io" "strconv" "strings" jsoniter "github.com/json-iterator/go" ) // JSON is a Formatter type for JSON logs. type JSON struct { // Indent in spaces. // Note that, if set to > 0 then jsoniter is used instead of easyjson. Indent string EscapeHTML bool HumanTime bool jsoniter jsoniter.API ac *AccessLog } // SetOutput creates the json encoder writes to the "dest". // It's called automatically by the middleware when this Formatter is used. func (f *JSON) SetOutput(dest io.Writer) { f.ac, _ = dest.(*AccessLog) if indentStep := strings.Count(f.Indent, " "); indentStep > 0 { // Note that: indent setting should always be spaces // as the jsoniter does not support other chars. f.jsoniter = jsoniter.Config{ TagKey: "json", IndentionStep: indentStep, EscapeHTML: f.EscapeHTML, SortMapKeys: true, }.Froze() } } var ( timestampKeyB = []byte(`"timestamp":`) timestampKeyIndentB = append(timestampKeyB, ' ') timestampKeyVB = append(timestampKeyB, '0') timestampIndentKeyVB = append(timestampKeyIndentB, '0') ) // Format prints the logs in JSON format. // Writes to the destination directly, // locks on each Format call. func (f *JSON) Format(log *Log) (bool, error) { if f.jsoniter != nil { if f.HumanTime { // 1. Don't write the unix timestamp, // key will be visible though as we don't omit the field. log.Timestamp = 0 } b, err := f.jsoniter.Marshal(log) if err != nil { return true, err } if f.HumanTime { // 2. Get the time text based on the configuration. t := log.Now.Format(log.TimeFormat) // 3. Find the "timestamp:$indent" // and set it to the text one. var ( oldT []byte tsKey []byte ) if f.Indent != "" { oldT = timestampIndentKeyVB tsKey = timestampKeyIndentB } else { oldT = timestampKeyVB tsKey = timestampKeyB } newT := append(tsKey, strconv.Quote(t)...) b = bytes.Replace(b, oldT, newT, 1) } f.ac.Write(append(b, newLine)) return true, nil } err := f.writeEasyJSON(log) return true, err } ================================================ FILE: middleware/accesslog/json_easy.go ================================================ // Code generated by easyjson but it's highly edited. Please manually add fields as Log is grow, // in-short: all decode methods removed, rename of the methods, add a new line breaker, // remove the easyjson import requirement. package accesslog import ( "encoding/json" "github.com/kataras/iris/v12/core/memstore" "github.com/mailru/easyjson/jwriter" ) func (f *JSON) writeEasyJSON(in *Log) error { out := &jwriter.Writer{NoEscapeHTML: !f.EscapeHTML} out.RawByte('{') first := true _ = first { const prefix string = ",\"timestamp\":" if first { first = false out.RawString(prefix[1:]) } else { out.RawString(prefix) } if f.HumanTime { t := in.Now.Format(in.TimeFormat) out.String(t) } else { out.Int64(in.Timestamp) } } { const prefix string = ",\"latency\":" out.RawString(prefix) out.Int64(int64(in.Latency)) } { const prefix string = ",\"code\":" out.RawString(prefix) out.Int(int(in.Code)) } { const prefix string = ",\"method\":" out.RawString(prefix) out.String(in.Method) } { const prefix string = ",\"path\":" out.RawString(prefix) out.String(in.Path) } if in.IP != "" { const prefix string = ",\"ip\":" out.RawString(prefix) out.String(in.IP) } if len(in.Query) != 0 { const prefix string = ",\"query\":" out.RawString(prefix) { out.RawByte('[') for v4, v5 := range in.Query { if v4 > 0 { out.RawByte(',') } easyJSONStringEntry(out, v5) } out.RawByte(']') } } if len(in.PathParams) != 0 { const prefix string = ",\"params\":" out.RawString(prefix) { out.RawByte('[') for v6, v7 := range in.PathParams { if v6 > 0 { out.RawByte(',') } easyJSONEntry(out, v7) } out.RawByte(']') } } if len(in.Fields) != 0 { const prefix string = ",\"fields\":" out.RawString(prefix) { out.RawByte('[') for v8, v9 := range in.Fields { if v8 > 0 { out.RawByte(',') } easyJSONEntry(out, v9) } out.RawByte(']') } } if in.Logger.RequestBody { const prefix string = ",\"request\":" out.RawString(prefix) out.String(string(in.Request)) } if in.Logger.ResponseBody { const prefix string = ",\"response\":" out.RawString(prefix) out.String(string(in.Response)) } if in.BytesReceived != 0 { const prefix string = ",\"bytes_received\":" out.RawString(prefix) out.Int(int(in.BytesReceived)) } if in.BytesSent != 0 { const prefix string = ",\"bytes_sent\":" out.RawString(prefix) out.Int(int(in.BytesSent)) } out.RawByte('}') out.RawByte(newLine) if out.Error != nil { return out.Error } f.ac.Write(out.Buffer.BuildBytes()) return nil } func easyJSONEntry(out *jwriter.Writer, in memstore.Entry) { out.RawByte('{') first := true _ = first { const prefix string = ",\"key\":" out.RawString(prefix[1:]) out.String(string(in.Key)) } { const prefix string = ",\"value\":" out.RawString(prefix) if m, ok := in.ValueRaw.(json.Marshaler); ok { out.Raw(m.MarshalJSON()) } else { out.Raw(json.Marshal(in.ValueRaw)) } } out.RawByte('}') } func easyJSONStringEntry(out *jwriter.Writer, in memstore.StringEntry) { out.RawByte('{') first := true _ = first { const prefix string = ",\"key\":" out.RawString(prefix[1:]) out.String(string(in.Key)) } { const prefix string = ",\"value\":" out.RawString(prefix) out.String(string(in.Value)) } out.RawByte('}') } ================================================ FILE: middleware/accesslog/log.go ================================================ package accesslog import ( "fmt" "io" "strings" "time" "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/core/memstore" ) // Log represents the log data specifically for the accesslog middleware. // //easyjson:json type Log struct { // The AccessLog instance this Log was created of. Logger *AccessLog `json:"-" yaml:"-" toml:"-"` // The time the log is created. Now time.Time `json:"-" yaml:"-" toml:"-"` // TimeFormat selected to print the Time as string, // useful on Template Formatter. TimeFormat string `json:"-" yaml:"-" toml:"-"` // Timestamp the Now's unix timestamp (milliseconds). Timestamp int64 `json:"timestamp" csv:"timestamp"` // Request-Response latency. Latency time.Duration `json:"latency" csv:"latency"` // The response status code. Code int `json:"code" csv:"code"` // Init request's Method and Path. Method string `json:"method" csv:"method"` Path string `json:"path" csv:"path"` // The Remote Address. IP string `json:"ip,omitempty" csv:"ip,omitempty"` // Sorted URL Query arguments. Query []memstore.StringEntry `json:"query,omitempty" csv:"query,omitempty"` // Dynamic path parameters. PathParams memstore.Store `json:"params,omitempty" csv:"params,omitempty"` // Fields any data information useful to represent this Log. Fields memstore.Store `json:"fields,omitempty" csv:"fields,omitempty"` // The Request and Response raw bodies. // If they are escaped (e.g. JSON), // A third-party software can read it through: // data, _ := strconv.Unquote(log.Request) // err := json.Unmarshal([]byte(data), &customStruct) Request string `json:"request,omitempty" csv:"request,omitempty"` Response string `json:"response,omitempty" csv:"response,omitempty"` // The actual number of bytes received and sent on the network (headers + body or body only). BytesReceived int `json:"bytes_received,omitempty" csv:"bytes_received,omitempty"` BytesSent int `json:"bytes_sent,omitempty" csv:"bytes_sent,omitempty"` // A copy of the Request's Context when Async is true (safe to use concurrently), // otherwise it's the current Context (not safe for concurrent access). Ctx *context.Context `json:"-" yaml:"-" toml:"-"` } // Clone returns a raw copy value of this Log. func (l *Log) Clone() Log { return *l } // RequestValuesLine returns a string line which // combines the path parameters, query and custom fields. func (l *Log) RequestValuesLine() string { buf := new(strings.Builder) _, n := parseRequestValues(buf, l.Code, l.PathParams, l.Query, l.Fields) if n == 0 { return "" } requestValues := buf.String() if n > 1 { return requestValues[0 : n-1] // remove last space. } return requestValues } // BytesReceivedLine returns the formatted bytes received length. func (l *Log) BytesReceivedLine() string { if !l.Logger.BytesReceived && !l.Logger.BytesReceivedBody { return "" } return formatBytes(l.BytesReceived) } // BytesSentLine returns the formatted bytes sent length. func (l *Log) BytesSentLine() string { if !l.Logger.BytesSent && !l.Logger.BytesSentBody { return "" } return formatBytes(l.BytesSent) } func formatBytes(b int) string { if b <= 0 { return "0 B" } const unit = 1024 if b < unit { return fmt.Sprintf("%d B", b) } div, exp := int64(unit), 0 for n := b / unit; n >= unit; n /= unit { div *= unit exp++ } return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "KMGTPE"[exp]) } const ( eq = '=' space = ' ' ) // parses the request values (params, query and fields). // returns the length of written bytes for parsing request values // and the total. func parseRequestValues(buf interface { io.StringWriter io.ByteWriter Len() int }, code int, pathParams memstore.Store, query []memstore.StringEntry, fields memstore.Store) (int, int) { n := buf.Len() if !context.StatusCodeNotSuccessful(code) { // collect path parameters on a successful request-response only. for _, entry := range pathParams { buf.WriteString(entry.Key) buf.WriteByte(eq) buf.WriteString(fmt.Sprintf("%v", entry.ValueRaw)) buf.WriteByte(space) } } for _, entry := range query { buf.WriteString(entry.Key) buf.WriteByte(eq) buf.WriteString(entry.Value) buf.WriteByte(space) } for _, entry := range fields { buf.WriteString(entry.Key) buf.WriteByte(eq) buf.WriteString(fmt.Sprintf("%v", entry.ValueRaw)) buf.WriteByte(space) } total := buf.Len() return total - n, total } type ( // Formatter is responsible to print a Log to the accesslog's writer. Formatter interface { // SetOutput should inject the accesslog's direct output, // if this "dest" is used then the Formatter // should manually control its concurrent use. SetOutput(dest io.Writer) // Format should print the Log. // Returns nil error on handle successfully, // otherwise the log will be printed using the default formatter // and the error will be printed to the Iris Application's error log level. // Should return true if this handled the logging, otherwise false to // continue with the default print format. Format(log *Log) (bool, error) } // Flusher can be implemented by a Writer or Formatter // to call its Flush method on AccessLog.Close // and on panic errors. Flusher interface{ Flush() error } // BufferTruncater can be implemented by writers // that support buffering. BufferTruncater interface{ Truncate(n int) } // FileTruncater can be implemented by files // that can support runtime size change. FileTruncater interface{ Truncate(size int64) error } ) var ( _ Formatter = (*JSON)(nil) _ Formatter = (*Template)(nil) _ Formatter = (*CSV)(nil) ) ================================================ FILE: middleware/accesslog/template.go ================================================ package accesslog import ( "bytes" "io" "text/template" ) // Template is a Formatter. // It's used to print the Log in a text/template way. // The caller has full control over the printable result; // certain fields can be ignored, change the display order and e.t.c. // // For faster execution you can create a custom Formatter // and compile your own quicktemplate: https://github.com/valyala/quicktemplate // // This one uses the standard text/template syntax. type Template struct { // Custom template source. // Use this or `Tmpl/TmplName` fields. Text string // Custom template funcs to used when `Text` is not empty. Funcs template.FuncMap // Custom template to use, overrides the `Text` and `Funcs` fields. Tmpl *template.Template // If not empty then this named template/block renders the log line. TmplName string ac *AccessLog } // SetOutput creates the default template if missing // when this formatter is registered. func (f *Template) SetOutput(dest io.Writer) { f.ac, _ = dest.(*AccessLog) if f.Tmpl == nil { tmpl := template.New("") text := f.Text if text != "" { tmpl.Funcs(f.Funcs) } else { text = defaultTmplText } f.Tmpl = template.Must(tmpl.Parse(text)) } } const defaultTmplText = "{{.Now.Format .TimeFormat}}|{{.Latency}}|{{.Code}}|{{.Method}}|{{.Path}}|{{.IP}}|{{.RequestValuesLine}}|{{.BytesReceivedLine}}|{{.BytesSentLine}}|{{.Request}}|{{.Response}}|\n" func (f *Template) LogText(log *Log) (string, error) { var err error // A template may be executed safely in parallel, although if parallel // executions share a Writer the output may be interleaved. // We solve that using a buffer pool, no locks when template is executing (x2 performance boost). temp := f.ac.bufPool.Get().(*bytes.Buffer) if f.TmplName != "" { err = f.Tmpl.ExecuteTemplate(temp, f.TmplName, log) } else { err = f.Tmpl.Execute(temp, log) } if err != nil { f.ac.bufPool.Put(temp) return "", err } text := temp.String() temp.Reset() f.ac.bufPool.Put(temp) return text, nil } // Format prints the logs in text/template format. func (f *Template) Format(log *Log) (bool, error) { var err error // A template may be executed safely in parallel, although if parallel // executions share a Writer the output may be interleaved. // We solve that using a buffer pool, no locks when template is executing (x2 performance boost). temp := f.ac.bufPool.Get().(*bytes.Buffer) if f.TmplName != "" { err = f.Tmpl.ExecuteTemplate(temp, f.TmplName, log) } else { err = f.Tmpl.Execute(temp, log) } if err != nil { f.ac.bufPool.Put(temp) return true, err } f.ac.Write(temp.Bytes()) temp.Reset() f.ac.bufPool.Put(temp) return true, nil } ================================================ FILE: middleware/basicauth/basicauth.go ================================================ package basicauth import ( stdContext "context" "net/http" "net/url" "strconv" "strings" "sync" "time" "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/sessions" ) func init() { context.SetHandlerName("iris/middleware/basicauth.*", "iris.basicauth") } const ( // DefaultRealm is the default realm directive value on Default and Load functions. DefaultRealm = "Authorization Required" // DefaultMaxTriesCookie is the default cookie name to store the // current amount of login failures when MaxTries > 0. DefaultMaxTriesCookie = "basicmaxtries" // DefaultCookieMaxAge is the default cookie max age on MaxTries, // when the Options.MaxAge is zero. DefaultCookieMaxAge = time.Hour ) const ( authorizationType = "Basic Authentication" authenticateHeaderKey = "WWW-Authenticate" proxyAuthenticateHeaderKey = "Proxy-Authenticate" authorizationHeaderKey = "Authorization" proxyAuthorizationHeaderKey = "Proxy-Authorization" ) // AuthFunc accepts the current request and the username and password user inputs // and it should optionally return a user value and report whether the login succeed or not. // Look the Options.Allow field. // // Default implementations are: // AllowUsers and AllowUsersFile functions. type AuthFunc func(ctx *context.Context, username, password string) (any, bool) // ErrorHandler should handle the given request credentials failure. // See Options.ErrorHandler and DefaultErrorHandler for details. type ErrorHandler func(ctx *context.Context, err error) // Options holds the necessary information that the BasicAuth instance needs to perform. // The only required value is the Allow field. // // Usage: // // opts := Options { ... } // auth := New(opts) type Options struct { // Realm directive, read http://tools.ietf.org/html/rfc2617#section-1.2 for details. // E.g. "Authorization Required". Realm string // In the case of proxies, the challenging status code is 407 (Proxy Authentication Required), // the Proxy-Authenticate response header contains at least one challenge applicable to the proxy, // and the Proxy-Authorization request header is used for providing the credentials to the proxy server. // // Proxy should be used to gain access to a resource behind a proxy server. // It authenticates the request to the proxy server, allowing it to transmit the request further. Proxy bool // If set to true then any non-https request will immediately // dropped with a 505 status code (StatusHTTPVersionNotSupported) response. // // Defaults to false. HTTPSOnly bool // Allow is the only one required field for the Options type. // Can be customized to validate a username and password combination // and return a user object, e.g. fetch from database. // // There are two available builtin values, the AllowUsers and AllowUsersFile, // both of them decode a static list of users and compares with the user input (see BCRYPT function too). // Usage: // - Allow: AllowUsers(iris.Map{"username": "...", "password": "...", "other_field": ...}, [BCRYPT]) // - Allow: AllowUsersFile("users.yml", [BCRYPT]) // Look the user.go source file for details. Allow AuthFunc // MaxAge sets expiration duration for the in-memory credentials map. // By default an old map entry will be removed when the user visits a page. // In order to remove old entries automatically please take a look at the `GC` option too. // // Usage: // MaxAge: 30 * time.Minute MaxAge time.Duration // If greater than zero then the server will send 403 forbidden status code afer // MaxTries amount of sign in failures (see MaxTriesCookie). // Note that the client can modify the cookie and its value, // do NOT depend for any type of custom domain logic based on this field. // By default the server will re-ask for credentials on invalid credentials, each time. MaxTries int // MaxTriesCookie is the cookie name the middleware uses to // store the failures amount on the client side. // The lifetime of the cookie is the same as the configured MaxAge or one hour, // therefore a forbidden client can request for authentication again after expiration. // // You can always set custom logic on the Allow field as you have access to the current request instance. // // Defaults to "basicmaxtries". // The MaxTries should be set to greater than zero. MaxTriesCookie string // If not empty then this session key will be used to store // the current tries of login failures. If not a session manager // was registered then the application will log an error. // Note that this field has a priority over the MaxTriesCookie. MaxTriesSession string // ErrorHandler handles the given request credentials failure. // E.g when the client tried to access a protected resource // with empty or invalid or expired credentials or // when Allow returned false and MaxTries consumed. // // Defaults to the DefaultErrorHandler, do not modify if you don't need to. ErrorHandler ErrorHandler // GC automatically clears old entries every x duration. // Note that, by old entries we mean expired credentials therefore // the `MaxAge` option should be already set, // if it's not then all entries will be removed on "every" duration. // The standard context can be used for the internal ticker cancelation, it can be nil. // // Usage: // GC: basicauth.GC{Every: 2 * time.Hour} GC GC } // GC holds the context and the tick duration to clear expired stored credentials. // See the Options.GC field. type GC struct { Context stdContext.Context Every time.Duration } // BasicAuth implements the basic access authentication. // It is a method for an HTTP client (e.g. a web browser) // to provide a user name and password when making a request. // Basic authentication implementation is the simplest technique // for enforcing access controls to web resources because it does not require // cookies, session identifiers, or login pages; rather, // HTTP Basic authentication uses standard fields in the HTTP header. // // As the username and password are passed over the network as clear text // the basic authentication scheme is not secure on plain HTTP communication. // It is base64 encoded, but base64 is a reversible encoding. // HTTPS/TLS should be used with basic authentication. // Without these additional security enhancements, // basic authentication should NOT be used to protect sensitive or valuable information. // // Read https://tools.ietf.org/html/rfc2617 and // https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication for details. type BasicAuth struct { opts Options // built based on proxy field askCode int authorizationHeader string authenticateHeader string // built based on realm field. authenticateHeaderValue string // credentials stores the user expiration, // key = username:password, value = expiration time (if MaxAge > 0). credentials map[string]*time.Time // TODO: think of just a uint64 here (unix seconds). // protects the credentials concurrent access. mu sync.RWMutex } // New returns a new basic authentication middleware. // The result should be used to wrap an existing handler or the HTTP application's root router. // // Example Code: // // opts := basicauth.Options{ // Realm: basicauth.DefaultRealm, // ErrorHandler: basicauth.DefaultErrorHandler, // MaxAge: 2 * time.Hour, // GC: basicauth.GC{ // Every: 3 * time.Hour, // }, // Allow: basicauth.AllowUsers(users), // } // auth := basicauth.New(opts) // app.Use(auth) // // Access the user in the route handler with: ctx.User().GetRaw().(*myCustomType). // // Look the BasicAuth type docs for more information. func New(opts Options) context.Handler { var ( askCode = http.StatusUnauthorized authorizationHeader = authorizationHeaderKey authenticateHeader = authenticateHeaderKey authenticateHeaderValue = "Basic" ) if opts.Allow == nil { panic("BasicAuth: Allow field is required") } if opts.Realm != "" { authenticateHeaderValue += " realm=" + strconv.Quote(opts.Realm) } if opts.Proxy { askCode = http.StatusProxyAuthRequired authenticateHeader = proxyAuthenticateHeaderKey authorizationHeader = proxyAuthorizationHeaderKey } if opts.MaxTries > 0 && opts.MaxTriesCookie == "" { opts.MaxTriesCookie = DefaultMaxTriesCookie } if opts.ErrorHandler == nil { opts.ErrorHandler = DefaultErrorHandler } b := &BasicAuth{ opts: opts, askCode: askCode, authorizationHeader: authorizationHeader, authenticateHeader: authenticateHeader, authenticateHeaderValue: authenticateHeaderValue, credentials: make(map[string]*time.Time), } if opts.GC.Every > 0 { go b.runGC(opts.GC.Context, opts.GC.Every) } return b.serveHTTP } // Default returns a new basic authentication middleware // based on pre-defined user list. // A user can hold any custom fields but the username and password // are required as they are compared against the user input // when access to protected resource is requested. // A user list can defined with one of the following values: // // map[string]string form of: {username:password, ...} // map[string]any form of: {"username": {"password": "...", "other_field": ...}, ...} // []T which T completes the User interface, where T is a struct value // []T which T contains at least Username and Password fields. // // Usage: // // auth := Default(map[string]string{ // "admin": "admin", // "john": "p@ss", // }) func Default(users any, userOpts ...UserAuthOption) context.Handler { opts := Options{ Realm: DefaultRealm, Allow: AllowUsers(users, userOpts...), } return New(opts) } // Load same as Default but instead of a hard-coded user list it accepts // a filename to load the users from. // // Usage: // // auth := Load("users.yml") func Load(jsonOrYamlFilename string, userOpts ...UserAuthOption) context.Handler { opts := Options{ Realm: DefaultRealm, Allow: AllowUsersFile(jsonOrYamlFilename, userOpts...), } return New(opts) } func (b *BasicAuth) getCurrentTries(ctx *context.Context) (tries int) { if key := b.opts.MaxTriesSession; key != "" { if sess := sessions.Get(ctx); sess != nil { tries = sess.GetIntDefault(key, 0) } else { ctx.Application().Logger().Error("basicauth: getCurrentTries: session key: %s but no session manager is registered", key) return } } else { cookie := ctx.GetCookie(b.opts.MaxTriesCookie) if cookie != "" { tries, _ = strconv.Atoi(cookie) } } return } func (b *BasicAuth) setCurrentTries(ctx *context.Context, tries int) { if key := b.opts.MaxTriesSession; key != "" { if sess := sessions.Get(ctx); sess != nil { sess.Set(key, tries) } else { ctx.Application().Logger().Error("basicauth: setCurrentTries: session key: %s but no session manager is registered", key) return } } else { maxAge := b.opts.MaxAge if maxAge == 0 { maxAge = DefaultCookieMaxAge // 1 hour. } c := &http.Cookie{ Name: b.opts.MaxTriesCookie, Path: "/", Value: url.QueryEscape(strconv.Itoa(tries)), HttpOnly: true, Expires: time.Now().Add(maxAge), MaxAge: int(maxAge.Seconds()), } ctx.SetCookie(c) } } func (b *BasicAuth) resetCurrentTries(ctx *context.Context) { if key := b.opts.MaxTriesSession; key != "" { if sess := sessions.Get(ctx); sess != nil { sess.Delete(key) } else { ctx.Application().Logger().Error("basicauth: resetCurrentTries: session key: %s but no session manager is registered", key) return } } else { ctx.RemoveCookie(b.opts.MaxTriesCookie) } } func isHTTPS(r *http.Request) bool { return (strings.EqualFold(r.URL.Scheme, "https") || r.TLS != nil) && r.ProtoMajor == 2 } func (b *BasicAuth) handleError(ctx *context.Context, err error) { ctx.Application().Logger().Debug(err) // should not be nil as it's defaulted on New. b.opts.ErrorHandler(ctx, err) } // serveHTTP is the main method of this middleware, // checks and verifies the auhorization header for basic authentication, // next handlers will only be executed when the client is allowed to continue. func (b *BasicAuth) serveHTTP(ctx *context.Context) { if b.opts.HTTPSOnly && !isHTTPS(ctx.Request()) { b.handleError(ctx, ErrHTTPVersion{}) return } header := ctx.GetHeader(b.authorizationHeader) fullUser, username, password, ok := decodeHeader(header) if !ok { // Header is malformed or missing (e.g. browser cancel button on user prompt). b.handleError(ctx, ErrCredentialsMissing{ Header: header, AuthenticateHeader: b.authenticateHeader, AuthenticateHeaderValue: b.authenticateHeaderValue, Code: b.askCode, }) return } var ( maxTries = b.opts.MaxTries tries int ) if maxTries > 0 { tries = b.getCurrentTries(ctx) } user, ok := b.opts.Allow(ctx, username, password) if !ok { // This username:password combination was not allowed. if maxTries > 0 { tries++ b.setCurrentTries(ctx, tries) if tries >= maxTries { // e.g. if MaxTries == 1 then it should be allowed only once, so we must send forbidden now. b.handleError(ctx, ErrCredentialsForbidden{ Username: username, Password: password, Tries: tries, Age: b.opts.MaxAge, }) return } } b.handleError(ctx, ErrCredentialsInvalid{ Username: username, Password: password, CurrentTries: tries, AuthenticateHeader: b.authenticateHeader, AuthenticateHeaderValue: b.authenticateHeaderValue, Code: b.askCode, }) return } if tries > 0 { // had failures but it's ok, reset the tries on success. b.resetCurrentTries(ctx) } b.mu.RLock() expiresAt, ok := b.credentials[fullUser] b.mu.RUnlock() var authorizedAt time.Time if ok { if expiresAt != nil { // Has expiration. if expiresAt.Before(time.Now()) { // Has been expired. b.mu.Lock() // Delete the entry. delete(b.credentials, fullUser) b.mu.Unlock() // Re-ask for new credentials. b.handleError(ctx, ErrCredentialsExpired{ Username: username, Password: password, AuthenticateHeader: b.authenticateHeader, AuthenticateHeaderValue: b.authenticateHeaderValue, Code: b.askCode, }) return } // It's ok, find the time authorized to fill the user below, if necessary. authorizedAt = expiresAt.Add(-b.opts.MaxAge) } } else { // Saved credential not found, first login. if b.opts.MaxAge > 0 { // Expiration is enabled, set the value. authorizedAt = time.Now() t := authorizedAt.Add(b.opts.MaxAge) expiresAt = &t } b.mu.Lock() b.credentials[fullUser] = expiresAt b.mu.Unlock() } if user == nil { // No custom uset was set by the auth func, // it is passed though, set a simple user here: user = &context.SimpleUser{ Authorization: authorizationType, AuthorizedAt: authorizedAt, ID: username, Username: username, Password: password, } } // Store user instance and logout function. // Note that the end-developer has always have access // to the Request.BasicAuth, however, we support any user struct, // so we must store it on this request instance so it can be retrieved later on. ctx.SetUser(user) ctx.SetLogoutFunc(b.logout) ctx.Next() } // logout clears the current user's credentials. func (b *BasicAuth) logout(ctx *context.Context) { var ( fullUser, username, password string ok bool ) if u := ctx.User(); u != nil { // Get the saved ones, if any. username, _ = u.GetUsername() password, _ = u.GetPassword() fullUser = username + colonLiteral + password ok = username != "" && password != "" } if !ok { // If the custom user does // not implement the User interface, then extract from the request header (most common scenario): header := ctx.GetHeader(b.authorizationHeader) fullUser, _, _, ok = decodeHeader(header) } if ok { // If it's authorized then try to lock and delete. ctx.SetUser(nil) ctx.SetLogoutFunc(nil) if b.opts.Proxy { ctx.Request().Header.Del(proxyAuthorizationHeaderKey) } // delete the request header so future Request().BasicAuth are empty. ctx.Request().Header.Del(authorizationHeaderKey) b.mu.Lock() delete(b.credentials, fullUser) b.mu.Unlock() if b.opts.MaxTries > 0 { b.setCurrentTries(ctx, 0) } ctx.StatusCode(http.StatusUnauthorized) } } // runGC runs a function in a separate go routine // every x duration to clear in-memory expired credential entries. func (b *BasicAuth) runGC(ctx stdContext.Context, every time.Duration) { if ctx == nil { ctx = stdContext.Background() } t := time.NewTicker(every) defer t.Stop() for { select { case <-ctx.Done(): return case <-t.C: b.gc() } } } // gc removes all entries expired based on the max age or all entries (if max age is missing), // note that this does not mean that the server will send 401/407 to the next request, // when the request header credentials are still valid (Allow passed). func (b *BasicAuth) gc() int { now := time.Now() var markedForDeletion []string b.mu.RLock() for fullUser, expiresAt := range b.credentials { if expiresAt == nil || expiresAt.Before(now) { markedForDeletion = append(markedForDeletion, fullUser) } } b.mu.RUnlock() n := len(markedForDeletion) if n > 0 { for _, fullUser := range markedForDeletion { b.mu.Lock() delete(b.credentials, fullUser) b.mu.Unlock() } } return n } ================================================ FILE: middleware/basicauth/basicauth_test.go ================================================ // Package basicauth_tests performs black-box testing of the basicauth middleware. // Note that, a secondary test is also available at: _examples/auth/basicauth/main_test.go package basicauth_test import ( "fmt" "testing" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/httptest" "github.com/kataras/iris/v12/middleware/basicauth" ) func TestBasicAuthUseRouter(t *testing.T) { app := iris.New() users := map[string]string{ "usr": "pss", "admin": "admin", } auth := basicauth.New(basicauth.Options{ Allow: basicauth.AllowUsers(users), Realm: basicauth.DefaultRealm, MaxTries: 1, }) app.UseRouter(auth) app.Get("/user_json", func(ctx iris.Context) { ctx.JSON(ctx.User()) }) app.Get("/user_string", func(ctx iris.Context) { user := ctx.User() authorization, _ := user.GetAuthorization() username, _ := user.GetUsername() password, _ := user.GetPassword() ctx.Writef("%s\n%s\n%s", authorization, username, password) }) app.Get("/", func(ctx iris.Context) { username, _, _ := ctx.Request().BasicAuth() ctx.Writef("Hello, %s!", username) }) app.Subdomain("static").Get("/", func(ctx iris.Context) { username, _, _ := ctx.Request().BasicAuth() ctx.Writef("Static, %s", username) }) resetWithUseRouter := app.Subdomain("reset_with_use_router").ResetRouterFilters() resetWithUseRouter.UseRouter(func(ctx iris.Context) { ctx.Record() ctx.Writef("with use router\n") ctx.Next() }) resetWithUseRouter.Get("/", func(ctx iris.Context) { username, _, _ := ctx.Request().BasicAuth() ctx.Writef("%s", username) // username should be empty. }) // ^ order of these should not matter. app.Subdomain("reset").ResetRouterFilters().Get("/", func(ctx iris.Context) { username, _, _ := ctx.Request().BasicAuth() ctx.Writef("%s", username) // username should be empty. }) e := httptest.New(t, app.Configure( iris.WithFireMethodNotAllowed, iris.WithResetOnFireErrorCode, )) for username, password := range users { // Test pass authentication and route found. e.GET("/").WithBasicAuth(username, password).Expect(). Status(httptest.StatusOK).Body().IsEqual(fmt.Sprintf("Hello, %s!", username)) e.GET("/user_json").WithBasicAuth(username, password).Expect(). Status(httptest.StatusOK).JSON().Object().ContainsMap(iris.Map{ "username": username, }) e.GET("/user_string").WithBasicAuth(username, password).Expect(). Status(httptest.StatusOK).Body(). Equal(fmt.Sprintf("%s\n%s\n%s", "Basic Authentication", username, password)) // Test empty auth. e.GET("/").Expect().Status(httptest.StatusUnauthorized).Body().IsEqual("Unauthorized") // Test invalid auth. e.GET("/").WithBasicAuth(username, "invalid_password").Expect(). Status(httptest.StatusForbidden) e.GET("/").WithBasicAuth("invaid_username", password).Expect(). Status(httptest.StatusForbidden) // Test different method, it should pass the authentication (no stop on 401) // but it doesn't fire the GET route, instead it gives 405. e.POST("/").WithBasicAuth(username, password).Expect(). Status(httptest.StatusMethodNotAllowed).Body().IsEqual("Method Not Allowed") // Test pass the authentication but route not found. e.GET("/notfound").WithBasicAuth(username, password).Expect(). Status(httptest.StatusNotFound).Body().IsEqual("Not Found") // Test empty auth. e.GET("/notfound").Expect().Status(httptest.StatusUnauthorized).Body().IsEqual("Unauthorized") // Test invalid auth. e.GET("/notfound").WithBasicAuth(username, "invalid_password").Expect(). Status(httptest.StatusForbidden) e.GET("/notfound").WithBasicAuth("invaid_username", password).Expect(). Status(httptest.StatusForbidden) // Test subdomain inherited. sub := e.Builder(func(req *httptest.Request) { req.WithURL("http://static.mydomain.com") }) // Test pass and route found. sub.GET("/").WithBasicAuth(username, password).Expect(). Status(httptest.StatusOK).Body().IsEqual(fmt.Sprintf("Static, %s", username)) // Test empty auth. sub.GET("/").Expect().Status(httptest.StatusUnauthorized) // Test invalid auth. sub.GET("/").WithBasicAuth(username, "invalid_password").Expect(). Status(httptest.StatusForbidden) sub.GET("/").WithBasicAuth("invaid_username", password).Expect(). Status(httptest.StatusForbidden) // Test pass the authentication but route not found. sub.GET("/notfound").WithBasicAuth(username, password).Expect(). Status(httptest.StatusNotFound).Body().IsEqual("Not Found") // Test empty auth. sub.GET("/notfound").Expect().Status(httptest.StatusUnauthorized).Body().IsEqual("Unauthorized") // Test invalid auth. sub.GET("/notfound").WithBasicAuth(username, "invalid_password").Expect(). Status(httptest.StatusForbidden) sub.GET("/notfound").WithBasicAuth("invaid_username", password).Expect(). Status(httptest.StatusForbidden) // Test a reset-ed Party with a single one UseRouter // which writes on matched routes and reset and send the error on errors. // (all should pass without auth). sub = e.Builder(func(req *httptest.Request) { req.WithURL("http://reset_with_use_router.mydomain.com") }) sub.GET("/").Expect().Status(httptest.StatusOK).Body().IsEqual("with use router\n") sub.POST("/").Expect().Status(httptest.StatusMethodNotAllowed).Body().IsEqual("Method Not Allowed") sub.GET("/notfound").Expect().Status(httptest.StatusNotFound).Body().IsEqual("Not Found") // Test a reset-ed Party (all should pass without auth). sub = e.Builder(func(req *httptest.Request) { req.WithURL("http://reset.mydomain.com") }) sub.GET("/").Expect().Status(httptest.StatusOK).Body().IsEmpty() sub.POST("/").Expect().Status(httptest.StatusMethodNotAllowed).Body().IsEqual("Method Not Allowed") sub.GET("/notfound").Expect().Status(httptest.StatusNotFound).Body().IsEqual("Not Found") } } ================================================ FILE: middleware/basicauth/error.go ================================================ package basicauth import ( "fmt" "net/http" "time" "github.com/kataras/iris/v12/context" ) type ( // ErrHTTPVersion is fired when Options.HTTPSOnly was enabled // and the current request is a plain http one. ErrHTTPVersion struct{} // ErrCredentialsForbidden is fired when Options.MaxTries have been consumed // by the user and the client is forbidden to retry at least for "Age" time. ErrCredentialsForbidden struct { Username string Password string Tries int Age time.Duration } // ErrCredentialsMissing is fired when the authorization header is empty or malformed. ErrCredentialsMissing struct { Header string AuthenticateHeader string AuthenticateHeaderValue string Code int } // ErrCredentialsInvalid is fired when the user input does not match with an existing user. ErrCredentialsInvalid struct { Username string Password string CurrentTries int AuthenticateHeader string AuthenticateHeaderValue string Code int } // ErrCredentialsExpired is fired when the username:password combination is valid // but the memory stored user has been expired. ErrCredentialsExpired struct { Username string Password string AuthenticateHeader string AuthenticateHeaderValue string Code int } ) func (e ErrHTTPVersion) Error() string { return "http version not supported" } func (e ErrCredentialsForbidden) Error() string { return fmt.Sprintf("credentials: forbidden <%s:%s> for <%s> after <%d> attempts", e.Username, e.Password, e.Age, e.Tries) } func (e ErrCredentialsMissing) Error() string { if e.Header != "" { return fmt.Sprintf("credentials: malformed <%s>", e.Header) } return "empty credentials" } func (e ErrCredentialsInvalid) Error() string { return fmt.Sprintf("credentials: invalid <%s:%s> current tries <%d>", e.Username, e.Password, e.CurrentTries) } func (e ErrCredentialsExpired) Error() string { return fmt.Sprintf("credentials: expired <%s:%s>", e.Username, e.Password) } // DefaultErrorHandler is the default error handler for the Options.ErrorHandler field. func DefaultErrorHandler(ctx *context.Context, err error) { switch e := err.(type) { case ErrHTTPVersion: ctx.StopWithStatus(http.StatusHTTPVersionNotSupported) case ErrCredentialsForbidden: // If a (proxy) server receives valid credentials that are inadequate to access a given resource, // the server should respond with the 403 Forbidden status code. // Unlike 401 Unauthorized or 407 Proxy Authentication Required, authentication is impossible for this user. ctx.StopWithStatus(http.StatusForbidden) case ErrCredentialsMissing: unauthorize(ctx, e.AuthenticateHeader, e.AuthenticateHeaderValue, e.Code) case ErrCredentialsInvalid: unauthorize(ctx, e.AuthenticateHeader, e.AuthenticateHeaderValue, e.Code) case ErrCredentialsExpired: unauthorize(ctx, e.AuthenticateHeader, e.AuthenticateHeaderValue, e.Code) default: // This will never happen. ctx.StopWithText(http.StatusInternalServerError, fmt.Sprintf("unknown error: %v", err)) } } // unauthorize sends a 401 status code (or 407 if Proxy was set to true) // which client should catch and prompt for username:password credentials. func unauthorize(ctx *context.Context, authHeader, authHeaderValue string, code int) { ctx.Header(authHeader, authHeaderValue) ctx.StopWithStatus(code) } ================================================ FILE: middleware/basicauth/header.go ================================================ package basicauth import ( "encoding/base64" "strings" ) const ( spaceChar = ' ' colonChar = ':' colonLiteral = string(colonChar) basicLiteral = "Basic" basicSpaceLiteral = "Basic " basicSpaceLiteralLen = len(basicSpaceLiteral) ) // The username and password are combined with a single colon (:). // This means that the username itself cannot contain a colon. // URL encoding (e.g. https://Aladdin:OpenSesame@www.example.com/index.html) // has been deprecated by rfc3986. func encodeHeader(username, password string) (string, bool) { if strings.Contains(username, colonLiteral) || strings.Contains(password, colonLiteral) { return "", false } fullUser := []byte(username + colonLiteral + password) header := basicSpaceLiteral + base64.StdEncoding.EncodeToString(fullUser) return header, true } // Like net/http.parseBasicAuth func decodeHeader(header string) (fullUser, username, password string, ok bool) { if len(header) < basicSpaceLiteralLen || !strings.EqualFold(header[:basicSpaceLiteralLen], basicSpaceLiteral) { return } c, err := base64.StdEncoding.DecodeString(header[basicSpaceLiteralLen:]) if err != nil { return } cs := string(c) s := strings.IndexByte(cs, colonChar) if s < 0 { return } return cs, cs[:s], cs[s+1:], true /* for i := 0; i < n; i++ { if header[i] == spaceChar { prefix := header[:i] if prefix != basicLiteral { return } if n <= i+1 { return } decodedFullUser, err := base64.RawStdEncoding.DecodeString(header[i+1:]) if err != nil { return } fullUser = string(decodedFullUser) break } } n = len(fullUser) for i := n - 1; i > -1; i-- { if fullUser[i] == colonChar { username = fullUser[:i] password = fullUser[i+1:] if strings.TrimSpace(username) == "" || strings.TrimSpace(password) == "" { ok = false } else { ok = true } return } } return*/ } ================================================ FILE: middleware/basicauth/header_test.go ================================================ package basicauth import "testing" func TestHeaderEncode(t *testing.T) { var tests = []struct { username string password string header string ok bool }{ { username: "user", password: "pass", header: "Basic dXNlcjpwYXNz", ok: true, }, { username: "user", password: "p:(notallowed)ass", header: "", ok: false, }, { username: "123u%ser", password: "pass132$", header: "Basic MTIzdSVzZXI6cGFzczEzMiQ=", ok: true, }, } for i, tt := range tests { got, ok := encodeHeader(tt.username, tt.password) if tt.ok != ok { t.Fatalf("[%d] expected: %v but got: %v (username=%s,password=%s)", i, tt.ok, ok, tt.username, tt.password) } if tt.header != got { t.Fatalf("[%d] expected result header: %q but got: %q", i, tt.header, got) } } } func TestHeaderDecode(t *testing.T) { var tests = []struct { header string ok bool username string password string }{ { header: "Basic dXNlcjpwYXNz", ok: true, username: "user", password: "pass", }, { header: "dXNlcjpwYXNz", ok: false, }, { header: "Basic ", ok: false, }, { header: "Basic dXNlcjp", ok: false, }, { header: "dXNlcjpwYXNz Basic", ok: false, }, { header: "dXNlcjpwYXNzBasic", ok: false, }, } for i, tt := range tests { fullUser, username, password, ok := decodeHeader(tt.header) if expected, got := tt.ok, ok; expected != got { t.Fatalf("[%d] expected: %v but got: %v (header=%s)", i, expected, got, tt.header) } if expected, got := tt.username, username; expected != got { t.Fatalf("[%d] expected username: %q but got: %q", i, expected, got) } if expected, got := tt.password, password; expected != got { t.Fatalf("[%d] expected password: %q but got: %q", i, expected, got) } if tt.username != "" || tt.password != "" { if expected, got := tt.username+colonLiteral+tt.password, fullUser; expected != got { t.Fatalf("[%d] expected username:password to be: %q but got: %q", i, expected, got) } } else { if fullUser != "" { t.Fatalf("[%d] expected username:password to be empty but got: %q", i, fullUser) } } } } ================================================ FILE: middleware/basicauth/user.go ================================================ package basicauth import ( "encoding/json" "fmt" "os" "reflect" "strings" "github.com/kataras/iris/v12/context" "golang.org/x/crypto/bcrypt" "gopkg.in/yaml.v3" ) // ReadFile can be used to customize the way the // AllowUsersFile function is loading the filename from. // Example of usage: embedded users.yml file. // Defaults to the `os.ReadFile` which reads the file from the physical disk. var ReadFile = os.ReadFile // User is a partial part of the iris.User interface. // It's used to declare a static slice of registered User for authentication. type User interface { context.UserGetUsername context.UserGetPassword } // UserAuthOptions holds optional user authentication options // that can be given to the builtin Default and Load (and AllowUsers, AllowUsersFile) functions. type UserAuthOptions struct { // Defaults to plain check, can be modified for encrypted passwords, // see the BCRYPT optional function. ComparePassword func(stored, userPassword string) bool } // UserAuthOption is the option function type // for the Default and Load (and AllowUsers, AllowUsersFile) functions. // // See BCRYPT for an implementation. type UserAuthOption func(*UserAuthOptions) // BCRYPT it is a UserAuthOption, it compares a bcrypt hashed password with its user input. // Reports true on success and false on failure. // // Useful when the users passwords are encrypted // using the Provos and Mazières's bcrypt adaptive hashing algorithm. // See https://www.usenix.org/legacy/event/usenix99/provos/provos.pdf. // // Usage: // // Default(..., BCRYPT) OR // Load(..., BCRYPT) OR // Options.Allow = AllowUsers(..., BCRYPT) OR // OPtions.Allow = AllowUsersFile(..., BCRYPT) func BCRYPT(opts *UserAuthOptions) { opts.ComparePassword = func(stored, userPassword string) bool { err := bcrypt.CompareHashAndPassword([]byte(stored), []byte(userPassword)) return err == nil } } func toUserAuthOptions(opts []UserAuthOption) (options UserAuthOptions) { for _, opt := range opts { opt(&options) } if options.ComparePassword == nil { options.ComparePassword = func(stored, userPassword string) bool { return stored == userPassword } } return options } // AllowUsers is an AuthFunc which authenticates user input based on a (static) user list. // The "users" input parameter can be one of the following forms: // // map[string]string e.g. {username: password, username: password...}. // []map[string]any e.g. []{"username": "...", "password": "...", "other_field": ...}, ...}. // []T which T completes the User interface. // []T which T contains at least Username and Password fields. // // Usage: // New(Options{Allow: AllowUsers(..., [BCRYPT])}) func AllowUsers(users any, opts ...UserAuthOption) AuthFunc { // create a local user structure to be used in the map copy, // takes longer to initialize but faster to serve. type user struct { password string ref any } cp := make(map[string]*user) v := reflect.Indirect(reflect.ValueOf(users)) switch v.Kind() { case reflect.Slice: for i := 0; i < v.Len(); i++ { elem := v.Index(i).Interface() // MUST contain a username and password. username, password, ok := extractUsernameAndPassword(elem) if !ok { continue } cp[username] = &user{ password: password, ref: elem, } } case reflect.Map: elem := v.Interface() switch m := elem.(type) { case map[string]string: return userMap(m, opts...) case map[string]any: username, password, ok := mapUsernameAndPassword(m) if !ok { break } cp[username] = &user{ password: password, ref: m, } default: panic(fmt.Sprintf("unsupported type of map: %T", users)) } default: panic(fmt.Sprintf("unsupported type: %T", users)) } options := toUserAuthOptions(opts) return func(_ *context.Context, username, password string) (any, bool) { if u, ok := cp[username]; ok { // fast map access, if options.ComparePassword(u.password, password) { return u.ref, true } } return nil, false } } func userMap(usernamePassword map[string]string, opts ...UserAuthOption) AuthFunc { options := toUserAuthOptions(opts) return func(_ *context.Context, username, password string) (any, bool) { pass, ok := usernamePassword[username] return nil, ok && options.ComparePassword(pass, password) } } // AllowUsersFile is an AuthFunc which authenticates user input based on a (static) user list // loaded from a file on initialization. // // Example Code: // // New(Options{Allow: AllowUsersFile("users.yml", BCRYPT)}) // // The users.yml file looks like the following: // - username: kataras // password: kataras_pass // age: 27 // role: admin // - username: makis // password: makis_password // ... func AllowUsersFile(jsonOrYamlFilename string, opts ...UserAuthOption) AuthFunc { var ( usernamePassword map[string]string // no need to support too much forms, this would be for: // "$username": { "password": "$pass", "other_field": ...} userList []map[string]any ) if err := decodeFile(jsonOrYamlFilename, &usernamePassword, &userList); err != nil { panic(err) } if len(usernamePassword) > 0 { // JSON Form: { "$username":"$pass", "$username": "$pass" } // YAML Form: $username: $pass // $username: $pass return userMap(usernamePassword, opts...) } if len(userList) > 0 { // JSON Form: [{"username": "$username", "password": "$pass", "other_field": ...}, {"username": ...}, ... ] // YAML Form: // - username: $username // password: $password // other_field: ... return AllowUsers(userList, opts...) } panic("malformed document file: " + jsonOrYamlFilename) } func decodeFile(src string, dest ...any) error { data, err := ReadFile(src) if err != nil { return err } // We use unmarshal instead of file decoder // as we may need to read it more than once (dests, see below). var ( unmarshal func(data []byte, v any) error ext string ) if idx := strings.LastIndexByte(src, '.'); idx > 0 { ext = src[idx:] } switch ext { case "", ".json": unmarshal = json.Unmarshal case ".yml", ".yaml": unmarshal = yaml.Unmarshal default: return fmt.Errorf("unexpected file extension: %s", ext) } var ( ok bool lastErr error ) for _, d := range dest { if err = unmarshal(data, d); err == nil { ok = true } else { lastErr = err } } if !ok { return lastErr } return nil // if at least one is succeed we are ok. } func extractUsernameAndPassword(s any) (username, password string, ok bool) { if s == nil { return } switch u := s.(type) { case User: username = u.GetUsername() password = u.GetPassword() ok = username != "" && password != "" return case map[string]any: return mapUsernameAndPassword(u) default: b, err := json.Marshal(u) if err != nil { return } var m map[string]any if err = json.Unmarshal(b, &m); err != nil { return } return mapUsernameAndPassword(m) } } func mapUsernameAndPassword(m map[string]any) (username, password string, ok bool) { // type of username: password. if len(m) == 1 { for username, v := range m { if password, ok := v.(string); ok { ok := username != "" && password != "" return username, password, ok } } } var usernameFound, passwordFound bool for k, v := range m { switch k { case "username", "Username": username, usernameFound = v.(string) case "password", "Password": password, passwordFound = v.(string) } if usernameFound && passwordFound { ok = true break } } return } ================================================ FILE: middleware/basicauth/user_test.go ================================================ package basicauth import ( "errors" "os" "reflect" "testing" "github.com/kataras/iris/v12/context" "golang.org/x/crypto/bcrypt" "gopkg.in/yaml.v3" ) type IUserRepository interface { GetByUsernameAndPassword(dest any, username, password string) error } // Test a custom implementation of AuthFunc with a user repository. // This is a usage example of custom AuthFunc implementation. func UserRepository(repo IUserRepository, newUserPtr func() any) AuthFunc { return func(_ *context.Context, username, password string) (any, bool) { dest := newUserPtr() err := repo.GetByUsernameAndPassword(dest, username, password) if err == nil { return dest, true } return nil, false } } type testUser struct { username string password string email string // custom field. } // GetUsername & Getpassword complete the User interface. func (u *testUser) GetUsername() string { return u.username } func (u *testUser) GetPassword() string { return u.password } type testRepo struct { entries []testUser } // Implements IUserRepository interface. func (r *testRepo) GetByUsernameAndPassword(dest any, username, password string) error { for _, e := range r.entries { if e.username == username && e.password == password { *dest.(*testUser) = e return nil } } return errors.New("invalid credentials") } func TestAllowUserRepository(t *testing.T) { repo := &testRepo{ entries: []testUser{ {username: "kataras", password: "kataras_pass", email: "kataras2006@hotmail.com"}, }, } allow := UserRepository(repo, func() any { return new(testUser) }) var tests = []struct { username string password string ok bool user *testUser }{ { username: "kataras", password: "kataras_pass", ok: true, user: &testUser{username: "kataras", password: "kataras_pass", email: "kataras2006@hotmail.com"}, }, { username: "makis", password: "makis_password", ok: false, }, } for i, tt := range tests { v, ok := allow(nil, tt.username, tt.password) if tt.ok != ok { t.Fatalf("[%d] expected: %v but got: %v (username=%s,password=%s)", i, tt.ok, ok, tt.username, tt.password) } if !ok { continue } u, ok := v.(*testUser) if !ok { t.Fatalf("[%d] a user should be type of *testUser but got: %#+v (%T)", i, v, v) } if !reflect.DeepEqual(tt.user, u) { t.Fatalf("[%d] expected user:\n%#+v\nbut got:\n%#+v", i, tt.user, u) } } } func TestAllowUsers(t *testing.T) { users := []User{ &testUser{username: "kataras", password: "kataras_pass", email: "kataras2006@hotmail.com"}, } allow := AllowUsers(users) var tests = []struct { username string password string ok bool user *testUser }{ { username: "kataras", password: "kataras_pass", ok: true, user: &testUser{username: "kataras", password: "kataras_pass", email: "kataras2006@hotmail.com"}, }, { username: "makis", password: "makis_password", ok: false, }, } for i, tt := range tests { v, ok := allow(nil, tt.username, tt.password) if tt.ok != ok { t.Fatalf("[%d] expected: %v but got: %v (username=%s,password=%s)", i, tt.ok, ok, tt.username, tt.password) } if !ok { continue } u, ok := v.(*testUser) if !ok { t.Fatalf("[%d] a user should be type of *testUser but got: %#+v (%T)", i, v, v) } if !reflect.DeepEqual(tt.user, u) { t.Fatalf("[%d] expected user:\n%#+v\nbut got:\n%#+v", i, tt.user, u) } } } // Test YAML user loading with b-encrypted passwords. func TestAllowUsersFile(t *testing.T) { f, err := os.CreateTemp("", "*users.yml") if err != nil { t.Fatal(err) } defer func() { f.Close() os.Remove(f.Name()) }() // f.WriteString(` // - username: kataras // password: kataras_pass // age: 27 // role: admin // - username: makis // password: makis_password // `) // This form is supported too, although its features are limited (no custom fields): // f.WriteString(` // kataras: kataras_pass // makis: makis_password // `) var tests = []struct { username string password string // hashed, auto-filled later on. inputPassword string ok bool user context.Map }{ { username: "kataras", inputPassword: "kataras_pass", ok: true, user: context.Map{"age": 27, "role": "admin"}, // username and password are auto-filled in our tests below. }, { username: "makis", inputPassword: "makis_password", ok: true, user: context.Map{}, }, { username: "invalid", password: "invalid_pass", ok: false, }, { username: "notvalid", password: "", ok: false, }, } // Write the tests to the users YAML file. var usersToWrite []context.Map for _, tt := range tests { if tt.ok { // store the hashed password. tt.password = mustGeneratePassword(t, tt.inputPassword) // store and write the username and hashed password. tt.user["username"] = tt.username tt.user["password"] = tt.password // cannot write it as a stream, write it as a slice. // enc.Encode(tt.user) usersToWrite = append(usersToWrite, tt.user) } // bcrypt.GenerateFromPassword([]byte("kataras_pass"), bcrypt.DefaultCost) } fileContents, err := yaml.Marshal(usersToWrite) if err != nil { t.Fatal(err) } f.Write(fileContents) // Build the authentication func. allow := AllowUsersFile(f.Name(), BCRYPT) for i, tt := range tests { v, ok := allow(nil, tt.username, tt.inputPassword) if tt.ok != ok { t.Fatalf("[%d] expected: %v but got: %v (username=%s,password=%s,user=%#+v)", i, tt.ok, ok, tt.username, tt.inputPassword, v) } if !ok { continue } if len(tt.user) == 0 { // when username: password form. continue } u, ok := v.(context.Map) if !ok { t.Fatalf("[%d] a user loaded from external source or file should be alway type of map[string]any but got: %#+v (%T)", i, v, v) } if expected, got := len(tt.user), len(u); expected != got { t.Fatalf("[%d] expected user map length to be equal, expected: %d but got: %d\n%#+v\n%#+v", i, expected, got, tt.user, u) } for k, v := range tt.user { if u[k] != v { t.Fatalf("[%d] expected user map %q to be %q but got: %q", i, k, v, u[k]) } } } } func mustGeneratePassword(t *testing.T, userPassword string) string { t.Helper() hashed, err := bcrypt.GenerateFromPassword([]byte(userPassword), bcrypt.DefaultCost) if err != nil { t.Fatal(err) } return string(hashed) } ================================================ FILE: middleware/cors/cors.go ================================================ package cors import ( "errors" "net/http" "regexp" "strconv" "strings" "time" "github.com/kataras/iris/v12/context" ) func init() { context.SetHandlerName("iris/middleware/cors.*", "iris.cors") } var ( // ErrOriginNotAllowed is given to the error handler // when the error is caused because an origin was not allowed to pass through. ErrOriginNotAllowed = errors.New("origin not allowed") // AllowAnyOrigin allows all origins to pass. AllowAnyOrigin = func(_ *context.Context, _ string) bool { return true } // DefaultErrorHandler is the default error handler which // fires forbidden status (403) on disallowed origins. DefaultErrorHandler = func(ctx *context.Context, _ error) { ctx.StopWithStatus(http.StatusForbidden) } // DefaultOriginExtractor is the default method which // an origin is extracted. It returns the value of the request's "Origin" header // and always true, means that it allows empty origin headers as well. DefaultOriginExtractor = func(ctx *context.Context) (string, bool) { header := ctx.GetHeader(originRequestHeader) return header, true } // StrictOriginExtractor is an ExtractOriginFunc type // which is a bit more strictly than the DefaultOriginExtractor. // It allows only non-empty "Origin" header values to be passed. // If the header is missing, the middleware will not allow the execution // of the next handler(s). StrictOriginExtractor = func(ctx *context.Context) (string, bool) { header := ctx.GetHeader(originRequestHeader) return header, header != "" } ) type ( // ExtractOriginFunc describes the function which should return the request's origin or false. ExtractOriginFunc = func(ctx *context.Context) (string, bool) // AllowOriginFunc describes the function which is called when the // middleware decides if the request's origin should be allowed or not. AllowOriginFunc = func(ctx *context.Context, origin string) bool // HandleErrorFunc describes the function which is fired // when a request by a specific (or empty) origin was not allowed to pass through. HandleErrorFunc = func(ctx *context.Context, err error) // CORS holds the customizations developers can // do on the cors middleware. // // Read more at: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS. CORS struct { extractOriginFunc ExtractOriginFunc allowOriginFunc AllowOriginFunc errorHandler HandleErrorFunc allowCredentialsValue string exposeHeadersValue string allowHeadersValue string allowMethodsValue string maxAgeSecondsValue string referrerPolicyValue string } ) // New returns the default CORS middleware. // For a more advanced type of protection middleware with more options // please refer to: https://github.com/iris-contrib/middleware repository instead. // // Example Code: // // import "github.com/kataras/iris/v12/middleware/cors" // import "github.com/kataras/iris/v12/x/errors" // // app.UseRouter(cors.New(). // HandleErrorFunc(func(ctx iris.Context, err error) { // errors.FailedPrecondition.Err(ctx, err) // }). // ExtractOriginFunc(cors.StrictOriginExtractor). // ReferrerPolicy(cors.NoReferrerWhenDowngrade). // AllowOrigin("domain1.com,domain2.com,domain3.com"). // Handler()) func New() *CORS { return &CORS{ extractOriginFunc: DefaultOriginExtractor, allowOriginFunc: AllowAnyOrigin, errorHandler: DefaultErrorHandler, allowCredentialsValue: "true", exposeHeadersValue: "*, Authorization, X-Authorization", allowHeadersValue: "*", // This field cannot be modified by the end-developer, // as we have another type of controlling the HTTP verbs per handler. allowMethodsValue: "*", maxAgeSecondsValue: "86400", referrerPolicyValue: NoReferrerWhenDowngrade.String(), } } // ExtractOriginFunc sets the function which should return the request's origin. func (c *CORS) ExtractOriginFunc(fn ExtractOriginFunc) *CORS { c.extractOriginFunc = fn return c } // AllowOriginFunc sets the function which decides if an origin(domain) is allowed // to continue or not. // // Read more at: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#access-control-allow-origin. func (c *CORS) AllowOriginFunc(fn AllowOriginFunc) *CORS { c.allowOriginFunc = fn return c } // AllowOrigin calls the "AllowOriginFunc" method // and registers a function which accepts any incoming // request with origin of the given "originLine". // The originLine can contain one or more domains separated by comma. // See "AllowOrigins" to set a list of strings instead. func (c *CORS) AllowOrigin(originLine string) *CORS { return c.AllowOrigins(strings.Split(originLine, ",")...) } // AllowOriginMatcherFunc sets the allow origin func without iris.Context // as its first parameter, i.e. a regular expression. func (c *CORS) AllowOriginMatcherFunc(fn func(origin string) bool) *CORS { return c.AllowOriginFunc(func(ctx *context.Context, origin string) bool { return fn(origin) }) } // AllowOriginRegex calls the "AllowOriginFunc" method // and registers a function which accepts any incoming // request with origin that matches at least one of the given "regexpLines". func (c *CORS) AllowOriginRegex(regexpLines ...string) *CORS { matchers := make([]func(string) bool, 0, len(regexpLines)) for _, line := range regexpLines { matcher := regexp.MustCompile(line).MatchString matchers = append(matchers, matcher) } return c.AllowOriginFunc(func(ctx *context.Context, origin string) bool { for _, m := range matchers { if m(origin) { return true } } return false }) } // AllowOrigins calls the "AllowOriginFunc" method // and registers a function which accepts any incoming // request with origin of one of the given "origins". func (c *CORS) AllowOrigins(origins ...string) *CORS { allowOrigins := make(map[string]struct{}, len(origins)) // read-only at serve time. for _, origin := range origins { if origin == "*" { // If AllowOrigins called with asterix, it is a missuse of this // middleware (set AllowAnyOrigin instead). allowOrigins = nil return c.AllowOriginFunc(AllowAnyOrigin) // panic("wildcard is not allowed, use AllowOriginFunc(AllowAnyOrigin) instead") // No ^ let's register a function which allows all and continue. } origin = strings.TrimSpace(origin) allowOrigins[origin] = struct{}{} } return c.AllowOriginFunc(func(ctx *context.Context, origin string) bool { _, allow := allowOrigins[origin] return allow }) } // HandleErrorFunc sets the function which is called // when an error of origin not allowed is fired. func (c *CORS) HandleErrorFunc(fn HandleErrorFunc) *CORS { c.errorHandler = fn return c } // DisallowCredentials sets the "Access-Control-Allow-Credentials" header to false. // // Read more at: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#access-control-allow-credentials. func (c *CORS) DisallowCredentials() *CORS { c.allowCredentialsValue = "false" return c } // ExposeHeaders sets the "Access-Control-Expose-Headers" header value. // // Read more at: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#access-control-expose-headers. func (c *CORS) ExposeHeaders(headers ...string) *CORS { c.exposeHeadersValue = strings.Join(headers, ", ") return c } // AllowHeaders sets the "Access-Control-Allow-Headers" header value. // // Read more at: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#access-control-allow-headers. func (c *CORS) AllowHeaders(headers ...string) *CORS { c.allowHeadersValue = strings.Join(headers, ", ") return c } // ReferrerPolicy type for referrer-policy header value. type ReferrerPolicy string // All available referrer policies. // Read more at: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy. const ( NoReferrer ReferrerPolicy = "no-referrer" NoReferrerWhenDowngrade ReferrerPolicy = "no-referrer-when-downgrade" Origin ReferrerPolicy = "origin" OriginWhenCrossOrigin ReferrerPolicy = "origin-when-cross-origin" SameOrigin ReferrerPolicy = "same-origin" StrictOrigin ReferrerPolicy = "strict-origin" StrictOriginWhenCrossOrigin ReferrerPolicy = "strict-origin-when-cross-origin" UnsafeURL ReferrerPolicy = "unsafe-url" ) // String returns the text representation of the "r" ReferrerPolicy. func (r ReferrerPolicy) String() string { return string(r) } // ReferrerPolicy sets the "Referrer-Policy" header value. // Defaults to "no-referrer-when-downgrade". // // Read more at: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy // and https://developer.mozilla.org/en-US/docs/Web/Security/Referer_header:_privacy_and_security_concerns. func (c *CORS) ReferrerPolicy(referrerPolicy ReferrerPolicy) *CORS { c.referrerPolicyValue = referrerPolicy.String() return c } // MaxAge sets the "Access-Control-Max-Age" header value. // // Read more at: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#access-control-max-age. func (c *CORS) MaxAge(d time.Duration) *CORS { c.maxAgeSecondsValue = strconv.FormatFloat(d.Seconds(), 'E', -1, 64) return c } const ( originRequestHeader = "Origin" allowOriginHeader = "Access-Control-Allow-Origin" allowCredentialsHeader = "Access-Control-Allow-Credentials" referrerPolicyHeader = "Referrer-Policy" exposeHeadersHeader = "Access-Control-Expose-Headers" requestMethodHeader = "Access-Control-Request-Method" requestHeadersHeader = "Access-Control-Request-Headers" allowMethodsHeader = "Access-Control-Allow-Methods" allowAllMethodsValue = "*" allowHeadersHeader = "Access-Control-Allow-Headers" maxAgeHeader = "Access-Control-Max-Age" varyHeader = "Vary" ) func (c *CORS) addVaryHeaders(ctx *context.Context) { ctx.Header(varyHeader, originRequestHeader) if ctx.Method() == http.MethodOptions { ctx.Header(varyHeader, requestMethodHeader) ctx.Header(varyHeader, requestHeadersHeader) } } // Handler method returns the Iris CORS Handler with basic features. // Note that the caller should NOT modify any of the CORS instance fields afterwards. func (c *CORS) Handler() context.Handler { return func(ctx *context.Context) { c.addVaryHeaders(ctx) // add vary headers at any case. origin, ok := c.extractOriginFunc(ctx) if !ok || !c.allowOriginFunc(ctx, origin) { c.errorHandler(ctx, ErrOriginNotAllowed) return } if origin == "" { // if we allow empty origins, set it to wildcard. origin = "*" } ctx.Header(allowOriginHeader, origin) ctx.Header(allowCredentialsHeader, c.allowCredentialsValue) // 08 July 2021 Mozzila updated the following document: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy ctx.Header(referrerPolicyHeader, c.referrerPolicyValue) ctx.Header(exposeHeadersHeader, c.exposeHeadersValue) if ctx.Method() == http.MethodOptions { ctx.Header(allowMethodsHeader, allowAllMethodsValue) ctx.Header(allowHeadersHeader, c.allowHeadersValue) ctx.Header(maxAgeHeader, c.maxAgeSecondsValue) ctx.StatusCode(http.StatusNoContent) return } ctx.Next() } } ================================================ FILE: middleware/grpc/grpc.go ================================================ package grpc import ( "net/http" "strings" "github.com/kataras/iris/v12/core/router" ) // New returns a new gRPC Iris router wrapper for a gRPC server. // useful when you want to share one port (such as 443 for https) between gRPC and Iris. // // The Iris server SHOULD run under HTTP/2 and clients too. // // Usage: // // import grpcWrapper "github.com/kataras/iris/v12/middleware/grpc" // [...] // app := iris.New() // grpcServer := grpc.NewServer() // app.WrapRouter(grpcWrapper.New(grpcServer)) func New(grpcServer http.Handler) router.WrapperFunc { return func(w http.ResponseWriter, r *http.Request, mux http.HandlerFunc) { if r.ProtoMajor == 2 && strings.HasPrefix(r.Header.Get("Content-Type"), "application/grpc") { grpcServer.ServeHTTP(w, r) return } mux.ServeHTTP(w, r) } } ================================================ FILE: middleware/hcaptcha/ARTICLE.md ================================================ # How to use hCAPTCHA with Iris In this article, we will learn how to use hCAPTCHA with Iris, a web framework for Go that provides fast and easy development of web applications. hCAPTCHA is a service that protects websites from bots and spam by presenting challenges to human visitors. By using hCAPTCHA, we can ensure that only real users can access our website and prevent automated attacks. ## What is hCAPTCHA? hCAPTCHA is a service that provides a widget that can be embedded in any web page. The widget displays a challenge that requires human intelligence to solve, such as identifying objects in images or typing words. The widget communicates with the hCAPTCHA server and verifies if the user has passed the challenge or not. If the user passes the challenge, the widget generates a token that can be used to validate the user's request on the server side. hCAPTCHA is similar to reCAPTCHA, another popular service that offers similar functionality. However, hCAPTCHA claims to have some advantages over reCAPTCHA, such as: - Better privacy: hCAPTCHA does not track users across websites or collect personal data. - Better performance: hCAPTCHA uses less resources and loads faster than reCAPTCHA. - Better rewards: hCAPTCHA pays website owners for using their service and supports various causes and charities. ## How to use hCAPTCHA with Iris? To use hCAPTCHA with Iris, we need to do two things: - Import the `github.com/kataras/iris/v12/middleware/hcaptcha` package, which provides an Iris middleware for hCAPTCHA. - Use the `hcaptcha.New` function to create an `iris.Handler` and register it in our Iris app. First, we need to install the Iris Web Framework, which contains the middleware too: ```sh $ go get github.com/kataras/iris/v12@latest ``` Then, we need to import it in our main.go file: ```go import ( "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/middleware/hcaptcha" ) ``` Next, we need to use the `hcaptcha.New` function to create an `iris.Handler`. The function takes two arguments: a secret key and, optionally, one or more functions to configure the hCAPTCHA Client instance. The secret key is a string that we can obtain from the hCAPTCHA website after creating an account and registering our website. We'll register an error handler through the second article. The error handler (`FailureHandler`) is a function that handles any errors that may occur during the validation process. We can use the following code to create the handler: ```go // Replace with your own secret key. secret := "0x123456789abcdef" // Create a hcaptcha middleware with the secret key and a custom error handler. hcaptchaMiddleware := hcaptcha.New(secret, func(c *hcaptcha.Client) { c.FailureHandler = func(ctx iris.Context) { // Handle the error as you wish, for example: ctx.StopWithText(iris.StatusBadRequest, "hcaptcha verification failed: %v", err) } }) ``` The secret key can be found through: https://dashboard.hcaptcha.com. Please store it on a secure and private place. To test hCAPTCHA on a local environment please read the following instructions at: https://docs.hcaptcha.com/#localdev. Finally, we need to register the hcaptcha middleware in our Iris app. We can use the `app.UseRouter` method to apply the middleware to all routes, or the `app.Use/UseGlobal` method to apply it to all non-error routes. For example: ```go // Create an Iris app instance. app := iris.New() // Apply the hcaptcha middleware to all non-error routes. app.Use(hcaptchaMiddleware) // Or apply the hcaptcha middleware to specific routes. app.Get("/protected", hcaptchaMiddleware, func(ctx iris.Context) { // This route is protected by hcaptcha. ctx.WriteString("Hello, human!") }) ``` ## Conclusion In this article, we have learned how to use basic hCAPTCHA middleware with Iris. For a more complete example please navigate through: https://github.com/kataras/iris/tree/main/_examples/auth/hcaptcha. By using these kind of features, we can create a more secure, user-friendly, and robust web application. I hope you enjoyed this article and found it useful. If you have any questions or feedback, please feel free to leave a comment below. Thank you for reading! 😊 ================================================ FILE: middleware/hcaptcha/hcaptcha.go ================================================ package hcaptcha import ( "encoding/json" "fmt" "io" "net/http" "net/url" "github.com/kataras/iris/v12/context" ) func init() { context.SetHandlerName("iris/middleware/hcaptcha.*", "iris.hCaptcha") } var ( // ResponseContextKey is the default request's context key that response of a hcaptcha request is kept. ResponseContextKey string = "iris.hcaptcha" // DefaultFailureHandler is the default HTTP handler that is fired on hcaptcha failures. // See `Client.FailureHandler`. DefaultFailureHandler = func(ctx *context.Context) { ctx.StopWithStatus(http.StatusTooManyRequests) } ) // Client represents the hcaptcha client. type Client struct { // FailureHandler if specified, fired when user does not complete hcaptcha successfully. // Failure and error codes information are kept as `Response` type // at the Request's Context key of "hcaptcha". // // Defaults to a handler that writes a status code of 429 (Too Many Requests) // and without additional information. FailureHandler context.Handler // Optional checks for siteverify. // // The user's remote IP address. RemoteIP string // The sitekey form field you expect to see. SiteKey string secret string } // Option declares an option for the hcaptcha client. // See `New` package-level function. type Option func(*Client) // WithRemoteIP sets the remote ip field to the given value. // It sends the remoteip form field on "SiteVerify". func WithRemoteIP(remoteIP string) Option { return func(c *Client) { c.RemoteIP = remoteIP } } // WithSiteKey sets the site key field to the given value. // It sends the sitekey form field on "SiteVerify". func WithSiteKey(siteKey string) Option { return func(c *Client) { c.SiteKey = siteKey } } // Response is the hcaptcha JSON response. type Response struct { ChallengeTS string `json:"challenge_ts"` Hostname string `json:"hostname"` ErrorCodes []string `json:"error-codes,omitempty"` Success bool `json:"success"` Credit bool `json:"credit,omitempty"` } // New accepts a hpcatcha secret key and returns a new hcaptcha HTTP Client. // // Instructions at: https://docs.hcaptcha.com/. // // See its `Handler` and `SiteVerify` for details. // See the `WithRemoteIP` and `WithSiteKey` package-level functions too. func New(secret string, options ...Option) context.Handler { c := &Client{ FailureHandler: DefaultFailureHandler, secret: secret, } for _, opt := range options { opt(c) } return c.Handler } // Handler is the HTTP route middleware featured hcaptcha validation. // It calls the `SiteVerify` method and fires the "next" when user completed the hcaptcha successfully, // // otherwise it calls the Client's `FailureHandler`. // // The hcaptcha's `Response` (which contains any `ErrorCodes`) // is saved on the Request's Context (see `GetResponseFromContext`). func (c *Client) Handler(ctx *context.Context) { v := SiteVerify(ctx, c.secret, c.RemoteIP, c.SiteKey) ctx.Values().Set(ResponseContextKey, v) if v.Success { ctx.Next() return } if c.FailureHandler != nil { c.FailureHandler(ctx) } } // responseFormValue = "h-captcha-response" const apiURL = "https://hcaptcha.com/siteverify" // SiteVerify accepts an Iris Context and a secret key (https://dashboard.hcaptcha.com/settings). // It returns the hcaptcha's `Response`. // The `response.Success` reports whether the validation passed. // Any errors are passed through the `response.ErrorCodes` field. // // The remoteIP and siteKey input arguments are optional. func SiteVerify(ctx *context.Context, secret, remoteIP, siteKey string) (response Response) { generatedResponseID := ctx.FormValue("h-captcha-response") if generatedResponseID == "" { response.ErrorCodes = append(response.ErrorCodes, "form[h-captcha-response] is empty") return } values := url.Values{ "secret": {secret}, "response": {generatedResponseID}, } if remoteIP != "" { values.Add("remoteip", remoteIP) } if siteKey != "" { values.Add("sitekey", siteKey) } resp, err := http.DefaultClient.PostForm(apiURL, values) if err != nil { response.ErrorCodes = append(response.ErrorCodes, err.Error()) return } body, err := io.ReadAll(resp.Body) resp.Body.Close() if err != nil { response.ErrorCodes = append(response.ErrorCodes, err.Error()) return } err = json.Unmarshal(body, &response) if err != nil { response.ErrorCodes = append(response.ErrorCodes, err.Error()) return } return } // Get returns the hcaptcha `Response` of the current request and reports whether was found or not. func Get(ctx *context.Context) (Response, bool) { v := ctx.Values().Get(ResponseContextKey) if v != nil { if response, ok := v.(Response); ok { return response, true } } return Response{}, false } // Script is the hCaptcha's javascript source file that should be incldued in the HTML head or body. const Script = "https://hcaptcha.com/1/api.js" // HTMLForm is the default HTML form for clients. // It's totally optional, use your own code for the best possible result depending on your web application. // See `ParseForm` and `RenderForm` for more. var HTMLForm = `
      ` // ParseForm parses the `HTMLForm` with the necessary parameters and returns // its result for render. func ParseForm(dataSiteKey, postActionRelativePath string) string { return fmt.Sprintf(HTMLForm, postActionRelativePath, Script, dataSiteKey) } // RenderForm writes the `HTMLForm` to "w" response writer. // See `_examples/auth/hcaptcha/templates/register_form.html` example for a custom form instead. func RenderForm(ctx *context.Context, dataSiteKey, postActionRelativePath string) (int, error) { return ctx.HTML(ParseForm(dataSiteKey, postActionRelativePath)) } ================================================ FILE: middleware/jwt/ARTICLE.md ================================================ # How to use JWT authentication with Iris In this tutorial, we will learn how to use JWT (JSON Web Token) authentication with [Iris](https://www.iris-go.com/), a fast and simple web framework for Go. JWT is a standard for securely transmitting information between parties as a JSON object. It can be used to authenticate users and protect API endpoints from unauthorized access. ## Prerequisites To follow this tutorial, you will need: - Go 1.20 or higher installed on your machine - A basic understanding of Go and Iris - A text editor or IDE of your choice (e.g. [VS Code](https://code.visualstudio.com/)) ## Creating a new Iris project First, we will create a new Iris project using the `go mod` command. To do that, run the following commands: ```bash $ mkdir iris-jwt $ cd iris-jwt $ go mod init iris-jwt $ go get github.com/kataras/iris/v12@latest ``` This will create a new folder called `iris-jwt` with the following files: ```bash iris-jwt ├── go.mod └── go.sum ``` The `go.mod` file contains the module name and the dependency on Iris. The `go.sum` file contains the checksums of the dependencies. Next, we will create a file called `main.go` in the same folder and write some basic code to start an Iris server on port 8080. We will modify this file later to add our JWT logic. ```go package main import "github.com/kataras/iris/v12" func main() { // Create a new Iris application. app := iris.New() // Register a simple GET handler at the root path. app.Get("/", func(ctx iris.Context) { ctx.WriteString("Hello, world!") }) // Start the server at http://localhost:8080. app.Listen(":8080") } ``` To run the server, run the following command: ```bash $ go run main.go ``` You should see something like this in your terminal: ```bash Now listening on: http://localhost:8080 Application started. Press CTRL+C to shut down. ``` You can also visit http://localhost:8080 in your browser and see the message "Hello, world!". ## Defining the user model and the JWT claims Now, we will define a simple user model and a custom JWT claims struct in our `main.go` file. The user model will represent the data of our users, such as username and password. The JWT claims struct will contain the information that we want to store in our JWT tokens, such as user ID and expiration time. We will also define some constants for our secret keys and token expiration durations. ```go package main import ( "time" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/middleware/jwt" ) // User is a simple user model. type User struct { ID int64 `json:"id"` Username string `json:"username"` Password string `json:"password"` } // Claims is a custom JWT claims struct. type Claims struct { jwt.Claims // embeds standard claims iat, exp and sub. UserID int64 `json:"user_id"` } // Define some constants for our secret keys and token expiration durations. const ( accessSecret = "my-access-secret" refreshSecret = "my-refresh-secret" accessExpire = 15 * time.Minute refreshExpire = 24 * time.Hour ) ``` ## Creating a signer and a verifier for JWT Next, we will create a signer and a verifier for JWT using the Iris middleware/jwt package. The signer will be used to generate and sign JWT tokens with our secret keys and claims. The verifier will be used to verify and validate JWT tokens from the client requests. We will create two signers and one verifiers, one pair for the access token and one signer for the refresh token. We will also use encryption for our tokens to add an extra layer of security. ```go package main import ( "time" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/middleware/jwt" ) // ... // Create a signer for the access token. accessSigner := jwt.NewSigner(jwt.HS256, accessSecret, accessExpire). // Use encryption for the access token. WithEncryption([]byte("my-access-encryption-key"), nil) // Create a signer for the refresh token. refreshSigner := jwt.NewSigner(jwt.HS256, refreshSecret, refreshExpire). // Use encryption for the refresh token. WithEncryption([]byte("my-refresh-encryption-key"), nil) // Create a verifier for the access token. accessVerifier := jwt.NewVerifier(jwt.HS256, accessSecret). // Use decryption for the access token. WithDecryption([]byte("my-access-encryption-key"), nil) // Use a blocklist to revoke tokens. // .WithDefaultBlocklist() ``` ## Creating some mock users and a login handler For the sake of simplicity, we will create some mock users in a map and use them to simulate authentication. In a real application, you would use a database or another storage system to store and retrieve your users. We will also create a login handler that will take a username and password from the client request, check if they match with one of our mock users, and if so, generate an access token and a refresh token for that user and send them back to the client. ```go package main import ( "time" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/middleware/jwt" ) // ... // Create some mock users in a map. users := map[string]User{ "alice": {ID: 1, Username: "alice", Password: "1234"}, "bob": {ID: 2, Username: "bob", Password: "5678"}, } // Create a login handler that will generate JWT tokens for authenticated users. loginHandler := func(ctx iris.Context) { // Get the username and password from the request body. var user User if err := ctx.ReadJSON(&user); err != nil { ctx.StopWithError(iris.StatusBadRequest, err) return } // Check if the username and password match with one of our mock users. if u, ok := users[user.Username]; !ok || u.Password != user.Password { ctx.StopWithStatus(iris.StatusUnauthorized) return } // Generate an access token with the user ID as the subject claim. accessClaims := Claims{ Claims: jwt.Claims{Subject: u.Username}, UserID: u.ID, } accessToken, err := accessSigner.Sign(accessClaims) if err != nil { ctx.StopWithError(iris.StatusInternalServerError, err) return } // Generate a refresh token with the user ID as the subject claim. refreshClaims := Claims{ Claims: jwt.Claims{Subject: u.Username}, UserID: u.ID, } refreshToken, err := refreshSigner.Sign(refreshClaims) if err != nil { ctx.StopWithError(iris.StatusInternalServerError, err) return } /* OR tokenPair, err := refreshSigner.NewTokenPair(accessClaims, refreshClaims, refreshExpire) // [handle err...] ctx.JSON(tokenPair) */ // Send the tokens to the client as JSON. ctx.JSON(iris.Map{ "access_token": string(accessToken), "refresh_token": string(refreshToken), "expires_in": int64(accessExpire.Seconds()), }) } ``` ## Creating a protected API handler Next, we will create a protected API handler that will only allow authorized users to access it. We will use the access verifier as a middleware to verify and validate the access token from the client request. If the token is valid, we will extract the user ID from it and send it back to the client as JSON. ```go package main import ( "time" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/middleware/jwt" ) // ... // Create a protected API handler that will only allow authorized users to access it. protectedHandler := func(ctx iris.Context) { // Get the verified token from the context. token := jwt.GetVerifiedToken(ctx) // important step. // Get the custom claims from the token. var claims Claims if err := token.Claims(&claims); err != nil { // important step. ctx.StopWithError(iris.StatusInternalServerError, err) return } // Get the user ID from the claims. userID := claims.UserID // Send the user ID to the client as JSON. // This is just an example, you can do whatever you want here. ctx.JSON(iris.Map{ "user_id": userID, }) } ``` ## Creating a refresh handler Finally, we will create a refresh handler that will allow users to refresh their access tokens using their refresh tokens. We will use the refresh verifier as a middleware to verify and validate the refresh token from the client request. If the token is valid, we will generate a new access token and a new refresh token for the same user and send them back to the client. ```go package main import ( "time" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/middleware/jwt" ) // ... // Create a refresh handler that will allow users to refresh their access tokens using their refresh tokens. refreshHandler := func(ctx iris.Context) { // Get the verified token from the context. token := jwt.GetVerifiedToken(ctx) // Get the custom claims from the token. var claims Claims if err := token.Claims(&claims); err != nil { ctx.StopWithError(iris.StatusInternalServerError, err) return } // Get the user ID and username from the claims. userID := claims.UserID username := claims.Subject // Generate a new access token with the same user ID and username as the subject claim. accessClaims := Claims{ Claims: jwt.Claims{Subject: username}, UserID: userID, } accessToken, err := accessSigner.Sign(accessClaims) if err != nil { ctx.StopWithError(iris.StatusInternalServerError, err) return } // Generate a new refresh token with the same user ID and username as the subject claim. refreshClaims := Claims{ Claims: jwt.Claims{Subject: username}, UserID: userID, } refreshToken, err := refreshSigner.Sign(refreshClaims) if err != nil { ctx.StopWithError(iris.StatusInternalServerError, err) return } // Send the new tokens to the client as JSON. ctx.JSON(iris.Map{ "access_token": string(accessToken), "refresh_token": string(refreshToken), "expires_in": int64(accessExpire.Seconds()), }) } ``` ## Registering the handlers and testing the application Now that we have created all our handlers, we can register them with our Iris application and test our application. We will use the `app.Post` method to register our login, protected, and refresh handlers with different paths. We will also use the `accessVerifier.Verify` method to register our verifiers as middlewares for the rest of the protected handlers. ```go package main import ( "time" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/middleware/jwt" ) // ... func main() { // Create a new Iris application. app := iris.New() // Register our login handler at /login path. app.Post("/login", loginHandler) // Register a single protected handler at /protected path. // This handler verifies the request manually as we've seen above. app.Post("/protected", protectedHandler) // Register our refresh handler at /refresh path with refresh signer. app.Post("/refresh", refreshHandler) // You can also register the pre-defined jwt middleware for all protected routes // which performs verification automatically and set the custom Claims to the Context // for next handlers to use through: claims := jwt.Get(ctx).(*Claims). app.Use(accessVerifier.Verify(func() any { return new(Claims) })) // [more routes...] // Start the server at http://localhost:8080. app.Listen(":8080") } ``` To test our application, we can use a tool like curl or Postman to send HTTP requests to our server. Here are some examples of how to do that: - To login as Alice and get the access token and the refresh token, we can send a POST request to http://localhost:8080/login with the following JSON body: ```json { "username": "alice", "password": "1234" } ``` We should get a response like this: ```json { "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MzYwNjQ3MzEsImlhdCI6MTYzNjA2MzIzMSwic3ViIjoiYWxpY2UiLCJ1c2VySWQiOjF9.8f7a7b8f7a7b8f7a7b8f7a7b8f7a7b8f7a7b8f7a7b8f7a7b8f7a7b8f", "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MzYxNTAzMzEsImlhdCI6MTYzNjA2MzIzMSwic3ViIjoiYWxpY2UiLCJ1c2VySWQiOjF9.9f8a8b9f8a8b9f8a8b9f8a8b9f8a8b9f8a8b9f8a8b9f8a8b9f8a8b9f", "expires_in": 900 } ``` - To access the protected API endpoint with the access token, we can send a POST request to http://localhost:8080/protected with the following header: ```bash Authorization: Bearer ``` We should get a response like this: ```json { "user_id": 1 } ``` - To refresh the access token with the refresh token, we can send a POST request to http://localhost:8080/refresh with the following header: ```bash Authorization: Bearer ``` We should get a response like this: ```json { "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MzYwNjQ4NTMsImlhdCI6MTYzNjA2MzM1Mywic3ViIjoiYWxpY2UiLCJ1c2VySWQiOjF9.9g7a7c9g7a7c9g7a7c9g7a7c9g7a7c9g7a7c9g7a7c9g7a7c9g7a7c9g", "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MzYxNTA0NTMsImlhdCI6MTYzNjA2MzM1Mywic3ViIjoiYWxpY2UiLCJ1c2VySWQiOjF9.aG8aBbaG8aBbaG8aBbaG8aBbaG8aBbaG8aBbaG8aBbaG8aBbaG8aBbaG", "expires_in": 900 } ``` ## Conclusion In this tutorial, we learned how to use JWT authentication with Iris, a fast and simple web framework for Go. We learned how to create a signer and a verifier for JWT, how to generate and validate JWT tokens, how to protect an API endpoint with JWT, and how to refresh JWT tokens. We also learned how to use the Iris middleware/jwt package, which provides a common Iris handler for JWT authentication. You can find the complete code for this tutorial on GitHub: https://github.com/kataras/iris/tree/main/_examples/auth/jwt/tutorial and https://github.com/kataras/jwt. If you want to learn more about Iris, you can visit its official website: https://www.iris-go.com/ If you want to learn more about JWT, you can visit its official website: https://jwt.io/ I hope you enjoyed this tutorial and found it useful. Thank you for reading.😊 ================================================ FILE: middleware/jwt/aliases.go ================================================ package jwt import "github.com/kataras/jwt" // Error values. var ( ErrBlocked = jwt.ErrBlocked ErrDecrypt = jwt.ErrDecrypt ErrExpected = jwt.ErrExpected ErrExpired = jwt.ErrExpired ErrInvalidKey = jwt.ErrInvalidKey ErrIssuedInTheFuture = jwt.ErrIssuedInTheFuture ErrMissing = jwt.ErrMissing ErrMissingKey = jwt.ErrMissingKey ErrNotValidYet = jwt.ErrNotValidYet ErrTokenAlg = jwt.ErrTokenAlg ErrTokenForm = jwt.ErrTokenForm ErrTokenSignature = jwt.ErrTokenSignature ) // Signature algorithms. var ( EdDSA = jwt.EdDSA HS256 = jwt.HS256 HS384 = jwt.HS384 HS512 = jwt.HS512 RS256 = jwt.RS256 RS384 = jwt.RS384 RS512 = jwt.RS512 ES256 = jwt.ES256 ES384 = jwt.ES384 ES512 = jwt.ES512 PS256 = jwt.PS256 PS384 = jwt.PS384 PS512 = jwt.PS512 ) // Signature algorithm helpers. var ( MustLoadHMAC = jwt.MustLoadHMAC LoadHMAC = jwt.LoadHMAC MustLoadRSA = jwt.MustLoadRSA LoadPrivateKeyRSA = jwt.LoadPrivateKeyRSA LoadPublicKeyRSA = jwt.LoadPublicKeyRSA ParsePrivateKeyRSA = jwt.ParsePrivateKeyRSA ParsePublicKeyRSA = jwt.ParsePublicKeyRSA MustLoadECDSA = jwt.MustLoadECDSA LoadPrivateKeyECDSA = jwt.LoadPrivateKeyECDSA LoadPublicKeyECDSA = jwt.LoadPublicKeyECDSA ParsePrivateKeyECDSA = jwt.ParsePrivateKeyECDSA ParsePublicKeyECDSA = jwt.ParsePublicKeyECDSA MustLoadEdDSA = jwt.MustLoadEdDSA LoadPrivateKeyEdDSA = jwt.LoadPrivateKeyEdDSA LoadPublicKeyEdDSA = jwt.LoadPublicKeyEdDSA ParsePrivateKeyEdDSA = jwt.ParsePrivateKeyEdDSA ParsePublicKeyEdDSA = jwt.ParsePublicKeyEdDSA ) // Type alises for the underline jwt package. type ( // Alg is the signature algorithm interface alias. Alg = jwt.Alg // Claims represents the standard claim values (as specified in RFC 7519). Claims = jwt.Claims // Expected is a TokenValidator which performs simple checks // between standard claims values. // // Usage: // expecteed := jwt.Expected{ // Issuer: "my-app", // } // verifiedToken, err := verifier.Verify(..., expected) Expected = jwt.Expected // TokenValidator is the token validator interface alias. TokenValidator = jwt.TokenValidator // VerifiedToken is the type alias for the verfieid token type, // the result of the VerifyToken function. VerifiedToken = jwt.VerifiedToken // SignOption used to set signing options at Sign function. SignOption = jwt.SignOption // TokenPair is just a helper structure which holds both access and refresh tokens. TokenPair = jwt.TokenPair ) // Encryption algorithms. var ( GCM = jwt.GCM // Helper to generate random key, // can be used to generate hmac signature key and GCM+AES for testing. MustGenerateRandom = jwt.MustGenerateRandom ) var ( // Leeway adds validation for a leeway expiration time. // If the token was not expired then a comparison between // this "leeway" and the token's "exp" one is expected to pass instead (now+leeway > exp). // Example of use case: disallow tokens that are going to be expired in 3 seconds from now, // this is useful to make sure that the token is valid when the when the user fires a database call for example. // Usage: // verifiedToken, err := verifier.Verify(..., jwt.Leeway(5*time.Second)) Leeway = jwt.Leeway // MaxAge is a SignOption to set the expiration "exp", "iat" JWT standard claims. // Can be passed as last input argument of the `Sign` function. // // If maxAge > second then sets expiration to the token. // It's a helper field to set the "exp" and "iat" claim values. // Usage: // signer.Sign(..., jwt.MaxAge(15*time.Minute)) MaxAge = jwt.MaxAge // ID is a shurtcut to set jwt ID on Sign. ID = func(id string) jwt.SignOptionFunc { return func(c *Claims) { c.ID = id } } ) // Shortcuts for Signing and Verifying. var ( Verify = jwt.Verify VerifyEncryptedToken = jwt.VerifyEncrypted Sign = jwt.Sign SignEncrypted = jwt.SignEncrypted ) ================================================ FILE: middleware/jwt/blocklist/redis/blocklist.go ================================================ package redis import ( "context" "io" "sync/atomic" "github.com/kataras/iris/v12/core/host" "github.com/kataras/iris/v12/middleware/jwt" "github.com/redis/go-redis/v9" ) var defaultContext = context.Background() type ( // Options is just a type alias for the go-redis Client Options. Options = redis.Options // ClusterOptions is just a type alias for the go-redis Cluster Client Options. ClusterOptions = redis.ClusterOptions ) // Client is the interface which both // go-redis Client and Cluster Client implements. type Client interface { redis.Cmdable // Commands. io.Closer // CloseConnection. } // Blocklist is a jwt.Blocklist backed by Redis. type Blocklist struct { // GetKey is a function which can be used how to extract // the unique identifier for a token. // Required. By default the token key is extracted through the claims.ID ("jti"). GetKey func(token []byte, claims jwt.Claims) string // Prefix the token key into the redis database. // Note that if you can also select a different database // through ClientOptions (or ClusterOptions). // Defaults to empty string (no prefix). Prefix string // Both Client and ClusterClient implements this interface. client Client connected uint32 // Customize any go-redis fields manually // before Connect. ClientOptions Options ClusterOptions ClusterOptions } var _ jwt.Blocklist = (*Blocklist)(nil) // NewBlocklist returns a new redis-based Blocklist. // Modify its ClientOptions or ClusterOptions depending the application needs // and call its Connect. // // Usage: // // blocklist := NewBlocklist() // blocklist.ClientOptions.Addr = ... // err := blocklist.Connect() // // And register it: // // verifier := jwt.NewVerifier(...) // verifier.Blocklist = blocklist func NewBlocklist() *Blocklist { return &Blocklist{ GetKey: defaultGetKey, Prefix: "", ClientOptions: Options{ Addr: "127.0.0.1:6379", // The rest are defaulted to good values already. }, // If its Addrs > 0 before connect then cluster client is used instead. ClusterOptions: ClusterOptions{}, } } func defaultGetKey(_ []byte, claims jwt.Claims) string { return claims.ID } // Connect prepares the redis client and fires a ping response to it. func (b *Blocklist) Connect() error { if b.Prefix != "" { getKey := b.GetKey b.GetKey = func(token []byte, claims jwt.Claims) string { return b.Prefix + getKey(token, claims) } } if len(b.ClusterOptions.Addrs) > 0 { // Use cluster client. b.client = redis.NewClusterClient(&b.ClusterOptions) } else { b.client = redis.NewClient(&b.ClientOptions) } _, err := b.client.Ping(defaultContext).Result() if err != nil { return err } host.RegisterOnInterrupt(func() { atomic.StoreUint32(&b.connected, 0) b.client.Close() }) atomic.StoreUint32(&b.connected, 1) return nil } // IsConnected reports whether the Connect function was called. func (b *Blocklist) IsConnected() bool { return atomic.LoadUint32(&b.connected) > 0 } // ValidateToken checks if the token exists and func (b *Blocklist) ValidateToken(token []byte, c jwt.Claims, err error) error { if err != nil { if err == jwt.ErrExpired { b.Del(b.GetKey(token, c)) } return err // respect the previous error. } has, err := b.Has(b.GetKey(token, c)) if err != nil { return err } else if has { return jwt.ErrBlocked } return nil } // InvalidateToken invalidates a verified JWT token. func (b *Blocklist) InvalidateToken(token []byte, c jwt.Claims) error { key := b.GetKey(token, c) return b.client.SetEx(defaultContext, key, token, c.Timeleft()).Err() } // Del removes a token from the storage. func (b *Blocklist) Del(key string) error { return b.client.Del(defaultContext, key).Err() } // Has reports whether a specific token exists in the storage. func (b *Blocklist) Has(key string) (bool, error) { n, err := b.client.Exists(defaultContext, key).Result() return n > 0, err } // Count returns the total amount of tokens stored. func (b *Blocklist) Count() (int64, error) { if b.Prefix == "" { return b.client.DBSize(defaultContext).Result() } keys, err := b.getKeys(0) if err != nil { return 0, err } return int64(len(keys)), nil } func (b *Blocklist) getKeys(cursor uint64) ([]string, error) { keys, cursor, err := b.client.Scan(defaultContext, cursor, b.Prefix+"*", 300000).Result() if err != nil { return nil, err } if cursor != 0 { moreKeys, err := b.getKeys(cursor) if err != nil { return nil, err } keys = append(keys, moreKeys...) } return keys, nil } ================================================ FILE: middleware/jwt/blocklist.go ================================================ package jwt import ( "github.com/kataras/jwt" ) // Blocklist should hold and manage invalidated-by-server tokens. // The `NewBlocklist` and `NewBlocklistContext` functions // returns a memory storage of tokens, // it is the internal "blocklist" struct. // // The end-developer can implement her/his own blocklist, // e.g. a redis one to keep persistence of invalidated tokens on server restarts. // and bind to the JWT middleware's Blocklist field. type Blocklist interface { jwt.TokenValidator // InvalidateToken should invalidate a verified JWT token. InvalidateToken(token []byte, c Claims) error // Del should remove a token from the storage. Del(key string) error // Has should report whether a specific token exists in the storage. Has(key string) (bool, error) // Count should return the total amount of tokens stored. Count() (int64, error) } type blocklistConnect interface { Connect() error IsConnected() bool } ================================================ FILE: middleware/jwt/extractor.go ================================================ package jwt import ( "strings" "github.com/kataras/iris/v12/context" ) // TokenExtractor is a function that takes a context as input and returns // a token. An empty string should be returned if no token found // without additional information. type TokenExtractor func(*context.Context) string // FromHeader is a token extractor. // It reads the token from the Authorization request header of form: // Authorization: "Bearer {token}". func FromHeader(ctx *context.Context) string { authHeader := ctx.GetHeader("Authorization") if authHeader == "" { return "" } // pure check: authorization header format must be Bearer {token} authHeaderParts := strings.Split(authHeader, " ") if len(authHeaderParts) != 2 || strings.ToLower(authHeaderParts[0]) != "bearer" { return "" } return authHeaderParts[1] } // FromQuery is a token extractor. // It reads the token from the "token" url query parameter. func FromQuery(ctx *context.Context) string { return ctx.URLParam("token") } // FromJSON is a token extractor. // Reads a json request body and extracts the json based on the given field. // The request content-type should contain the: application/json header value, otherwise // this method will not try to read and consume the body. func FromJSON(jsonKey string) TokenExtractor { return func(ctx *context.Context) string { if ctx.GetContentTypeRequested() != context.ContentJSONHeaderValue { return "" } var m context.Map ctx.RecordRequestBody(true) defer ctx.RecordRequestBody(false) if err := ctx.ReadJSON(&m); err != nil { return "" } if m == nil { return "" } v, ok := m[jsonKey] if !ok { return "" } tok, ok := v.(string) if !ok { return "" } return tok } } ================================================ FILE: middleware/jwt/jwt.go ================================================ package jwt import "github.com/kataras/iris/v12/context" func init() { context.SetHandlerName("iris/middleware/jwt.*", "iris.jwt") } ================================================ FILE: middleware/jwt/jwt_test.go ================================================ package jwt_test import ( "fmt" "testing" "time" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/httptest" "github.com/kataras/iris/v12/middleware/jwt" ) var testAlg, testSecret = jwt.HS256, []byte("sercrethatmaycontainch@r$") type fooClaims struct { Foo string `json:"foo"` } // The actual tests are inside the kataras/jwt repository. // This runs simple checks of just the middleware part. func TestJWT(t *testing.T) { app := iris.New() signer := jwt.NewSigner(testAlg, testSecret, 3*time.Second) app.Get("/", func(ctx iris.Context) { claims := fooClaims{Foo: "bar"} token, err := signer.Sign(claims) if err != nil { ctx.StopWithError(iris.StatusInternalServerError, err) return } ctx.Write(token) }) verifier := jwt.NewVerifier(testAlg, testSecret) verifier.ErrorHandler = func(ctx iris.Context, err error) { // app.OnErrorCode(401, ...) ctx.StopWithError(iris.StatusUnauthorized, err) } middleware := verifier.Verify(func() any { return new(fooClaims) }) app.Get("/protected", middleware, func(ctx iris.Context) { claims := jwt.Get(ctx).(*fooClaims) ctx.WriteString(claims.Foo) }) e := httptest.New(t, app) // Get generated token. token := e.GET("/").Expect().Status(iris.StatusOK).Body().Raw() // Test Header. headerValue := fmt.Sprintf("Bearer %s", token) e.GET("/protected").WithHeader("Authorization", headerValue).Expect(). Status(iris.StatusOK).Body().IsEqual("bar") // Test URL query. e.GET("/protected").WithQuery("token", token).Expect(). Status(iris.StatusOK).Body().IsEqual("bar") // Test unauthorized. e.GET("/protected").Expect().Status(iris.StatusUnauthorized) e.GET("/protected").WithHeader("Authorization", "missing bearer").Expect().Status(iris.StatusUnauthorized) e.GET("/protected").WithQuery("token", "invalid_token").Expect().Status(iris.StatusUnauthorized) // Test expired (note checks happen based on second round). time.Sleep(5 * time.Second) e.GET("/protected").WithHeader("Authorization", headerValue).Expect(). Status(iris.StatusUnauthorized).Body().IsEqual("jwt: token expired") } ================================================ FILE: middleware/jwt/signer.go ================================================ package jwt import ( "fmt" "time" "github.com/kataras/jwt" ) // Signer holds common options to sign and generate a token. // Its Sign method can be used to generate a token which can be sent to the client. // Its NewTokenPair can be used to construct a token pair (access_token, refresh_token). // // It does not support JWE, JWK. type Signer struct { Alg Alg Key any // MaxAge to set "exp" and "iat". // Recommended value for access tokens: 15 minutes. // Defaults to 0, no limit. MaxAge time.Duration Options []SignOption Encrypt func([]byte) ([]byte, error) } // NewSigner accepts the signature algorithm among with its (private or shared) key // and the max life time duration of generated tokens and returns a JWT signer. // See its Sign method. // // Usage: // // signer := NewSigner(HS256, secret, 15*time.Minute) // token, err := signer.Sign(userClaims{Username: "kataras"}) func NewSigner(signatureAlg Alg, signatureKey any, maxAge time.Duration) *Signer { if signatureAlg == HS256 { // A tiny helper if the end-developer uses string instead of []byte for hmac keys. if k, ok := signatureKey.(string); ok { signatureKey = []byte(k) } } s := &Signer{ Alg: signatureAlg, Key: signatureKey, MaxAge: maxAge, } if maxAge > 0 { s.Options = []SignOption{MaxAge(maxAge)} } return s } // WithEncryption enables AES-GCM payload-only decryption. func (s *Signer) WithEncryption(key, additionalData []byte) *Signer { encrypt, _, err := jwt.GCM(key, additionalData) if err != nil { panic(err) // important error before serve, stop everything. } s.Encrypt = encrypt return s } // Sign generates a new token based on the given "claims" which is valid up to "s.MaxAge". func (s *Signer) Sign(claims any, opts ...SignOption) ([]byte, error) { if len(opts) > 0 { opts = append(opts, s.Options...) } else { opts = s.Options } return SignEncrypted(s.Alg, s.Key, s.Encrypt, claims, opts...) } // NewTokenPair accepts the access and refresh claims plus the life time duration for the refresh token // and generates a new token pair which can be sent to the client. // The same token pair can be json-decoded. func (s *Signer) NewTokenPair(accessClaims any, refreshClaims any, refreshMaxAge time.Duration, accessOpts ...SignOption) (TokenPair, error) { if refreshMaxAge <= s.MaxAge { return TokenPair{}, fmt.Errorf("refresh max age should be bigger than access token's one[%d - %d]", refreshMaxAge, s.MaxAge) } accessToken, err := s.Sign(accessClaims, accessOpts...) if err != nil { return TokenPair{}, err } refreshToken, err := Sign(s.Alg, s.Key, refreshClaims, MaxAge(refreshMaxAge)) if err != nil { return TokenPair{}, err } tokenPair := jwt.NewTokenPair(accessToken, refreshToken) return tokenPair, nil } ================================================ FILE: middleware/jwt/verifier.go ================================================ package jwt import ( "reflect" "time" "github.com/kataras/iris/v12/context" "github.com/kataras/jwt" ) const ( claimsContextKey = "iris.jwt.claims" verifiedTokenContextKey = "iris.jwt.token" ) // Get returns the claims decoded by a verifier. func Get(ctx *context.Context) any { if v := ctx.Values().Get(claimsContextKey); v != nil { return v } return nil } // GetVerifiedToken returns the verified token structure // which holds information about the decoded token // and its standard claims. func GetVerifiedToken(ctx *context.Context) *VerifiedToken { if v := ctx.Values().Get(verifiedTokenContextKey); v != nil { if tok, ok := v.(*VerifiedToken); ok { return tok } } return nil } // Verifier holds common options to verify an incoming token. // Its Verify method can be used as a middleware to allow authorized clients to access an API. // // It does not support JWE, JWK. type Verifier struct { Alg Alg Key any Decrypt func([]byte) ([]byte, error) Extractors []TokenExtractor Blocklist Blocklist Validators []TokenValidator ErrorHandler func(ctx *context.Context, err error) // DisableContextUser disables the registration of the claims as context User. DisableContextUser bool } // NewVerifier accepts the algorithm for the token's signature among with its (public) key // and optionally some token validators for all verify middlewares that may initialized under this Verifier. // See its Verify method. // // Usage: // // verifier := NewVerifier(HS256, secret) // // OR // // verifier := NewVerifier(HS256, secret, Expected{Issuer: "my-app"}) // // claimsGetter := func() any { return new(userClaims) } // middleware := verifier.Verify(claimsGetter) // // OR // // middleware := verifier.Verify(claimsGetter, Expected{Issuer: "my-app"}) // // Register the middleware, e.g. // // app.Use(middleware) // // Get the claims: // // claims := jwt.Get(ctx).(*userClaims) // username := claims.Username // // Get the context user: // // username, err := ctx.User().GetUsername() func NewVerifier(signatureAlg Alg, signatureKey any, validators ...TokenValidator) *Verifier { if signatureAlg == HS256 { // A tiny helper if the end-developer uses string instead of []byte for hmac keys. if k, ok := signatureKey.(string); ok { signatureKey = []byte(k) } } return &Verifier{ Alg: signatureAlg, Key: signatureKey, Extractors: []TokenExtractor{FromHeader, FromQuery}, ErrorHandler: func(ctx *context.Context, err error) { ctx.StopWithError(401, context.PrivateError(err)) }, Validators: validators, } } // WithDecryption enables AES-GCM payload-only encryption. func (v *Verifier) WithDecryption(key, additionalData []byte) *Verifier { _, decrypt, err := jwt.GCM(key, additionalData) if err != nil { panic(err) // important error before serve, stop everything. } v.Decrypt = decrypt return v } // WithDefaultBlocklist attaches an in-memory blocklist storage // to invalidate tokens through server-side. // To invalidate a token simply call the Context.Logout method. func (v *Verifier) WithDefaultBlocklist() *Verifier { v.Blocklist = jwt.NewBlocklist(30 * time.Minute) return v } func (v *Verifier) invalidate(ctx *context.Context) { if verifiedToken := GetVerifiedToken(ctx); verifiedToken != nil { v.Blocklist.InvalidateToken(verifiedToken.Token, verifiedToken.StandardClaims) ctx.Values().Remove(claimsContextKey) ctx.Values().Remove(verifiedTokenContextKey) ctx.SetUser(nil) ctx.SetLogoutFunc(nil) } } // RequestToken extracts the token from the request. func (v *Verifier) RequestToken(ctx *context.Context) (token string) { for _, extract := range v.Extractors { if token = extract(ctx); token != "" { break // ok we found it. } } return } type ( // ClaimsValidator is a special interface which, if the destination claims // implements it then the verifier runs its Validate method before return. ClaimsValidator interface { Validate() error } // ClaimsContextValidator same as ClaimsValidator but it accepts // a request context which can be used for further checks before // validating the incoming token's claims. ClaimsContextValidator interface { Validate(*context.Context) error } ) // VerifyToken simply verifies the given "token" and validates its standard claims (such as expiration). // Returns a structure which holds the token's information. See the Verify method instead. func (v *Verifier) VerifyToken(token []byte, validators ...TokenValidator) (*VerifiedToken, error) { return jwt.VerifyEncrypted(v.Alg, v.Key, v.Decrypt, token, validators...) } // Verify is the most important piece of code inside the Verifier. // It accepts the "claimsType" function which should return a pointer to a custom structure // which the token's decode claims valuee will be binded and validated to. // Returns a common Iris handler which can be used as a middleware to protect an API // from unauthorized client requests. After this, the route handlers can access the claims // through the jwt.Get package-level function. // // By default it extracts the token from Authorization: Bearer $token header and ?token URL Query parameter, // to change that behavior modify its Extractors field. // // By default a 401 status code with a generic message will be sent to the client on // a token verification or claims validation failure, to change that behavior // modify its ErrorHandler field or register OnErrorCode(401, errorHandler) and // retrieve the error through Context.GetErr method. // // If the "claimsType" is nil then only the jwt.GetVerifiedToken is available // and the handler should unmarshal the payload to extract the claims by itself. func (v *Verifier) Verify(claimsType func() any, validators ...TokenValidator) context.Handler { unmarshal := jwt.Unmarshal if claimsType != nil { c := claimsType() if hasRequired(c) { unmarshal = jwt.UnmarshalWithRequired } } if v.Blocklist != nil { // If blocklist implements the connect interface, // try to connect if it's not already connected manually by developer, // if errored then just return a handler which will fire this error every single time. if bc, ok := v.Blocklist.(blocklistConnect); ok { if !bc.IsConnected() { if err := bc.Connect(); err != nil { return func(ctx *context.Context) { v.ErrorHandler(ctx, err) } } } } validators = append([]TokenValidator{v.Blocklist}, append(v.Validators, validators...)...) } return func(ctx *context.Context) { token := []byte(v.RequestToken(ctx)) verifiedToken, err := v.VerifyToken(token, validators...) if err != nil { v.ErrorHandler(ctx, err) return } if claimsType != nil { dest := claimsType() if err = unmarshal(verifiedToken.Payload, dest); err != nil { v.ErrorHandler(ctx, err) return } if validator, ok := dest.(ClaimsValidator); ok { if err = validator.Validate(); err != nil { v.ErrorHandler(ctx, err) return } } else if contextValidator, ok := dest.(ClaimsContextValidator); ok { if err = contextValidator.Validate(ctx); err != nil { v.ErrorHandler(ctx, err) return } } if !v.DisableContextUser { ctx.SetUser(dest) } ctx.Values().Set(claimsContextKey, dest) } if v.Blocklist != nil { ctx.SetLogoutFunc(v.invalidate) } ctx.Values().Set(verifiedTokenContextKey, verifiedToken) ctx.Next() } } func hasRequired(i any) bool { val := reflect.Indirect(reflect.ValueOf(i)) typ := val.Type() if typ.Kind() != reflect.Struct { return false } for i := 0; i < val.NumField(); i++ { field := typ.Field(i) if jwt.HasRequiredJSONTag(field) { return true } } return false } ================================================ FILE: middleware/logger/config.go ================================================ package logger import ( "time" "github.com/kataras/iris/v12/context" ) // The SkipperFunc signature, used to serve the main request without logs. // See `Configuration` too. type SkipperFunc func(ctx *context.Context) bool // Config contains the options for the logger middleware // can be optionally be passed to the `New`. type Config struct { // Status displays status code (bool). // // Defaults to true. Status bool // IP displays request's remote address (bool). // // Defaults to true. IP bool // Method displays the http method (bool). // // Defaults to true. Method bool // Path displays the request path (bool). // See `Query` and `PathAfterHandler` too. // // Defaults to true. Path bool // PathAfterHandler displays the request path // which may be set and modified // after the handler chain is executed. // See `Query` too. // // Defaults to false. PathAfterHandler bool // Query will append the URL Query to the Path. // Path should be true too. // // Defaults to false. Query bool // TraceRoute displays the debug // information about the current route executed. // // Defaults to false. TraceRoute bool // MessageContextKeys if not empty, // the middleware will try to fetch // the contents with `ctx.Values().Get(MessageContextKey)` // and if available then these contents will be // appended as part of the logs (with `%v`, in order to be able to set a struct too), // // Defaults to empty. MessageContextKeys []string // MessageHeaderKeys if not empty, // the middleware will try to fetch // the contents with `ctx.Values().Get(MessageHeaderKey)` // and if available then these contents will be // appended as part of the logs (with `%v`, in order to be able to set a struct too), // // Defaults to empty. MessageHeaderKeys []string // LogFunc is the writer which logs are written to, // if missing the logger middleware uses the app.Logger().Infof instead. // Note that message argument can be empty. LogFunc func(endTime time.Time, latency time.Duration, status, ip, method, path string, message any, headerMessage any) // LogFuncCtx can be used instead of `LogFunc` if handlers need to customize the output based on // custom request-time information that the LogFunc isn't aware of. LogFuncCtx func(ctx *context.Context, latency time.Duration) // Skippers used to skip the logging i.e by `ctx.Path()` and serve // the next/main handler immediately. Skippers []SkipperFunc // the Skippers as one function in order to reduce the time needed to // combine them at serve time. skip SkipperFunc } // DefaultConfig returns a default config // that have all boolean fields to true, // all strings are empty, // LogFunc and Skippers to nil as well. func DefaultConfig() Config { return Config{ Status: true, IP: true, Method: true, Path: true, PathAfterHandler: false, Query: false, TraceRoute: false, LogFunc: nil, LogFuncCtx: nil, Skippers: nil, skip: nil, } } // AddSkipper adds a skipper to the configuration. func (c *Config) AddSkipper(sk SkipperFunc) { c.Skippers = append(c.Skippers, sk) c.buildSkipper() } func (c *Config) buildSkipper() { if len(c.Skippers) == 0 { return } skippersLocked := c.Skippers[0:] c.skip = func(ctx *context.Context) bool { for _, s := range skippersLocked { if s(ctx) { return true } } return false } } ================================================ FILE: middleware/logger/logger.go ================================================ // Package logger provides request logging via middleware. See _examples/logging/request-logger package logger import ( "fmt" "strconv" "time" "github.com/kataras/iris/v12/context" ) func init() { context.SetHandlerName("iris/middleware/logger.*", "iris.logger") } type requestLoggerMiddleware struct { config Config } // New creates and returns a new request logger middleware. // Do not confuse it with the framework's Logger. // This is for the http requests. // // Receives an optional configuation. // Usage: app.UseRouter(logger.New()). func New(cfg ...Config) context.Handler { c := DefaultConfig() if len(cfg) > 0 { c = cfg[0] } c.buildSkipper() l := &requestLoggerMiddleware{config: c} return l.ServeHTTP } func (l *requestLoggerMiddleware) getPath(ctx *context.Context) string { if l.config.Query { return ctx.Request().URL.RequestURI() } return ctx.Path() } // Serve serves the middleware func (l *requestLoggerMiddleware) ServeHTTP(ctx *context.Context) { // skip logs and serve the main request immediately if l.config.skip != nil { if l.config.skip(ctx) { ctx.Next() return } } // all except latency to string var status, ip, method, path string var latency time.Duration var startTime, endTime time.Time startTime = time.Now() // Before Next. if l.config.IP { ip = ctx.RemoteAddr() } if l.config.Method { method = ctx.Method() } if l.config.Path { path = l.getPath(ctx) } ctx.Next() // no time.Since in order to format it well after endTime = time.Now() latency = endTime.Sub(startTime) if l.config.PathAfterHandler /* we don't care if Path is disabled */ { path = l.getPath(ctx) // note: we could just use the r.RequestURI which is the original one, // but some users may need the stripped one (on HandleDir). } if l.config.Status { status = strconv.Itoa(ctx.GetStatusCode()) } var message any if ctxKeys := l.config.MessageContextKeys; len(ctxKeys) > 0 { for _, key := range ctxKeys { msg := ctx.Values().Get(key) if message == nil { message = msg } else { message = fmt.Sprintf(" %v %v", message, msg) } } } var headerMessage any if headerKeys := l.config.MessageHeaderKeys; len(headerKeys) > 0 { for _, key := range headerKeys { msg := ctx.GetHeader(key) if headerMessage == nil { headerMessage = msg } else { headerMessage = fmt.Sprintf(" %v %v", headerMessage, msg) } } } // print the logs if logFunc := l.config.LogFunc; logFunc != nil { logFunc(endTime, latency, status, ip, method, path, message, headerMessage) return } else if logFuncCtx := l.config.LogFuncCtx; logFuncCtx != nil { logFuncCtx(ctx, latency) return } // no new line, the framework's logger is responsible how to render each log. line := fmt.Sprintf("%v %4v %s %s %s", status, latency, ip, method, path) if message != nil { line += fmt.Sprintf(" %v", message) } if headerMessage != nil { line += fmt.Sprintf(" %v", headerMessage) } if context.StatusCodeNotSuccessful(ctx.GetStatusCode()) { ctx.Application().Logger().Warn(line) } else { ctx.Application().Logger().Info(line) } if l.config.TraceRoute && ctx.GetCurrentRoute() != nil /* it is nil on unhandled error codes */ { // Get the total length of handlers and see if all are executed. // Note(@kataras): we get those after handler executed, because // filters (and overlap) feature will set the handlers on router build // state to fullfil their needs. And we need to respect // any dev's custom SetHandlers&Do actions too so we don't give false info. // if n, idx := len(ctx.Handlers()), ctx.HandlerIndex(-1); idx < n-1 { // // } // Let's pass it into the Trace function itself which will "mark" // every handler that is eventually executed. // Note that if StopExecution is called, the index is always -1, // so no "mark" signs will be printed at all <- this can be fixed by introducing a new ctx field. ctx.GetCurrentRoute().Trace(ctx.Application().Logger().Printer, ctx.HandlerIndex(-1)) } } ================================================ FILE: middleware/methodoverride/methodoverride.go ================================================ package methodoverride import ( stdContext "context" "net/http" "strings" "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/core/router" ) func init() { context.SetHandlerName("iris/middleware/methodoverride.*", "iris.methodoverride") } type options struct { getters []GetterFunc methods []string saveOriginalMethodContextKey any // if not nil original value will be saved. } func (o *options) configure(opts ...Option) { for _, opt := range opts { opt(o) } } func (o *options) canOverride(method string) bool { for _, s := range o.methods { if s == method { return true } } return false } func (o *options) get(w http.ResponseWriter, r *http.Request) string { for _, getter := range o.getters { if v := getter(w, r); v != "" { return strings.ToUpper(v) } } return "" } // Option sets options for a fresh method override wrapper. // See `New` package-level function for more. type Option func(*options) // Methods can be used to add methods that can be overridden. // Defaults to "POST". func Methods(methods ...string) Option { for i, s := range methods { methods[i] = strings.ToUpper(s) } return func(opts *options) { opts.methods = append(opts.methods, methods...) } } // SaveOriginalMethod will save the original method // on Context.Request().Context().Value(requestContextKey). // // Defaults to nil, don't save it. func SaveOriginalMethod(requestContextKey any) Option { return func(opts *options) { if requestContextKey == nil { opts.saveOriginalMethodContextKey = nil } opts.saveOriginalMethodContextKey = requestContextKey } } // GetterFunc is the type signature for declaring custom logic // to extract the method name which a POST request will be replaced with. type GetterFunc func(http.ResponseWriter, *http.Request) string // Getter sets a custom logic to use to extract the method name // to override the POST method with. // Defaults to nil. func Getter(customFunc GetterFunc) Option { return func(opts *options) { opts.getters = append(opts.getters, customFunc) } } // Headers that client can send to specify a method // to override the POST method with. // // Defaults to: // X-HTTP-Method // X-HTTP-Method-Override // X-Method-Override func Headers(headers ...string) Option { getter := func(w http.ResponseWriter, r *http.Request) string { for _, s := range headers { if v := r.Header.Get(s); v != "" { w.Header().Add("Vary", s) return v } } return "" } return Getter(getter) } // FormField specifies a form field to use to determinate the method // to override the POST method with. // // Example Field: // // // Defaults to: "_method". func FormField(fieldName string) Option { return FormFieldWithConf(fieldName, nil) } // FormFieldWithConf same as `FormField` but it accepts the application's // configuration to parse the form based on the app core configuration. func FormFieldWithConf(fieldName string, conf context.ConfigurationReadOnly) Option { var ( postMaxMemory int64 = 32 << 20 // 32 MB resetBody = false ) if conf != nil { postMaxMemory = conf.GetPostMaxMemory() resetBody = conf.GetDisableBodyConsumptionOnUnmarshal() } getter := func(w http.ResponseWriter, r *http.Request) string { return context.FormValueDefault(r, fieldName, "", postMaxMemory, resetBody) } return Getter(getter) } // Query specifies a url parameter name to use to determinate the method // to override the POST methos with. // // Example URL Query string: // http://localhost:8080/path?_method=DELETE // // Defaults to: "_method". func Query(paramName string) Option { getter := func(w http.ResponseWriter, r *http.Request) string { return r.URL.Query().Get(paramName) } return Getter(getter) } // Only clears all default or previously registered values // and uses only the "o" option(s). // // The default behavior is to check for all the following by order: // headers, form field, query string // and any custom getter (if set). // Use this method to override that // behavior and use only the passed option(s) // to determinate the method to override with. // // Use cases: // // 1. When need to check only for headers and ignore other fields: // New(Only(Headers("X-Custom-Header"))) // // 2. When need to check only for (first) form field and (second) custom getter: // New(Only(FormField("fieldName"), Getter(...))) func Only(o ...Option) Option { return func(opts *options) { opts.getters = opts.getters[0:0] opts.configure(o...) } } // New returns a new method override wrapper // which can be registered with `Application.WrapRouter`. // // Use this wrapper when you expecting clients // that do not support certain HTTP operations such as DELETE or PUT for security reasons. // This wrapper will accept a method, based on criteria, to override the POST method with. // // Read more at: // https://github.com/kataras/iris/issues/1325 func New(opt ...Option) router.WrapperFunc { opts := new(options) // Default values. opts.configure( Methods(http.MethodPost), Headers("X-HTTP-Method", "X-HTTP-Method-Override", "X-Method-Override"), FormField("_method"), Query("_method"), ) opts.configure(opt...) return func(w http.ResponseWriter, r *http.Request, proceed http.HandlerFunc) { originalMethod := strings.ToUpper(r.Method) if opts.canOverride(originalMethod) { newMethod := opts.get(w, r) if newMethod != "" { if opts.saveOriginalMethodContextKey != nil { r = r.WithContext(stdContext.WithValue(r.Context(), opts.saveOriginalMethodContextKey, originalMethod)) } r.Method = newMethod } } proceed(w, r) } } ================================================ FILE: middleware/methodoverride/methodoverride_test.go ================================================ package methodoverride_test import ( "testing" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/httptest" "github.com/kataras/iris/v12/middleware/methodoverride" ) func TestMethodOverrideWrapper(t *testing.T) { app := iris.New() mo := methodoverride.New( // Defaults to nil. // methodoverride.SaveOriginalMethod("_originalMethod"), // Default values. // // methodoverride.Methods(http.MethodPost), // methodoverride.Headers("X-HTTP-Method", "X-HTTP-Method-Override", "X-Method-Override"), // methodoverride.FormField("_method"), // methodoverride.Query("_method"), ) // Register it with `WrapRouter`. app.WrapRouter(mo) var ( expectedDelResponse = "delete resp" expectedPostResponse = "post resp" ) app.Post("/path", func(ctx iris.Context) { ctx.WriteString(expectedPostResponse) }) app.Delete("/path", func(ctx iris.Context) { ctx.WriteString(expectedDelResponse) }) app.Delete("/path2", func(ctx iris.Context) { _, err := ctx.Writef("%s%s", expectedDelResponse, ctx.Request().Context().Value("_originalMethod")) if err != nil { t.Fatal(err) } }) e := httptest.New(t, app) // Test headers. e.POST("/path").WithHeader("X-HTTP-Method", iris.MethodDelete).Expect(). Status(iris.StatusOK).Body().IsEqual(expectedDelResponse) e.POST("/path").WithHeader("X-HTTP-Method-Override", iris.MethodDelete).Expect(). Status(iris.StatusOK).Body().IsEqual(expectedDelResponse) e.POST("/path").WithHeader("X-Method-Override", iris.MethodDelete).Expect(). Status(iris.StatusOK).Body().IsEqual(expectedDelResponse) // Test form field value. e.POST("/path").WithFormField("_method", iris.MethodDelete).Expect(). Status(iris.StatusOK).Body().IsEqual(expectedDelResponse) // Test URL Query (although it's the same as form field in this case). e.POST("/path").WithQuery("_method", iris.MethodDelete).Expect(). Status(iris.StatusOK).Body().IsEqual(expectedDelResponse) // Test saved original method and // Test without registered "POST" route. e.POST("/path2").WithQuery("_method", iris.MethodDelete).Expect(). Status(iris.StatusOK).Body().IsEqual(expectedDelResponse + iris.MethodPost) // Test simple POST request without method override fields. e.POST("/path").Expect().Status(iris.StatusOK).Body().IsEqual(expectedPostResponse) // Test simple DELETE request. e.DELETE("/path").Expect().Status(iris.StatusOK).Body().IsEqual(expectedDelResponse) } ================================================ FILE: middleware/modrevision/modrevision.go ================================================ package modrevision import ( "fmt" "strings" "time" "github.com/kataras/iris/v12/context" ) func init() { context.SetHandlerName("iris/middleware/modrevision.*", "iris.modrevision") } // Options holds the necessary values to render the server name, environment and build information. // See the `New` package-level function. type Options struct { // The ServerName, e.g. Iris Server. ServerName string // The Environment, e.g. development. Env string // The Developer, e.g. kataras. Developer string // True to display the build time as unix (seconds). UnixTime bool // A non nil time location value to customize the display of the build time. TimeLocation *time.Location } // New returns an Iris Handler which renders // the server name (env), build information (if available) // and an OK message. The handler displays simple debug information such as build commit id and time. // It does NOT render information about the Go language itself or any operating system confgiuration // for security reasons. // // Example Code: // // app.Get("/health", modrevision.New(modrevision.Options{ // ServerName: "Iris Server", // Env: "development", // Developer: "kataras", // TimeLocation: time.FixedZone("Greece/Athens", 7200), // })) func New(opts Options) context.Handler { buildTime, buildRevision := context.BuildTime, context.BuildRevision if opts.UnixTime { if t, err := time.Parse(time.RFC3339, buildTime); err == nil { buildTime = fmt.Sprintf("%d", t.Unix()) } } else if opts.TimeLocation != nil { if t, err := time.Parse(time.RFC3339, buildTime); err == nil { buildTime = t.In(opts.TimeLocation).String() } } var buildInfo string if buildInfo = opts.ServerName; buildInfo != "" { if env := opts.Env; env != "" { buildInfo += fmt.Sprintf(" (%s)", env) } } if buildRevision != "" && buildTime != "" { buildTitle := ">>>> build" tab := strings.Repeat(" ", len(buildTitle)) buildInfo += fmt.Sprintf("\n\n%s\n%[2]srevision %[3]s\n%[2]sbuildtime %[4]s\n%[2]sdeveloper %[5]s", buildTitle, tab, buildRevision, buildTime, opts.Developer) } contents := []byte(buildInfo) if len(contents) > 0 { contents = append(contents, []byte("\n\nOK")...) } else { contents = []byte("OK") } return func(ctx *context.Context) { ctx.Write(contents) } } ================================================ FILE: middleware/monitor/expvar_uint64.go ================================================ package monitor import ( "expvar" "strconv" "sync/atomic" ) // Uint64 completes the expvar metric interface, holds an uint64 value. type Uint64 struct { value uint64 } // Set sets v to value. func (v *Uint64) Set(value uint64) { atomic.StoreUint64(&v.value, value) } // Value returns the underline uint64 value. func (v *Uint64) Value() uint64 { return atomic.LoadUint64(&v.value) } // String returns the text representation of the underline uint64 value. func (v *Uint64) String() string { return strconv.FormatUint(atomic.LoadUint64(&v.value), 10) } func newUint64(name string) *Uint64 { v := new(Uint64) expvar.Publish(name, v) return v } ================================================ FILE: middleware/monitor/monitor.go ================================================ package monitor import ( "bytes" "fmt" "os" "time" "github.com/kataras/iris/v12/context" "github.com/shirou/gopsutil/v3/process" ) func init() { context.SetHandlerName("iris/middleware/monitor.*", "iris.monitor") } // Options holds the optional fields for the Monitor structure. type Options struct { // Optional process id, defaults to the current one. PID int32 `json:"pid" yaml:"PID"` RefreshInterval time.Duration `json:"refresh_interval" yaml:"RefreshInterval"` ViewRefreshInterval time.Duration `json:"view_refresh_interval" yaml:"ViewRefreshInterval"` // If more than zero enables line animation. Defaults to zero. ViewAnimationInterval time.Duration `json:"view_animation_interval" yaml:"ViewAnimationInterval"` // The title of the monitor HTML document. ViewTitle string `json:"view_title" yaml:"ViewTitle"` } // Monitor tracks and renders the server's process and operating system statistics. // // Look its `Stats` and `View` methods. // Initialize with the `New` package-level function. type Monitor struct { opts Options Holder *StatsHolder viewBody []byte } // New returns a new Monitor. // Metrics stored through expvar standard package: // - pid_cpu // - pid_ram // - pid_conns // - os_cpu // - os_ram // - os_total_ram // - os_load_avg // - os_conns // // Check https://github.com/iris-contrib/middleware/tree/master/expmetric // which can be integrated with datadog or other platforms. func New(opts Options) *Monitor { if opts.PID == 0 { opts.PID = int32(os.Getpid()) } if opts.RefreshInterval <= 0 { opts.RefreshInterval = 2 * opts.RefreshInterval } if opts.ViewRefreshInterval <= 0 { opts.ViewRefreshInterval = opts.RefreshInterval } viewRefreshIntervalBytes := []byte(fmt.Sprintf("%d", opts.ViewRefreshInterval.Milliseconds())) viewBody := bytes.Replace(defaultViewBody, viewRefreshIntervalTmplVar, viewRefreshIntervalBytes, 1) viewAnimationIntervalBytes := []byte(fmt.Sprintf("%d", opts.ViewAnimationInterval.Milliseconds())) viewBody = bytes.Replace(viewBody, viewAnimationIntervalTmplVar, viewAnimationIntervalBytes, 2) viewTitleBytes := []byte(opts.ViewTitle) viewBody = bytes.Replace(viewBody, viewTitleTmplVar, viewTitleBytes, 2) proc, err := process.NewProcess(opts.PID) if err != nil { panic(err) } sh := startNewStatsHolder(proc, opts.RefreshInterval) m := &Monitor{ opts: opts, Holder: sh, viewBody: viewBody, } return m } // Stop terminates the retrieve stats loop for // the process and the operating system statistics. // No other monitor instance should be initialized after the first Stop call. func (m *Monitor) Stop() { m.Holder.Stop() } // Stats sends the stats as json. func (m *Monitor) Stats(ctx *context.Context) { ctx.JSON(m.Holder.GetStats()) } // View renders a default view for the stats. func (m *Monitor) View(ctx *context.Context) { ctx.ContentType("text/html") ctx.Write(m.viewBody) } ================================================ FILE: middleware/monitor/stats.go ================================================ package monitor import ( "expvar" "sync" "time" "github.com/shirou/gopsutil/v3/cpu" "github.com/shirou/gopsutil/v3/load" "github.com/shirou/gopsutil/v3/mem" "github.com/shirou/gopsutil/v3/net" "github.com/shirou/gopsutil/v3/process" ) // Stats holds the process and operating system statistics values. // // Note that each statistic has its own expvar metric that you can use // to render e.g. through statsd. Available values: // * pid_cpu // * pid_ram // * pid_conns // * os_cpu // * os_ram // * os_total_ram // * os_load_avg // * os_conns type Stats struct { PIDCPU float64 `json:"pid_cpu" yaml:"PIDCPU"` PIDRAM uint64 `json:"pid_ram" yaml:"PIDRAM"` PIDConns int64 `json:"pid_conns" yaml:"PIDConns"` OSCPU float64 `json:"os_cpu" yaml:"OSCPU"` OSRAM uint64 `json:"os_ram" yaml:"OSRAM"` OSTotalRAM uint64 `json:"os_total_ram" yaml:"OSTotalRAM"` OSLoadAvg float64 `json:"os_load_avg" yaml:"OSLoadAvg"` OSConns int64 `json:"os_conns" yaml:"OSConns"` } // StatsHolder holds and refreshes the statistics. type StatsHolder struct { proc *process.Process stats *Stats mu sync.RWMutex started bool closeCh chan struct{} errCh chan error } func startNewStatsHolder(proc *process.Process, refreshInterval time.Duration) *StatsHolder { sh := newStatsHolder(proc) sh.start(refreshInterval) return sh } func newStatsHolder(proc *process.Process) *StatsHolder { sh := &StatsHolder{ proc: proc, stats: new(Stats), closeCh: make(chan struct{}), errCh: make(chan error, 1), } return sh } // Err returns a read-only channel which may be filled with errors // came from the refresh stats operation. func (sh *StatsHolder) Err() <-chan error { return sh.errCh } // Stop terminates the routine retrieves the stats. // Note that no other monitor can be initialized after Stop. func (sh *StatsHolder) Stop() { if !sh.started { return } sh.closeCh <- struct{}{} sh.started = false } func (sh *StatsHolder) start(refreshInterval time.Duration) { if sh.started { return } sh.started = true once.Do(func() { go func() { ticker := time.NewTicker(refreshInterval) defer ticker.Stop() for { select { case <-sh.closeCh: // close(sh.errCh) return case <-ticker.C: err := refresh(sh.proc) if err != nil { // push the error to the channel and continue the execution, // the only way to stop it is through its "Stop" method. sh.errCh <- err } } } }() }) } var ( once = new(sync.Once) metricPidCPU = expvar.NewFloat("pid_cpu") metricPidRAM = newUint64("pid_ram") metricPidConns = expvar.NewInt("pid_conns") metricOsCPU = expvar.NewFloat("os_cpu") metricOsRAM = newUint64("os_ram") metricOsTotalRAM = newUint64("os_total_ram") metricOsLoadAvg = expvar.NewFloat("os_load_avg") metricOsConns = expvar.NewInt("os_conns") ) // refresh updates the process and operating system statistics. func refresh(proc *process.Process) error { // Collect the stats. // // Process. pidCPU, err := proc.CPUPercent() if err != nil { return err } pidRAM, err := proc.MemoryInfo() if err != nil { return err } pidConns, err := net.ConnectionsPid("tcp", proc.Pid) if err != nil { return err } // Operating System. osCPU, err := cpu.Percent(0, false) if err != nil { return err } osRAM, err := mem.VirtualMemory() if err != nil { return err } osLoadAvg, err := load.Avg() if err != nil { return err } osConns, err := net.Connections("tcp") if err != nil { return err } // Update the fields. // // Process. metricPidCPU.Set(pidCPU / 10) metricPidRAM.Set(pidRAM.RSS) metricPidConns.Set(int64(len(pidConns))) // Operating System. if len(osCPU) > 0 { metricOsCPU.Set(osCPU[0]) } metricOsRAM.Set(osRAM.Used) metricOsTotalRAM.Set(osRAM.Total) metricOsLoadAvg.Set(osLoadAvg.Load1) metricOsConns.Set(int64(len(osConns))) return nil } // GetStats returns a copy of the latest stats available. func (sh *StatsHolder) GetStats() Stats { sh.mu.Lock() statsCopy := Stats{ PIDCPU: metricPidCPU.Value(), PIDRAM: metricPidRAM.Value(), PIDConns: metricPidConns.Value(), OSCPU: metricOsCPU.Value(), OSRAM: metricOsRAM.Value(), OSTotalRAM: metricOsTotalRAM.Value(), OSLoadAvg: metricOsLoadAvg.Value(), OSConns: metricOsConns.Value(), } sh.mu.Unlock() return statsCopy } ================================================ FILE: middleware/monitor/view.go ================================================ package monitor var ( viewRefreshIntervalTmplVar = []byte(`{{.ViewRefreshInterval}}`) viewAnimationIntervalTmplVar = []byte(`{{.ViewAnimationInterval}}`) viewTitleTmplVar = []byte(`{{.ViewTitle}}`) defaultViewBody = []byte(`` + string(viewTitleTmplVar) + `

      ` + string(viewTitleTmplVar) + `

      CPU Usage

      0.00%

      Memory Usage

      0.00 MB

      Response Time

      0ms

      Open Connections

      0

      `) ) ================================================ FILE: middleware/pprof/pprof.go ================================================ // Package pprof provides native pprof support via middleware. See _examples/pprof package pprof import ( "html/template" "net/http/pprof" rpprof "runtime/pprof" "sort" "github.com/kataras/iris/v12/context" ) func init() { context.SetHandlerName("iris/middleware/pprof.*", "iris.profiling") /* for our readers: apps.OnApplicationRegistered(func(app *iris.Application) { app.Any("/debug/pprof/cmdline", iris.FromStd(pprof.Cmdline)) app.Any("/debug/pprof/profile", iris.FromStd(pprof.Profile)) app.Any("/debug/pprof/symbol", iris.FromStd(pprof.Symbol)) app.Any("/debug/pprof/trace", iris.FromStd(pprof.Trace)) app.Any("/debug/pprof /debug/pprof/{action:string}", New()) }) */ } // net/http/pprof copy: var profileDescriptions = map[string]string{ "allocs": "A sampling of all past memory allocations", "block": "Stack traces that led to blocking on synchronization primitives", "cmdline": "The command line invocation of the current program", "goroutine": "Stack traces of all current goroutines", "heap": "A sampling of memory allocations of live objects. You can specify the gc GET parameter to run GC before taking the heap sample.", "mutex": "Stack traces of holders of contended mutexes", "profile": "CPU profile. You can specify the duration in the seconds GET parameter. After you get the profile file, use the go tool pprof command to investigate the profile.", "threadcreate": "Stack traces that led to the creation of new OS threads", "trace": "A trace of execution of the current program. You can specify the duration in the seconds GET parameter. After you get the trace file, use the go tool trace command to investigate the trace.", } // New returns a new pprof (profile, cmdline, symbol, goroutine, heap, threadcreate, debug/block) Middleware. // Note: Route MUST have the last named parameter wildcard named '{action:path}'. // Example: // // app.HandleMany("GET", "/debug/pprof /debug/pprof/{action:path}", pprof.New()) func New() context.Handler { return func(ctx *context.Context) { if action := ctx.Params().Get("action"); action != "" { pprof.Handler(action).ServeHTTP(ctx.ResponseWriter(), ctx.Request()) return } ctx.Header("X-Content-Type-Options", "nosniff") ctx.Header("Content-Type", "text/html; charset=utf-8") type profile struct { Name string Href string Desc string Count int } type page struct { Path string Profiles []profile } var profiles []profile for _, p := range rpprof.Profiles() { profiles = append(profiles, profile{ Name: p.Name(), Href: p.Name() + "?debug=1", Desc: profileDescriptions[p.Name()], Count: p.Count(), }) } // Adding other profiles exposed from within this package for _, p := range []string{"cmdline", "profile", "trace"} { profiles = append(profiles, profile{ Name: p, Href: p, Desc: profileDescriptions[p], }) } sort.Slice(profiles, func(i, j int) bool { return profiles[i].Name < profiles[j].Name }) if err := indexTmpl.Execute(ctx, page{ Path: ctx.Path(), Profiles: profiles, }); err != nil { ctx.Application().Logger().Error(err) } } } var indexTmpl = template.Must(template.New("index").Parse(` {{.Path}} {{.Path}}

      Types of profiles available: {{ $path := .Path}} {{range .Profiles}} {{end}}
      CountProfile
      {{.Count}}{{.Name}}
      full goroutine stack dump

      Profile Descriptions:

        {{range .Profiles}}
      • {{.Name}}:
        {{.Desc}}
      • {{end}}

      `)) ================================================ FILE: middleware/rate/rate.go ================================================ // Package rate implements rate limiter for Iris client requests. // Example can be found at: _examples/request-ratelimit/main.go. package rate import ( "math" "sync" "time" "github.com/kataras/iris/v12/context" "golang.org/x/time/rate" ) func init() { context.SetHandlerName("iris/middleware/rate.(*Limiter).serveHTTP-fm", "iris.ratelimit") } // Option declares a function which can be passed on `Limit` package-level // to modify its internal fields. Available Options are: // * ExceedHandler // * ClientData // * PurgeEvery type Option func(*Limiter) // ExceedHandler is an `Option` that can be passed at the `Limit` package-level function. // It accepts a handler that will be executed every time a client tries to reach a page/resource // which is not accessible for that moment. func ExceedHandler(handler context.Handler) Option { return func(l *Limiter) { l.exceedHandler = handler } } // ClientData is an `Option` that can be passed at the `Limit` package-level function. // It accepts a function which provides the Iris Context and should return custom data // that will be stored to the Client and be retrieved as `Get(ctx).Client.Data` later on. func ClientData(clientDataFunc func(ctx *context.Context) any) Option { return func(l *Limiter) { l.clientDataFunc = clientDataFunc } } // PurgeEvery is an `Option` that can be passed at the `Limit` package-level function. // This function will check for old entries and remove them. // // E.g. Limit(..., PurgeEvery(time.Minute, 5*time.Minute)) to // check every 1 minute if a client's last visit was 5 minutes ago ("old" entry) // and remove it from the memory. func PurgeEvery(every time.Duration, maxLifetime time.Duration) Option { condition := func(c *Client) bool { // for a custom purger the end-developer may use the c.Data filled from a `ClientData` option. return time.Since(c.LastSeen()) > maxLifetime } return func(l *Limiter) { go func() { for { time.Sleep(every) l.Purge(condition) } }() } } // Every converts a minimum time interval between events to a limit. // Usage: Limit(Every(1*time.Minute), 3, options...) func Every(interval time.Duration) float64 { if interval <= 0 { return Inf } return 1 / interval.Seconds() } type ( // Limiter is featured with the necessary functions to limit requests per second. // It has a single exported method `Purge` which helps to manually remove // old clients from the memory. Limiter is not exposed by a function, // callers should use it inside an `Option` for the `Limit` package-level function. Limiter struct { clientDataFunc func(ctx *context.Context) any // fill the Client's Data field. exceedHandler context.Handler // when too many requests. limit rate.Limit burstSize int clients map[string]*Client mu sync.RWMutex // mutex for clients. } // Client holds some request information and the rate limiter itself. // It can be retrieved by the `Get` package-level function. // It can be used to manually add RateLimit response headers. Client struct { ID string Data any Limiter *rate.Limiter lastSeen time.Time mu sync.RWMutex // mutex for lastSeen. } ) // Inf is the infinite rate limit; it allows all events (even if burst is zero). const Inf = math.MaxFloat64 // Limit returns a new rate limiter handler that allows requests up to rate "limit" and permits // bursts of at most "burst" tokens. See `rate.SetKey(ctx, key string)` and `rate.Get` too. // // E.g. Limit(1, 5) to allow 1 request per second, with a maximum burst size of 5. // // See `ExceedHandler`, `ClientData` and `PurgeEvery` for the available "options". func Limit(limit float64, burst int, options ...Option) context.Handler { l := &Limiter{ clients: make(map[string]*Client), limit: rate.Limit(limit), burstSize: burst, exceedHandler: func(ctx *context.Context) { ctx.StopWithStatus(429) // Too Many Requests. }, } for _, opt := range options { opt(l) } return l.serveHTTP } // Purge removes client entries from the memory based on the given "condition". func (l *Limiter) Purge(condition func(*Client) bool) { l.mu.Lock() for id, client := range l.clients { if condition(client) { delete(l.clients, id) } } l.mu.Unlock() } func (l *Limiter) serveHTTP(ctx *context.Context) { id := getIdentifier(ctx) l.mu.RLock() client, ok := l.clients[id] l.mu.RUnlock() if !ok { client = &Client{ ID: id, Limiter: rate.NewLimiter(l.limit, l.burstSize), } if l.clientDataFunc != nil { client.Data = l.clientDataFunc(ctx) } // if l.store(ctx, client) { // ^ no, let's keep it simple. l.mu.Lock() l.clients[id] = client l.mu.Unlock() } client.mu.Lock() client.lastSeen = time.Now() client.mu.Unlock() ctx.Values().Set(clientContextKey, client) if client.Limiter.Allow() { ctx.Next() return } if l.exceedHandler != nil { l.exceedHandler(ctx) } } const identifierContextKey = "iris.ratelimit.identifier" // SetIdentifier can be called manually from a handler or a middleare // to change the identifier per client. The default key for a client is its Remote IP. func SetIdentifier(ctx *context.Context, key string) { ctx.Values().Set(identifierContextKey, key) } func getIdentifier(ctx *context.Context) string { if entry, ok := ctx.Values().GetEntry(identifierContextKey); ok { return entry.ValueRaw.(string) } return ctx.RemoteAddr() } const clientContextKey = "iris.ratelimit.client" // Get returns the current rate limited `Client`. // Use it when you want to log or add response headers based on the current request limitation. // // You can read more about X-RateLimit response headers at: // https://tools.ietf.org/id/draft-polli-ratelimit-headers-00.html. // A good example of that is the GitHub API itself: https://developer.github.com/v3/#rate-limiting func Get(ctx *context.Context) *Client { if v := ctx.Values().Get(clientContextKey); v != nil { if c, ok := v.(*Client); ok { return c } } return nil } // LastSeen reports the last Client's visit. func (c *Client) LastSeen() time.Time { c.mu.RLock() t := c.lastSeen c.mu.RUnlock() return t } // TokensFromDuration is a unit conversion function from a time duration to the number of tokens // which could be accumulated during that duration at a rate of limit tokens per second. func (c *Client) TokensFromDuration(d time.Duration) float64 { // rate.go#tokensFromDuration limit := float64(c.Limiter.Limit()) sec := float64(d/time.Second) * limit nsec := float64(d%time.Second) * limit return sec + nsec/1e9 } // DurationFromTokens is a unit conversion function from the number of tokens to the duration // of time it takes to accumulate them at a rate of limit tokens per second. func (c *Client) DurationFromTokens(tokens float64) time.Duration { // rate.go#durationFromTokens seconds := tokens / float64(c.Limiter.Limit()) return time.Nanosecond * time.Duration(1e9*seconds) } ================================================ FILE: middleware/recaptcha/recaptcha.go ================================================ package recaptcha import ( "encoding/json" "fmt" "io" "net/url" "time" "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/core/netutil" ) func init() { context.SetHandlerName("iris/middleware/recaptcha.*", "iris.reCAPTCHA") } const ( // responseFormValue = "g-recaptcha-response" apiURL = "https://www.google.com/recaptcha/api/siteverify" ) // Response is the google's recaptcha response as JSON. type Response struct { ChallengeTS time.Time `json:"challenge_ts"` Hostname string `json:"hostname"` ErrorCodes []string `json:"error-codes"` Success bool `json:"success"` } // Client is the default `net/http#Client` instance which // is used to send requests to the Google API. // // Change Client only if you know what you're doing. var Client = netutil.Client(time.Duration(20 * time.Second)) // New accepts the google's recaptcha secret key and returns // a middleware that verifies the request by sending a response to the google's API(V2-latest). // Secret key can be obtained by https://www.google.com/recaptcha. // // Used for communication between your site and Google. Be sure to keep it a secret. // // Use `SiteVerify` to verify a request inside another handler if needed. func New(secret string) context.Handler { return func(ctx *context.Context) { if SiteVerify(ctx, secret).Success { ctx.Next() } } } // SiteVerify accepts context and the secret key(https://www.google.com/recaptcha) and // returns the google's recaptcha response, if `response.Success` is true then validation passed. // // Use `New` for middleware use instead. func SiteVerify(ctx *context.Context, secret string) (response Response) { generatedResponseID := ctx.FormValue("g-recaptcha-response") if generatedResponseID == "" { response.ErrorCodes = append(response.ErrorCodes, "form value[g-recaptcha-response] is empty") return } r, err := Client.PostForm(apiURL, url.Values{ "secret": {secret}, "response": {generatedResponseID}, // optional: let's no track our users "remoteip": {ctx.RemoteAddr()}, }, ) if err != nil { response.ErrorCodes = append(response.ErrorCodes, err.Error()) return } body, err := io.ReadAll(r.Body) r.Body.Close() if err != nil { response.ErrorCodes = append(response.ErrorCodes, err.Error()) return } err = json.Unmarshal(body, &response) if err != nil { response.ErrorCodes = append(response.ErrorCodes, err.Error()) return } return response } var recaptchaForm = `
      ` // GetFormHTML can be used on pages where empty form // is enough to verify that the client is not a "robot". // i.e: GetFormHTML("public_key", "/contact") // will return form tag which imports the google API script, // with a simple submit button where redirects to the // "postActionRelativePath". // // The "postActionRelativePath" MUST use the `SiteVerify` or // followed by the `New()`'s context.Handler (with the secret key this time) // in order to validate if the recaptcha verified. // // The majority of applications will use a custom form, // this function is here for ridiculous simple use cases. // // Example Code: // // Method: "POST" | Path: "/contact" // // func postContact(ctx *context.Context) { // // [...] // response := recaptcha.SiteVerify(ctx, recaptchaSecret) // // if response.Success { // // [your action here, i.e sendEmail(...)] // } // // ctx.JSON(response) // } // // Method: "GET" | Path: "/contact" // // func getContact(ctx *context.Context) { // // render the recaptcha form // ctx.HTML(recaptcha.GetFormHTML(recaptchaPublic, "/contact")) // } func GetFormHTML(dataSiteKey string, postActionRelativePath string) string { return fmt.Sprintf(recaptchaForm, postActionRelativePath, dataSiteKey) } ================================================ FILE: middleware/recover/recover.go ================================================ // Package recover provides recovery for specific routes or for the whole app via middleware. See _examples/recover package recover import ( "fmt" "net/http/httputil" "runtime" "runtime/debug" "github.com/kataras/iris/v12/context" ) func init() { context.SetHandlerName("iris/middleware/recover.*", "iris.recover") // this name won't work because New() is a function that returns a handler. } // New returns a new recovery middleware, // it recovers from panics and logs the // panic message to the application's logger "Warn" level. func New() context.Handler { return func(ctx *context.Context) { defer func() { if err := PanicRecoveryError(ctx, recover()); err != nil { ctx.StopWithPlainError(500, err) ctx.Application().Logger().Warn(err.LogMessage()) } // else it's already handled. }() ctx.Next() } } // PanicRecoveryError returns a new ErrPanicRecovery error. func PanicRecoveryError(ctx *context.Context, err any) *context.ErrPanicRecovery { if recoveryErr, ok := ctx.IsRecovered(); ok { // If registered before any other recovery middleware, get its error. // Because of defer this will be executed last, after the recovery middleware in this case. return recoveryErr } if err == nil { return nil } else if ctx.IsStopped() { return nil } var callers []string for i := 2; ; /* 1 for New() 2 for NewPanicRecoveryError */ i++ { _, file, line, got := runtime.Caller(i) if !got { break } callers = append(callers, fmt.Sprintf("%s:%d", file, line)) } // get the list of registered handlers and the // handler which panic derived from. handlers := ctx.Handlers() handlersFileLines := make([]string, 0, len(handlers)) currentHandlerIndex := ctx.HandlerIndex(-1) currentHandlerFileLine := "???" for i, h := range ctx.Handlers() { file, line := context.HandlerFileLine(h) fileline := fmt.Sprintf("%s:%d", file, line) handlersFileLines = append(handlersFileLines, fileline) if i == currentHandlerIndex { currentHandlerFileLine = fileline } } // see accesslog.wasRecovered too. recoveryErr := &context.ErrPanicRecovery{ Cause: err, Callers: callers, Stack: debug.Stack(), RegisteredHandlers: handlersFileLines, CurrentHandlerFileLine: currentHandlerFileLine, CurrentHandlerName: ctx.HandlerName(), Request: getRequestLogs(ctx), } return recoveryErr } func getRequestLogs(ctx *context.Context) string { rawReq, _ := httputil.DumpRequest(ctx.Request(), false) return string(rawReq) } ================================================ FILE: middleware/requestid/requestid.go ================================================ package requestid import ( "crypto/sha256" "encoding/hex" "net/http/httputil" "github.com/kataras/iris/v12/context" "github.com/google/uuid" ) func init() { context.SetHandlerName("iris/middleware/requestid.*", "iris.request.id") } const xRequestIDHeaderKey = "X-Request-Id" // Generator defines the function which should extract or generate // a Request ID. See `DefaultGenerator` and `New` package-level functions. type Generator func(ctx *context.Context) string // DefaultGenerator is the default `Generator` that is used // when nil is passed on `New` package-level function. // It extracts the ID from the "X-Request-ID" request header value // or, if missing, it generates a new UUID(v4) and sets the header and context value. // // See `Get` package-level function too. var DefaultGenerator Generator = func(ctx *context.Context) string { id := ctx.ResponseWriter().Header().Get(xRequestIDHeaderKey) if id != "" { return id } id = ctx.GetHeader(xRequestIDHeaderKey) if id == "" { uid, err := uuid.NewRandom() if err != nil { ctx.StopWithStatus(500) return "" } id = uid.String() } ctx.Header(xRequestIDHeaderKey, id) return id } // HashGenerator uses the request's hash to generate a fixed-length Request ID. // Note that one or many requests may contain the same ID, so it's not unique. func HashGenerator(includeBody bool) Generator { return func(ctx *context.Context) string { ctx.Header(xRequestIDHeaderKey, Hash(ctx, includeBody)) return DefaultGenerator(ctx) } } // New returns a new request id middleware. // It optionally accepts an ID Generator. // The Generator can stop the handlers chain with an error or // return a valid ID (string). // If it's nil then the `DefaultGenerator` will be used instead. func New(generator ...Generator) context.Handler { gen := DefaultGenerator if len(generator) > 0 { gen = generator[0] } return func(ctx *context.Context) { if Get(ctx) != "" { ctx.Next() return } id := gen(ctx) if ctx.IsStopped() { // ctx.Next checks that // but we don't want to call SetID if generator failed. return } ctx.SetID(id) ctx.Next() } } // Get returns the Request ID or empty string. // // A shortcut of `context.GetID().(string)`. func Get(ctx *context.Context) string { v := ctx.GetID() if v != nil { if id, ok := v.(string); ok { return id } } return "" } // Hash returns the sha1 hash of the request. // It does not capture error, instead it returns an empty string. func Hash(ctx *context.Context, includeBody bool) string { h := sha256.New() // sha1 fits here as well. b, err := httputil.DumpRequest(ctx.Request(), includeBody) if err != nil { return "" } h.Write(b) return hex.EncodeToString(h.Sum(nil)) } ================================================ FILE: middleware/requestid/requestid_test.go ================================================ package requestid_test import ( "testing" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/httptest" "github.com/kataras/iris/v12/middleware/requestid" ) func TestRequestID(t *testing.T) { app := iris.New() h := func(ctx iris.Context) { ctx.WriteString(requestid.Get(ctx)) } def := app.Party("/default") { def.Use(requestid.New()) def.Get("/", h) } const expectedCustomID = "my_id" custom := app.Party("/custom") { customGen := func(ctx *context.Context) string { return expectedCustomID } custom.Use(requestid.New(customGen)) custom.Get("/", h) } const expectedErrMsg = "no id" customWithErr := app.Party("/custom_err") { customGen := func(ctx *context.Context) string { ctx.StopWithText(iris.StatusUnauthorized, expectedErrMsg) return "" } customWithErr.Use(requestid.New(customGen)) customWithErr.Get("/", h) } const expectedCustomIDFromOtherMiddleware = "my custom id" changeID := app.Party("/custom_change_id") { changeID.Use(func(ctx iris.Context) { ctx.SetID(expectedCustomIDFromOtherMiddleware) ctx.Next() }) changeID.Use(requestid.New()) changeID.Get("/", h) } const expectedClientSentID = "client sent id" clientSentID := app.Party("/client_id") { clientSentID.Use(requestid.New()) clientSentID.Get("/", h) } e := httptest.New(t, app) e.GET("/default").Expect().Status(httptest.StatusOK).Body().NotEmpty() e.GET("/custom").Expect().Status(httptest.StatusOK).Body().IsEqual(expectedCustomID) e.GET("/custom_err").Expect().Status(httptest.StatusUnauthorized).Body().IsEqual(expectedErrMsg) e.GET("/custom_change_id").Expect().Status(httptest.StatusOK).Body().IsEqual(expectedCustomIDFromOtherMiddleware) e.GET("/client_id").WithHeader("X-Request-Id", expectedClientSentID).Expect().Header("X-Request-Id").IsEqual(expectedClientSentID) } ================================================ FILE: middleware/rewrite/rewrite.go ================================================ package rewrite import ( "encoding/json" "fmt" "net/http" "os" "regexp" "strconv" "strings" "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/core/router" "github.com/kataras/golog" "gopkg.in/yaml.v3" ) // Options holds the developer input to customize // the redirects for the Rewrite Engine. // Look the `New` and `Load` package-level functions. type Options struct { // RedirectMatch accepts a slice of lines // of form: // REDIRECT_CODE PATH_PATTERN TARGET_PATH // Example: []{"301 /seo/(.*) /$1"}. RedirectMatch []string `json:"redirectMatch" yaml:"RedirectMatch"` // Root domain requests redirect automatically to primary subdomain. // Example: "www" to redirect always to www. // Note that you SHOULD NOT create a www subdomain inside the Iris Application. // This field takes care of it for you, the root application instance // will be used to serve the requests. PrimarySubdomain string `json:"primarySubdomain" yaml:"PrimarySubdomain"` } // LoadOptions loads rewrite Options from a system file. func LoadOptions(filename string) (opts Options) { ext := ".yml" if index := strings.LastIndexByte(filename, '.'); index > 1 && len(filename)-1 > index { ext = filename[index:] } f, err := os.Open(filename) if err != nil { panic("iris: rewrite: " + err.Error()) } defer f.Close() switch ext { case ".yaml", ".yml": err = yaml.NewDecoder(f).Decode(&opts) case ".json": err = json.NewDecoder(f).Decode(&opts) default: panic("iris: rewrite: unexpected file extension: " + filename) } if err != nil { panic("iris: rewrite: decode: " + err.Error()) } return } // Rewrite is a struct that represents a rewrite engine for Iris web framework. // It contains a slice of redirect rules, an options struct, a logger, and a domain validator function. // It provides methods to create, configure, and apply rewrite rules to HTTP requests and responses. // // Navigate through _examples/routing/rewrite for more. type Engine struct { redirects []*redirectMatch options Options logger *golog.Logger domainValidator func(string) bool } // Load decodes the "filename" options // and returns a new Rewrite Engine Router Wrapper. // It panics on errors. // Usage: // redirects := Load("redirects.yml") // app.WrapRouter(redirects) // See `New` too. func Load(filename string) router.WrapperFunc { opts := LoadOptions(filename) engine, err := New(opts) if err != nil { panic(err) } return engine.Rewrite } // New returns a new Rewrite Engine based on "opts". // It reports any parser error. // See its `Handler` or `Rewrite` methods. Depending // on the needs, select one. func New(opts Options) (*Engine, error) { redirects := make([]*redirectMatch, 0, len(opts.RedirectMatch)) for _, line := range opts.RedirectMatch { r, err := parseRedirectMatchLine(line) if err != nil { return nil, err } redirects = append(redirects, r) } if opts.PrimarySubdomain != "" && !strings.HasSuffix(opts.PrimarySubdomain, ".") { opts.PrimarySubdomain += "." // www -> www. } e := &Engine{ options: opts, redirects: redirects, domainValidator: func(root string) bool { return !strings.HasSuffix(root, localhost) }, } return e, nil } // SetLogger attachs a logger to the Rewrite Engine, // used only for debugging. // Defaults to nil. func (e *Engine) SetLogger(logger *golog.Logger) *Engine { e.logger = logger.Child(e).SetChildPrefix("rewrite") return e } // init the request logging with [DBUG]. func (e *Engine) initDebugf(format string, args ...any) { if e.logger == nil { return } e.logger.Debugf(format, args...) } var skipDBUGSpace = strings.Repeat(" ", 7) // continue debugging the same request with new lines and spacing, // easier to read. func (e *Engine) debugf(format string, args ...any) { if e.logger == nil || e.logger.Level < golog.DebugLevel { return } fmt.Fprintf(e.logger.Printer, skipDBUGSpace+format, args...) } // Handler is an Iris Handler that can be used as a router or party or route middleware. // For a global alternative, if you want to wrap the entire Iris Application // use the `Wrapper` instead. // Usage: // app.UseRouter(engine.Handler) func (e *Engine) Handler(ctx *context.Context) { e.Rewrite(ctx.ResponseWriter(), ctx.Request(), func(http.ResponseWriter, *http.Request) { ctx.Next() }) } const localhost = "localhost" // Rewrite is used to wrap the entire Iris Router. // Rewrite is a bit faster than Handler because it's executed // even before any route matched and it stops on redirect pattern match. // Use it to wrap the entire Iris Application, otherwise look `Handler` instead. // // Usage: // app.WrapRouter(engine.Rewrite). func (e *Engine) Rewrite(w http.ResponseWriter, r *http.Request, routeHandler http.HandlerFunc) { if primarySubdomain := e.options.PrimarySubdomain; primarySubdomain != "" { hostport := context.GetHost(r) root := context.GetDomain(hostport) e.initDebugf("Begin request: full host: %s and root domain: %s\n", hostport, root) // Note: // localhost and 127.0.0.1 are not supported for subdomain rewrite, by purpose, // use a virtual host instead. // GetDomain will return will return localhost or www.localhost // on expected loopbacks. if e.domainValidator(root) { root += getPort(hostport) subdomain := strings.TrimSuffix(hostport, root) e.debugf("Domain is not a loopback, requested subdomain: %s\n", subdomain) if subdomain == "" { // we are in root domain, full redirect to its primary subdomain. newHost := primarySubdomain + root e.debugf("Redirecting from root domain to: %s\n", newHost) r.Host = newHost r.URL.Host = newHost http.Redirect(w, r, r.URL.String(), http.StatusMovedPermanently) return } if subdomain == primarySubdomain { // keep root domain as the Host field inside the next handlers, // for consistently use and // to bypass the subdomain router (`routeHandler`) // do not return, redirects should be respected. rootHost := strings.TrimPrefix(hostport, subdomain) e.debugf("Request host field was modified to: %s. Proceed without redirection\n", rootHost) // modify those for the next redirects or the route handler. r.Host = rootHost r.URL.Host = rootHost } // maybe other subdomain or not at all, let's continue. } else { e.debugf("Primary subdomain is: %s but redirect response was not sent. Domain is a loopback?\n", primarySubdomain) } } for _, rd := range e.redirects { src := r.URL.Path if !rd.isRelativePattern { // don't change the request, use a full redirect. src = context.GetScheme(r) + context.GetHost(r) + r.URL.RequestURI() } if target, ok := rd.matchAndReplace(src); ok { if target == src { e.debugf("WARNING: source and target URLs match: %s\n", src) routeHandler(w, r) return } if rd.noRedirect { u, err := r.URL.Parse(target) if err != nil { http.Error(w, err.Error(), http.StatusMisdirectedRequest) return } e.debugf("No redirect: handle request: %s as: %s\n", r.RequestURI, u) r.URL = u routeHandler(w, r) return } if !rd.isRelativePattern { // this performs better, no need to check query or host, // the uri already built. e.debugf("Full redirect: from: %s to: %s\n", src, target) router.RedirectAbsolute(w, r, target, rd.code) } else { e.debugf("Path redirect: from: %s to: %s\n", src, target) http.Redirect(w, r, target, rd.code) } return } } routeHandler(w, r) } type redirectMatch struct { code int pattern *regexp.Regexp target string isRelativePattern bool noRedirect bool } func (r *redirectMatch) matchAndReplace(src string) (string, bool) { if r.pattern.MatchString(src) { if match := r.pattern.ReplaceAllString(src, r.target); match != "" { return match, true } } return "", false } func parseRedirectMatchLine(s string) (*redirectMatch, error) { parts := strings.Split(strings.TrimSpace(s), " ") if len(parts) != 3 { return nil, fmt.Errorf("redirect match: invalid line: %s", s) } codeStr, pattern, target := parts[0], parts[1], parts[2] for i, ch := range codeStr { if !isDigit(ch) { return nil, fmt.Errorf("redirect match: status code digits: %s [%d:%c]", codeStr, i, ch) } } code, err := strconv.Atoi(codeStr) if err != nil { // this should not happen, we check abt digit // and correctly position the error too but handle it. return nil, fmt.Errorf("redirect match: status code digits: %s: %v", codeStr, err) } regex := regexp.MustCompile(pattern) if regex.MatchString(target) { return nil, fmt.Errorf("redirect match: loop detected: pattern: %s vs target: %s", pattern, target) } v := &redirectMatch{ code: code, pattern: regex, target: target, noRedirect: code <= 0, isRelativePattern: pattern[0] == '/', // search by path. } return v, nil } func isDigit(ch rune) bool { return '0' <= ch && ch <= '9' } func getPort(hostport string) string { // returns :port, note that this is only called on non-loopbacks. if portIdx := strings.IndexByte(hostport, ':'); portIdx > 0 { return hostport[portIdx:] } return "" } ================================================ FILE: middleware/rewrite/rewrite_test.go ================================================ package rewrite import "testing" func TestRedirectMatch(t *testing.T) { tests := []struct { line string parseErr string inputs map[string]string // input, expected. Order should not matter. }{ { "301 /seo/(.*) /$1", "", map[string]string{ "/seo/path": "/path", }, }, { "301 /old(.*) /deprecated$1", "", map[string]string{ "/old": "/deprecated", "/old/any": "/deprecated/any", "/old/thing/here": "/deprecated/thing/here", }, }, { "301 /old(.*) /", "", map[string]string{ "/oldblabla": "/", "/old/any": "/", "/old/thing/here": "/", }, }, { "301 /old/(.*) /deprecated/$1", "", map[string]string{ "/old/": "/deprecated/", "/old/any": "/deprecated/any", "/old/thing/here": "/deprecated/thing/here", }, }, { "3d /seo/(.*) /$1", "redirect match: status code digits: 3d [1:d]", nil, }, { "301 /$1", "redirect match: invalid line: 301 /$1", nil, }, { "301 /* /$1", "redirect match: loop detected: pattern: /* vs target: /$1", nil, }, { "301 /* /", "redirect match: loop detected: pattern: /* vs target: /", nil, }, } for i, tt := range tests { r, err := parseRedirectMatchLine(tt.line) if err != nil { if tt.parseErr == "" { t.Fatalf("[%d] unexpected parse error: %v", i, err) } errStr := err.Error() if tt.parseErr != err.Error() { t.Fatalf("[%d] a parse error was expected but it differs: expected: %s but got: %s", i, tt.parseErr, errStr) } } else if tt.parseErr != "" { t.Fatalf("[%d] expected an error of: %s but got nil", i, tt.parseErr) } for input, expected := range tt.inputs { got, _ := r.matchAndReplace(input) if expected != got { t.Fatalf(`[%d:%s] expected: "%s" but got: "%s"`, i, input, expected, got) } } } } ================================================ FILE: mvc/aliases.go ================================================ package mvc import ( "github.com/kataras/iris/v12/hero" "github.com/kataras/iris/v12/versioning" ) type ( // Result is a type alias for the `hero#Result`, useful for output controller's methods. Result = hero.Result // Response is a type alias for the `hero#Response`, useful for output controller's methods. Response = hero.Response // View is a type alias for the `hero#View`, useful for output controller's methods. View = hero.View // Code is a type alias for the `hero#Code`, useful for // http error handling in controllers. // This can be one of the input parameters of the `Controller.HandleHTTPError`. Code = hero.Code // Err is a type alias for the `hero#Err`. // It is a special type for error stored in mvc responses or context. // It's used for a builtin dependency to map the error given by a previous // method or middleware. Err = hero.Err // DeprecationOptions describes the deprecation headers key-values. // Is a type alias for the `versioning#DeprecationOptions`. // // See `Deprecated` package-level option. DeprecationOptions = versioning.DeprecationOptions ) // Try is a type alias for the `hero#Try`, // useful to return a result based on two cases: failure(including panics) and a succeess. var Try = hero.Try ================================================ FILE: mvc/controller.go ================================================ package mvc import ( "fmt" "reflect" "strings" "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/core/router" "github.com/kataras/iris/v12/hero" "github.com/kataras/iris/v12/macro" ) // BaseController is the optional controller interface, if it's // completed by the end controller then the BeginRequest and EndRequest // are called between the controller's method responsible for the incoming request. type BaseController interface { BeginRequest(*context.Context) EndRequest(*context.Context) } type shared interface { Name() string Router() router.Party GetRoute(methodName string) *router.Route GetRoutes(methodName string) []*router.Route Handle(httpMethod, path, funcName string, middleware ...context.Handler) *router.Route HandleMany(httpMethod, path, funcName string, middleware ...context.Handler) []*router.Route } // BeforeActivation is being used as the only one input argument of a // `func(c *Controller) BeforeActivation(b mvc.BeforeActivation) {}`. // // It's being called before the controller's dependencies binding to the fields or the input arguments // but before server ran. // // It's being used to customize a controller if needed inside the controller itself, // it's called once per application. type BeforeActivation interface { shared Dependencies() *hero.Container } // AfterActivation is being used as the only one input argument of a // `func(c *Controller) AfterActivation(a mvc.AfterActivation) {}`. // // It's being called after the `BeforeActivation`, // and after controller's dependencies bind-ed to the fields or the input arguments but before server ran. // // It's being used to customize a controller if needed inside the controller itself, // it's called once per application. type AfterActivation interface { shared Singleton() bool DependenciesReadOnly() []*hero.Dependency } var ( _ BeforeActivation = (*ControllerActivator)(nil) _ AfterActivation = (*ControllerActivator)(nil) ) // IgnoreEmbeddedControllers is a global variable which indicates whether // the controller's method parser should skip converting embedded struct's methods to http handlers. // // If no global use is necessary, developers can do the same for individual controllers // through the `IgnoreEmbedded` Controller Option on `mvc.Application.Handle` method. // // Defaults to false. var IgnoreEmbeddedControllers = false // ControllerActivator returns a new controller type info description. // Its functionality can be overridden by the end-dev. type ControllerActivator struct { app *Application injector *hero.Struct // initRef BaseController // the BaseController as it's passed from the end-dev. Value reflect.Value // the BaseController's Value. Type reflect.Type // raw type of the BaseController (initRef). // FullName it's the last package path segment + "." + the Name. // i.e: if login-example/user/controller.go, the FullName is "user.Controller". fullName string // the already-registered routes, key = the controller's function name. // End-devs can change some properties of the *Route on the `BeforeActivator` by using the // `GetRoute/GetRoutes(functionName)`. routes map[string][]*router.Route skipMethodNames []string // BeginHandlers is a slice of middleware for this controller. // These handlers will be prependend to each one of // the route that this controller will register(Handle/HandleMany/struct methods) // to the targeted Party. // Look the `Use` method too. BeginHandlers context.Handlers // true if this controller listens and serves to websocket events. servesWebsocket bool // true to skip the internal "activate". activated bool } // NameOf returns the package name + the struct type's name, // it's used to take the full name of an Controller, the `ControllerActivator#Name`. func NameOf(v any) string { elemTyp := indirectType(reflect.ValueOf(v).Type()) typName := elemTyp.Name() pkgPath := elemTyp.PkgPath() fullname := pkgPath[strings.LastIndexByte(pkgPath, '/')+1:] + "." + typName return fullname } func newControllerActivator(app *Application, controller any) *ControllerActivator { if controller == nil { return nil } if c, ok := controller.(*ControllerActivator); ok { return c } typ := reflect.TypeOf(controller) c := &ControllerActivator{ // give access to the Router to the end-devs if they need it for some reason, // i.e register done handlers. app: app, Value: reflect.ValueOf(controller), Type: typ, // the full name of the controller: its type including the package path. fullName: NameOf(controller), // set some methods that end-dev cann't use accidentally // to register a route via the `Handle`, // all available exported and compatible methods // are being appended to the slice at the `parseMethods`, // if a new method is registered via `Handle` its function name // is also appended to that slice. routes: whatReservedMethods(typ), } if IgnoreEmbeddedControllers { c.SkipEmbeddedMethods() } return c } // It's a dynamic method, can be exist or not, it can accept input arguments // and can write through output values like any other dev-designed method. // See 'parseHTTPErrorMethod'. // Example at: _examples/mvc/error-handler-http const handleHTTPErrorMethodName = "HandleHTTPError" func whatReservedMethods(typ reflect.Type) map[string][]*router.Route { methods := []string{"BeforeActivation", "AfterActivation", handleHTTPErrorMethodName} // BeforeActivatior/AfterActivation are not routes but they are // reserved names* if isBaseController(typ) { methods = append(methods, "BeginRequest", "EndRequest") } routes := make(map[string][]*router.Route, len(methods)) for _, m := range methods { routes[m] = []*router.Route{} } return routes } func whatEmbeddedMethods(typ reflect.Type) []string { var embeddedMethodsToIgnore []string controllerType := typ if controllerType.Kind() == reflect.Ptr { controllerType = controllerType.Elem() } for i := 0; i < controllerType.NumField(); i++ { structField := controllerType.Field(i) structType := structField.Type if !structField.Anonymous { continue } // var structValuePtr reflect.Value if structType.Kind() == reflect.Ptr { // keep both ptr and value instances of the struct so we can ignore all of its methods. structType = structType.Elem() // structValuePtr = reflect.ValueOf(reflect.ValueOf(controller).Field(i)) } if structType.Kind() != reflect.Struct { continue } newEmbeddedStructType := reflect.New(structField.Type).Type() // let's take its methods and add to methods to ignore from the parent, the controller itself. for j := 0; j < newEmbeddedStructType.NumMethod(); j++ { embeddedMethod := newEmbeddedStructType.Method(j) embeddedMethodName := embeddedMethod.Name // An exception should happen if the controller itself overrides the embedded method, // but Go (1.20) so far doesn't support this detection, as it handles the structure as one. /* shouldKeepBecauseParentOverrides := false if v, existsOnParent := typ.MethodByName(embeddedMethodName); existsOnParent { embeddedIndex := newEmbeddedStructType.Method(j).Index controllerMethodIndex := v.Index if v.Type.In(0) == typ && embeddedIndex == controllerMethodIndex { fmt.Printf("%s exists on parent = true, receiver = %s\n", v.Name, typ.String()) shouldKeepBecauseParentOverrides = true continue } } */ embeddedMethodsToIgnore = append(embeddedMethodsToIgnore, embeddedMethodName) } } return embeddedMethodsToIgnore } // Name returns the full name of the controller, its package name + the type name. // Can used at both `BeforeActivation` and `AfterActivation`. func (c *ControllerActivator) Name() string { return c.fullName } // RelName returns the path relatively to the main package. func (c *ControllerActivator) RelName() string { return strings.TrimPrefix(c.fullName, "main.") } // SkipMethods can be used to individually skip one or more controller's method handlers. func (c *ControllerActivator) SkipMethods(methodNames ...string) { c.skipMethodNames = append(c.skipMethodNames, methodNames...) } // SkipEmbeddedMethods should be ran before controller parsing. // It skips all embedded struct's methods conversation to http handlers. // // Note that even if the controller overrides the embedded methods // they will be still ignored because Go doesn't support this detection so far. // // See https://github.com/kataras/iris/issues/2103 for more. func (c *ControllerActivator) SkipEmbeddedMethods() { methodsToIgnore := whatEmbeddedMethods(c.Type) c.SkipMethods(methodsToIgnore...) } // Router is the standard Iris router's public API. // With this you can register middleware, view layouts, subdomains, serve static files // and even add custom standard iris handlers as normally. // // This Router is the router instance that came from the parent MVC Application, // it's the `app.Party(...)` argument. // // Can used at both `BeforeActivation` and `AfterActivation`. func (c *ControllerActivator) Router() router.Party { return c.app.Router } // GetRoute returns the first registered route based on the controller's method name. // It can be used to change the route's name, which is useful for reverse routing // inside views. Custom routes can be registered with `Handle`, which returns the *Route. // This method exists mostly for the automatic method parsing based on the known patterns // inside a controller. // // A check for `nil` is necessary for unregistered methods. // // See `GetRoutes` and `Handle` too. func (c *ControllerActivator) GetRoute(methodName string) *router.Route { routes := c.GetRoutes(methodName) if len(routes) > 0 { return routes[0] } return nil } // GetRoutes returns one or more registered route based on the controller's method name. // It can be used to change the route's name, which is useful for reverse routing // inside views. Custom routes can be registered with `Handle`, which returns the *Route. // This method exists mostly for the automatic method parsing based on the known patterns // inside a controller. // // A check for `nil` is necessary for unregistered methods. // // See `Handle` too. func (c *ControllerActivator) GetRoutes(methodName string) []*router.Route { for name, routes := range c.routes { if name == methodName { return routes } } return nil } // Use registers a middleware for this Controller. // It appends one or more handlers to the `BeginHandlers`. // It's like the `Party.Use` but specifically // for the routes that this controller will register to the targeted `Party`. func (c *ControllerActivator) Use(handlers ...context.Handler) *ControllerActivator { c.BeginHandlers = append(c.BeginHandlers, handlers...) return c } // Singleton returns new if all incoming clients' requests // have the same controller instance. // This is done automatically by iris to reduce the creation // of a new controller on each request, if the controller doesn't contain // any unexported fields and all fields are services-like, static. func (c *ControllerActivator) Singleton() bool { if c.injector == nil { panic("MVC: Singleton called from wrong state the API gives access to it only `AfterActivation`, report this as bug") } return c.injector.Singleton } // DependenciesReadOnly returns a list of dependencies, including the controller's one. func (c *ControllerActivator) DependenciesReadOnly() []*hero.Dependency { if c.injector == nil { panic("MVC: DependenciesReadOnly called from wrong state the API gives access to it only `AfterActivation`, report this as bug") } return c.injector.Container.Dependencies } // Dependencies returns a value which can manage the controller's dependencies. func (c *ControllerActivator) Dependencies() *hero.Container { return c.app.container // although the controller's one are: c.injector.Container } // checks if a method is already registered. func (c *ControllerActivator) isReservedMethod(name string) bool { for methodName := range c.routes { if methodName == name { return true } } for _, methodName := range c.skipMethodNames { if methodName == name { return true } } return false } func (c *ControllerActivator) isReservedMethodHandler(method, path string) bool { for _, routes := range c.routes { for _, r := range routes { if r.Method == method && r.Path == path { return true } } } return false } func (c *ControllerActivator) markAsWebsocket() { c.servesWebsocket = true c.attachInjector() } func (c *ControllerActivator) attachInjector() { if c.injector == nil { partyCountParams := macro.CountParams(c.app.Router.GetRelPath(), *c.app.Router.Macros()) c.injector = c.app.container.Struct(c.Value, partyCountParams) } } // Activated can be called to skip the internal method parsing. func (c *ControllerActivator) Activated() bool { b := c.activated c.activated = true return b } func (c *ControllerActivator) activate() { if c.Activated() { return } c.parseMethods() c.parseHTTPErrorHandler() } func (c *ControllerActivator) parseHTTPErrorHandler() { if m, ok := c.Type.MethodByName(handleHTTPErrorMethodName); ok { c.handleHTTPError(m.Name) } } // register all available, exported methods to handlers if possible. func (c *ControllerActivator) parseMethods() { n := c.Type.NumMethod() for i := 0; i < n; i++ { m := c.Type.Method(i) c.parseMethod(m) } } func (c *ControllerActivator) parseMethod(m reflect.Method) { httpMethod, httpPath, err := parseMethod(c.app.Router.Macros(), m, c.isReservedMethod, c.app.customPathWordFunc) if err != nil { if err != errSkip { c.logErrorf("MVC: fail to parse the route path and HTTP method for '%s.%s': %v", c.fullName, m.Name, err) } return } c.Handle(httpMethod, httpPath, m.Name) } func (c *ControllerActivator) logErrorf(format string, args ...any) { c.Router().Logger().Errorf(format, args...) } // Handle registers a route based on a http method, the route's path // and a function name that belongs to the controller, it accepts // a forth, optionally, variadic parameter which is the before handlers. // // Just like `Party#Handle`, it returns the `*router.Route`, if failed // then it logs the errors and it returns nil, you can check the errors // programmatically by the `Party#GetReporter`. // // Handle will add a route to the "funcName". func (c *ControllerActivator) Handle(method, path, funcName string, middleware ...context.Handler) *router.Route { routes := c.handleMany(method, path, funcName, false, middleware...) if len(routes) == 0 { return nil } return routes[0] } // handleHTTPError is called when a controller's method // with the "HandleHTTPError" is found. That method // can accept dependencies like the rest but if it's not called manually // then any dynamic dependencies depending on successful requests // may fail - this is end-developer's job; // to register the correct dependencies or not do it all on that method. // // Note that if more than one controller in the same Party // tries to register an http error handler then the // overlap route rule should be used and a dependency // on the controller (or method) level that will select // between the two should exist (see mvc/authenticated-controller example). func (c *ControllerActivator) handleHTTPError(funcName string) *router.Route { handler := c.handlerOf("/", funcName) routes := c.app.Router.OnAnyErrorCode(handler) if len(routes) == 0 { c.logErrorf("MVC: unable to register an HTTP error code handler for '%s.%s'", c.fullName, funcName) return nil } c.saveRoutes(funcName, routes, true) return routes[0] } // HandleMany like `Handle` but can register more than one path and HTTP method routes // separated by whitespace on the same controller's method. // Keep note that if the controller's method input arguments are path parameters dependencies // they should match with each of the given paths. // // Just like `Party#HandleMany`:, it returns the `[]*router.Routes`. // Usage: // // func (*Controller) BeforeActivation(b mvc.BeforeActivation) { // b.HandleMany("GET", "/path /path1" /path2", "HandlePath") // } // // HandleMany will override any routes of this "funcName". func (c *ControllerActivator) HandleMany(method, path, funcName string, middleware ...context.Handler) []*router.Route { return c.handleMany(method, path, funcName, true, middleware...) } func (c *ControllerActivator) handleMany(method, path, funcName string, override bool, middleware ...context.Handler) []*router.Route { if method == "" || path == "" || funcName == "" || (c.isReservedMethod(funcName) && c.isReservedMethodHandler(method, path)) { // isReservedMethod -> if it's already registered // by a previous Handle or analyze methods internally. return nil } handler := c.handlerOf(path, funcName) middleware = context.JoinHandlers(c.BeginHandlers, middleware) // register the handler now. routes := c.app.Router.HandleMany(method, path, append(middleware, handler)...) if routes == nil { c.logErrorf("MVC: unable to register a route for the path for '%s.%s'", c.fullName, funcName) return nil } c.saveRoutes(funcName, routes, override) return routes } func (c *ControllerActivator) saveRoutes(funcName string, routes []*router.Route, override bool) { m, ok := c.Type.MethodByName(funcName) if !ok { return } sourceFileName, sourceLineNumber := getSourceFileLine(c.Type, m) relName := c.RelName() for _, r := range routes { r.Description = relName r.MainHandlerName = fmt.Sprintf("%s.%s", relName, funcName) r.SourceFileName, r.SourceLineNumber = sourceFileName, sourceLineNumber } // add this as a reserved method name in order to // be sure that the same route // (method is allowed to be registered more than one on different routes - v11.2). existingRoutes, exist := c.routes[funcName] if override || !exist { c.routes[funcName] = routes } else { c.routes[funcName] = append(existingRoutes, routes...) } } func (c *ControllerActivator) handlerOf(relPath, methodName string) context.Handler { c.attachInjector() fullpath := c.app.Router.GetRelPath() + relPath paramsCount := macro.CountParams(fullpath, *c.app.Router.Macros()) handler := c.injector.MethodHandler(methodName, paramsCount) if isBaseController(c.Type) { return func(ctx *context.Context) { ctrl, err := c.injector.Acquire(ctx) if err != nil { // if err != hero.ErrStopExecution { // c.injector.Container.GetErrorHandler(ctx).HandleError(ctx, err) // } c.injector.Container.GetErrorHandler(ctx).HandleError(ctx, err) // allow skipping struct field bindings // errors by a custom error handler. if ctx.IsStopped() { return } } b := ctrl.Interface().(BaseController) // init the request. b.BeginRequest(ctx) // if begin request stopped the execution. if ctx.IsStopped() { return } handler(ctx) b.EndRequest(ctx) } } return handler } ================================================ FILE: mvc/controller_handle_test.go ================================================ package mvc_test import ( "testing" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/httptest" . "github.com/kataras/iris/v12/mvc" ) // service type ( // these testService and testServiceImpl could be in lowercase, unexported // but the `Say` method should be exported however we have those exported // because of the controller handler test. testService interface { Say(string) string } testServiceImpl struct { prefix string } ) func (s *testServiceImpl) Say(message string) string { return s.prefix + " " + message } type testControllerHandle struct { Ctx *context.Context Service testService reqField string } func (c *testControllerHandle) BeforeActivation(b BeforeActivation) { b.Handle("GET", "/histatic", "HiStatic") b.Handle("GET", "/hiservice", "HiService") b.Handle("GET", "/hiservice/{ps:string}", "HiServiceBy") b.Handle("GET", "/hiparam/{ps:string}", "HiParamBy") b.Handle("GET", "/hiparamempyinput/{ps:string}", "HiParamEmptyInputBy") b.HandleMany("GET", "/custom/{ps:string} /custom2/{ps:string}", "CustomWithParameter") b.HandleMany("GET", "/custom3/{ps:string}/{pssecond:string}", "CustomWithParameters") } // test `GetRoute` for custom routes. func (c *testControllerHandle) AfterActivation(a AfterActivation) { // change automatic parser's route change name. rget := a.GetRoute("Get") if rget == nil { panic("route from function name: 'Get' doesn't exist on `AfterActivation`") } rget.Name = "index_route" // change a custom route's name. r := a.GetRoute("HiStatic") if r == nil { panic("route from function name: HiStatic doesn't exist on `AfterActivation`") } // change the name here, and test if name changed in the handler. r.Name = "hi_static_route" } func (c *testControllerHandle) BeginRequest(ctx iris.Context) { c.reqField = ctx.URLParam("reqfield") } func (c *testControllerHandle) EndRequest(ctx iris.Context) {} func (c *testControllerHandle) Get() string { if c.Ctx.GetCurrentRoute().Name() != "index_route" { return "Get's route's name didn't change on AfterActivation" } return "index" } func (c *testControllerHandle) HiStatic() string { if c.Ctx.GetCurrentRoute().Name() != "hi_static_route" { return "HiStatic's route's name didn't change on AfterActivation" } return c.reqField } func (c *testControllerHandle) HiService() string { return c.Service.Say("hi") } func (c *testControllerHandle) HiServiceBy(v string) string { return c.Service.Say("hi with param: " + v) } func (c *testControllerHandle) HiParamBy(v string) string { return v } func (c *testControllerHandle) HiParamEmptyInputBy() string { return "empty in but served with ctx.Params.Get('ps')=" + c.Ctx.Params().Get("ps") } func (c *testControllerHandle) CustomWithParameter(param1 string) string { return param1 } func (c *testControllerHandle) CustomWithParameters(param1, param2 string) string { return param1 + param2 } type testSmallController struct{} // test ctx + id in the same time. func (c *testSmallController) GetHiParamEmptyInputWithCtxBy(ctx *context.Context, id string) string { return "empty in but served with ctx.Params.Get('param2')= " + ctx.Params().Get("param2") + " == id == " + id } func TestControllerHandle(t *testing.T) { app := iris.New() m := New(app) m.Register(&testServiceImpl{prefix: "service:"}) m.Handle(new(testControllerHandle)) m.Handle(new(testSmallController)) e := httptest.New(t, app) // test the index, is not part of the current package's implementation but do it. e.GET("/").Expect().Status(httptest.StatusOK).Body().IsEqual("index") // the important things now. // this test ensures that the BeginRequest of the controller will be // called correctly and also the controller is binded to the first input argument // (which is the function's receiver, if any, in this case the *testController in go). expectedReqField := "this is a request field filled by this url param" e.GET("/histatic").WithQuery("reqfield", expectedReqField).Expect().Status(httptest.StatusOK). Body().IsEqual(expectedReqField) // this test makes sure that the binded values of the controller is handled correctly // and can be used in a user-defined, dynamic "mvc handler". e.GET("/hiservice").Expect().Status(httptest.StatusOK). Body().IsEqual("service: hi") e.GET("/hiservice/value").Expect().Status(httptest.StatusOK). Body().IsEqual("service: hi with param: value") // this worked with a temporary variadic on the resolvemethodfunc which is not // correct design, I should split the path and params with the rest of implementation // in order a simple template.Src can be given. e.GET("/hiparam/value").Expect().Status(httptest.StatusOK). Body().IsEqual("value") e.GET("/hiparamempyinput/value").Expect().Status(httptest.StatusOK). Body().IsEqual("empty in but served with ctx.Params.Get('ps')=value") e.GET("/custom/value1").Expect().Status(httptest.StatusOK). Body().IsEqual("value1") e.GET("/custom2/value2").Expect().Status(httptest.StatusOK). Body().IsEqual("value2") e.GET("/custom3/value1/value2").Expect().Status(httptest.StatusOK). Body().IsEqual("value1value2") e.GET("/custom3/value1").Expect().Status(httptest.StatusNotFound) e.GET("/hi/param/empty/input/with/ctx/value").Expect().Status(httptest.StatusOK). Body().IsEqual("empty in but served with ctx.Params.Get('param2')= value == id == value") } type testControllerHandleWithDynamicPathPrefix struct { Ctx iris.Context } func (c *testControllerHandleWithDynamicPathPrefix) GetBy(id string) string { params := c.Ctx.Params() return params.Get("model") + params.Get("action") + id } func TestControllerHandleWithDynamicPathPrefix(t *testing.T) { app := iris.New() New(app.Party("/api/data/{model:string}/{action:string}")).Handle(new(testControllerHandleWithDynamicPathPrefix)) e := httptest.New(t, app) e.GET("/api/data/mymodel/myaction/myid").Expect().Status(httptest.StatusOK). Body().IsEqual("mymodelmyactionmyid") } type testControllerGetBy struct{} func (c *testControllerGetBy) GetBy(age int64) *testCustomStruct { return &testCustomStruct{ Age: int(age), Name: "name", } } func TestControllerGetByWithAllowMethods(t *testing.T) { app := iris.New() app.Configure(iris.WithFireMethodNotAllowed) // ^ this 405 status will not be fired on POST: project/... because of // .AllowMethods, but it will on PUT. New(app.Party("/project").AllowMethods(iris.MethodGet, iris.MethodPost)).Handle(new(testControllerGetBy)) e := httptest.New(t, app) e.GET("/project/42").Expect().Status(httptest.StatusOK). JSON().IsEqual(&testCustomStruct{Age: 42, Name: "name"}) e.POST("/project/42").Expect().Status(httptest.StatusOK) e.PUT("/project/42").Expect().Status(httptest.StatusMethodNotAllowed) } ================================================ FILE: mvc/controller_method_parser.go ================================================ package mvc import ( "errors" "fmt" "reflect" "strconv" "strings" "unicode" "github.com/kataras/iris/v12/core/router" "github.com/kataras/iris/v12/macro" "golang.org/x/text/cases" "golang.org/x/text/language" ) const ( tokenBy = "By" tokenWildcard = "Wildcard" // "ByWildcard". ) // word lexer, not characters. type methodLexer struct { words []string cur int } func newMethodLexer(s string) *methodLexer { l := new(methodLexer) l.reset(s) return l } /* var allowedCapitalWords = map[string]struct{}{ "ID": {}, "JSON": {}, } */ func (l *methodLexer) reset(s string) { l.cur = -1 var words []string if s != "" { end := len(s) start := -1 // outter: for i, n := 0, end; i < n; i++ { c := rune(s[i]) if unicode.IsUpper(c) { // it doesn't count the last uppercase if start != -1 { /* for allowedCapitalWord := range allowedCapitalWords { capitalWordEnd := i + len(allowedCapitalWord) // takes last char too, e.g. ReadJSON, we need the JSON. if len(s) >= capitalWordEnd { word := s[i:capitalWordEnd] if word == allowedCapitalWord { words = append(words, word) i = capitalWordEnd start = i continue outter } } } */ end = i words = append(words, s[start:end]) } start = i continue } end = i + 1 } if end > 0 && len(s) >= end { words = append(words, s[start:]) } } l.words = words } func (l *methodLexer) next() (w string) { cur := l.cur + 1 if w = l.peek(cur); w != "" { l.cur++ } return } func (l *methodLexer) skip() { if cur := l.cur + 1; cur < len(l.words) { l.cur = cur } else { l.cur = len(l.words) - 1 } } func (l *methodLexer) peek(idx int) string { if idx < len(l.words) { return l.words[idx] } return "" } func (l *methodLexer) peekNext() (w string) { return l.peek(l.cur + 1) } func genParamKey(argIdx int) string { return "param" + strconv.Itoa(argIdx) // param0, param1, param2... } type methodParser struct { lexer *methodLexer fn reflect.Method macros *macro.Macros customPathWordFunc CustomPathWordFunc } func parseMethod(macros *macro.Macros, fn reflect.Method, skipper func(string) bool, wordFunc CustomPathWordFunc) (method, path string, err error) { if skipper(fn.Name) { return "", "", errSkip } p := &methodParser{ fn: fn, lexer: newMethodLexer(fn.Name), macros: macros, customPathWordFunc: wordFunc, } return p.parse() } func methodTitle(httpMethod string) string { caser := cases.Title(language.English) return caser.String(strings.ToLower(httpMethod)) } var errSkip = errors.New("skip") var allMethods = append(router.AllMethods[0:], []string{"ALL", "ANY"}...) // CustomPathWordFunc describes the function which can be passed // through `Application.SetCustomPathWordFunc` to customize // the controllers method parsing. type CustomPathWordFunc func(path, w string, wordIndex int) string func addPathWord(path, w string) string { if path[len(path)-1] != '/' { path += "/" } path += strings.ToLower(w) return path } func (p *methodParser) parse() (method, path string, err error) { funcArgPos := 0 path = "/" // take the first word and check for the method. w := p.lexer.next() for _, httpMethod := range allMethods { possibleMethodFuncName := methodTitle(httpMethod) if strings.Index(w, possibleMethodFuncName) == 0 { method = httpMethod break } } if method == "" { // this is not a valid method to parse, we just skip it, // it may be used for end-dev's use cases. return "", "", errSkip } wordIndex := 0 for { w := p.lexer.next() if w == "" { break } if w == tokenBy { funcArgPos++ // starting with 1 because in typ.NumIn() the first is the struct receiver. // No need for these: // ByBy will act like /{param:type}/{param:type} as users expected // if func input arguments are there, else act By like normal path /by. // // if p.lexer.peekPrev() == tokenBy || typ.NumIn() == 1 { // ByBy, then act this second By like a path // a.relPath += "/" + strings.ToLower(w) // continue // } if path, funcArgPos, err = p.parsePathParam(path, w, funcArgPos); err != nil { return "", "", err } continue } // custom static path. if p.customPathWordFunc != nil { path = p.customPathWordFunc(path, w, wordIndex) } else { // default static path. path = addPathWord(path, w) } wordIndex++ } return } func (p *methodParser) parsePathParam(path string, w string, funcArgPos int) (string, int, error) { typ := p.fn.Type if typ.NumIn() <= funcArgPos { // By found but input arguments are not there, so act like /by path without restricts. path = addPathWord(path, w) return path, funcArgPos, nil } var ( paramKey = genParamKey(funcArgPos) // argfirst, argsecond... m = p.macros.GetMaster() // default (String by-default) trailings = p.macros.GetTrailings() ) // string, int... goType := typ.In(funcArgPos).Kind() nextWord := p.lexer.peekNext() if nextWord == tokenWildcard { p.lexer.skip() // skip the Wildcard word. if len(trailings) == 0 { return "", 0, errors.New("no trailing path parameter found") } m = trailings[0] } else { // validMacros := p.macros.LookupForGoType(goType) // instead of mapping with a reflect.Kind which has its limitation, // we map the param types with a go type as a string, // so custom structs such as "user" can be mapped to a macro with indent || alias == "user". m = p.macros.Get(strings.ToLower(goType.String())) if m == nil { if typ.NumIn() > funcArgPos { // has more input arguments but we are not in the correct // index now, maybe the first argument was an `iris/context.Context` // so retry with the "funcArgPos" incremented. // // the "funcArgPos" will be updated to the caller as well // because we return it among the path and the error. return p.parsePathParam(path, w, funcArgPos+1) } return "", 0, fmt.Errorf("invalid syntax: the standard go type: %s found in controller's function: %s at position: %d does not match any valid macro", goType, p.fn.Name, funcArgPos) } } // /{argfirst:path}, /{argfirst:int64}... if path[len(path)-1] != '/' { path += "/" } path += fmt.Sprintf("{%s:%s}", paramKey, m.Indent()) if nextWord == "" && typ.NumIn() > funcArgPos+1 { // By is the latest word but func is expected // more path parameters values, i.e: // GetBy(name string, age int) // The caller (parse) doesn't need to know // about the incremental funcArgPos because // it will not need it. return p.parsePathParam(path, nextWord, funcArgPos+1) } return path, funcArgPos, nil } ================================================ FILE: mvc/controller_method_result_test.go ================================================ package mvc_test import ( "errors" "testing" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/httptest" . "github.com/kataras/iris/v12/mvc" ) type testControllerMethodResult struct { Ctx *context.Context } func (c *testControllerMethodResult) Get() Result { return Response{ Text: "Hello World!", } } func (c *testControllerMethodResult) GetWithStatus() Response { // or Result again, no problem. return Response{ Text: "This page doesn't exist", Code: iris.StatusNotFound, } } type testCustomStruct struct { Name string `json:"name" xml:"name"` Age int `json:"age" xml:"age"` } func (c *testControllerMethodResult) GetJson() Result { var err error if c.Ctx.URLParamExists("err") { err = errors.New("error here") } return Response{ Err: err, // if err != nil then it will fire the error's text with a BadRequest. Object: testCustomStruct{Name: "Iris", Age: 2}, } } var things = []string{"thing 0", "thing 1", "thing 2"} func (c *testControllerMethodResult) GetThingWithTryBy(index int) Result { failure := Response{ Text: "thing does not exist", Code: iris.StatusNotFound, } return Try(func() Result { // if panic because of index exceed the slice // then the "failure" response will be returned instead. return Response{Text: things[index]} }, failure) } func (c *testControllerMethodResult) GetThingWithTryDefaultBy(index int) Result { return Try(func() Result { // if panic because of index exceed the slice // then the default failure response will be returned instead (400 bad request). return Response{Text: things[index]} }) } func TestControllerMethodResult(t *testing.T) { app := iris.New() New(app).Handle(new(testControllerMethodResult)) e := httptest.New(t, app) e.GET("/").Expect().Status(iris.StatusOK). Body().IsEqual("Hello World!") e.GET("/with/status").Expect().Status(iris.StatusNotFound). Body().IsEqual("This page doesn't exist") e.GET("/json").Expect().Status(iris.StatusOK). JSON().IsEqual(iris.Map{ "name": "Iris", "age": 2, }) e.GET("/json").WithQuery("err", true).Expect(). Status(iris.StatusBadRequest). Body().IsEqual("error here") e.GET("/thing/with/try/1").Expect(). Status(iris.StatusOK). Body().IsEqual("thing 1") // failure because of index exceed the slice e.GET("/thing/with/try/3").Expect(). Status(iris.StatusNotFound). Body().IsEqual("thing does not exist") e.GET("/thing/with/try/default/3").Expect(). Status(iris.StatusBadRequest). Body().IsEqual("Bad Request") } type testControllerMethodResultTypes struct { Ctx *context.Context } func (c *testControllerMethodResultTypes) GetText() string { return "text" } func (c *testControllerMethodResultTypes) GetStatus() int { return iris.StatusBadGateway } func (c *testControllerMethodResultTypes) GetTextWithStatusOk() (string, int) { return "OK", iris.StatusOK } // tests should have output arguments mixed func (c *testControllerMethodResultTypes) GetStatusWithTextNotOkBy(first string, second string) (int, string) { return iris.StatusForbidden, "NOT_OK_" + first + second } func (c *testControllerMethodResultTypes) GetTextAndContentType() (string, string) { return "text", "text/html" } type testControllerMethodCustomResult struct { HTML string } // The only one required function to make that a custom Response dispatcher. func (r testControllerMethodCustomResult) Dispatch(ctx *context.Context) { ctx.HTML(r.HTML) } func (c *testControllerMethodResultTypes) GetCustomResponse() testControllerMethodCustomResult { return testControllerMethodCustomResult{"text"} } func (c *testControllerMethodResultTypes) GetCustomResponseWithStatusOk() (testControllerMethodCustomResult, int) { return testControllerMethodCustomResult{"OK"}, iris.StatusOK } func (c *testControllerMethodResultTypes) GetCustomResponseWithStatusNotOk() (testControllerMethodCustomResult, int) { return testControllerMethodCustomResult{"internal server error"}, iris.StatusInternalServerError } func (c *testControllerMethodResultTypes) GetCustomStruct() testCustomStruct { return testCustomStruct{"Iris", 2} } func (c *testControllerMethodResultTypes) GetCustomStructWithStatusNotOk() (testCustomStruct, int) { return testCustomStruct{"Iris", 2}, iris.StatusInternalServerError } func (c *testControllerMethodResultTypes) GetCustomStructWithContentType() (testCustomStruct, string) { return testCustomStruct{"Iris", 2}, "text/xml" } func (c *testControllerMethodResultTypes) GetCustomStructWithError() (s testCustomStruct, err error) { s = testCustomStruct{"Iris", 2} if c.Ctx.URLParamExists("err") { err = errors.New("omit return of testCustomStruct and fire error") } // it should send the testCustomStruct as JSON if error is nil // otherwise it should fire the default error(BadRequest) with the error's text. return } func TestControllerMethodResultTypes(t *testing.T) { app := iris.New() New(app).Handle(new(testControllerMethodResultTypes)) e := httptest.New(t, app) e.GET("/text").Expect().Status(iris.StatusOK). Body().IsEqual("text") e.GET("/status").Expect().Status(iris.StatusBadGateway) e.GET("/text/with/status/ok").Expect().Status(iris.StatusOK). Body().IsEqual("OK") e.GET("/status/with/text/not/ok/first/second").Expect().Status(iris.StatusForbidden). Body().IsEqual("NOT_OK_firstsecond") // Author's note: <-- if that fails means that the last binder called for both input args, // see path_param_binder.go e.GET("/text/and/content/type").Expect().Status(iris.StatusOK). ContentType("text/html", "utf-8"). Body().IsEqual("text") e.GET("/custom/response").Expect().Status(iris.StatusOK). ContentType("text/html", "utf-8"). Body().IsEqual("text") e.GET("/custom/response/with/status/ok").Expect().Status(iris.StatusOK). ContentType("text/html", "utf-8"). Body().IsEqual("OK") e.GET("/custom/response/with/status/not/ok").Expect().Status(iris.StatusInternalServerError). ContentType("text/html", "utf-8"). Body().IsEqual("internal server error") expectedResultFromCustomStruct := map[string]any{ "name": "Iris", "age": 2, } e.GET("/custom/struct").Expect().Status(iris.StatusOK). JSON().IsEqual(expectedResultFromCustomStruct) e.GET("/custom/struct/with/status/not/ok").Expect().Status(iris.StatusInternalServerError). JSON().IsEqual(expectedResultFromCustomStruct) e.GET("/custom/struct/with/content/type").Expect().Status(iris.StatusOK). ContentType("text/xml", "utf-8") e.GET("/custom/struct/with/error").Expect().Status(iris.StatusOK). JSON().IsEqual(expectedResultFromCustomStruct) e.GET("/custom/struct/with/error").WithQuery("err", true).Expect(). Status(iris.StatusBadRequest). // the default status code if error is not nil // the content should be not JSON it should be the status code's text // it will fire the error's text Body().IsEqual("omit return of testCustomStruct and fire error") } type testControllerViewResultRespectCtxViewData struct { T *testing.T } func (t *testControllerViewResultRespectCtxViewData) BeginRequest(ctx *context.Context) { ctx.ViewData("name_begin", "iris_begin") } func (t *testControllerViewResultRespectCtxViewData) EndRequest(ctx *context.Context) { // check if data is not overridden by return View {Data: context.Map...} dataWritten := ctx.GetViewData() if dataWritten == nil { t.T.Fatalf("view data is nil, both BeginRequest and Get failed to write the data") return } if dataWritten["name_begin"] == nil { t.T.Fatalf(`view data[name_begin] is nil, BeginRequest's ctx.ViewData call have been overridden by Get's return View {Data: }. Total view data: %v`, dataWritten) } if dataWritten["name"] == nil { t.T.Fatalf("view data[name] is nil, Get's return View {Data: } didn't work. Total view data: %v", dataWritten) } } func (t *testControllerViewResultRespectCtxViewData) Get() Result { return View{ Name: "doesnt_exists.html", Data: context.Map{"name": "iris"}, // we care about this only. Code: iris.StatusInternalServerError, } } func TestControllerViewResultRespectCtxViewData(t *testing.T) { app := iris.New() m := New(app.Party("/")) m.Register(t) m.Handle(new(testControllerViewResultRespectCtxViewData)) e := httptest.New(t, app) e.GET("/").Expect().Status(iris.StatusInternalServerError) } ================================================ FILE: mvc/controller_overlap_test.go ================================================ // black-box testing // Note: there is a test, for end-devs, of Controllers overlapping at _examples/mvc/authenticated-controller too. package mvc_test import ( "fmt" "testing" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/httptest" "github.com/kataras/iris/v12/mvc" ) func TestControllerOverlap(t *testing.T) { app := iris.New() userRouter := app.Party("/user") { userRouter.SetRegisterRule(iris.RouteOverlap) // Initialize a new MVC application on top of the "userRouter". userApp := mvc.New(userRouter) // Register Dependencies. userApp.Register(authDependency) // Register Controllers. userApp.Handle(new(AuthenticatedUserController)) userApp.Handle(new(UnauthenticatedUserController)) } e := httptest.New(t, app) e.GET("/user").Expect().Status(httptest.StatusUnauthorized).Body().IsEqual("unauth") // Test raw stop execution with a status code sent on the controller's method. e.GET("/user/with/status/on/method").Expect().Status(httptest.StatusBadRequest).Body().IsEqual("unauth") // Test stop execution with status but last code sent through the controller's method. e.GET("/user/with/status/on/method/too").Expect().Status(httptest.StatusInternalServerError).Body().IsEqual("unauth") // Test raw stop execution and no status code sent on controller's method (should be OK). e.GET("/user/with/no/status").Expect().Status(httptest.StatusOK).Body().IsEqual("unauth") // Test authenticated request. e.GET("/user").WithQuery("id", 42).Expect().Status(httptest.StatusOK).Body().IsEqual("auth: 42") // Test HandleHTTPError method accepts a not found and returns a 404 // from a shared controller and overlapped, the url parameter matters because this method was overlapped. e.GET("/user/notfound").Expect().Status(httptest.StatusBadRequest). Body().IsEqual("error: *mvc_test.UnauthenticatedUserController: from: 404 to: 400") e.GET("/user/notfound").WithQuery("id", 42).Expect().Status(httptest.StatusBadRequest). Body().IsEqual("error: *mvc_test.AuthenticatedUserController: from: 404 to: 400") } type AuthenticatedTest uint64 func authDependency(ctx iris.Context) AuthenticatedTest { // this will be executed on not found too and that's what we expect. userID := ctx.URLParamUint64("id") // just for the test. if userID == 0 { if ctx.GetStatusCode() == iris.StatusNotFound || // do not send 401 on not founds, keep 404 and let controller decide. ctx.Path() == "/user/with/status/on/method" || ctx.Path() == "/user/with/np/status" { // leave controller method decide, raw stop execution. ctx.StopExecution() } else { ctx.StopWithStatus(iris.StatusUnauthorized) } return 0 } return AuthenticatedTest(userID) } type BaseControllerTest struct{} func (c *BaseControllerTest) HandleHTTPError(ctx iris.Context, code mvc.Code) (string, int) { if ctx.GetStatusCode() != int(code) { // should never happen. panic("Context current status code and given mvc code do not match!") } ctrlName := ctx.Controller().Type().String() newCode := 400 return fmt.Sprintf("error: %s: from: %d to: %d", ctrlName, int(code), newCode), newCode } type UnauthenticatedUserController struct { BaseControllerTest } func (c *UnauthenticatedUserController) Get() string { return "unauth" } func (c *UnauthenticatedUserController) GetWithNoStatus() string { return "unauth" } func (c *UnauthenticatedUserController) GetWithStatusOnMethod() (string, int) { return "unauth", iris.StatusBadRequest } func (c *UnauthenticatedUserController) GetWithStatusOnMethodToo() (string, int) { return "unauth", iris.StatusInternalServerError } type AuthenticatedUserController struct { BaseControllerTest CurrentUserID AuthenticatedTest } func (c *AuthenticatedUserController) Get() string { return fmt.Sprintf("auth: %d", c.CurrentUserID) } ================================================ FILE: mvc/controller_test.go ================================================ // black-box testing package mvc_test import ( "fmt" "testing" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/core/router" "github.com/kataras/iris/v12/hero" "github.com/kataras/iris/v12/httptest" . "github.com/kataras/iris/v12/mvc" ) type testController struct { Ctx *context.Context } var writeMethod = func(ctx *context.Context) { ctx.WriteString(ctx.Method()) } func (c *testController) Get() { writeMethod(c.Ctx) } func (c *testController) Post() { writeMethod(c.Ctx) } func (c *testController) Put() { writeMethod(c.Ctx) } func (c *testController) Delete() { writeMethod(c.Ctx) } func (c *testController) Connect() { writeMethod(c.Ctx) } func (c *testController) Head() { writeMethod(c.Ctx) } func (c *testController) Patch() { writeMethod(c.Ctx) } func (c *testController) Options() { writeMethod(c.Ctx) } func (c *testController) Trace() { writeMethod(c.Ctx) } type ( testControllerAll struct{ Ctx *context.Context } testControllerAny struct{ Ctx *context.Context } // exactly the same as All. ) func (c *testControllerAll) All() { writeMethod(c.Ctx) } func (c *testControllerAny) Any() { writeMethod(c.Ctx) } func TestControllerMethodFuncs(t *testing.T) { app := iris.New() New(app).Handle(new(testController)) New(app.Party("/all")).Handle(new(testControllerAll)) New(app.Party("/any")).Handle(new(testControllerAny)) e := httptest.New(t, app) for _, method := range router.AllMethods { e.Request(method, "/").Expect().Status(iris.StatusOK). Body().IsEqual(method) e.Request(method, "/all").Expect().Status(iris.StatusOK). Body().IsEqual(method) e.Request(method, "/any").Expect().Status(iris.StatusOK). Body().IsEqual(method) } } type testControllerBeginAndEndRequestFunc struct { Ctx *context.Context Username string } // called before of every method (Get() or Post()). // // useful when more than one methods using the // same request values or context's function calls. func (c *testControllerBeginAndEndRequestFunc) BeginRequest(ctx *context.Context) { c.Username = ctx.Params().Get("username") } // called after every method (Get() or Post()). func (c *testControllerBeginAndEndRequestFunc) EndRequest(ctx *context.Context) { ctx.WriteString("done") // append "done" to the response } func (c *testControllerBeginAndEndRequestFunc) Get() { c.Ctx.WriteString(c.Username) } func (c *testControllerBeginAndEndRequestFunc) Post() { c.Ctx.WriteString(c.Username) } func TestControllerBeginAndEndRequestFunc(t *testing.T) { app := iris.New() New(app.Party("/profile/{username}")). Handle(new(testControllerBeginAndEndRequestFunc)) e := httptest.New(t, app) usernames := []string{ "kataras", "makis", "efi", "rg", "bill", "whoisyourdaddy", } doneResponse := "done" for _, username := range usernames { e.GET("/profile/" + username).Expect().Status(iris.StatusOK). Body().IsEqual(username + doneResponse) e.POST("/profile/" + username).Expect().Status(iris.StatusOK). Body().IsEqual(username + doneResponse) } } func TestControllerBeginAndEndRequestFuncBindMiddleware(t *testing.T) { app := iris.New() usernames := map[string]bool{ "kataras": true, "makis": false, "efi": true, "rg": false, "bill": true, "whoisyourdaddy": false, } middlewareCheck := func(ctx *context.Context) { for username, allow := range usernames { if ctx.Params().Get("username") == username && allow { ctx.Next() return } } ctx.StatusCode(iris.StatusForbidden) ctx.Writef("forbidden") } app.PartyFunc("/profile/{username}", func(r iris.Party) { r.Use(middlewareCheck) New(r).Handle(new(testControllerBeginAndEndRequestFunc)) }) e := httptest.New(t, app) doneResponse := "done" for username, allow := range usernames { getEx := e.GET("/profile/" + username).Expect() if allow { getEx.Status(iris.StatusOK). Body().IsEqual(username + doneResponse) } else { getEx.Status(iris.StatusForbidden).Body().IsEqual("forbidden") } postEx := e.POST("/profile/" + username).Expect() if allow { postEx.Status(iris.StatusOK). Body().IsEqual(username + doneResponse) } else { postEx.Status(iris.StatusForbidden).Body().IsEqual("forbidden") } } } type Model struct { Username string } type testControllerEndRequestAwareness struct { Ctx *context.Context } func (c *testControllerEndRequestAwareness) Get() { username := c.Ctx.Params().Get("username") c.Ctx.Values().Set(c.Ctx.Application().ConfigurationReadOnly().GetViewDataContextKey(), map[string]any{ "TestModel": Model{Username: username}, "myModel": Model{Username: username + "2"}, }) } func writeModels(ctx *context.Context, names ...string) { if expected, got := len(names), len(ctx.GetViewData()); expected != got { ctx.Writef("expected view data length: %d but got: %d for names: %s", expected, got, names) return } for _, name := range names { m, ok := ctx.GetViewData()[name] if !ok { ctx.Writef("fail load and set the %s", name) return } model, ok := m.(Model) if !ok { ctx.Writef("fail to override the %s' name by the tag", name) return } ctx.WriteString(model.Username) } } func (c *testControllerEndRequestAwareness) BeginRequest(ctx *context.Context) {} func (c *testControllerEndRequestAwareness) EndRequest(ctx *context.Context) { writeModels(ctx, "TestModel", "myModel") } func TestControllerEndRequestAwareness(t *testing.T) { app := iris.New() New(app.Party("/era/{username}")).Handle(new(testControllerEndRequestAwareness)) e := httptest.New(t, app) usernames := []string{ "kataras", "makis", } for _, username := range usernames { e.GET("/era/" + username).Expect().Status(iris.StatusOK). Body().IsEqual(username + username + "2") } } type testBindType struct { title string } type testControllerBindStruct struct { Ctx *context.Context // should start with upper letter of course TitlePointer *testBindType // should have the value of the "myTitlePtr" on test TitleValue testBindType // should have the value of the "myTitleV" on test Other string // just another type to check the field collection, should be empty } func (t *testControllerBindStruct) Get() { t.Ctx.WriteString(t.TitlePointer.title + t.TitleValue.title + t.Other) } // test if context can be binded to the controller's function // without need to declare it to a struct if not needed. func (t *testControllerBindStruct) GetCtx(ctx iris.Context) { ctx.StatusCode(iris.StatusContinue) } type testControllerBindDeep struct { testControllerBindStruct } func (t *testControllerBindDeep) BeforeActivation(b BeforeActivation) { b.Dependencies().Register(func(ctx iris.Context) (v testCustomStruct, err error) { err = ctx.ReadJSON(&v) return }) } func (t *testControllerBindDeep) Get() { // t.testControllerBindStruct.Get() t.Ctx.WriteString(t.TitlePointer.title + t.TitleValue.title + t.Other) } func (t *testControllerBindDeep) Post(v testCustomStruct) string { return v.Name } func TestControllerDependencies(t *testing.T) { app := iris.New() // app.Logger().SetLevel("debug") t1, t2 := "my pointer title", "val title" // test bind pointer to pointer of the correct type myTitlePtr := &testBindType{title: t1} // test bind value to value of the correct type myTitleV := testBindType{title: t2} m := New(app) m.Register(myTitlePtr, myTitleV) m.Handle(new(testControllerBindStruct)) m.Clone(app.Party("/deep")).Handle(new(testControllerBindDeep)) e := httptest.New(t, app) expected := t1 + t2 e.GET("/").Expect().Status(iris.StatusOK). Body().IsEqual(expected) e.GET("/ctx").Expect().Status(iris.StatusContinue) e.GET("/deep").Expect().Status(iris.StatusOK). Body().IsEqual(expected) e.POST("/deep").WithJSON(iris.Map{"name": "kataras"}).Expect().Status(iris.StatusOK). Body().IsEqual("kataras") e.POST("/deep").Expect().Status(iris.StatusBadRequest). Body().IsEqual("EOF") } type testCtrl0 struct { testCtrl00 } func (c *testCtrl0) Get() string { return c.Ctx.Params().Get("username") } func (c *testCtrl0) EndRequest(ctx *context.Context) { if c.TitlePointer == nil { ctx.WriteString("\nTitlePointer is nil!\n") } else { ctx.WriteString(c.TitlePointer.title) } // should be the same as `.testCtrl000.testCtrl0000.EndRequest(ctx)` c.testCtrl00.EndRequest(ctx) } type testCtrl00 struct { Ctx *context.Context testCtrl000 } type testCtrl000 struct { testCtrl0000 TitlePointer *testBindType } type testCtrl0000 struct { } func (c *testCtrl0000) BeginRequest(ctx *context.Context) {} func (c *testCtrl0000) EndRequest(ctx *context.Context) { ctx.Writef("finish") } func TestControllerInsideControllerRecursively(t *testing.T) { var ( username = "gerasimos" title = "mytitle" expected = username + title + "finish" ) app := iris.New() m := New(app.Party("/user/{username}")) m.Register(&testBindType{title: title}) m.Handle(new(testCtrl0)) e := httptest.New(t, app) e.GET("/user/" + username).Expect(). Status(iris.StatusOK).Body().IsEqual(expected) } type testControllerRelPathFromFunc struct{} func (c *testControllerRelPathFromFunc) BeginRequest(ctx *context.Context) {} func (c *testControllerRelPathFromFunc) EndRequest(ctx *context.Context) { ctx.Writef("%s:%s", ctx.Method(), ctx.Path()) } func (c *testControllerRelPathFromFunc) Get() {} func (c *testControllerRelPathFromFunc) GetBy(uint64) {} func (c *testControllerRelPathFromFunc) GetUint8RatioBy(uint8) {} func (c *testControllerRelPathFromFunc) GetInt64RatioBy(int64) {} func (c *testControllerRelPathFromFunc) GetAnythingByWildcard(string) {} func (c *testControllerRelPathFromFunc) GetLogin() {} func (c *testControllerRelPathFromFunc) PostLogin() {} func (c *testControllerRelPathFromFunc) GetAdminLogin() {} func (c *testControllerRelPathFromFunc) PutSomethingIntoThis() {} func (c *testControllerRelPathFromFunc) GetSomethingBy(bool) {} func (c *testControllerRelPathFromFunc) GetSomethingByBy(string, int) {} func (c *testControllerRelPathFromFunc) GetSomethingNewBy(string, int) {} // two input arguments, one By which is the latest word. func (c *testControllerRelPathFromFunc) GetSomethingByElseThisBy(bool, int) {} // two input arguments func (c *testControllerRelPathFromFunc) GetLocationX() {} func (c *testControllerRelPathFromFunc) GetLocationXY() {} func (c *testControllerRelPathFromFunc) GetLocationZBy(int) {} func TestControllerRelPathFromFunc(t *testing.T) { app := iris.New() New(app).Handle(new(testControllerRelPathFromFunc)) e := httptest.New(t, app) e.GET("/").Expect().Status(iris.StatusOK). Body().IsEqual("GET:/") e.GET("/18446744073709551615").Expect().Status(iris.StatusOK). Body().IsEqual("GET:/18446744073709551615") e.GET("/uint8/ratio/255").Expect().Status(iris.StatusOK). Body().IsEqual("GET:/uint8/ratio/255") e.GET("/uint8/ratio/256").Expect().Status(iris.StatusNotFound) e.GET("/int64/ratio/-42").Expect().Status(iris.StatusOK). Body().IsEqual("GET:/int64/ratio/-42") e.GET("/something/true").Expect().Status(iris.StatusOK). Body().IsEqual("GET:/something/true") e.GET("/something/false").Expect().Status(iris.StatusOK). Body().IsEqual("GET:/something/false") e.GET("/something/truee").Expect().Status(iris.StatusNotFound) e.GET("/something/falsee").Expect().Status(iris.StatusNotFound) e.GET("/something/kataras/42").Expect().Status(iris.StatusOK). Body().IsEqual("GET:/something/kataras/42") e.GET("/something/new/kataras/42").Expect().Status(iris.StatusOK). Body().IsEqual("GET:/something/new/kataras/42") e.GET("/something/true/else/this/42").Expect().Status(iris.StatusOK). Body().IsEqual("GET:/something/true/else/this/42") e.GET("/login").Expect().Status(iris.StatusOK). Body().IsEqual("GET:/login") e.POST("/login").Expect().Status(iris.StatusOK). Body().IsEqual("POST:/login") e.GET("/admin/login").Expect().Status(iris.StatusOK). Body().IsEqual("GET:/admin/login") e.PUT("/something/into/this").Expect().Status(iris.StatusOK). Body().IsEqual("PUT:/something/into/this") e.GET("/42").Expect().Status(iris.StatusOK). Body().IsEqual("GET:/42") e.GET("/anything/here").Expect().Status(iris.StatusOK). Body().IsEqual("GET:/anything/here") e.GET("/location/x").Expect().Status(iris.StatusOK). Body().IsEqual("GET:/location/x") e.GET("/location/x/y").Expect().Status(iris.StatusOK). Body().IsEqual("GET:/location/x/y") e.GET("/location/z/42").Expect().Status(iris.StatusOK). Body().IsEqual("GET:/location/z/42") } type testControllerActivateListener struct { TitlePointer *testBindType } func (c *testControllerActivateListener) BeforeActivation(b BeforeActivation) { b.Dependencies().Register(&testBindType{title: "overrides the dependency but not the field"}) // overrides the `Register` previous calls. // b.Handle("POST", "/me/tos-read", "MeTOSRead") // b.Handle("GET", "/me/tos-read", "MeTOSRead") // OR: b.HandleMany("GET POST", "/me/tos-read", "MeTOSRead") } func (c *testControllerActivateListener) Get() string { return c.TitlePointer.title } func (c *testControllerActivateListener) MeTOSRead() string { return "MeTOSRead" } func TestControllerActivateListener(t *testing.T) { app := iris.New() New(app).Handle(new(testControllerActivateListener)) m := New(app) m.Register(&testBindType{ title: "my title", }) m.Party("/manual").Handle(new(testControllerActivateListener)) // or m.Party("/manual2").Handle(&testControllerActivateListener{ TitlePointer: &testBindType{ title: "my manual title", }, }) e := httptest.New(t, app) e.GET("/").Expect().Status(iris.StatusOK). Body().IsEqual("overrides the dependency but not the field") e.GET("/me/tos-read").Expect().Status(iris.StatusOK). Body().IsEqual("MeTOSRead") e.POST("/me/tos-read").Expect().Status(iris.StatusOK). Body().IsEqual("MeTOSRead") e.GET("/manual").Expect().Status(iris.StatusOK). Body().IsEqual("overrides the dependency but not the field") e.GET("/manual2").Expect().Status(iris.StatusOK). Body().IsEqual("my manual title") } type testControllerNotCreateNewDueManuallySettingAllFields struct { T *testing.T TitlePointer *testBindType } func (c *testControllerNotCreateNewDueManuallySettingAllFields) AfterActivation(a AfterActivation) { if n := len(a.DependenciesReadOnly()) - len(hero.BuiltinDependencies) - 1; /* Application */ n != 1 { c.T.Fatalf(`expecting 1 dependency; - the 'T' and the 'TitlePointer' are manually binded (nonzero fields on initilization) - controller has no more than these two fields, it's a singleton - however, the dependencies length here should be 1 because the injector's options handler dependencies contains the controller's value dependency itself -- got dependencies length: %d`, n) } if !a.Singleton() { c.T.Fatalf(`this controller should be tagged as Singleton. It shouldn't be tagged used as request scoped(create new instances on each request), it doesn't contain any dynamic value or dependencies that should be binded via the iris mvc engine`) } } func (c *testControllerNotCreateNewDueManuallySettingAllFields) Get() string { return c.TitlePointer.title } func TestControllerNotCreateNewDueManuallySettingAllFields(t *testing.T) { app := iris.New() New(app).Handle(&testControllerNotCreateNewDueManuallySettingAllFields{ T: t, TitlePointer: &testBindType{ title: "my title", }, }) e := httptest.New(t, app) e.GET("/").Expect().Status(iris.StatusOK). Body().IsEqual("my title") } type testControllerRequestScopedDependencies struct { MyContext *testMyContext CustomStruct *testCustomStruct } func (c *testControllerRequestScopedDependencies) Get() *testCustomStruct { return c.CustomStruct } func (c *testControllerRequestScopedDependencies) GetCustomContext() string { return c.MyContext.OtherField } func newRequestDep1(ctx *context.Context) *testCustomStruct { return &testCustomStruct{ Name: ctx.URLParam("name"), Age: ctx.URLParamIntDefault("age", 0), } } type testMyContext struct { Context *context.Context OtherField string } func newRequestDep2(ctx *context.Context) *testMyContext { return &testMyContext{ Context: ctx, OtherField: "test", } } func TestControllerRequestScopedDependencies(t *testing.T) { app := iris.New() m := New(app) m.Register(newRequestDep1) m.Register(newRequestDep2) m.Handle(new(testControllerRequestScopedDependencies)) e := httptest.New(t, app) e.GET("/").WithQuery("name", "kataras").WithQuery("age", 27). Expect().Status(httptest.StatusOK).JSON().IsEqual(&testCustomStruct{ Name: "kataras", Age: 27, }) e.GET("/custom/context").Expect().Status(httptest.StatusOK).Body().IsEqual("test") } type ( testServiceDoSomething struct{} TestControllerAsDeepDep struct { Ctx iris.Context Service *testServiceDoSomething } FooController struct { TestControllerAsDeepDep } BarController struct { FooController } FinalController struct { BarController } ) func (s *testServiceDoSomething) DoSomething(ctx iris.Context) { ctx.WriteString("foo bar") } func (c *FinalController) GetSomething() { c.Service.DoSomething(c.Ctx) } func TestControllersInsideControllerDeep(t *testing.T) { app := iris.New() m := New(app) m.Register(new(testServiceDoSomething)) m.Handle(new(FinalController)) e := httptest.New(t, app) e.GET("/something").Expect().Status(httptest.StatusOK).Body().IsEqual("foo bar") } type testApplicationDependency struct { App *Application } func (c *testApplicationDependency) Get() string { return c.App.Name } func TestApplicationDependency(t *testing.T) { app := iris.New() m := New(app).SetName("app1") m.Handle(new(testApplicationDependency)) m2 := m.Clone(app.Party("/other")).SetName("app2") m2.Handle(new(testApplicationDependency)) e := httptest.New(t, app) e.GET("/").Expect().Status(httptest.StatusOK).Body().IsEqual("app1") e.GET("/other").Expect().Status(httptest.StatusOK).Body().IsEqual("app2") } type testControllerMethodHandlerBindStruct struct{} type bindStructData struct { Name string `json:"name" url:"name"` } func (*testControllerMethodHandlerBindStruct) Any(data bindStructData) bindStructData { return data } func (*testControllerMethodHandlerBindStruct) PostBySlice(id uint64, manyData []bindStructData) []bindStructData { return manyData } type dataSlice []bindStructData func (*testControllerMethodHandlerBindStruct) PostBySlicetype(id uint64, manyData dataSlice) dataSlice { return manyData } type dataSlicePtr []*bindStructData func (*testControllerMethodHandlerBindStruct) PostBySlicetypeptr(id uint64, manyData dataSlicePtr) dataSlicePtr { return manyData } func TestControllerMethodHandlerBindStruct(t *testing.T) { app := iris.New() m := New(app.Party("/data")) m.HandleError(func(ctx iris.Context, err error) { t.Fatalf("Path: %s, Error: %v", ctx.Path(), err) }) m.Handle(new(testControllerMethodHandlerBindStruct)) data := bindStructData{Name: "kataras"} manyData := []bindStructData{data, {"john doe"}} e := httptest.New(t, app) e.GET("/data").WithQueryObject(data).Expect().Status(httptest.StatusOK).JSON().IsEqual(data) e.PATCH("/data").WithJSON(data).Expect().Status(httptest.StatusOK).JSON().IsEqual(data) e.POST("/data/42/slice").WithJSON(manyData).Expect().Status(httptest.StatusOK).JSON().IsEqual(manyData) e.POST("/data/42/slicetype").WithJSON(manyData).Expect().Status(httptest.StatusOK).JSON().IsEqual(manyData) e.POST("/data/42/slicetypeptr").WithJSON(manyData).Expect().Status(httptest.StatusOK).JSON().IsEqual(manyData) // more tests inside the hero package itself. } func TestErrorHandlerContinue(t *testing.T) { app := iris.New() m := New(app) m.Handle(new(testControllerErrorHandlerContinue)) m.Handle(new(testControllerFieldErrorHandlerContinue)) e := httptest.New(t, app) for _, path := range []string{"/test", "/test/field"} { e.POST(path).WithMultipart(). WithFormField("username", "makis"). WithFormField("age", "27"). WithFormField("unknown", "continue"). Expect().Status(httptest.StatusOK).Body().IsEqual("makis is 27 years old\n") } } type testControllerErrorHandlerContinue struct{} type registerForm struct { Username string `form:"username"` Age int `form:"age"` } func (c *testControllerErrorHandlerContinue) HandleError(ctx iris.Context, err error) { if iris.IsErrPath(err) { return // continue. } ctx.StopWithError(iris.StatusBadRequest, err) } func (c *testControllerErrorHandlerContinue) PostTest(form registerForm) string { return fmt.Sprintf("%s is %d years old\n", form.Username, form.Age) } type testControllerFieldErrorHandlerContinue struct { Form *registerForm } func (c *testControllerFieldErrorHandlerContinue) HandleError(ctx iris.Context, err error) { if iris.IsErrPath(err) { return // continue. } ctx.StopWithError(iris.StatusBadRequest, err) } func (c *testControllerFieldErrorHandlerContinue) PostTestField() string { return fmt.Sprintf("%s is %d years old\n", c.Form.Username, c.Form.Age) } ================================================ FILE: mvc/grpc.go ================================================ package mvc import ( "net/http" "path" "github.com/kataras/iris/v12/context" ) // GRPC registers a controller which serves gRPC clients. // It accepts the controller ptr to a struct value, // the gRPCServer itself, and a strict option which is explained below. // // The differences between an GRPC-based controller and a common one are: // HTTP verb: only POST (Party.AllowMethods can be used for more), // method parsing is disabled: path is the function name as it is, // if 'strictMode' option is true then this controller will only serve gRPC-based clients // and fires 404 on common HTTP clients, // otherwise HTTP clients can send and receive JSON (protos contain json struct fields by-default). type GRPC struct { // Server is required and should be gRPC Server derives from google's grpc package. Server http.Handler // ServiceName is required and should be the name of the service (used to build the gRPC route path), // e.g. "helloworld.Greeter". // For a controller's method of "SayHello" and ServiceName "helloworld.Greeter", // both gRPC and common HTTP request path is: "/helloworld.Greeter/SayHello". // // Tip: the ServiceName can be fetched through proto's file descriptor, e.g. // serviceName := pb.File_helloworld_proto.Services().Get(0).FullName(). ServiceName string // When Strict option is true then this controller will only serve gRPC-based clients // and fires 404 on common HTTP clients. Strict bool } var _ Option = GRPC{} // Apply parses the controller's methods and registers gRPC handlers to the application. func (g GRPC) Apply(c *ControllerActivator) { defer c.Activated() pre := func(ctx *context.Context) { if ctx.IsGRPC() { // gRPC, consumes and produces protobuf. g.Server.ServeHTTP(ctx.ResponseWriter(), ctx.Request()) ctx.StopExecution() return } // If strict was true fires 404 on common HTTP clients. if g.Strict { ctx.NotFound() ctx.StopExecution() return } // Allow common HTTP clients, consumes and produces JSON. ctx.Next() } for i := 0; i < c.Type.NumMethod(); i++ { m := c.Type.Method(i) path := path.Join(g.ServiceName, m.Name) if g.Strict { c.app.Router.HandleMany(http.MethodPost, path, pre) } else if route := c.Handle(http.MethodPost, path, m.Name, pre); route != nil { bckp := route.Description route.Description = "gRPC" if g.Strict { route.Description += "-only" } route.Description += " " + bckp // e.g. "gRPC controller" } } } ================================================ FILE: mvc/mvc.go ================================================ package mvc import ( "fmt" "reflect" "strings" "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/core/router" "github.com/kataras/iris/v12/hero" "github.com/kataras/iris/v12/websocket" "github.com/kataras/golog" "github.com/kataras/pio" ) // Application is the high-level component of the "mvc" package. // It's the API that you will be using to register controllers among with their // dependencies that your controllers may expecting. // It contains the Router(iris.Party) in order to be able to register // template layout, middleware, done handlers as you used with the // standard Iris APIBuilder. // // The Engine is created by the `New` method and it's the dependencies holder // and controllers factory. // // See `mvc#New` for more. type Application struct { container *hero.Container // This Application's Name. Keep names unique to each other. Name string Router router.Party Controllers []*ControllerActivator websocketControllers []websocket.ConnHandler // Disables verbose logging for controllers under this and its children mvc apps. // Defaults to false. controllersNoLog bool // Set custom path customPathWordFunc CustomPathWordFunc } func newApp(subRouter router.Party, container *hero.Container) *Application { app := &Application{ Router: subRouter, container: container, } // Register this Application so any field or method's input argument of // *mvc.Application can point to the current MVC application that the controller runs on. registerBuiltinDependencies(container, app) return app } // See `hero.BuiltinDependencies` too, here we are registering dependencies per MVC Application. func registerBuiltinDependencies(container *hero.Container, deps ...any) { for _, dep := range deps { depTyp := reflect.TypeOf(dep) for i, dependency := range container.Dependencies { if dependency.Static { if dependency.DestType == depTyp { // Remove any existing before register this one (see app.Clone). copy(container.Dependencies[i:], container.Dependencies[i+1:]) container.Dependencies = container.Dependencies[:len(container.Dependencies)-1] break } } } container.Register(dep) } } // New returns a new mvc Application based on a "party". // Application creates a new engine which is responsible for binding the dependencies // and creating and activating the app's controller(s). // // Example: `New(app.Party("/todo"))` or `New(app)` as it's the same as `New(app.Party("/"))`. func New(party router.Party) *Application { return newApp(party, party.ConfigureContainer().Container.Clone()) } // Configure creates a new controller and configures it, // this function simply calls the `New(party)` and its `.Configure(configurators...)`. // // A call of `mvc.New(app.Party("/path").Configure(buildMyMVC)` is equal to // // `mvc.Configure(app.Party("/path"), buildMyMVC)`. // // Read more at `New() Application` and `Application#Configure` methods. func Configure(party router.Party, configurators ...func(*Application)) *Application { // Author's Notes-> // About the Configure's comment: +5 space to be shown in equal width to the previous or after line. // // About the Configure's design chosen: // Yes, we could just have a `New(party, configurators...)` // but I think the `New()` and `Configure(configurators...)` API seems more native to programmers, // at least to me and the people I ask for their opinion between them. // Because the `New()` can actually return something that can be fully configured without its `Configure`, // its `Configure` is there just to design the apps better and help end-devs to split their code wisely. return New(party).Configure(configurators...) } // Configure can be used to pass one or more functions that accept this // Application, use this to add dependencies and controller(s). // // Example: `New(app.Party("/todo")).Configure(func(mvcApp *mvc.Application){...})`. func (app *Application) Configure(configurators ...func(*Application)) *Application { for _, c := range configurators { c(app) } return app } // SetName sets a unique name to this MVC Application. // Used for logging, not used in runtime yet, but maybe useful for future features. // // It returns this Application. func (app *Application) SetName(appName string) *Application { app.Name = appName return app } // SetCustomPathWordFunc sets a custom function // which is responsible to override the existing controllers method parsing. func (app *Application) SetCustomPathWordFunc(wordFunc CustomPathWordFunc) *Application { app.customPathWordFunc = wordFunc return app } // SetControllersNoLog disables verbose logging for next registered controllers // under this App and its children of `Application.Party` or `Application.Clone`. // // To disable logging for routes under a Party, // see `Party.SetRoutesNoLog` instead. // // Defaults to false when log level is "debug". func (app *Application) SetControllersNoLog(disable bool) *Application { app.controllersNoLog = disable return app } // EnableStructDependents will try to resolve // the fields of a struct value, if any, when it's a dependent struct value // based on the previous registered dependencies. func (app *Application) EnableStructDependents() *Application { app.container.EnableStructDependents = true return app } // Register appends one or more values as dependencies. // The value can be a single struct value-instance or a function // which has one input and one output, the input should be // an `iris.Context` and the output can be any type, that output type // will be bind-ed to the controller's field, if matching or to the // controller's methods, if matching. // // These dependencies "dependencies" can be changed per-controller as well, // via controller's `BeforeActivation` and `AfterActivation` methods, // look the `Handle` method for more. // // It returns this Application. // // Example: `.Register(loggerService{prefix: "dev"}, func(ctx iris.Context) User {...})`. func (app *Application) Register(dependencies ...any) *Application { if len(dependencies) > 0 && len(app.container.Dependencies) == len(hero.BuiltinDependencies) && len(app.Controllers) > 0 { allControllerNamesSoFar := make([]string, len(app.Controllers)) for i := range app.Controllers { allControllerNamesSoFar[i] = app.Controllers[i].Name() } golog.Warnf(`mvc.Application#Register called after mvc.Application#Handle. The controllers[%s] may miss required dependencies. Set the Logger's Level to "debug" to view the active dependencies per controller.`, strings.Join(allControllerNamesSoFar, ",")) } for _, dependency := range dependencies { app.container.Register(dependency) } return app } type ( // Option is an interface which does contain a single `Apply` method that accepts // a `ControllerActivator`. It can be passed on `Application.Handle` method to // mdoify the behavior right after the `BeforeActivation` state. // // See `GRPC` package-level structure // and `Version` package-level function too. Option interface { Apply(*ControllerActivator) } // OptionFunc is the functional type of `Option`. // Read `Option` docs. OptionFunc func(*ControllerActivator) ) // Apply completes the `Option` interface. func (opt OptionFunc) Apply(c *ControllerActivator) { opt(c) } // IgnoreEmbedded is an Option which can be used to ignore all embedded struct's method handlers. // Note that even if the controller overrides the embedded methods // they will be still ignored because Go doesn't support this detection so far. // For global affect, set the `IgnoreEmbeddedControllers` package-level variable to true. var IgnoreEmbedded OptionFunc = func(c *ControllerActivator) { c.SkipEmbeddedMethods() } // Handle serves a controller for the current mvc application's Router. // It accept any custom struct which its functions will be transformed // to routes. // // If "controller" has `BeforeActivation(b mvc.BeforeActivation)` // or/and `AfterActivation(a mvc.AfterActivation)` then these will be called between the controller's `.activate`, // use those when you want to modify the controller before or/and after // the controller will be registered to the main Iris Application. // // It returns this mvc Application. // // Usage: `.Handle(new(TodoController))`. // // Controller accepts a sub router and registers any custom struct // as controller, if struct doesn't have any compatible methods // neither are registered via `ControllerActivator`'s `Handle` method // then the controller is not registered at all. // // A Controller may have one or more methods // that are wrapped to a handler and registered as routes before the server ran. // The controller's method can accept any input argument that are previously binded // via the dependencies or route's path accepts dynamic path parameters. // The controller's fields are also bindable via the dependencies, either a // static value (service) or a function (dynamically) which accepts a context // and returns a single value (this type is being used to find the relative field or method's input argument). // // func(c *ExampleController) Get() string | // (string, string) | // (string, int) | // int | // (int, string | // (string, error) | // bool | // (any, bool) | // error | // (int, error) | // (customStruct, error) | // customStruct | // (customStruct, int) | // (customStruct, string) | // Result or (Result, error) // where Get is an HTTP Method func. // // Default behavior can be changed through second, variadic, variable "options", // e.g. Handle(controller, GRPC {Server: grpcServer, Strict: true}) // // Examples at: https://github.com/kataras/iris/tree/main/_examples/mvc func (app *Application) Handle(controller any, options ...Option) *Application { c := app.handle(controller, options...) // Note: log on register-time, so they can catch any failures before build. if !app.controllersNoLog { // log only http (and versioned) or grpc controllers, // websocket is already logging itself. logController(app.Router.Logger(), c) } return app } // HandleWebsocket handles a websocket specific controller. // Its exported methods are the events. // If a "Namespace" field or method exists then namespace is set, otherwise empty namespace. // Note that a websocket controller is registered and ran under a specific connection connected to a namespace // and it cannot send HTTP responses on that state. // However all static and dynamic dependency injection features are working, as expected, like any regular MVC Controller. func (app *Application) HandleWebsocket(controller any) *websocket.Struct { c := app.handle(controller) c.markAsWebsocket() websocketController := websocket.NewStruct(c.Value).SetInjector(makeInjector(c.injector)) app.websocketControllers = append(app.websocketControllers, websocketController) return websocketController } func makeInjector(s *hero.Struct) websocket.StructInjector { return func(_ reflect.Type, nsConn *websocket.NSConn) reflect.Value { v, _ := s.Acquire(websocket.GetContext(nsConn.Conn)) return v } } var _ websocket.ConnHandler = (*Application)(nil) // GetNamespaces completes the websocket ConnHandler interface. // It returns a collection of namespace and events that // were registered through `HandleWebsocket` controllers. func (app *Application) GetNamespaces() websocket.Namespaces { if logger := app.Router.Logger(); logger.Level == golog.DebugLevel && !app.controllersNoLog { websocket.EnableDebug(logger) } return websocket.JoinConnHandlers(app.websocketControllers...).GetNamespaces() } func (app *Application) handle(controller any, options ...Option) *ControllerActivator { // initialize the controller's activator, nothing too magical so far. c := newControllerActivator(app, controller) // check the controller's "BeforeActivation" or/and "AfterActivation" method(s) between the `activate` // call, which is simply parses the controller's methods, end-dev can register custom controller's methods // by using the BeforeActivation's (a ControllerActivation) `.Handle` method. if before, ok := controller.(interface { BeforeActivation(BeforeActivation) }); ok { before.BeforeActivation(c) } for _, opt := range options { if opt != nil { opt.Apply(c) } } c.activate() if after, okAfter := controller.(interface { AfterActivation(AfterActivation) }); okAfter { after.AfterActivation(c) } app.Controllers = append(app.Controllers, c) return c } // HandleError registers a `hero.ErrorHandlerFunc` which will be fired when // application's controllers' functions returns an non-nil error. // Each controller can override it by implementing the `hero.ErrorHandler`. func (app *Application) HandleError(handler func(ctx *context.Context, err error)) *Application { errorHandler := hero.ErrorHandlerFunc(handler) app.container.GetErrorHandler = func(*context.Context) hero.ErrorHandler { return errorHandler } return app } // Clone returns a new mvc Application which has the dependencies // of the current mvc Application's `Dependencies` and its `ErrorHandler`. // // Example: `.Clone(app.Party("/path")).Handle(new(TodoSubController))`. func (app *Application) Clone(party router.Party) *Application { cloned := newApp(party, app.container.Clone()) cloned.controllersNoLog = app.controllersNoLog return cloned } // Party returns a new child mvc Application based on the current path + "relativePath". // The new mvc Application has the same dependencies of the current mvc Application, // until otherwise specified later manually. // // The router's root path of this child will be the current mvc Application's root path + "relativePath". func (app *Application) Party(relativePath string, middleware ...context.Handler) *Application { return app.Clone(app.Router.Party(relativePath, middleware...)) } var childNameReplacer = strings.NewReplacer("*", "", "(", "", ")", "") func getArrowSymbol(static bool, field bool) string { if field { if static { return "╺" } return "⦿" } if static { return "•" } return "⦿" } // TODO: instead of this I want to get in touch with tools like "graphviz" // so we can put all that information (and the API) inside web graphs, // it will be easier for developers to see the flow of the whole application, // but probalby I will never find time for that as we have higher priorities...just a reminder though. func logController(logger *golog.Logger, c *ControllerActivator) { if logger.Level != golog.DebugLevel { return } if c.injector == nil { // when no actual controller methods are registered. return } /* [DBUG] controller.GreetController ╺ Service → ./service/greet_service.go:16 ╺ Get GET /greet • iris.Context • service.Other → ./service/other_service.go:11 */ bckpNewLine := logger.NewLine bckpTimeFormat := logger.TimeFormat logger.NewLine = false logger.TimeFormat = "" printer := logger.Printer reports := c.injector.Container.Reports ctrlName := c.RelName() ctrlScopeType := "" if !c.injector.Singleton { ctrlScopeType = getArrowSymbol(false, false) + " " } logger.Debugf("%s%s\n", ctrlScopeType, ctrlName) longestNameLen := 0 for _, report := range reports { for _, entry := range report.Entries { if n := len(entry.InputFieldName); n > longestNameLen { if strings.HasSuffix(entry.InputFieldName, ctrlName) { continue } longestNameLen = n } } } longestMethodName := 0 for methodName := range c.routes { if n := len(methodName); n > longestMethodName { longestMethodName = n } } lastColorCode := -1 for _, report := range reports { childName := childNameReplacer.Replace(report.Name) if idx := strings.Index(childName, c.Name()); idx >= 0 { childName = childName[idx+len(c.Name()):] // it's always +1 otherwise should be reported as BUG. } if childName != "" && childName[0] == '.' { // It's a struct's method. childName = childName[1:] for _, route := range c.routes[childName] { if route.NoLog { continue } // Let them be logged again with the middlewares, e.g UseRouter or UseGlobal after this MVC app created. // route.NoLog = true colorCode := router.TraceTitleColorCode(route.Method) // group same methods (or errors). if lastColorCode == -1 { lastColorCode = colorCode } else if lastColorCode != colorCode { lastColorCode = colorCode fmt.Fprintln(printer) } fmt.Fprint(printer, " ╺ ") pio.WriteRich(printer, childName, colorCode) entries := report.Entries[1:] // the ctrl value is always the first input argument so 1:.. if len(entries) == 0 { fmt.Print("()") } fmt.Fprintln(printer) // pio.WriteRich(printer, " "+route.GetTitle(), colorCode) fmt.Fprintf(printer, " %s\n", route.String()) for _, entry := range entries { fileLine := "" if !strings.Contains(entry.DependencyFile, "kataras/iris/") { fileLine = fmt.Sprintf("→ %s:%d", entry.DependencyFile, entry.DependencyLine) } fieldName := entry.InputFieldName spaceRequired := longestNameLen - len(fieldName) if spaceRequired < 0 { spaceRequired = 0 } // → ⊳ ↔ fmt.Fprintf(printer, " • %s%s %s\n", fieldName, strings.Repeat(" ", spaceRequired), fileLine) } } } else { // It's a struct's field. for _, entry := range report.Entries { fileLine := "" if !strings.Contains(entry.DependencyFile, "kataras/iris/") { fileLine = fmt.Sprintf("→ %s:%d", entry.DependencyFile, entry.DependencyLine) } fieldName := entry.InputFieldName spaceRequired := longestNameLen + 2 - len(fieldName) // plus the two spaces because it's not collapsed. if spaceRequired < 0 { spaceRequired = 0 } arrowSymbol := getArrowSymbol(entry.Static, true) fmt.Fprintf(printer, " %s %s%s %s\n", arrowSymbol, fieldName, strings.Repeat(" ", spaceRequired), fileLine) } } } // fmt.Fprintln(printer) logger.NewLine = bckpNewLine logger.TimeFormat = bckpTimeFormat } ================================================ FILE: mvc/reflect.go ================================================ package mvc import ( "reflect" "github.com/kataras/iris/v12/context" ) var baseControllerTyp = reflect.TypeOf((*BaseController)(nil)).Elem() func isBaseController(ctrlTyp reflect.Type) bool { return ctrlTyp.Implements(baseControllerTyp) } // indirectType returns the value of a pointer-type "typ". // If "typ" is a pointer, array, chan, map or slice it returns its Elem, // otherwise returns the typ as it's. func indirectType(typ reflect.Type) reflect.Type { switch typ.Kind() { case reflect.Ptr, reflect.Array, reflect.Chan, reflect.Map, reflect.Slice: return typ.Elem() } return typ } func getSourceFileLine(ctrlType reflect.Type, m reflect.Method) (string, int) { // used for debug logs. sourceFileName, sourceLineNumber := context.HandlerFileLineRel(m.Func) if sourceFileName == "" { elem := indirectType(ctrlType) for i, n := 0, elem.NumField(); i < n; i++ { if f := elem.Field(i); f.Anonymous { typ := indirectType(f.Type) if typ.Kind() != reflect.Struct { continue // field is not a struct. } // why we do that? // because if the element is not Ptr // then it's probably used as: // type ctrl { // BaseCtrl // } // but BaseCtrl has not the method, *BaseCtrl does: // (c *BaseCtrl) HandleHTTPError(...) // so we are creating a new temporary value ptr of that type // and searching inside it for the method instead. typ = reflect.New(typ).Type() if embeddedMethod, ok := typ.MethodByName(m.Name); ok { sourceFileName, sourceLineNumber = context.HandlerFileLineRel(embeddedMethod.Func) } } } } return sourceFileName, sourceLineNumber } ================================================ FILE: mvc/versioning.go ================================================ package mvc import ( "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/core/router" "github.com/kataras/iris/v12/versioning" ) // Version returns a valid `Option` that can be passed to the `Application.Handle` method. // It requires a specific "version" constraint for a Controller, // e.g. ">1.0.0 <=2.0.0". // // Usage: // // m := mvc.New(dataRouter) // m.Handle(new(v1Controller), mvc.Version("1.0.0"), mvc.Deprecated(mvc.DeprecationOptions{})) // m.Handle(new(v2Controller), mvc.Version("2.3.0")) // m.Handle(new(v3Controller), mvc.Version(">=3.0.0 <4.0.0")) // m.Handle(new(noVersionController)) // // See the `versioning` package's documentation for more information on // how the version is extracted from incoming requests. // // Note that this Option will set the route register rule to `RouteOverlap`. func Version(version string) OptionFunc { return func(c *ControllerActivator) { c.Router().SetRegisterRule(router.RouteOverlap) // required for this feature. // Note: Do not use a group, we need c.Use for the specific controller's routes. c.Use(versioning.Handler(version)) } } // Deprecated marks a specific Controller as a deprecated one. // Deprecated can be used to tell the clients that // a newer version of that specific resource is available instead. func Deprecated(options DeprecationOptions) OptionFunc { return func(c *ControllerActivator) { c.Use(func(ctx *context.Context) { versioning.WriteDeprecated(ctx, options) ctx.Next() }) } } ================================================ FILE: sessions/config.go ================================================ package sessions import ( "time" "github.com/kataras/iris/v12/context" "github.com/google/uuid" "github.com/kataras/golog" ) const ( // DefaultCookieName the secret cookie's name for sessions DefaultCookieName = "irissessionid" ) type ( // Config is the configuration for sessions. Please read it before using sessions. Config struct { // Logger instance for sessions usage, e.g. { Logger: app.Logger() }. // Defaults to a child of "sessions" of the latest Iris Application's main Logger. Logger *golog.Logger // Cookie string, the session's client cookie name, for example: "mysessionid" // // Defaults to "irissessionid". Cookie string // CookieSecureTLS set to true if server is running over TLS // and you need the session's cookie "Secure" field to be set true. // Defaults to false. CookieSecureTLS bool // AllowReclaim will allow to // Destroy and Start a session in the same request handler. // All it does is that it removes the cookie for both `Request` and `ResponseWriter` while `Destroy` // or add a new cookie to `Request` while `Start`. // // Defaults to false. AllowReclaim bool // Encoding should encodes and decodes // authenticated and optionally encrypted cookie values. // // Defaults to nil. Encoding context.SecureCookie // Expires the duration of which the cookie must expires (created_time.Add(Expires)). // If you want to delete the cookie when the browser closes, set it to -1. // However, if you use a database storage setting this value to -1 may // cause you problems because of the fact that the database // may has its own expiration mechanism and value will be expired and removed immediately. // // 0 means no expire, (24 years) // -1 means when browser closes // > 0 is the time.Duration which the session cookies should expire. // // Defaults to infinitive/unlimited life duration(0). Expires time.Duration // SessionIDGenerator can be set to a function which // return a unique session id. // By default we will use a uuid impl package to generate // that, but developers can change that with simple assignment. SessionIDGenerator func(ctx *context.Context) string // DisableSubdomainPersistence set it to true in order dissallow your subdomains to have access to the session cookie // // Defaults to false. DisableSubdomainPersistence bool } ) // Validate corrects missing fields configuration fields and returns the right configuration func (c Config) Validate() Config { if c.Logger == nil { c.Logger = context.DefaultLogger("sessions") } if c.Cookie == "" { c.Cookie = DefaultCookieName } if c.SessionIDGenerator == nil { c.SessionIDGenerator = func(ctx *context.Context) string { id, err := uuid.NewRandom() if err != nil { ctx.StopWithError(400, err) return "" } return id.String() } } return c } ================================================ FILE: sessions/database.go ================================================ package sessions import ( "errors" "reflect" "sync" "time" "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/core/memstore" "github.com/kataras/golog" ) // ErrNotImplemented is returned when a particular feature is not yet implemented yet. // It can be matched directly, i.e: `isNotImplementedError := sessions.ErrNotImplemented.Equal(err)`. var ErrNotImplemented = errors.New("not implemented yet") // Database is the interface which all session databases should implement // By design it doesn't support any type of cookie session like other frameworks. // I want to protect you, believe me. // The scope of the database is to store somewhere the sessions in order to // keep them after restarting the server, nothing more. // // Synchronization are made automatically, you can register one using `UseDatabase`. // // Look the `sessiondb` folder for databases implementations. type Database interface { // SetLogger should inject a logger to this Database. SetLogger(*golog.Logger) // Acquire receives a session's lifetime from the database, // if the return value is LifeTime{} then the session manager sets the life time based on the expiration duration lives in configuration. Acquire(sid string, expires time.Duration) memstore.LifeTime // OnUpdateExpiration should re-set the expiration (ttl) of the session entry inside the database, // it is fired on `ShiftExpiration` and `UpdateExpiration`. // If the database does not support change of ttl then the session entry will be cloned to another one // and the old one will be removed, it depends on the chosen database storage. // // Check of error is required, if error returned then the rest session's keys are not proceed. // // If a database does not support this feature then an `ErrNotImplemented` will be returned instead. OnUpdateExpiration(sid string, newExpires time.Duration) error // Set sets a key value of a specific session. // The "immutable" input argument depends on the store, it may not implement it at all. Set(sid string, key string, value any, ttl time.Duration, immutable bool) error // Get retrieves a session value based on the key. Get(sid string, key string) any // Decode binds the "outPtr" to the value associated to the provided "key". Decode(sid, key string, outPtr any) error // Visit loops through all session keys and values. Visit(sid string, cb func(key string, value any)) error // Len returns the length of the session's entries (keys). Len(sid string) int // Delete removes a session key value based on its key. Delete(sid string, key string) (deleted bool) // Clear removes all session key values but it keeps the session entry. Clear(sid string) error // Release destroys the session, it clears and removes the session entry, // session manager will create a new session ID on the next request after this call. Release(sid string) error // Close should terminate the database connection. It's called automatically on interrupt signals. Close() error } // DatabaseRequestHandler is an optional interface that a sessions database // can implement. It contains a single EndRequest method which is fired // on the very end of the request life cycle. It should be used to Flush // any local session's values to the client. type DatabaseRequestHandler interface { EndRequest(ctx *context.Context, session *Session) } type mem struct { values map[string]*memstore.Store mu sync.RWMutex } var _ Database = (*mem)(nil) func newMemDB() Database { return &mem{values: make(map[string]*memstore.Store)} } func (s *mem) SetLogger(*golog.Logger) {} func (s *mem) Acquire(sid string, expires time.Duration) memstore.LifeTime { s.mu.Lock() s.values[sid] = new(memstore.Store) s.mu.Unlock() return memstore.LifeTime{} } // Do nothing, the `LifeTime` of the Session will be managed by the callers automatically on memory-based storage. func (s *mem) OnUpdateExpiration(string, time.Duration) error { return nil } // immutable depends on the store, it may not implement it at all. func (s *mem) Set(sid string, key string, value any, _ time.Duration, immutable bool) error { s.mu.RLock() store, ok := s.values[sid] s.mu.RUnlock() if ok { store.Save(key, value, immutable) } return nil } func (s *mem) Get(sid string, key string) any { s.mu.RLock() store, ok := s.values[sid] s.mu.RUnlock() if ok { return store.Get(key) } return nil } func (s *mem) Decode(sid string, key string, outPtr any) error { v := s.Get(sid, key) if v != nil { reflect.ValueOf(outPtr).Set(reflect.ValueOf(v)) } return nil } func (s *mem) Visit(sid string, cb func(key string, value any)) error { s.mu.RLock() store, ok := s.values[sid] s.mu.RUnlock() if ok { store.Visit(cb) } return nil } func (s *mem) Len(sid string) int { s.mu.RLock() store, ok := s.values[sid] s.mu.RUnlock() if ok { return store.Len() } return 0 } func (s *mem) Delete(sid string, key string) (deleted bool) { s.mu.RLock() store, ok := s.values[sid] s.mu.RUnlock() if ok { deleted = store.Remove(key) } return } func (s *mem) Clear(sid string) error { s.mu.RLock() store, ok := s.values[sid] s.mu.RUnlock() if ok { store.Reset() } return nil } func (s *mem) Release(sid string) error { s.mu.Lock() delete(s.values, sid) s.mu.Unlock() return nil } func (s *mem) Close() error { return nil } ================================================ FILE: sessions/provider.go ================================================ package sessions import ( "errors" "sync" "time" "github.com/kataras/iris/v12/context" ) type ( // provider contains the sessions and external databases (load and update). // It's the session memory manager provider struct { mu sync.RWMutex sessions map[string]*Session db Database dbRequestHandler DatabaseRequestHandler destroyListeners []DestroyListener } ) // newProvider returns a new sessions provider func newProvider() *provider { p := &provider{ sessions: make(map[string]*Session), db: newMemDB(), } return p } // RegisterDatabase sets a session database. func (p *provider) RegisterDatabase(db Database) { if db == nil { return } p.mu.Lock() // for any case p.db = db if dbreq, ok := db.(DatabaseRequestHandler); ok { p.dbRequestHandler = dbreq } p.mu.Unlock() } // newSession returns a new session from sessionid func (p *provider) newSession(man *Sessions, sid string, expires time.Duration) *Session { sess := &Session{ sid: sid, Man: man, provider: p, } onExpire := func() { p.mu.Lock() p.deleteSession(sess) p.mu.Unlock() } lifetime := p.db.Acquire(sid, expires) // simple and straight: if !lifetime.IsZero() { // if stored time is not zero // start a timer based on the stored time, if not expired. lifetime.Revive(onExpire) } else { // Remember: if db not exist or it has been expired // then the stored time will be zero(see loadSessionFromDB) and the values will be empty. // // Even if the database has an unlimited session (possible by a previous app run) // priority to the "expires" is given, // again if <=0 then it does nothing. lifetime.Begin(expires, onExpire) } sess.Lifetime = &lifetime return sess } // Init creates the session and returns it func (p *provider) Init(man *Sessions, sid string, expires time.Duration) *Session { newSession := p.newSession(man, sid, expires) newSession.isNew = true p.mu.Lock() p.sessions[sid] = newSession p.mu.Unlock() return newSession } func (p *provider) EndRequest(ctx *context.Context, session *Session) { if p.dbRequestHandler != nil { p.dbRequestHandler.EndRequest(ctx, session) } } // ErrNotFound may be returned from `UpdateExpiration` of a non-existing or // invalid session entry from memory storage or databases. // Usage: // // if err != nil && err.Is(err, sessions.ErrNotFound) { // [handle error...] // } var ErrNotFound = errors.New("session not found") // UpdateExpiration resets the expiration of a session. // if expires > 0 then it will try to update the expiration and destroy task is delayed. // if expires <= 0 then it does nothing it returns nil, to destroy a session call the `Destroy` func instead. // // If the session is not found, it returns a `NotFound` error, this can only happen when you restart the server and you used the memory-based storage(default), // because the call of the provider's `UpdateExpiration` is always called when the client has a valid session cookie. // // If a backend database is used then it may return an `ErrNotImplemented` error if the underline database does not support this operation. func (p *provider) UpdateExpiration(sid string, expires time.Duration) error { if expires <= 0 { return nil } p.mu.RLock() sess, found := p.sessions[sid] p.mu.RUnlock() if !found { return ErrNotFound } sess.Lifetime.Shift(expires) return p.db.OnUpdateExpiration(sid, expires) } // Read returns the store which sid parameter belongs func (p *provider) Read(man *Sessions, sid string, expires time.Duration) *Session { p.mu.RLock() sess, found := p.sessions[sid] p.mu.RUnlock() if found { sess.mu.Lock() sess.isNew = false sess.mu.Unlock() sess.runFlashGC() // run the flash messages GC, new request here of existing session return sess } return p.Init(man, sid, expires) // if not found create new } func (p *provider) registerDestroyListener(ln DestroyListener) { if ln == nil { return } p.destroyListeners = append(p.destroyListeners, ln) } func (p *provider) fireDestroy(sid string) { for _, ln := range p.destroyListeners { ln(sid) } } // Destroy destroys the session, removes all sessions and flash values, // the session itself and updates the registered session databases, // this called from sessionManager which removes the client's cookie also. func (p *provider) Destroy(sid string) { p.mu.Lock() if sess, found := p.sessions[sid]; found { p.deleteSession(sess) } p.mu.Unlock() } // DestroyAll removes all sessions // from the server-side memory (and database if registered). // Client's session cookie will still exist but it will be reseted on the next request. func (p *provider) DestroyAll() { p.mu.Lock() for _, sess := range p.sessions { p.deleteSession(sess) } p.mu.Unlock() } func (p *provider) deleteSession(sess *Session) { sid := sess.sid delete(p.sessions, sid) p.db.Release(sid) p.fireDestroy(sid) } /* func (p *provider) regenerateID(ctx *context.Context, oldsid string) { p.mu.RLock() sess, ok := p.sessions[oldsid] p.mu.RUnlock() if ok { newsid := sess.Man.config.SessionIDGenerator(ctx) sess.mu.Lock() sess.sid = newsid sess.mu.Unlock() p.mu.Lock() p.sessions[newsid] = sess delete(p.sessions, oldsid) p.mu.Unlock() } } */ ================================================ FILE: sessions/session.go ================================================ package sessions import ( "reflect" "strconv" "sync" "github.com/kataras/iris/v12/core/memstore" ) type ( // Session should expose the Sessions's end-user API. // It is the session's storage controller which you can // save or retrieve values based on a key. // // This is what will be returned when sess := sessions.Start(). Session struct { sid string isNew bool flashes map[string]*flashMessage mu sync.RWMutex // for flashes. // Lifetime it contains the expiration data, use it for read-only information. // See `Sessions.UpdateExpiration` too. Lifetime *memstore.LifeTime // Man is the sessions manager that this session created of. Man *Sessions provider *provider } flashMessage struct { // if true then this flash message is removed on the flash gc shouldRemove bool value any } ) // Destroy destroys this session, it removes its session values and any flashes. // This session entry will be removed from the server, // the registered session databases will be notified for this deletion as well. // // Note that this method does NOT remove the client's cookie, although // it should be reseted if new session is attached to that (client). // // Use the session's manager `Destroy(ctx)` in order to remove the cookie instead. func (s *Session) Destroy() { s.provider.Destroy(s.sid) } // ID returns the session's ID. func (s *Session) ID() string { return s.sid } // IsNew returns true if this session is just // created by the current application's process. func (s *Session) IsNew() bool { return s.isNew } // Get returns a value based on its "key". func (s *Session) Get(key string) any { return s.provider.db.Get(s.sid, key) } // Decode binds the given "outPtr" to the value associated to the provided "key". func (s *Session) Decode(key string, outPtr any) error { return s.provider.db.Decode(s.sid, key, outPtr) } // when running on the session manager removes any 'old' flash messages. func (s *Session) runFlashGC() { s.mu.Lock() for key, v := range s.flashes { if v.shouldRemove { delete(s.flashes, key) } } s.mu.Unlock() } // HasFlash returns true if this session has available flash messages. func (s *Session) HasFlash() bool { s.mu.RLock() has := len(s.flashes) > 0 s.mu.RUnlock() return has } // GetFlash returns a stored flash message based on its "key" // which will be removed on the next request. // // To check for flash messages we use the HasFlash() Method // and to obtain the flash message we use the GetFlash() Method. // There is also a method GetFlashes() to fetch all the messages. // // Fetching a message deletes it from the session. // This means that a message is meant to be displayed only on the first page served to the user. func (s *Session) GetFlash(key string) any { fv, ok := s.peekFlashMessage(key) if !ok { return nil } fv.shouldRemove = true return fv.value } // PeekFlash returns a stored flash message based on its "key". // Unlike GetFlash, this will keep the message valid for the next requests, // until GetFlashes or GetFlash("key"). func (s *Session) PeekFlash(key string) any { fv, ok := s.peekFlashMessage(key) if !ok { return nil } return fv.value } func (s *Session) peekFlashMessage(key string) (*flashMessage, bool) { s.mu.RLock() fv, found := s.flashes[key] s.mu.RUnlock() if !found { return nil, false } return fv, true } // GetString same as Get but returns its string representation, // if key doesn't exist then it returns an empty string. func (s *Session) GetString(key string) string { if value := s.Get(key); value != nil { if v, ok := value.(string); ok { return v } if v, ok := value.(int); ok { return strconv.Itoa(v) } if v, ok := value.(int64); ok { return strconv.FormatInt(v, 10) } } return "" } // GetStringDefault same as Get but returns its string representation, // if key doesn't exist then it returns the "defaultValue". func (s *Session) GetStringDefault(key string, defaultValue string) string { if v := s.GetString(key); v != "" { return v } return defaultValue } // GetFlashString same as `GetFlash` but returns its string representation, // if key doesn't exist then it returns an empty string. func (s *Session) GetFlashString(key string) string { return s.GetFlashStringDefault(key, "") } // GetFlashStringDefault same as `GetFlash` but returns its string representation, // if key doesn't exist then it returns the "defaultValue". func (s *Session) GetFlashStringDefault(key string, defaultValue string) string { if value := s.GetFlash(key); value != nil { if v, ok := value.(string); ok { return v } } return defaultValue } // ErrEntryNotFound similar to core/memstore#ErrEntryNotFound but adds // the value (if found) matched to the requested key-value pair of the session's memory storage. type ErrEntryNotFound struct { Err *memstore.ErrEntryNotFound Value any } func (e *ErrEntryNotFound) Error() string { return e.Err.Error() } // Unwrap method implements the dynamic Unwrap interface of the std errors package. func (e *ErrEntryNotFound) Unwrap() error { return e.Err } // As method implements the dynamic As interface of the std errors package. // As should be NOT used directly, use `errors.As` instead. func (e *ErrEntryNotFound) As(target any) bool { if v, ok := target.(*memstore.ErrEntryNotFound); ok && e.Err != nil { return e.Err.As(v) } v, ok := target.(*ErrEntryNotFound) if !ok { return false } if v.Value != nil { if v.Value != e.Value { return false } } if v.Err != nil { if e.Err != nil { return e.Err.As(v.Err) } return false } return true } func newErrEntryNotFound(key string, kind reflect.Kind, value any) *ErrEntryNotFound { return &ErrEntryNotFound{Err: &memstore.ErrEntryNotFound{Key: key, Kind: kind}, Value: value} } // GetInt same as `Get` but returns its int representation, // if key doesn't exist then it returns -1 and a non-nil error. func (s *Session) GetInt(key string) (int, error) { v := s.Get(key) if v != nil { if vint, ok := v.(int); ok { return vint, nil } if vfloat64, ok := v.(float64); ok { return int(vfloat64), nil } if vint64, ok := v.(int64); ok { return int(vint64), nil } if vstring, sok := v.(string); sok { return strconv.Atoi(vstring) } } return -1, newErrEntryNotFound(key, reflect.Int, v) } // GetIntDefault same as `Get` but returns its int representation, // if key doesn't exist then it returns the "defaultValue". func (s *Session) GetIntDefault(key string, defaultValue int) int { if v, err := s.GetInt(key); err == nil { return v } return defaultValue } // Increment increments the stored int value saved as "key" by +"n". // If value doesn't exist on that "key" then it creates one with the "n" as its value. // It returns the new, incremented, value. func (s *Session) Increment(key string, n int) (newValue int) { newValue = s.GetIntDefault(key, 0) newValue += n s.Set(key, newValue) return } // Decrement decrements the stored int value saved as "key" by -"n". // If value doesn't exist on that "key" then it creates one with the "n" as its value. // It returns the new, decremented, value even if it's less than zero. func (s *Session) Decrement(key string, n int) (newValue int) { newValue = s.GetIntDefault(key, 0) newValue -= n s.Set(key, newValue) return } // GetInt64 same as `Get` but returns its int64 representation, // if key doesn't exist then it returns -1 and a non-nil error. func (s *Session) GetInt64(key string) (int64, error) { v := s.Get(key) if v != nil { if vint64, ok := v.(int64); ok { return vint64, nil } if vfloat64, ok := v.(float64); ok { return int64(vfloat64), nil } if vint, ok := v.(int); ok { return int64(vint), nil } if vstring, sok := v.(string); sok { return strconv.ParseInt(vstring, 10, 64) } } return -1, newErrEntryNotFound(key, reflect.Int64, v) } // GetInt64Default same as `Get` but returns its int64 representation, // if key doesn't exist it returns the "defaultValue". func (s *Session) GetInt64Default(key string, defaultValue int64) int64 { if v, err := s.GetInt64(key); err == nil { return v } return defaultValue } // GetUint64 same as `Get` but returns as uint64, // if key doesn't exist then it returns 0 and a non-nil error. func (s *Session) GetUint64(key string) (uint64, error) { v := s.Get(key) if v != nil { switch vv := v.(type) { case string: val, err := strconv.ParseUint(vv, 10, 64) if err != nil { return 0, err } return uint64(val), nil case uint8: return uint64(vv), nil case uint16: return uint64(vv), nil case uint32: return uint64(vv), nil case uint64: return vv, nil case int64: return uint64(vv), nil case int: return uint64(vv), nil } } return 0, newErrEntryNotFound(key, reflect.Uint64, v) } // GetUint64Default same as `Get` but returns as uint64, // if key doesn't exist it returns the "defaultValue". func (s *Session) GetUint64Default(key string, defaultValue uint64) uint64 { if v, err := s.GetUint64(key); err == nil { return v } return defaultValue } // GetFloat32 same as `Get` but returns its float32 representation, // if key doesn't exist then it returns -1 and a non-nil error. func (s *Session) GetFloat32(key string) (float32, error) { v := s.Get(key) if vfloat32, ok := v.(float32); ok { return vfloat32, nil } if vfloat64, ok := v.(float64); ok { return float32(vfloat64), nil } if vint, ok := v.(int); ok { return float32(vint), nil } if vint64, ok := v.(int64); ok { return float32(vint64), nil } if vstring, sok := v.(string); sok { vfloat64, err := strconv.ParseFloat(vstring, 32) if err != nil { return -1, err } return float32(vfloat64), nil } return -1, newErrEntryNotFound(key, reflect.Float32, v) } // GetFloat32Default same as `Get` but returns its float32 representation, // if key doesn't exist then it returns the "defaultValue". func (s *Session) GetFloat32Default(key string, defaultValue float32) float32 { if v, err := s.GetFloat32(key); err == nil { return v } return defaultValue } // GetFloat64 same as `Get` but returns its float64 representation, // if key doesn't exist then it returns -1 and a non-nil error. func (s *Session) GetFloat64(key string) (float64, error) { v := s.Get(key) if vfloat32, ok := v.(float32); ok { return float64(vfloat32), nil } if vfloat64, ok := v.(float64); ok { return vfloat64, nil } if vint, ok := v.(int); ok { return float64(vint), nil } if vint64, ok := v.(int64); ok { return float64(vint64), nil } if vstring, sok := v.(string); sok { return strconv.ParseFloat(vstring, 32) } return -1, newErrEntryNotFound(key, reflect.Float64, v) } // GetFloat64Default same as `Get` but returns its float64 representation, // if key doesn't exist then it returns the "defaultValue". func (s *Session) GetFloat64Default(key string, defaultValue float64) float64 { if v, err := s.GetFloat64(key); err == nil { return v } return defaultValue } // GetBoolean same as `Get` but returns its boolean representation, // if key doesn't exist then it returns false and a non-nil error. func (s *Session) GetBoolean(key string) (bool, error) { v := s.Get(key) if v == nil { return false, newErrEntryNotFound(key, reflect.Bool, nil) } // here we could check for "true", "false" and 0 for false and 1 for true // but this may cause unexpected behavior from the developer if they expecting an error // so we just check if bool, if yes then return that bool, otherwise return false and an error. if vb, ok := v.(bool); ok { return vb, nil } if vstring, ok := v.(string); ok { return strconv.ParseBool(vstring) } return false, newErrEntryNotFound(key, reflect.Bool, v) } // GetBooleanDefault same as `Get` but returns its boolean representation, // if key doesn't exist then it returns the "defaultValue". func (s *Session) GetBooleanDefault(key string, defaultValue bool) bool { /* Note that here we can't do more than duplicate the GetBoolean's code, because of the "false". */ v := s.Get(key) if v == nil { return defaultValue } // here we could check for "true", "false" and 0 for false and 1 for true // but this may cause unexpected behavior from the developer if they expecting an error // so we just check if bool, if yes then return that bool, otherwise return false and an error. if vb, ok := v.(bool); ok { return vb } if vstring, ok := v.(string); ok { if b, err := strconv.ParseBool(vstring); err == nil { return b } } return defaultValue } // GetAll returns a copy of all session's values. func (s *Session) GetAll() map[string]any { items := make(map[string]any, s.provider.db.Len(s.sid)) s.mu.RLock() s.provider.db.Visit(s.sid, func(key string, value any) { items[key] = value }) s.mu.RUnlock() return items } // GetFlashes returns all flash messages as map[string](key) and any value // NOTE: this will cause at remove all current flash messages on the next request of the same user. func (s *Session) GetFlashes() map[string]any { flashes := make(map[string]any, len(s.flashes)) s.mu.Lock() for key, v := range s.flashes { flashes[key] = v.value v.shouldRemove = true } s.mu.Unlock() return flashes } // Visit loops each of the entries and calls the callback function func(key, value). func (s *Session) Visit(cb func(k string, v any)) { s.provider.db.Visit(s.sid, cb) } // Len returns the total number of stored values in this session. func (s *Session) Len() int { return s.provider.db.Len(s.sid) } func (s *Session) set(key string, value any, immutable bool) { s.provider.db.Set(s.sid, key, value, s.Lifetime.DurationUntilExpiration(), immutable) } // Set fills the session with an entry "value", based on its "key". func (s *Session) Set(key string, value any) { s.set(key, value, false) } // SetImmutable fills the session with an entry "value", based on its "key". // Unlike `Set`, the output value cannot be changed by the caller later on (when .Get) // An Immutable entry should be only changed with a `SetImmutable`, simple `Set` will not work // if the entry was immutable, for your own safety. // Use it consistently, it's far slower than `Set`. // Read more about muttable and immutable go types: https://stackoverflow.com/a/8021081 func (s *Session) SetImmutable(key string, value any) { s.set(key, value, true) } // SetFlash sets a flash message by its key. // // A flash message is used in order to keep a message in session through one or several requests of the same user. // It is removed from session after it has been displayed to the user. // Flash messages are usually used in combination with HTTP redirections, // because in this case there is no view, so messages can only be displayed in the request that follows redirection. // // A flash message has a name and a content (AKA key and value). // It is an entry of an associative array. The name is a string: often "notice", "success", or "error", but it can be anything. // The content is usually a string. You can put HTML tags in your message if you display it raw. // You can also set the message value to a number or an array: it will be serialized and kept in session like a string. // // Flash messages can be set using the SetFlash() Method // For example, if you would like to inform the user that his changes were successfully saved, // you could add the following line to your Handler: // // SetFlash("success", "Data saved!"); // // In this example we used the key 'success'. // If you want to define more than one flash messages, you will have to use different keys. func (s *Session) SetFlash(key string, value any) { s.mu.Lock() if s.flashes == nil { s.flashes = make(map[string]*flashMessage) } s.flashes[key] = &flashMessage{value: value} s.mu.Unlock() } // Delete removes an entry by its key, // returns true if actually something was removed. func (s *Session) Delete(key string) bool { removed := s.provider.db.Delete(s.sid, key) return removed } // DeleteFlash removes a flash message by its key. func (s *Session) DeleteFlash(key string) { s.mu.Lock() delete(s.flashes, key) s.mu.Unlock() } // Clear removes all entries. func (s *Session) Clear() { s.provider.db.Clear(s.sid) } // ClearFlashes removes all flash messages. func (s *Session) ClearFlashes() { s.mu.Lock() for key := range s.flashes { delete(s.flashes, key) } s.mu.Unlock() } ================================================ FILE: sessions/sessiondb/badger/database.go ================================================ package badger import ( "bytes" "errors" "os" "sync/atomic" "time" "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/core/memstore" "github.com/kataras/iris/v12/sessions" "github.com/dgraph-io/badger/v4" "github.com/kataras/golog" ) // DefaultFileMode used as the default database's "fileMode" // for creating the sessions directory path, opening and write the session file. var ( DefaultFileMode = 0755 ) // Database the badger(key-value file-based) session storage. type Database struct { // Service is the underline badger database connection, // it's initialized at `New` or `NewFromDB`. // Can be used to get stats. Service *badger.DB logger *golog.Logger closed uint32 // if 1 is closed. } var _ sessions.Database = (*Database)(nil) // New creates and returns a new badger(key-value file-based) storage // instance based on the "directoryPath". // DirectoryPath should is the directory which the badger database will store the sessions, // i.e ./sessions // // It will remove any old session files. func New(directoryPath string) (*Database, error) { if directoryPath == "" { return nil, errors.New("directoryPath is empty") } lindex := directoryPath[len(directoryPath)-1] if lindex != os.PathSeparator && lindex != '/' { directoryPath += string(os.PathSeparator) } // create directories if necessary if err := os.MkdirAll(directoryPath, os.FileMode(DefaultFileMode)); err != nil { return nil, err } opts := badger.DefaultOptions(directoryPath) badgerLogger := context.DefaultLogger("sessionsdb.badger").DisableNewLine() opts.Logger = badgerLogger service, err := badger.Open(opts) if err != nil { badgerLogger.Errorf("unable to initialize the badger-based session database: %v\n", err) return nil, err } return NewFromDB(service), nil } // NewFromDB same as `New` but accepts an already-created custom badger connection instead. func NewFromDB(service *badger.DB) *Database { db := &Database{Service: service} // runtime.SetFinalizer(db, closeDB) return db } // SetLogger sets the logger once before server ran. // By default the Iris one is injected. func (db *Database) SetLogger(logger *golog.Logger) { db.logger = logger } // Acquire receives a session's lifetime from the database, // if the return value is LifeTime{} then the session manager sets the life time based on the expiration duration lives in configuration. func (db *Database) Acquire(sid string, expires time.Duration) memstore.LifeTime { txn := db.Service.NewTransaction(true) defer txn.Commit() bsid := makePrefix(sid) item, err := txn.Get(bsid) if err == nil { // found, return the expiration. return memstore.LifeTime{Time: time.Unix(int64(item.ExpiresAt()), 0)} } // not found, create an entry with ttl and return an empty lifetime, session manager will do its job. if err != nil { if err == badger.ErrKeyNotFound { // create it and set the expiration, we don't care about the value there. err = txn.SetEntry(badger.NewEntry(bsid, bsid).WithTTL(expires)) } } if err != nil { db.logger.Error(err) } return memstore.LifeTime{} // session manager will handle the rest. } // OnUpdateExpiration not implemented here, yet. // Note that this error will not be logged, callers should catch it manually. func (db *Database) OnUpdateExpiration(sid string, newExpires time.Duration) error { return sessions.ErrNotImplemented } var delim = byte('_') func makePrefix(sid string) []byte { return append([]byte(sid), delim) } func makeKey(sid, key string) []byte { return append(makePrefix(sid), []byte(key)...) } // Set sets a key value of a specific session. // Ignore the "immutable". func (db *Database) Set(sid string, key string, value any, ttl time.Duration, immutable bool) error { valueBytes, err := sessions.DefaultTranscoder.Marshal(value) if err != nil { db.logger.Error(err) return err } err = db.Service.Update(func(txn *badger.Txn) error { return txn.SetEntry(badger.NewEntry(makeKey(sid, key), valueBytes).WithTTL(ttl)) }) if err != nil { db.logger.Error(err) } return err } // Get retrieves a session value based on the key. func (db *Database) Get(sid string, key string) (value any) { if err := db.Decode(sid, key, &value); err == nil { return value } return nil } // Decode binds the "outPtr" to the value associated to the provided "key". func (db *Database) Decode(sid, key string, outPtr any) error { err := db.Service.View(func(txn *badger.Txn) error { item, err := txn.Get(makeKey(sid, key)) if err != nil { return err } return item.Value(func(valueBytes []byte) error { return sessions.DefaultTranscoder.Unmarshal(valueBytes, outPtr) }) }) if err != nil && err != badger.ErrKeyNotFound { db.logger.Error(err) } return err } // validSessionItem reports whether the current iterator's item key // is a value of the session id "prefix". func validSessionItem(key, prefix []byte) bool { return len(key) > len(prefix) && bytes.Equal(key[0:len(prefix)], prefix) } // Visit loops through all session keys and values. func (db *Database) Visit(sid string, cb func(key string, value any)) error { prefix := makePrefix(sid) txn := db.Service.NewTransaction(false) defer txn.Discard() iter := txn.NewIterator(badger.DefaultIteratorOptions) defer iter.Close() for iter.Rewind(); ; iter.Next() { if !iter.Valid() { break } item := iter.Item() key := item.Key() if !validSessionItem(key, prefix) { continue } var value any err := item.Value(func(valueBytes []byte) error { return sessions.DefaultTranscoder.Unmarshal(valueBytes, &value) }) if err != nil { db.logger.Errorf("[sessionsdb.badger.Visit] %v", err) return err } cb(string(bytes.TrimPrefix(key, prefix)), value) } return nil } var iterOptionsNoValues = badger.IteratorOptions{ PrefetchValues: false, PrefetchSize: 100, Reverse: false, AllVersions: false, } // Len returns the length of the session's entries (keys). func (db *Database) Len(sid string) (n int) { prefix := makePrefix(sid) txn := db.Service.NewTransaction(false) iter := txn.NewIterator(iterOptionsNoValues) for iter.Rewind(); ; iter.Next() { if !iter.Valid() { break } if validSessionItem(iter.Item().Key(), prefix) { n++ } } iter.Close() txn.Discard() return } // Delete removes a session key value based on its key. func (db *Database) Delete(sid string, key string) (deleted bool) { txn := db.Service.NewTransaction(true) err := txn.Delete(makeKey(sid, key)) if err != nil { db.logger.Error(err) return false } return txn.Commit() == nil } // Clear removes all session key values but it keeps the session entry. func (db *Database) Clear(sid string) error { prefix := makePrefix(sid) txn := db.Service.NewTransaction(true) defer txn.Commit() iter := txn.NewIterator(iterOptionsNoValues) defer iter.Close() for iter.Rewind(); iter.ValidForPrefix(prefix); iter.Next() { key := iter.Item().Key() if err := txn.Delete(key); err != nil { db.logger.Warnf("Database.Clear: %s: %v", key, err) return err } } return nil } // Release destroys the session, it clears and removes the session entry, // session manager will create a new session ID on the next request after this call. func (db *Database) Release(sid string) error { // clear all $sid-$key. err := db.Clear(sid) if err != nil { return err } // and remove the $sid. txn := db.Service.NewTransaction(true) if err = txn.Delete([]byte(sid)); err != nil { db.logger.Warnf("Database.Release.Delete: %s: %v", sid, err) return err } if err = txn.Commit(); err != nil { db.logger.Debugf("Database.Release.Commit: %s: %v", sid, err) return err } return nil } // Close shutdowns the badger connection. func (db *Database) Close() error { return closeDB(db) } func closeDB(db *Database) error { if atomic.LoadUint32(&db.closed) > 0 { return nil } err := db.Service.Close() if err != nil { db.logger.Warnf("closing the badger connection: %v", err) } else { atomic.StoreUint32(&db.closed, 1) } return err } ================================================ FILE: sessions/sessiondb/boltdb/database.go ================================================ package boltdb import ( "errors" "os" "path/filepath" "time" "github.com/kataras/iris/v12/core/memstore" "github.com/kataras/iris/v12/sessions" "github.com/kataras/golog" bolt "go.etcd.io/bbolt" ) // DefaultFileMode used as the default database's "fileMode" // for creating the sessions directory path, opening and write // the session boltdb(file-based) storage. var ( DefaultFileMode = 0755 ) // Database the BoltDB(file-based) session storage. type Database struct { table []byte // Service is the underline BoltDB database connection, // it's initialized at `New` or `NewFromDB`. // Can be used to get stats. Service *bolt.DB logger *golog.Logger } var errPathMissing = errors.New("path is required") // New creates and returns a new BoltDB(file-based) storage // instance based on the "path". // Path should include the filename and the directory(aka fullpath), i.e sessions/store.db. // // It will remove any old session files. func New(path string, fileMode os.FileMode) (*Database, error) { if path == "" { golog.Error(errPathMissing) return nil, errPathMissing } if fileMode == 0 { fileMode = os.FileMode(DefaultFileMode) } // create directories if necessary if err := os.MkdirAll(filepath.Dir(path), fileMode); err != nil { golog.Errorf("error while trying to create the necessary directories for %s: %v", path, err) return nil, err } service, err := bolt.Open(path, fileMode, &bolt.Options{Timeout: 20 * time.Second}, ) if err != nil { golog.Errorf("unable to initialize the BoltDB-based session database: %v", err) return nil, err } return NewFromDB(service, "sessions") } // NewFromDB same as `New` but accepts an already-created custom boltdb connection instead. func NewFromDB(service *bolt.DB, bucketName string) (*Database, error) { bucket := []byte(bucketName) err := service.Update(func(tx *bolt.Tx) (err error) { _, err = tx.CreateBucketIfNotExists(bucket) return }) if err != nil { return nil, err } db := &Database{table: bucket, Service: service} // runtime.SetFinalizer(db, closeDB) return db, db.cleanup() } func (db *Database) getBucket(tx *bolt.Tx) *bolt.Bucket { return tx.Bucket(db.table) } func (db *Database) getBucketForSession(tx *bolt.Tx, sid string) *bolt.Bucket { b := db.getBucket(tx).Bucket([]byte(sid)) if b == nil { // session does not exist, it shouldn't happen, session bucket creation happens once at `Acquire`, // no need to accept the `bolt.bucket.CreateBucketIfNotExists`'s performance cost. db.logger.Debugf("unreachable session access for '%s'", sid) } return b } var ( expirationBucketName = []byte("expiration") delim = []byte("_") ) // expiration lives on its own bucket for each session bucket. func getExpirationBucketName(bsid []byte) []byte { return append(bsid, append(delim, expirationBucketName...)...) } // Cleanup removes any invalid(have expired) session entries on initialization. func (db *Database) cleanup() error { return db.Service.Update(func(tx *bolt.Tx) error { b := db.getBucket(tx) c := b.Cursor() // loop through all buckets, find one with expiration. for bsid, v := c.First(); bsid != nil; bsid, v = c.Next() { if len(bsid) == 0 { // empty key, continue to the next session bucket. continue } expirationName := getExpirationBucketName(bsid) if bExp := b.Bucket(expirationName); bExp != nil { // has expiration. _, expValue := bExp.Cursor().First() // the expiration bucket contains only one key(we don't care, see `Acquire`) value(time.Time) pair. if expValue == nil { db.logger.Debugf("cleanup: expiration is there but its value is empty '%s'", v) // should never happen. continue } var expirationTime time.Time if err := sessions.DefaultTranscoder.Unmarshal(expValue, &expirationTime); err != nil { db.logger.Debugf("cleanup: unable to retrieve expiration value for '%s'", v) continue } if expirationTime.Before(time.Now()) { // expired, delete the expiration bucket. if err := b.DeleteBucket(expirationName); err != nil { db.logger.Debugf("cleanup: unable to destroy a session '%s'", bsid) return err } // and the session bucket, if any. return b.DeleteBucket(bsid) } } } return nil }) } // SetLogger sets the logger once before server ran. // By default the Iris one is injected. func (db *Database) SetLogger(logger *golog.Logger) { db.logger = logger } var expirationKey = []byte("exp") // it can be random. // Acquire receives a session's lifetime from the database, // if the return value is LifeTime{} then the session manager sets the life time based on the expiration duration lives in configuration. func (db *Database) Acquire(sid string, expires time.Duration) (lifetime memstore.LifeTime) { bsid := []byte(sid) err := db.Service.Update(func(tx *bolt.Tx) (err error) { root := db.getBucket(tx) if expires > 0 { // should check or create the expiration bucket. name := getExpirationBucketName(bsid) b := root.Bucket(name) if b == nil { // not found, create a session bucket and an expiration bucket and save the given "expires" of time.Time, // don't return a lifetime, let it empty, session manager will do its job. b, err = root.CreateBucket(name) if err != nil { db.logger.Debugf("unable to create a session bucket for '%s': %v", sid, err) return err } expirationTime := time.Now().Add(expires) timeBytes, err := sessions.DefaultTranscoder.Marshal(expirationTime) if err != nil { db.logger.Debugf("unable to set an expiration value on session expiration bucket for '%s': %v", sid, err) return err } err = b.Put(expirationKey, timeBytes) if err == nil { // create the session bucket now, so the rest of the calls can be easly get the bucket without any further checks. _, err = root.CreateBucket(bsid) } return err } // found, get the associated expiration bucket, wrap its value and return. _, expValue := b.Cursor().First() if expValue == nil { return nil // does not expire. } var expirationTime time.Time if err = sessions.DefaultTranscoder.Unmarshal(expValue, &expirationTime); err != nil { db.logger.Debugf("acquire: unable to retrieve expiration value for '%s', value was: '%s': %v", sid, expValue, err) return } lifetime = memstore.LifeTime{Time: expirationTime} return nil } // does not expire, just create the session bucket if not exists so we can be ready later on. _, err = root.CreateBucketIfNotExists(bsid) return }) if err != nil { db.logger.Debugf("unable to acquire session '%s': %v", sid, err) return memstore.LifeTime{} } return } // OnUpdateExpiration will re-set the database's session's entry ttl. func (db *Database) OnUpdateExpiration(sid string, newExpires time.Duration) error { expirationTime := time.Now().Add(newExpires) timeBytes, err := sessions.DefaultTranscoder.Marshal(expirationTime) if err != nil { return err } err = db.Service.Update(func(tx *bolt.Tx) error { expirationName := getExpirationBucketName([]byte(sid)) root := db.getBucket(tx) b := root.Bucket(expirationName) if b == nil { // db.logger.Debugf("tried to reset the expiration value for '%s' while its configured lifetime is unlimited or the session is already expired and not found now", sid) return sessions.ErrNotFound } return b.Put(expirationKey, timeBytes) }) if err != nil { db.logger.Debugf("unable to reset the expiration value for '%s': %v", sid, err) } return err } func makeKey(key string) []byte { return []byte(key) } // Set sets a key value of a specific session. // Ignore the "immutable". func (db *Database) Set(sid string, key string, value any, ttl time.Duration, immutable bool) error { valueBytes, err := sessions.DefaultTranscoder.Marshal(value) if err != nil { db.logger.Debug(err) return err } err = db.Service.Update(func(tx *bolt.Tx) error { b := db.getBucketForSession(tx, sid) if b == nil { return nil } // Author's notes: // expiration is handlded by the session manager for the whole session, so the `db.Destroy` will be called when and if needed. // Therefore we don't have to implement a TTL here, but we need a `db.Cleanup`, as we did previously, method to delete any expired if server restarted // (badger does not need a `Cleanup` because we set the TTL based on the lifetime.DurationUntilExpiration()). return b.Put(makeKey(key), valueBytes) }) if err != nil { db.logger.Debug(err) } return err } // Get retrieves a session value based on the key. func (db *Database) Get(sid string, key string) (value any) { if err := db.Decode(sid, key, &value); err == nil { return value } return nil } // Decode binds the "outPtr" to the value associated to the provided "key". func (db *Database) Decode(sid, key string, outPtr any) error { err := db.Service.View(func(tx *bolt.Tx) error { b := db.getBucketForSession(tx, sid) if b == nil { return nil } valueBytes := b.Get(makeKey(key)) if len(valueBytes) == 0 { return nil } return sessions.DefaultTranscoder.Unmarshal(valueBytes, outPtr) }) if err != nil { db.logger.Debugf("session '%s' key '%s' cannot be retrieved: %v", sid, key, err) } return err } // Visit loops through all session keys and values. func (db *Database) Visit(sid string, cb func(key string, value any)) error { err := db.Service.View(func(tx *bolt.Tx) error { b := db.getBucketForSession(tx, sid) if b == nil { return nil } return b.ForEach(func(k []byte, v []byte) error { var value any if err := sessions.DefaultTranscoder.Unmarshal(v, &value); err != nil { db.logger.Debugf("unable to retrieve value of key '%s' of '%s': %v", k, sid, err) return err } cb(string(k), value) return nil }) }) if err != nil { db.logger.Debugf("Database.Visit: %s: %v", sid, err) } return err } // Len returns the length of the session's entries (keys). func (db *Database) Len(sid string) (n int) { err := db.Service.View(func(tx *bolt.Tx) error { b := db.getBucketForSession(tx, sid) if b == nil { return nil } n = int(int64(b.Stats().KeyN)) return nil }) if err != nil { db.logger.Debugf("Database.Len: %s: %v", sid, err) } return } // Delete removes a session key value based on its key. func (db *Database) Delete(sid string, key string) (deleted bool) { err := db.Service.Update(func(tx *bolt.Tx) error { b := db.getBucketForSession(tx, sid) if b == nil { return sessions.ErrNotFound } return b.Delete(makeKey(key)) }) return err == nil } // Clear removes all session key values but it keeps the session entry. func (db *Database) Clear(sid string) error { err := db.Service.Update(func(tx *bolt.Tx) error { b := db.getBucketForSession(tx, sid) if b == nil { return nil } return b.ForEach(func(k []byte, v []byte) error { return b.Delete(k) }) }) if err != nil { db.logger.Debugf("Database.Clear: %s: %v", sid, err) } return err } // Release destroys the session, it clears and removes the session entry, // session manager will create a new session ID on the next request after this call. func (db *Database) Release(sid string) error { err := db.Service.Update(func(tx *bolt.Tx) error { // delete the session bucket. b := db.getBucket(tx) bsid := []byte(sid) // try to delete the associated expiration bucket, if exists, ignore error. _ = b.DeleteBucket(getExpirationBucketName(bsid)) return b.DeleteBucket(bsid) }) if err != nil { db.logger.Debugf("Database.Release: %s: %v", sid, err) } return err } // Close shutdowns the BoltDB connection. func (db *Database) Close() error { return closeDB(db) } func closeDB(db *Database) error { err := db.Service.Close() if err != nil { db.logger.Warnf("closing the BoltDB connection: %v", err) } return err } ================================================ FILE: sessions/sessiondb/redis/database.go ================================================ package redis import ( "crypto/tls" "errors" "fmt" "time" "github.com/kataras/iris/v12/core/memstore" "github.com/kataras/iris/v12/sessions" "github.com/kataras/golog" ) const ( // DefaultRedisNetwork the redis network option, "tcp". DefaultRedisNetwork = "tcp" // DefaultRedisAddr the redis address option, "127.0.0.1:6379". DefaultRedisAddr = "127.0.0.1:6379" // DefaultRedisTimeout the redis idle timeout option, time.Duration(30) * time.Second. DefaultRedisTimeout = time.Duration(30) * time.Second ) // Config the redis configuration used inside sessions type Config struct { // Network protocol. Defaults to "tcp". Network string // Addr of a single redis server instance. // See "Clusters" field for clusters support. // Defaults to "127.0.0.1:6379". Addr string // Clusters a list of network addresses for clusters. // If not empty "Addr" is ignored and Redis clusters feature is used instead. // Note that this field is ignored when setgging a custom `GoRedisClient`. Clusters []string // Use the specified Username to authenticate the current connection // with one of the connections defined in the ACL list when connecting // to a Redis 6.0 instance, or greater, that is using the Redis ACL system. Username string // Optional password. Must match the password specified in the // requirepass server configuration option (if connecting to a Redis 5.0 instance, or lower), // or the User Password when connecting to a Redis 6.0 instance, or greater, // that is using the Redis ACL system. Password string // If Database is empty "" then no 'SELECT'. Defaults to "". Database string // Maximum number of socket connections. // Default is 10 connections per every CPU as reported by runtime.NumCPU. MaxActive int // Timeout for connect, write and read, defaults to 30 seconds, 0 means no timeout. Timeout time.Duration // Prefix "myprefix-for-this-website". Defaults to "". Prefix string // TLSConfig will cause Dial to perform a TLS handshake using the provided // config. If is nil then no TLS is used. // See https://golang.org/pkg/crypto/tls/#Config TLSConfig *tls.Config // A Driver should support be a go client for redis communication. // It can be set to a custom one or a mock one (for testing). // // Defaults to `GoRedis()`. Driver Driver } // DefaultConfig returns the default configuration for Redis service. func DefaultConfig() Config { return Config{ Network: DefaultRedisNetwork, Addr: DefaultRedisAddr, Username: "", Password: "", Database: "", MaxActive: 10, Timeout: DefaultRedisTimeout, Prefix: "", TLSConfig: nil, Driver: GoRedis(), } } // Database the redis back-end session database for the sessions. type Database struct { c Config logger *golog.Logger } var _ sessions.Database = (*Database)(nil) // New returns a new redis sessions database. func New(cfg ...Config) *Database { c := DefaultConfig() if len(cfg) > 0 { c = cfg[0] if c.Timeout < 0 { c.Timeout = DefaultRedisTimeout } if c.Network == "" { c.Network = DefaultRedisNetwork } if c.Addr == "" { c.Addr = DefaultRedisAddr } if c.Driver == nil { c.Driver = GoRedis() } } if err := c.Driver.Connect(c); err != nil { panic(err) } db := &Database{c: c} _, err := db.c.Driver.PingPong() if err != nil { panic(err) } // runtime.SetFinalizer(db, closeDB) return db } // SetLogger sets the logger once before server ran. // By default the Iris one is injected. func (db *Database) SetLogger(logger *golog.Logger) { db.logger = logger } func (db *Database) makeSID(sid string) string { return db.c.Prefix + sid } // SessionIDKey the session ID stored to the redis session itself. const SessionIDKey = "session_id" // Acquire receives a session's lifetime from the database, // if the return value is LifeTime{} then the session manager sets the life time based on the expiration duration lives in configuration. func (db *Database) Acquire(sid string, expires time.Duration) memstore.LifeTime { sidKey := db.makeSID(sid) if !db.c.Driver.Exists(sidKey) { if err := db.Set(sid, SessionIDKey, sid, 0, false); err != nil { db.logger.Debug(err) } else if expires > 0 { if err := db.c.Driver.UpdateTTL(sidKey, expires); err != nil { db.logger.Debug(err) } } return memstore.LifeTime{} // session manager will handle the rest. } untilExpire := db.c.Driver.TTL(sidKey) return memstore.LifeTime{Time: time.Now().Add(untilExpire)} } // OnUpdateExpiration will re-set the database's session's entry ttl. // https://redis.io/commands/expire#refreshing-expires func (db *Database) OnUpdateExpiration(sid string, newExpires time.Duration) error { return db.c.Driver.UpdateTTL(db.makeSID(sid), newExpires) } // Set sets a key value of a specific session. // Ignore the "immutable". func (db *Database) Set(sid string, key string, value any, _ time.Duration, _ bool) error { valueBytes, err := sessions.DefaultTranscoder.Marshal(value) if err != nil { db.logger.Error(err) return err } if err = db.c.Driver.Set(db.makeSID(sid), key, valueBytes); err != nil { db.logger.Debug(err) return err } return nil } // Get retrieves a session value based on the key. func (db *Database) Get(sid string, key string) (value any) { if err := db.Decode(sid, key, &value); err == nil { return value } return nil } // Decode binds the "outPtr" to the value associated to the provided "key". func (db *Database) Decode(sid, key string, outPtr any) error { sidKey := db.makeSID(sid) data, err := db.c.Driver.Get(sidKey, key) if err != nil { // not found. return err } if err = db.decodeValue(data, outPtr); err != nil { db.logger.Debugf("unable to unmarshal value of key: '%s%s': %v", sid, key, err) return err } return nil } func (db *Database) decodeValue(val any, outPtr any) error { if val == nil { return nil } switch data := val.(type) { case []byte: // this is the most common type, as we save all values as []byte, // the only exception is where the value is string on HGetAll command. return sessions.DefaultTranscoder.Unmarshal(data, outPtr) case string: return sessions.DefaultTranscoder.Unmarshal([]byte(data), outPtr) default: return fmt.Errorf("unknown value type of %T", data) } } func (db *Database) keys(fullSID string) []string { keys, err := db.c.Driver.GetKeys(fullSID) if err != nil { db.logger.Debugf("unable to get all redis keys of session '%s': %v", fullSID, err) return nil } return keys } // Visit loops through all session keys and values. func (db *Database) Visit(sid string, cb func(key string, value any)) error { kv, err := db.c.Driver.GetAll(db.makeSID(sid)) if err != nil { return err } for k, v := range kv { var value any // new value each time, we don't know what user will do in "cb". if err = db.decodeValue(v, &value); err != nil { db.logger.Debugf("unable to decode %s:%s: %v", sid, k, err) return err } cb(k, value) } return nil } // Len returns the length of the session's entries (keys). func (db *Database) Len(sid string) int { return db.c.Driver.Len(sid) } // Delete removes a session key value based on its key. func (db *Database) Delete(sid string, key string) (deleted bool) { err := db.c.Driver.Delete(db.makeSID(sid), key) if err != nil { db.logger.Error(err) } return err == nil } // Clear removes all session key values but it keeps the session entry. func (db *Database) Clear(sid string) error { sid = db.makeSID(sid) keys := db.keys(sid) for _, key := range keys { if key == SessionIDKey { continue } if err := db.c.Driver.Delete(sid, key); err != nil { db.logger.Debugf("unable to delete session '%s' value of key: '%s': %v", sid, key, err) return err } } return nil } // Release destroys the session, it clears and removes the session entry, // session manager will create a new session ID on the next request after this call. func (db *Database) Release(sid string) error { err := db.c.Driver.Delete(db.makeSID(sid), "") if err != nil { db.logger.Debugf("Database.Release.Driver.Delete: %s: %v", sid, err) } return err } // Close terminates the redis connection. func (db *Database) Close() error { return closeDB(db) } func closeDB(db *Database) error { return db.c.Driver.CloseConnection() } var ( // ErrRedisClosed an error with message 'redis: already closed' ErrRedisClosed = errors.New("redis: already closed") // ErrKeyNotFound a type of error of non-existing redis keys. // The producers(the library) of this error will dynamically wrap this error(fmt.Errorf) with the key name. // Usage: // if err != nil && errors.Is(err, ErrKeyNotFound) { // [...] // } ErrKeyNotFound = errors.New("key not found") ) ================================================ FILE: sessions/sessiondb/redis/driver.go ================================================ package redis import "time" // Driver is the interface which each supported redis client // should support in order to be used in the redis session database. type Driver interface { Connect(c Config) error PingPong() (bool, error) CloseConnection() error Set(sid, key string, value any) error Get(sid, key string) (any, error) Exists(sid string) bool TTL(sid string) time.Duration UpdateTTL(sid string, newLifetime time.Duration) error GetAll(sid string) (map[string]string, error) GetKeys(sid string) ([]string, error) Len(sid string) int Delete(sid, key string) error } var ( _ Driver = (*GoRedisDriver)(nil) ) // GoRedis returns the default Driver for the redis sessions database // It's the go-redis client. Learn more at: https://github.com/go-redis/redis. func GoRedis() *GoRedisDriver { return &GoRedisDriver{} } ================================================ FILE: sessions/sessiondb/redis/driver_goredis.go ================================================ package redis import ( stdContext "context" "io" "strconv" "time" "github.com/redis/go-redis/v9" ) type ( // Options is just a type alias for the go-redis Client Options. Options = redis.Options // ClusterOptions is just a type alias for the go-redis Cluster Client Options. ClusterOptions = redis.ClusterOptions ) // GoRedisClient is the interface which both // go-redis's Client and Cluster Client implements. type GoRedisClient interface { redis.Cmdable // Commands. io.Closer // CloseConnection. } // GoRedisDriver implements the Sessions Database Driver // for the go-redis redis driver. See driver.go file. type GoRedisDriver struct { // Both Client and ClusterClient implements this interface. // Custom one can be directly passed but if so, the // Connect method does nothing (so all connection and client settings are ignored). Client GoRedisClient // Customize any go-redis fields manually // before Connect. ClientOptions Options ClusterOptions ClusterOptions } var defaultContext = stdContext.Background() func (r *GoRedisDriver) mergeClientOptions(c Config) *Options { opts := r.ClientOptions if opts.Addr == "" { opts.Addr = c.Addr } if opts.Username == "" { opts.Username = c.Username } if opts.Password == "" { opts.Password = c.Password } if opts.DB == 0 { opts.DB, _ = strconv.Atoi(c.Database) } if opts.ReadTimeout == 0 { opts.ReadTimeout = c.Timeout } if opts.WriteTimeout == 0 { opts.WriteTimeout = c.Timeout } if opts.Network == "" { opts.Network = c.Network } if opts.TLSConfig == nil { opts.TLSConfig = c.TLSConfig } if opts.PoolSize == 0 { opts.PoolSize = c.MaxActive } return &opts } func (r *GoRedisDriver) mergeClusterOptions(c Config) *ClusterOptions { opts := r.ClusterOptions if opts.Username == "" { opts.Username = c.Username } if opts.Password == "" { opts.Password = c.Password } if opts.ReadTimeout == 0 { opts.ReadTimeout = c.Timeout } if opts.WriteTimeout == 0 { opts.WriteTimeout = c.Timeout } if opts.TLSConfig == nil { opts.TLSConfig = c.TLSConfig } if opts.PoolSize == 0 { opts.PoolSize = c.MaxActive } if len(opts.Addrs) == 0 { opts.Addrs = c.Clusters } return &opts } // SetClient sets an existing go redis client to the sessions redis driver. // // Returns itself. func (r *GoRedisDriver) SetClient(goRedisClient GoRedisClient) *GoRedisDriver { r.Client = goRedisClient return r } // Connect initializes the redis client. func (r *GoRedisDriver) Connect(c Config) error { if r.Client != nil { // if a custom one was given through SetClient. return nil } if len(c.Clusters) > 0 { r.Client = redis.NewClusterClient(r.mergeClusterOptions(c)) } else { r.Client = redis.NewClient(r.mergeClientOptions(c)) } return nil } // PingPong sends a ping message and reports whether // the PONG message received successfully. func (r *GoRedisDriver) PingPong() (bool, error) { pong, err := r.Client.Ping(defaultContext).Result() return pong == "PONG", err } // CloseConnection terminates the underline redis connection. func (r *GoRedisDriver) CloseConnection() error { return r.Client.Close() } // Set stores a "value" based on the session's "key". // The value should be type of []byte, so unmarshal can happen. func (r *GoRedisDriver) Set(sid, key string, value any) error { return r.Client.HSet(defaultContext, sid, key, value).Err() } // Get returns the associated value of the session's given "key". func (r *GoRedisDriver) Get(sid, key string) (any, error) { return r.Client.HGet(defaultContext, sid, key).Bytes() } // Exists reports whether a session exists or not. func (r *GoRedisDriver) Exists(sid string) bool { n, err := r.Client.Exists(defaultContext, sid).Result() if err != nil { return false } return n > 0 } // TTL returns any TTL value of the session. func (r *GoRedisDriver) TTL(sid string) time.Duration { dur, err := r.Client.TTL(defaultContext, sid).Result() if err != nil { return 0 } return dur } // UpdateTTL sets expiration duration of the session. func (r *GoRedisDriver) UpdateTTL(sid string, newLifetime time.Duration) error { _, err := r.Client.Expire(defaultContext, sid, newLifetime).Result() return err } // GetAll returns all the key values under the session. func (r *GoRedisDriver) GetAll(sid string) (map[string]string, error) { return r.Client.HGetAll(defaultContext, sid).Result() } // GetKeys returns all keys under the session. func (r *GoRedisDriver) GetKeys(sid string) ([]string, error) { return r.Client.HKeys(defaultContext, sid).Result() } // Len returns the total length of key-values of the session. func (r *GoRedisDriver) Len(sid string) int { return int(r.Client.HLen(defaultContext, sid).Val()) } // Delete removes a value from the redis store. func (r *GoRedisDriver) Delete(sid, key string) error { if key == "" { return r.Client.Del(defaultContext, sid).Err() } return r.Client.HDel(defaultContext, sid, key).Err() } ================================================ FILE: sessions/sessions.go ================================================ package sessions import ( "net/http" "net/url" "time" "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/core/host" ) func init() { context.SetHandlerName("iris/sessions.*Handler", "iris.session") } // A Sessions manager should be responsible to Start/Get a sesion, based // on a Context, which returns a *Session, type. // It performs automatic memory cleanup on expired sessions. // It can accept a `Database` for persistence across server restarts. // A session can set temporary values (flash messages). type Sessions struct { config Config provider *provider cookieOptions []context.CookieOption // options added on each session cookie action. } // New returns a new fast, feature-rich sessions manager // it can be adapted to an iris station func New(cfg Config) *Sessions { var cookieOptions []context.CookieOption if cfg.AllowReclaim { cookieOptions = append(cookieOptions, context.CookieAllowReclaim(cfg.Cookie)) } if !cfg.DisableSubdomainPersistence { cookieOptions = append(cookieOptions, context.CookieAllowSubdomains(cfg.Cookie)) } if cfg.CookieSecureTLS { cookieOptions = append(cookieOptions, context.CookieSecure) } if cfg.Encoding != nil { cookieOptions = append(cookieOptions, context.CookieEncoding(cfg.Encoding, cfg.Cookie)) } return &Sessions{ cookieOptions: cookieOptions, config: cfg.Validate(), provider: newProvider(), } } // UseDatabase adds a session database to the manager's provider, // a session db doesn't have write access func (s *Sessions) UseDatabase(db Database) { db.SetLogger(s.config.Logger) // inject the logger. host.RegisterOnInterrupt(func() { db.Close() }) s.provider.RegisterDatabase(db) } // GetCookieOptions returns the cookie options registered // for this sessions manager based on the configuration. func (s *Sessions) GetCookieOptions() []context.CookieOption { return s.cookieOptions } // updateCookie gains the ability of updating the session browser cookie to any method which wants to update it func (s *Sessions) updateCookie(ctx *context.Context, sid string, expires time.Duration, options ...context.CookieOption) { cookie := &http.Cookie{} // The RFC makes no mention of encoding url value, so here I think to encode both sessionid key and the value using the safe(to put and to use as cookie) url-encoding cookie.Name = s.config.Cookie cookie.Value = sid cookie.Path = "/" cookie.HttpOnly = true // MaxAge=0 means no 'Max-Age' attribute specified. // MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0' // MaxAge>0 means Max-Age attribute present and given in seconds if expires >= 0 { if expires == 0 { // unlimited life cookie.Expires = context.CookieExpireUnlimited } else { // > 0 cookie.Expires = time.Now().Add(expires) } cookie.MaxAge = int(time.Until(cookie.Expires).Seconds()) } s.upsertCookie(ctx, cookie, options) } func (s *Sessions) upsertCookie(ctx *context.Context, cookie *http.Cookie, cookieOptions []context.CookieOption) { opts := s.cookieOptions if len(cookieOptions) > 0 { opts = append(opts, cookieOptions...) } ctx.UpsertCookie(cookie, opts...) } func (s *Sessions) getCookieValue(ctx *context.Context, cookieOptions []context.CookieOption) string { c := s.getCookie(ctx, cookieOptions) if c == nil { return "" } return c.Value } func (s *Sessions) getCookie(ctx *context.Context, cookieOptions []context.CookieOption) *http.Cookie { opts := s.cookieOptions if len(cookieOptions) > 0 { opts = append(opts, cookieOptions...) } cookie, err := ctx.GetRequestCookie(s.config.Cookie, opts...) if err != nil { return nil } cookie.Value, _ = url.QueryUnescape(cookie.Value) return cookie } // Start creates or retrieves an existing session for the particular request. // Note that `Start` method will not respect configuration's `AllowReclaim`, `DisableSubdomainPersistence`, `CookieSecureTLS`, // and `Encoding` settings. // Register sessions as a middleware through the `Handler` method instead, // which provides automatic resolution of a *sessions.Session input argument // on MVC and APIContainer as well. // // NOTE: Use `app.Use(sess.Handler())` instead, avoid using `Start` manually. func (s *Sessions) Start(ctx *context.Context, cookieOptions ...context.CookieOption) *Session { // cookieValue := s.getCookieValue(ctx, cookieOptions) cookie := s.getCookie(ctx, cookieOptions) if cookie != nil { sid := cookie.Value if sid == "" { // rare case: a client may contains a cookie with session name but with empty value. // ctx.RemoveCookie(cookie.Name) cookie = nil } else if cookie.Expires.Add(time.Second).After(time.Now()) { // rare case: of custom clients that may hold expired cookies. s.DestroyByID(sid) // ctx.RemoveCookie(cookie.Name) cookie = nil } else { // rare case: new expiration configuration that it's lower // than the previous setting. expiresTime := time.Now().Add(s.config.Expires) if cookie.Expires.After(expiresTime) { s.DestroyByID(sid) // ctx.RemoveCookie(cookie.Name) cookie = nil } else { // untilExpirationDur := time.Until(cookie.Expires) // ^ this should be return s.provider.Read(s, sid, s.config.Expires) // cookie exists and it's valid, let's return its session. } } } // Cookie doesn't exist, let's generate a session and set a cookie. sid := s.config.SessionIDGenerator(ctx) sess := s.provider.Init(s, sid, s.config.Expires) // n := s.provider.db.Len(sid) // fmt.Printf("db.Len(%s) = %d\n", sid, n) // if n > 0 { // s.provider.db.Visit(sid, func(key string, value any) { // fmt.Printf("%s=%s\n", key, value) // }) // } s.updateCookie(ctx, sid, s.config.Expires, cookieOptions...) return sess } const sessionContextKey = "iris.session" // Handler returns a sessions middleware to register on application routes. // To return the request's Session call the `Get(ctx)` package-level function. // // Call `Handler()` once per sessions manager. func (s *Sessions) Handler(requestOptions ...context.CookieOption) context.Handler { return func(ctx *context.Context) { session := s.Start(ctx, requestOptions...) // this cookie's end-developer's custom options. ctx.Values().Set(sessionContextKey, session) ctx.Next() s.provider.EndRequest(ctx, session) } } // Get returns a *Session from the same request life cycle, // can be used inside a chain of handlers of a route. // // The `Sessions.Start` should be called previously, // e.g. register the `Sessions.Handler` as middleware. // Then call `Get` package-level function as many times as you want. // Note: It will return nil if the session got destroyed by the same request. // If you need to destroy and start a new session in the same request you need to call // sessions manager's `Start` method after Destroy. func Get(ctx *context.Context) *Session { if v := ctx.Values().Get(sessionContextKey); v != nil { if sess, ok := v.(*Session); ok { return sess } } // ctx.Application().Logger().Debugf("Sessions: Get: no session found, prior Destroy(ctx) calls in the same request should follow with a Start(ctx) call too") return nil } // StartWithPath same as `Start` but it explicitly accepts the cookie path option. func (s *Sessions) StartWithPath(ctx *context.Context, path string) *Session { return s.Start(ctx, context.CookiePath(path)) } // ShiftExpiration move the expire date of a session to a new date // by using session default timeout configuration. // It will return `ErrNotImplemented` if a database is used and it does not support this feature, yet. func (s *Sessions) ShiftExpiration(ctx *context.Context, cookieOptions ...context.CookieOption) error { return s.UpdateExpiration(ctx, s.config.Expires, cookieOptions...) } // UpdateExpiration change expire date of a session to a new date // by using timeout value passed by `expires` receiver. // It will return `ErrNotFound` when trying to update expiration on a non-existence or not valid session entry. // It will return `ErrNotImplemented` if a database is used and it does not support this feature, yet. func (s *Sessions) UpdateExpiration(ctx *context.Context, expires time.Duration, cookieOptions ...context.CookieOption) error { cookieValue := s.getCookieValue(ctx, cookieOptions) if cookieValue == "" { return ErrNotFound } // we should also allow it to expire when the browser closed err := s.provider.UpdateExpiration(cookieValue, expires) if err == nil || expires == -1 { s.updateCookie(ctx, cookieValue, expires, cookieOptions...) } return err } // DestroyListener is the form of a destroy listener. // Look `OnDestroy` for more. type DestroyListener func(sid string) // OnDestroy registers one or more destroy listeners. // A destroy listener is fired when a session has been removed entirely from the server (the entry) and client-side (the cookie). // Note that if a destroy listener is blocking, then the session manager will delay respectfully, // use a goroutine inside the listener to avoid that behavior. func (s *Sessions) OnDestroy(listeners ...DestroyListener) { for _, ln := range listeners { s.provider.registerDestroyListener(ln) } } // Destroy removes the session data, the associated cookie // and the Context's session value. // Next calls of `sessions.Get` will occur to a nil Session, // use `Sessions#Start` method for renewal // or use the Session's Destroy method which does keep the session entry with its values cleared. func (s *Sessions) Destroy(ctx *context.Context) { cookieValue := s.getCookieValue(ctx, nil) if cookieValue == "" { // nothing to destroy return } ctx.Values().Remove(sessionContextKey) ctx.RemoveCookie(s.config.Cookie, s.cookieOptions...) s.provider.Destroy(cookieValue) } // DestroyByID removes the session entry // from the server-side memory (and database if registered). // Client's session cookie will still exist but it will be reseted on the next request. // // It's safe to use it even if you are not sure if a session with that id exists. // // Note: the sid should be the original one (i.e: fetched by a store ) // it's not decoded. func (s *Sessions) DestroyByID(sid string) { s.provider.Destroy(sid) } // DestroyAll removes all sessions // from the server-side memory (and database if registered). // Client's session cookie will still exist but it will be reseted on the next request. func (s *Sessions) DestroyAll() { s.provider.DestroyAll() } ================================================ FILE: sessions/sessions_test.go ================================================ package sessions_test import ( "sync" "testing" "time" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/httptest" "github.com/kataras/iris/v12/sessions" ) func TestSessions(t *testing.T) { app := iris.New() sess := sessions.New(sessions.Config{Cookie: "mycustomsessionid"}) app.Use(sess.Handler()) testSessions(t, app) } const ( testEnableSubdomain = true ) func testSessions(t *testing.T, app *iris.Application) { values := map[string]any{ "Name": "iris", "Months": "4", "Secret": "dsads£2132215£%%Ssdsa", } writeValues := func(ctx *context.Context) { s := sessions.Get(ctx) sessValues := s.GetAll() err := ctx.JSON(sessValues) if err != nil { t.Fatal(err) } } if testEnableSubdomain { app.Party("subdomain.").Get("/get", writeValues) } app.Post("/set", func(ctx *context.Context) { s := sessions.Get(ctx) vals := make(map[string]any) if err := ctx.ReadJSON(&vals); err != nil { t.Fatalf("Cannot read JSON. Trace %s", err.Error()) } for k, v := range vals { s.Set(k, v) } }) app.Get("/get", func(ctx *context.Context) { writeValues(ctx) }) app.Get("/clear", func(ctx *context.Context) { sessions.Get(ctx).Clear() writeValues(ctx) }) app.Get("/destroy", func(ctx *context.Context) { session := sessions.Get(ctx) if session.IsNew() { t.Fatal("expected session not to be nil on destroy") } session.Man.Destroy(ctx) if sessions.Get(ctx) != nil { t.Fatal("expected session inside Context to be nil after Manager's Destroy call") } ctx.JSON(struct{}{}) // the cookie and all values should be empty }) // cookie should be new. app.Get("/after_destroy_renew", func(ctx *context.Context) { isNew := sessions.Get(ctx).IsNew() ctx.Writef("%v", isNew) }) app.Get("/multi_start_set_get", func(ctx *context.Context) { s := sessions.Get(ctx) s.Set("key", "value") ctx.Next() }, func(ctx *context.Context) { s := sessions.Get(ctx) _, err := ctx.WriteString(s.GetString("key")) if err != nil { t.Fatal(err) } }) e := httptest.New(t, app, httptest.URL("http://example.com")) e.POST("/set").WithJSON(values).Expect().Status(iris.StatusOK).Cookies().NotEmpty() e.GET("/get").Expect().Status(iris.StatusOK).JSON().Object().Equal(values) if testEnableSubdomain { es := e.Builder(func(req *httptest.Request) { req.WithURL("http://subdomain.example.com") }) es.Request("GET", "/get").Expect().Status(iris.StatusOK).JSON().Object().Equal(values) } // test destroy which also clears first d := e.GET("/destroy").Expect().Status(iris.StatusOK) d.JSON().Object().Empty() d = e.GET("/after_destroy_renew").Expect().Status(iris.StatusOK) d.Body().IsEqual("true") d.Cookies().NotEmpty() // set and clear again e.POST("/set").WithJSON(values).Expect().Status(iris.StatusOK) e.GET("/clear").Expect().Status(iris.StatusOK).JSON().Object().Empty() // test start on the same request but more than one times e.GET("/multi_start_set_get").Expect().Status(iris.StatusOK).Body().IsEqual("value") } func TestFlashMessages(t *testing.T) { app := iris.New() sess := sessions.New(sessions.Config{Cookie: "mycustomsessionid"}) valueSingleKey := "Name" valueSingleValue := "iris-sessions" values := map[string]any{ valueSingleKey: valueSingleValue, "Days": "1", "Secret": "dsads£2132215£%%Ssdsa", } writeValues := func(ctx *context.Context, values map[string]any) error { return ctx.JSON(values) } app.Post("/set", func(ctx *context.Context) { vals := make(map[string]any) if err := ctx.ReadJSON(&vals); err != nil { t.Fatalf("Cannot readjson. Trace %s", err.Error()) } s := sess.Start(ctx) for k, v := range vals { s.SetFlash(k, v) } ctx.StatusCode(iris.StatusOK) }) writeFlashValues := func(ctx *context.Context) { s := sess.Start(ctx) flashes := s.GetFlashes() if err := writeValues(ctx, flashes); err != nil { t.Fatalf("While serialize the flash values: %s", err.Error()) } } app.Get("/get_single", func(ctx *context.Context) { s := sess.Start(ctx) flashMsgString := s.GetFlashString(valueSingleKey) ctx.WriteString(flashMsgString) }) app.Get("/get", func(ctx *context.Context) { writeFlashValues(ctx) }) app.Get("/clear", func(ctx *context.Context) { s := sess.Start(ctx) s.ClearFlashes() writeFlashValues(ctx) }) app.Get("/destroy", func(ctx *context.Context) { sess.Destroy(ctx) writeFlashValues(ctx) ctx.StatusCode(iris.StatusOK) // the cookie and all values should be empty }) // request cookie should be empty app.Get("/after_destroy", func(ctx *context.Context) { ctx.StatusCode(iris.StatusOK) }) e := httptest.New(t, app, httptest.URL("http://example.com")) e.POST("/set").WithJSON(values).Expect().Status(iris.StatusOK).Cookies().NotEmpty() // get all e.GET("/get").Expect().Status(iris.StatusOK).JSON().Object().Equal(values) // get the same flash on other request should return nothing because the flash message is removed after fetch once e.GET("/get").Expect().Status(iris.StatusOK).JSON().Object().Empty() // test destroy which also clears first d := e.GET("/destroy").Expect().Status(iris.StatusOK) d.JSON().Object().Empty() e.GET("/after_destroy").Expect().Status(iris.StatusOK).Cookies().Empty() // set and clear again e.POST("/set").WithJSON(values).Expect().Status(iris.StatusOK).Cookies().NotEmpty() e.GET("/clear").Expect().Status(iris.StatusOK).JSON().Object().Empty() // set again in order to take the single one ( we don't test Cookies.NotEmpty because httpexpect default conf reads that from the request-only) e.POST("/set").WithJSON(values).Expect().Status(iris.StatusOK) e.GET("/get_single").Expect().Status(iris.StatusOK).Body().IsEqual(valueSingleValue) } func TestSessionsUpdateExpiration(t *testing.T) { app := iris.New() cookieName := "mycustomsessionid" sess := sessions.New(sessions.Config{ Cookie: cookieName, Expires: 30 * time.Minute, AllowReclaim: true, }) app.Use(sess.Handler()) type response struct { SessionID string `json:"sessionID"` Logged bool `json:"logged"` } var writeResponse = func(ctx *context.Context) { session := sessions.Get(ctx) ctx.JSON(response{ SessionID: session.ID(), Logged: session.GetBooleanDefault("logged", false), }) } app.Get("/get", func(ctx *context.Context) { writeResponse(ctx) }) app.Get("/set", func(ctx iris.Context) { sessions.Get(ctx).Set("logged", true) writeResponse(ctx) }) app.Post("/remember_me", func(ctx iris.Context) { // re-sends the cookie with the new Expires and MaxAge fields, // test checks that on same session id too. sessions.Get(ctx).Man.UpdateExpiration(ctx, 24*time.Hour) writeResponse(ctx) }) app.Get("/destroy", func(ctx iris.Context) { sessions.Get(ctx).Man.Destroy(ctx) // this will delete the cookie too. }) e := httptest.New(t, app, httptest.URL("http://example.com")) tt := e.GET("/set").Expect().Status(httptest.StatusOK) tt.Cookie(cookieName).MaxAge().InRange(29*time.Minute, 30*time.Minute) sessionID := tt.JSON().Object().Raw()["sessionID"].(string) expectedResponse := response{SessionID: sessionID, Logged: true} e.GET("/get").Expect().Status(httptest.StatusOK). JSON().IsEqual(expectedResponse) tt = e.POST("/remember_me").Expect().Status(httptest.StatusOK) tt.Cookie(cookieName).MaxAge().InRange(23*time.Hour, 24*time.Hour) tt.JSON().IsEqual(expectedResponse) // Test call `UpdateExpiration` when cookie is firstly created. e.GET("/destroy").Expect().Status(httptest.StatusOK) e.POST("/remember_me").Expect().Status(httptest.StatusOK). Cookie(cookieName).MaxAge().InRange(23*time.Hour, 24*time.Hour) } // go test -v -count=100 -run=TestSessionsUpdateExpirationConcurrently$ // #1488 func TestSessionsUpdateExpirationConcurrently(t *testing.T) { cookieName := "mycustomsessionid" sess := sessions.New(sessions.Config{ Cookie: cookieName, Expires: 30 * time.Minute, AllowReclaim: true, }) app := iris.New() app.Use(sess.Handler()) app.Use(func(ctx iris.Context) { // session will expire after 30 minute at the last visit sess.UpdateExpiration(ctx, 30*time.Minute) ctx.Next() }) app.Get("/get", func(ctx iris.Context) { ctx.WriteString(sessions.Get(ctx).ID()) }) e := httptest.New(t, app, httptest.URL("http://example.com")) id := e.GET("/get").Expect().Status(httptest.StatusOK).Body().Raw() i := 0 wg := sync.WaitGroup{} wg.Add(1000) for i < 1000 { go func() { tt := e.GET("/get").Expect().Status(httptest.StatusOK) tt.Body().IsEqual(id) tt.Cookie(cookieName).MaxAge().InRange(29*time.Minute, 30*time.Minute) wg.Done() }() i++ } wg.Wait() tt := e.GET("/get").Expect() tt.Status(httptest.StatusOK).Body().IsEqual(id) tt.Cookie(cookieName).MaxAge().InRange(29*time.Minute, 30*time.Minute) } ================================================ FILE: sessions/transcoding.go ================================================ package sessions import ( "bytes" "encoding/gob" "encoding/json" "reflect" "time" ) func init() { gob.Register(time.Time{}) } type ( // Marshaler is the common marshaler interface, used by transcoder. Marshaler interface { Marshal(any) ([]byte, error) } // Unmarshaler is the common unmarshaler interface, used by transcoder. Unmarshaler interface { Unmarshal([]byte, any) error } // Transcoder is the interface that transcoders should implement, it includes just the `Marshaler` and the `Unmarshaler`. Transcoder interface { Marshaler Unmarshaler } ) type ( defaultTranscoder struct{} // GobTranscoder can be set to `DefaultTranscoder` to modify the database(s) transcoder. GobTranscoder struct{} ) var ( _ Transcoder = (*defaultTranscoder)(nil) _ Transcoder = (*GobTranscoder)(nil) // DefaultTranscoder is the default transcoder across databases (when `UseDatabase` is used). // // The default database's values encoder and decoder // calls the value's `Marshal/Unmarshal` methods (if any) // otherwise JSON is selected, // the JSON format can be stored to any database and // it supports both builtin language types(e.g. string, int) and custom struct values. // Also, and the most important, the values can be // retrieved/logged/monitored by a third-party program // written in any other language as well. // // You can change this behavior by registering a custom `Transcoder`. // Iris provides a `GobTranscoder` which is mostly suitable // if your session values are going to be custom Go structs. // Select this if you always retrieving values through Go. // Don't forget to initialize a call of gob.Register when necessary. // Read https://golang.org/pkg/encoding/gob/ for more. // // You can also implement your own `sessions.Transcoder` and use it, // i.e: a transcoder which will allow(on Marshal: return its byte representation and nil error) // or dissalow(on Marshal: return non nil error) certain types. // // sessions.DefaultTranscoder = sessions.GobTranscoder{} DefaultTranscoder Transcoder = defaultTranscoder{} ) func (defaultTranscoder) Marshal(value any) ([]byte, error) { if tr, ok := value.(Marshaler); ok { return tr.Marshal(value) } if jsonM, ok := value.(json.Marshaler); ok { return jsonM.MarshalJSON() } return json.Marshal(value) } func (defaultTranscoder) Unmarshal(b []byte, outPtr any) error { if tr, ok := outPtr.(Unmarshaler); ok { return tr.Unmarshal(b, outPtr) } if jsonUM, ok := outPtr.(json.Unmarshaler); ok { return jsonUM.UnmarshalJSON(b) } return json.Unmarshal(b, outPtr) } // Marshal returns the gob encoding of "value". func (GobTranscoder) Marshal(value any) ([]byte, error) { var ( w = new(bytes.Buffer) enc = gob.NewEncoder(w) err error ) if v, ok := value.(reflect.Value); ok { err = enc.EncodeValue(v) } else { err = enc.Encode(&value) } if err != nil { return nil, err } return w.Bytes(), nil } // Unmarshal parses the gob-encoded data "b" and stores the result // in the value pointed to by "outPtr". func (GobTranscoder) Unmarshal(b []byte, outPtr any) error { dec := gob.NewDecoder(bytes.NewBuffer(b)) if v, ok := outPtr.(reflect.Value); ok { return dec.DecodeValue(v) } return dec.Decode(outPtr) } ================================================ FILE: versioning/deprecation.go ================================================ package versioning import ( "time" "github.com/kataras/iris/v12/context" ) // The response header keys when a resource is deprecated by the server. const ( APIWarnHeader = "X-Api-Warn" APIDeprecationDateHeader = "X-Api-Deprecation-Date" APIDeprecationInfoHeader = "X-Api-Deprecation-Info" ) // DeprecationOptions describes the deprecation headers key-values. // - "X-Api-Warn": options.WarnMessage // - "X-Api-Deprecation-Date": context.FormatTime(ctx, options.DeprecationDate)) // - "X-Api-Deprecation-Info": options.DeprecationInfo type DeprecationOptions struct { WarnMessage string DeprecationDate time.Time DeprecationInfo string } // ShouldHandle reports whether the deprecation headers should be present or no. func (opts DeprecationOptions) ShouldHandle() bool { return opts.WarnMessage != "" || !opts.DeprecationDate.IsZero() || opts.DeprecationInfo != "" } // DefaultDeprecationOptions are the default deprecation options, // it defaults the "X-API-Warn" header to a generic message. var DefaultDeprecationOptions = DeprecationOptions{ WarnMessage: "WARNING! You are using a deprecated version of this API.", } // WriteDeprecated writes the deprecated response headers // based on the given "options". // It can be used inside a middleware. // // See `Deprecated` to wrap an existing handler instead. func WriteDeprecated(ctx *context.Context, options DeprecationOptions) { if options.WarnMessage == "" { options.WarnMessage = DefaultDeprecationOptions.WarnMessage } ctx.Header(APIWarnHeader, options.WarnMessage) if !options.DeprecationDate.IsZero() { ctx.Header(APIDeprecationDateHeader, context.FormatTime(ctx, options.DeprecationDate)) } if options.DeprecationInfo != "" { ctx.Header(APIDeprecationInfoHeader, options.DeprecationInfo) } } // Deprecated wraps an existing API handler and // marks it as a deprecated one. // Deprecated can be used to tell the clients that // a newer version of that specific resource is available instead. func Deprecated(handler context.Handler, options DeprecationOptions) context.Handler { return func(ctx *context.Context) { WriteDeprecated(ctx, options) handler(ctx) } } ================================================ FILE: versioning/deprecation_test.go ================================================ package versioning_test import ( "testing" "time" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/httptest" "github.com/kataras/iris/v12/versioning" ) func TestDeprecated(t *testing.T) { app := iris.New() writeVesion := func(ctx iris.Context) { ctx.WriteString(versioning.GetVersion(ctx)) } opts := versioning.DeprecationOptions{ WarnMessage: "deprecated, see ", DeprecationDate: time.Now().UTC(), DeprecationInfo: "a bigger version is available, see for more information", } app.Get("/", versioning.Deprecated(writeVesion, opts)) e := httptest.New(t, app) ex := e.GET("/").WithHeader(versioning.AcceptVersionHeaderKey, "1.0").Expect() ex.Status(iris.StatusOK).Body().IsEqual("1.0") ex.Header("X-API-Warn").Equal(opts.WarnMessage) expectedDateStr := opts.DeprecationDate.Format(app.ConfigurationReadOnly().GetTimeFormat()) ex.Header("X-API-Deprecation-Date").Equal(expectedDateStr) } ================================================ FILE: versioning/group.go ================================================ package versioning import ( "strings" "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/core/router" "github.com/blang/semver/v4" ) // API is a type alias of router.Party. // This is required in order for a Group instance // to implement the Party interface without field conflict. type API = router.Party // Group represents a group of resources that should // be handled based on a version requested by the client. // See `NewGroup` for more. type Group struct { API validate semver.Range deprecation DeprecationOptions } // NewGroup returns a version Group based on the given "version" constraint. // Group completes the Party interface. // The returned Group wraps a cloned Party of the given "r" Party therefore, // any changes to its parent won't affect this one (e.g. register global middlewares afterwards). // // A version is extracted through the versioning.GetVersion function: // // Accept-Version: 1.0.0 // Accept: application/json; version=1.0.0 // // You can customize it by setting a version based on the request context: // // api.Use(func(ctx *context.Context) { // if version := ctx.URLParam("version"); version != "" { // SetVersion(ctx, version) // } // // ctx.Next() // }) // // OR: // // api.Use(versioning.FromQuery("version", "")) // // Examples at: _examples/routing/versioning // Usage: // // app := iris.New() // api := app.Party("/api") // v1 := versioning.NewGroup(api, ">=1.0.0 <2.0.0") // v1.Get/Post/Put/Delete... // // Valid ranges are: // - "<1.0.0" // - "<=1.0.0" // - ">1.0.0" // - ">=1.0.0" // - "1.0.0", "=1.0.0", "==1.0.0" // - "!1.0.0", "!=1.0.0" // // A Range can consist of multiple ranges separated by space: // Ranges can be linked by logical AND: // - ">1.0.0 <2.0.0" would match between both ranges, so "1.1.1" and "1.8.7" // // but not "1.0.0" or "2.0.0" // - ">1.0.0 <3.0.0 !2.0.3-beta.2" would match every version between 1.0.0 and 3.0.0 // // except 2.0.3-beta.2 // // Ranges can also be linked by logical OR: // - "<2.0.0 || >=3.0.0" would match "1.x.x" and "3.x.x" but not "2.x.x" // // AND has a higher precedence than OR. It's not possible to use brackets. // // Ranges can be combined by both AND and OR // // - `>1.0.0 <2.0.0 || >3.0.0 !4.2.1` would match `1.2.3`, `1.9.9`, `3.1.1`, // // but not `4.2.1`, `2.1.1` func NewGroup(r API, version string) *Group { version = strings.ReplaceAll(version, ",", " ") version = strings.TrimSpace(version) verRange, err := semver.ParseRange(version) if err != nil { r.Logger().Errorf("versioning: %s: %s", r.GetRelPath(), strings.ToLower(err.Error())) return &Group{API: r} } // Clone this one. r = r.Party("/") // Note that this feature alters the RouteRegisterRule to RouteOverlap // the RouteOverlap rule does not contain any performance downside // but it's good to know that if you registered other mode, this wanna change it. r.SetRegisterRule(router.RouteOverlap) handler := makeHandler(verRange) // This is required in order to not populate this middleware to the next group. r.UseOnce(handler) // This is required for versioned custom error handlers, // of course if the parent registered one then this will do nothing. r.UseError(handler) return &Group{ API: r, validate: verRange, } } // Deprecated marks this group and all its versioned routes // as deprecated versions of that endpoint. func (g *Group) Deprecated(options DeprecationOptions) *Group { // store it for future use, e.g. collect all deprecated APIs and notify the developer. g.deprecation = options g.API.UseOnce(func(ctx *context.Context) { WriteDeprecated(ctx, options) ctx.Next() }) return g } func makeHandler(validate semver.Range) context.Handler { return func(ctx *context.Context) { if !matchVersionRange(ctx, validate) { // The overlapped handler has an exception // of a type of context.NotFound (which versioning.ErrNotFound wraps) // to clear the status code // and the error to ignore this // when available match version exists (see `NewGroup`). if h := NotFoundHandler; h != nil { h(ctx) return } } ctx.Next() } } ================================================ FILE: versioning/group_test.go ================================================ package versioning_test import ( "testing" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/httptest" "github.com/kataras/iris/v12/versioning" ) const ( v10Response = "v1.0 handler" v2Response = "v2.x handler" ) func sendHandler(contents string) iris.Handler { return func(ctx iris.Context) { ctx.WriteString(contents) } } func TestNewGroup(t *testing.T) { app := iris.New() userAPI := app.Party("/api/user") // [... static serving, middlewares and etc goes here]. userAPIV10 := versioning.NewGroup(userAPI, "1.0.0").Deprecated(versioning.DefaultDeprecationOptions) userAPIV10.Get("/", sendHandler(v10Response)) userAPIV2 := versioning.NewGroup(userAPI, ">= 2.0.0 < 3.0.0") userAPIV2.Get("/", sendHandler(v2Response)) userAPIV2.Post("/", sendHandler(v2Response)) userAPIV2.Put("/other", sendHandler(v2Response)) e := httptest.New(t, app) ex := e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "1.0.0").Expect() ex.Status(iris.StatusOK).Body().IsEqual(v10Response) ex.Header("X-API-Warn").Equal(versioning.DefaultDeprecationOptions.WarnMessage) e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2.0.0").Expect(). Status(iris.StatusOK).Body().IsEqual(v2Response) e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2.1.0").Expect(). Status(iris.StatusOK).Body().IsEqual(v2Response) e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2.9.9").Expect(). Status(iris.StatusOK).Body().IsEqual(v2Response) e.POST("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2.0.0").Expect(). Status(iris.StatusOK).Body().IsEqual(v2Response) e.PUT("/api/user/other").WithHeader(versioning.AcceptVersionHeaderKey, "2.9.0").Expect(). Status(iris.StatusOK).Body().IsEqual(v2Response) e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "3.0").Expect(). Status(iris.StatusNotImplemented).Body().IsEqual("version not found") } ================================================ FILE: versioning/version.go ================================================ package versioning import ( "fmt" "strings" "github.com/kataras/iris/v12/context" "github.com/blang/semver/v4" ) const ( // APIVersionResponseHeader the response header which its value contains // the normalized semver matched version. APIVersionResponseHeader = "X-Api-Version" // AcceptVersionHeaderKey is the header key of "Accept-Version". AcceptVersionHeaderKey = "Accept-Version" // AcceptHeaderKey is the header key of "Accept". AcceptHeaderKey = "Accept" // AcceptHeaderVersionValue is the Accept's header value search term the requested version. AcceptHeaderVersionValue = "version" // NotFound is the key that can be used inside a `Map` or inside `ctx.SetVersion(versioning.NotFound)` // to tell that a version wasn't found, therefore the `NotFoundHandler` should handle the request instead. NotFound = "iris.api.version.notfound" // Empty is just an empty string. Can be used as a key for a version alias // when the requested version of a resource was not even specified by the client. // The difference between NotFound and Empty is important when version aliases are registered: // - A NotFound cannot be registered as version alias, it // means that the client sent a version with its request // but that version was not implemented by the server. // - An Empty indicates that the client didn't send any version at all. Empty = "" ) // ErrNotFound reports whether a requested version // does not match with any of the server's implemented ones. var ErrNotFound = fmt.Errorf("version %w", context.ErrNotFound) // NotFoundHandler is the default version not found handler that // is executed from `NewMatcher` when no version is registered as available to dispatch a resource. var NotFoundHandler = func(ctx *context.Context) { // 303 is an option too, // end-dev has the chance to change that behavior by using the NotFound in the map: // // https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html /* 10.5.2 501 Not Implemented The server does not support the functionality required to fulfill the request. This is the appropriate response when the server does not recognize the request method and is not capable of supporting it for any resource. */ ctx.StopWithPlainError(501, ErrNotFound) } // FromQuery is a simple helper which tries to // set the version constraint from a given URL Query Parameter. // The X-Api-Version is still valid. func FromQuery(urlQueryParameterName string, defaultVersion string) context.Handler { return func(ctx *context.Context) { version := ctx.URLParam(urlQueryParameterName) if version == "" { version = defaultVersion } if version != "" { SetVersion(ctx, version) } ctx.Next() } } // If reports whether the "got" matches the "expected" one. // the "expected" can be a constraint like ">=1.0.0 <2.0.0". // This function is just a helper, better use the Group instead. func If(got string, expected string) bool { v, err := semver.Make(got) if err != nil { return false } validate, err := semver.ParseRange(expected) if err != nil { return false } return validate(v) } // Match reports whether the request matches the expected version. // This function is just a helper, better use the Group instead. func Match(ctx *context.Context, expectedVersion string) bool { validate, err := semver.ParseRange(expectedVersion) if err != nil { return false } return matchVersionRange(ctx, validate) } func matchVersionRange(ctx *context.Context, validate semver.Range) bool { gotVersion := GetVersion(ctx) alias, aliasFound := GetVersionAlias(ctx, gotVersion) if aliasFound { SetVersion(ctx, alias) // set the version so next routes have it already. gotVersion = alias } if gotVersion == "" { return false } v, err := semver.Make(gotVersion) if err != nil { return false } if !validate(v) { return false } versionString := v.String() if !aliasFound { // don't lose any time to set if already set. SetVersion(ctx, versionString) } ctx.Header(APIVersionResponseHeader, versionString) return true } // GetVersion returns the current request version. // // By default the `GetVersion` will try to read from: // - "Accept" header, i.e Accept: "application/json; version=1.0.0" // - "Accept-Version" header, i.e Accept-Version: "1.0.0" // // However, the end developer can also set a custom version for a handler via a middleware by using the context's store key // for versions (see `Key` for further details on that). // // See `SetVersion` too. func GetVersion(ctx *context.Context) string { // firstly by context store, if manually set by a middleware. version := ctx.Values().GetString(ctx.Application().ConfigurationReadOnly().GetVersionContextKey()) if version != "" { return version } // secondly by the "Accept-Version" header. if version = ctx.GetHeader(AcceptVersionHeaderKey); version != "" { return version } // thirdly by the "Accept" header which is like"...; version=1.0" acceptValue := ctx.GetHeader(AcceptHeaderKey) if acceptValue != "" { if idx := strings.Index(acceptValue, AcceptHeaderVersionValue); idx != -1 { rem := acceptValue[idx:] startVersion := strings.Index(rem, "=") if startVersion == -1 || len(rem) < startVersion+1 { return "" } rem = rem[startVersion+1:] end := strings.Index(rem, " ") if end == -1 { end = strings.Index(rem, ";") } if end == -1 { end = len(rem) } if version = rem[:end]; version != "" { return version } } } return "" } // SetVersion force-sets the API Version. // It can be used inside a middleware. // Example of how you can change the default behavior to extract a requested version (which is by headers) // from a "version" url parameter instead: // // func(ctx iris.Context) { // &version=1 // version := ctx.URLParamDefault("version", "1.0.0") // versioning.SetVersion(ctx, version) // ctx.Next() // } // // See `GetVersion` too. func SetVersion(ctx *context.Context, constraint string) { ctx.Values().Set(ctx.Application().ConfigurationReadOnly().GetVersionContextKey(), constraint) } // AliasMap is just a type alias of the standard map[string]string. // Head over to the `Aliases` function below for more. type AliasMap = map[string]string // Aliases is a middleware which registers version constraint aliases // for the children Parties(routers). It's respected by versioning Groups. // // Example Code: // // app := iris.New() // // api := app.Party("/api") // api.Use(Aliases(map[string]string{ // versioning.Empty: "1.0.0", // when no version was provided by the client. // "beta": "4.0.0", // "stage": "5.0.0-alpha" // })) // // v1 := NewGroup(api, ">=1.0.0 < 2.0.0") // v1.Get/Post... // // v4 := NewGroup(api, ">=4.0.0 < 5.0.0") // v4.Get/Post... // // stage := NewGroup(api, "5.0.0-alpha") // stage.Get/Post... func Aliases(aliases AliasMap) context.Handler { cp := make(AliasMap, len(aliases)) // copy the map here so we are safe of later modifications by end-dev. for k, v := range aliases { cp[k] = v } return func(ctx *context.Context) { SetVersionAliases(ctx, cp, true) ctx.Next() } } // Handler returns a handler which is only fired // when the "version" is matched with the requested one. // It is not meant to be used by end-developers // (exported for version controller feature). // Use `NewGroup` instead. func Handler(version string) context.Handler { validate, err := semver.ParseRange(version) if err != nil { return func(ctx *context.Context) { ctx.StopWithError(500, err) } } return makeHandler(validate) } // GetVersionAlias returns the version alias of the given "gotVersion" // or empty. It Reports whether the alias was found. // See `SetVersionAliases`, `Aliases` and `Match` for more. func GetVersionAlias(ctx *context.Context, gotVersion string) (string, bool) { key := ctx.Application().ConfigurationReadOnly().GetVersionAliasesContextKey() if key == "" { return "", false } v := ctx.Values().Get(key) if v == nil { return "", false } aliases, ok := v.(AliasMap) if !ok { return "", false } version, ok := aliases[gotVersion] if !ok { return "", false } return strings.TrimSpace(version), true } // SetVersionAliases sets a map of version aliases when a requested // version of a resource was not implemented by the server. // Can be used inside a middleware to the parent Party // and always before the child versioning groups (see `Aliases` function). // // The map's key (string) should be the "got version" (by the client) // and the value should be the "version constraint to match" instead. // The map's value(string) should be a registered version // otherwise it will hit the NotFoundHandler (501, "version not found" by default). // // The given "aliases" is a type of standard map[string]string and // should NOT be modified afterwards. // // The last "override" input argument indicates whether any // existing aliases, registered by previous handlers in the chain, // should be overridden or copied to the previous map one. func SetVersionAliases(ctx *context.Context, aliases AliasMap, override bool) { key := ctx.Application().ConfigurationReadOnly().GetVersionAliasesContextKey() if key == "" { return } v := ctx.Values().Get(key) if v == nil || override { ctx.Values().Set(key, aliases) return } if existing, ok := v.(AliasMap); ok { for k, v := range aliases { existing[k] = v } } } ================================================ FILE: versioning/version_test.go ================================================ package versioning_test import ( "testing" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/httptest" "github.com/kataras/iris/v12/versioning" ) func TestIf(t *testing.T) { if expected, got := true, versioning.If("1.0.0", ">=1.0.0"); expected != got { t.Fatalf("expected %s to be %s", "1.0.0", ">= 1.0.0") } if expected, got := true, versioning.If("1.2.3", "> 1.2.0"); expected != got { t.Fatalf("expected %s to be %s", "1.2.3", "> 1.2.0") } } func TestGetVersion(t *testing.T) { app := iris.New() writeVesion := func(ctx iris.Context) { ctx.WriteString(versioning.GetVersion(ctx)) } app.Get("/", writeVesion) app.Get("/manual", func(ctx iris.Context) { versioning.SetVersion(ctx, "11.0.5") ctx.Next() }, writeVesion) e := httptest.New(t, app) e.GET("/").WithHeader(versioning.AcceptVersionHeaderKey, "1.0.0").Expect(). Status(iris.StatusOK).Body().IsEqual("1.0.0") e.GET("/").WithHeader(versioning.AcceptHeaderKey, "application/vnd.api+json; version=2.1.0").Expect(). Status(iris.StatusOK).Body().IsEqual("2.1.0") e.GET("/").WithHeader(versioning.AcceptHeaderKey, "application/vnd.api+json; version=2.1.0 ;other=dsa").Expect(). Status(iris.StatusOK).Body().IsEqual("2.1.0") e.GET("/").WithHeader(versioning.AcceptHeaderKey, "version=2.1.0").Expect(). Status(iris.StatusOK).Body().IsEqual("2.1.0") e.GET("/").WithHeader(versioning.AcceptHeaderKey, "version=1.0.0").Expect(). Status(iris.StatusOK).Body().IsEqual("1.0.0") // unknown versions. e.GET("/").WithHeader(versioning.AcceptVersionHeaderKey, "").Expect(). Status(iris.StatusOK).Body().IsEqual("") e.GET("/").WithHeader(versioning.AcceptHeaderKey, "application/vnd.api+json; version=").Expect(). Status(iris.StatusOK).Body().IsEqual("") e.GET("/").WithHeader(versioning.AcceptHeaderKey, "application/vnd.api+json; version= ;other=dsa").Expect(). Status(iris.StatusOK).Body().IsEqual("") e.GET("/").WithHeader(versioning.AcceptHeaderKey, "version=").Expect(). Status(iris.StatusOK).Body().IsEqual("") e.GET("/manual").Expect().Status(iris.StatusOK).Body().IsEqual("11.0.5") } func TestVersionAliases(t *testing.T) { app := iris.New() api := app.Party("/api") api.Use(versioning.Aliases(map[string]string{ versioning.Empty: "1.0.0", "stage": "2.0.0", })) writeVesion := func(ctx iris.Context) { ctx.WriteString(versioning.GetVersion(ctx)) } // A group without registration order. v3 := versioning.NewGroup(api, ">= 3.0.0 < 4.0.0") v3.Get("/", writeVesion) v1 := versioning.NewGroup(api, ">= 1.0.0 < 2.0.0") v1.Get("/", writeVesion) v2 := versioning.NewGroup(api, ">= 2.0.0 < 3.0.0") v2.Get("/", writeVesion) api.Get("/manual", func(ctx iris.Context) { versioning.SetVersion(ctx, "12.0.0") ctx.Next() }, writeVesion) e := httptest.New(t, app) // Make sure the SetVersion still works. e.GET("/api/manual").Expect().Status(iris.StatusOK).Body().IsEqual("12.0.0") // Test Empty default. e.GET("/api").WithHeader(versioning.AcceptVersionHeaderKey, "").Expect(). Status(iris.StatusOK).Body().IsEqual("1.0.0") // Test NotFound error, aliases are not responsible for that. e.GET("/api").WithHeader(versioning.AcceptVersionHeaderKey, "4.0.0").Expect(). Status(iris.StatusNotImplemented).Body().IsEqual("version not found") // Test "stage" alias. e.GET("/api").WithHeader(versioning.AcceptVersionHeaderKey, "stage").Expect(). Status(iris.StatusOK).Body().IsEqual("2.0.0") // Test version 2. e.GET("/api").WithHeader(versioning.AcceptVersionHeaderKey, "2.0.0").Expect(). Status(iris.StatusOK).Body().IsEqual("2.0.0") // Test version 3 (registered first). e.GET("/api").WithHeader(versioning.AcceptVersionHeaderKey, "3.1.0").Expect(). Status(iris.StatusOK).Body().IsEqual("3.1.0") } ================================================ FILE: view/README.md ================================================ # View Iris supports 7 template engines out-of-the-box, developers can still use any external golang template engine, as `Context.ResponseWriter()` is an `io.Writer`. All template engines share a common API i.e. Parse using embedded assets, Layouts and Party-specific layout, Template Funcs, Partial Render and more. | # | Name | Parser | |:---|:-----------|----------| | 1 | HTML | [html/template](https://pkg.go.dev/html/template) | | 2 | Blocks | [kataras/blocks](https://github.com/kataras/blocks) | | 3 | Django | [flosch/pongo2](https://github.com/flosch/pongo2) | | 4 | Pug | [Joker/jade](https://github.com/Joker/jade) | | 5 | Handlebars | [mailgun/raymond](https://github.com/mailgun/raymond) | | 6 | Jet | [CloudyKit/jet](https://github.com/CloudyKit/jet) | | 7 | Ace | [yosssi/ace](https://github.com/yosssi/ace) | [List of Examples](https://github.com/kataras/iris/tree/main/_examples/view). [Benchmarks](https://github.com/kataras/iris/tree/main/_benchmarks/view). You can serve [quicktemplate](https://github.com/valyala/quicktemplate) files too, simply by using the `Context.ResponseWriter`, take a look at the [iris/_examples/view/quicktemplate](https://github.com/kataras/iris/tree/main/_examples/view/quicktemplate) example. ## Overview ```go // file: main.go package main import "github.com/kataras/iris/v12" func main() { app := iris.New() // Load all templates from the "./views" folder // where extension is ".html" and parse them // using the standard `html/template` package. app.RegisterView(iris.HTML("./views", ".html")) // Method: GET // Resource: http://localhost:8080 app.Get("/", func(ctx iris.Context) { // Bind: {{.message}} with "Hello world!" ctx.ViewData("message", "Hello world!") // Render template file: ./views/hello.html if err := ctx.View("hello.html"); err != nil { ctx.HTML(fmt.Sprintf("

      %s

      ", err.Error())) return } }) // Method: GET // Resource: http://localhost:8080/user/42 app.Get("/user/{id:int64}", func(ctx iris.Context) { userID, _ := ctx.Params().GetInt64("id") ctx.Writef("User ID: %d", userID) }) // Start the server using a network address. app.Listen(":8080") } ``` ```html Hello Page

      {{.message}}

      ``` ## Template functions ```go package main import "github.com/kataras/iris/v12" func main() { app := iris.New() tmpl := iris.HTML("./templates", ".html") // builtin template funcs are: // // - {{ urlpath "mynamedroute" "pathParameter_ifneeded" }} // - {{ render "header.html" . }} // - {{ render_r "header.html" . }} // partial relative path to current page // - {{ yield . }} // - {{ current . }} // register a custom template func. tmpl.AddFunc("greet", func(s string) string { return "Greetings " + s + "!" }) // register the view engine to the views, this will load the templates. app.RegisterView(tmpl) app.Get("/", hi) // http://localhost:8080 app.Listen(":8080") } func hi(ctx iris.Context) { // render the template file "./templates/hi.html" if err := ctx.View("hi.html"); err != nil { ctx.HTML(fmt.Sprintf("

      %s

      ", err.Error())) return } } ``` ```html {{greet "kataras"}} ``` ## Embedded View engine supports bundled(https://github.com/go-bindata/go-bindata) template files too. Latest `go-bindata` release gives you a compatible `http.FileSystem` that can be provided as the first argument of a view engine's initialization, e.g. `HTML(AssetFile(), ".html")`. ```sh $ go install github.com/go-bindata/go-bindata/v3/go-bindata@latest $ go-bindata -fs -prefix "templates" ./templates/... $ go run . ``` Example Code: ```go package main import "github.com/kataras/iris/v12" func main() { app := iris.New() app.RegisterView(iris.HTML(AssetFile(), ".html")) app.Get("/", hi) // http://localhost:8080 app.Listen(":8080") } type page struct { Title, Name string } func hi(ctx iris.Context) { // {{.Page.Title}} and {{Page.Name}} ctx.ViewData("Page", page{Title: "Hi Page", Name: "iris"}) if err := ctx.View("hi.html"); err != nil { ctx.HTML(fmt.Sprintf("

      %s

      ", err.Error())) return } } ``` Examples can be found here: https://github.com/kataras/iris/tree/main/_examples/view/embedding-templates-into-app and https://github.com/kataras/iris/tree/main/_examples/view/embedding-templates-into-app-bindata. ## Reload Enable auto-reloading of templates on each request. Useful while developers are in dev mode as they no neeed to restart their app on every template edit. Example code: ```go pugEngine := iris.Pug("./templates", ".jade") pugEngine.Reload(true) // <--- set to true to re-build the templates on each request. app.RegisterView(pugEngine) ``` ================================================ FILE: view/ace.go ================================================ package view import ( "strings" "sync" "github.com/yosssi/ace" ) // AceEngine represents the Ace view engine. // See the `Ace` package-level function for more. type AceEngine struct { *HTMLEngine indent string } // SetIndent string used for indentation. // Do NOT use tabs, only spaces characters. // Defaults to minified response, no indentation. func (s *AceEngine) SetIndent(indent string) *AceEngine { s.indent = indent return s } // Ace returns a new Ace view engine. // It shares the same exactly logic with the // html view engine, it uses the same exactly configuration. // The given "extension" MUST begin with a dot. // Ace minifies the response automatically unless // SetIndent() method is set. // // Read more about the Ace Go Parser: https://github.com/yosssi/ace // // Usage: // Ace("./views", ".ace") or // Ace(iris.Dir("./views"), ".ace") or // Ace(embed.FS, ".ace") or Ace(AssetFile(), ".ace") for embedded data. func Ace(fs any, extension string) *AceEngine { s := &AceEngine{HTMLEngine: HTML(fs, extension), indent: ""} s.name = "Ace" funcs := make(map[string]any) once := new(sync.Once) s.middleware = func(name string, text []byte) (contents string, err error) { once.Do(func() { // on first template parse, all funcs are given. for k, v := range s.getBuiltinFuncs(name) { funcs[k] = v } for k, v := range s.funcs { funcs[k] = v } }) // name = path.Join(path.Clean(directory), name) src := ace.NewSource( ace.NewFile(name, text), ace.NewFile("", []byte{}), []*ace.File{}, ) if strings.Contains(name, "layout") { for k, v := range s.layoutFuncs { funcs[k] = v } } opts := &ace.Options{ Extension: extension[1:], FuncMap: funcs, DelimLeft: s.left, DelimRight: s.right, Indent: s.indent, } rslt, err := ace.ParseSource(src, opts) if err != nil { return "", err } t, err := ace.CompileResult(name, rslt, opts) if err != nil { return "", err } return t.Lookup(name).Tree.Root.String(), nil } return s } ================================================ FILE: view/blocks.go ================================================ package view import ( "html/template" "io" "github.com/kataras/blocks" ) // BlocksEngine is an Iris view engine adapter for the blocks view engine. // The blocks engine is based on the html/template standard Go package. // // To initialize a fresh one use the `Blocks` function. // To wrap an existing one use the `WrapBlocks` function. // // It contains the following four default template functions: // - url "routename" parameters... // - urlpath "routename" parameters... // - tr "language" "key" arguments... // - partial "template_name" data // // Read more at: https://github.com/kataras/blocks. type BlocksEngine struct { Engine *blocks.Blocks } var ( _ Engine = (*BlocksEngine)(nil) _ EngineFuncer = (*BlocksEngine)(nil) ) // WrapBlocks wraps an initialized blocks engine and returns its Iris adapter. // See `Blocks` package-level function too. func WrapBlocks(v *blocks.Blocks) *BlocksEngine { return &BlocksEngine{Engine: v} } // Blocks returns a new blocks view engine. // The given "extension" MUST begin with a dot. // // See `WrapBlocks` package-level function too. // // Usage: // Blocks("./views", ".html") or // Blocks(iris.Dir("./views"), ".html") or // Blocks(embed.FS, ".html") or Blocks(AssetFile(), ".html") for embedded data. func Blocks(fs any, extension string) *BlocksEngine { return WrapBlocks(blocks.New(fs).Extension(extension)) } // Name returns the blocks engine's name. func (s *BlocksEngine) Name() string { return "Blocks" } // RootDir sets the directory to use as the root one inside the provided File System. func (s *BlocksEngine) RootDir(root string) *BlocksEngine { s.Engine.RootDir(root) return s } // LayoutDir sets a custom layouts directory, // always relative to the "rootDir" one. // Layouts are recognised by their prefix names. // Defaults to "layouts". func (s *BlocksEngine) LayoutDir(relToDirLayoutDir string) *BlocksEngine { s.Engine.LayoutDir(relToDirLayoutDir) return s } // Ext returns empty ext as this template engine // supports template blocks without file suffix. // Note that, if more than one view engine is registered to a single // Iris application then, this Blocks engine should be the last entry one. func (s *BlocksEngine) Ext() string { return "" } // AddFunc implements the `EngineFuncer` which is being used // by the framework to add template functions like: // - url func(routeName string, args ...string) string // - urlpath func(routeName string, args ...string) string // - tr func(lang, key string, args ...any) string func (s *BlocksEngine) AddFunc(funcName string, funcBody any) { s.Engine.Funcs(template.FuncMap{funcName: funcBody}) } // AddLayoutFunc adds a template function for templates that are marked as layouts. func (s *BlocksEngine) AddLayoutFunc(funcName string, funcBody any) *BlocksEngine { s.Engine.LayoutFuncs(template.FuncMap{funcName: funcBody}) return s } // Layout sets the default layout which inside should use // the {{ template "content" . }} to render the main template. // // Example for ./views/layouts/main.html: // Blocks("./views", ".html").Layout("layouts/main") func (s *BlocksEngine) Layout(layoutName string) *BlocksEngine { s.Engine.DefaultLayout(layoutName) return s } // Reload if called with a true parameter, // each `ExecuteWriter` call will re-parse the templates. // Useful when the application is at a development stage. func (s *BlocksEngine) Reload(b bool) *BlocksEngine { s.Engine.Reload(b) return s } // Load parses the files into templates. func (s *BlocksEngine) Load() error { return s.Engine.Load() } // ExecuteWriter renders a template on "w". func (s *BlocksEngine) ExecuteWriter(w io.Writer, tmplName, layoutName string, data any) error { if layoutName == NoLayout { layoutName = "" } return s.Engine.ExecuteTemplate(w, tmplName, layoutName, data) } ================================================ FILE: view/django.go ================================================ package view import ( "bytes" "io" "io/fs" "os" stdPath "path" "path/filepath" "strings" "sync" "github.com/kataras/iris/v12/context" "github.com/fatih/structs" "github.com/flosch/pongo2/v4" ) type ( // Value type alias for pongo2.Value Value = pongo2.Value // Error type alias for pongo2.Error Error = pongo2.Error // FilterFunction type alias for pongo2.FilterFunction FilterFunction = pongo2.FilterFunction // Parser type alias for pongo2.Parser Parser = pongo2.Parser // Token type alias for pongo2.Token Token = pongo2.Token // INodeTag type alias for pongo2.InodeTag INodeTag = pongo2.INodeTag // TagParser the function signature of the tag's parser you will have // to implement in order to create a new tag. // // 'doc' is providing access to the whole document while 'arguments' // is providing access to the user's arguments to the tag: // // {% your_tag_name some "arguments" 123 %} // // start_token will be the *Token with the tag's name in it (here: your_tag_name). // // Please see the Parser documentation on how to use the parser. // See `RegisterTag` for more information about writing a tag as well. TagParser = pongo2.TagParser ) // AsValue converts any given value to a pongo2.Value // Usually being used within own functions passed to a template // through a Context or within filter functions. // // Example: // // AsValue("my string") // // Shortcut for `pongo2.AsValue`. var AsValue = pongo2.AsValue // AsSafeValue works like AsValue, but does not apply the 'escape' filter. // Shortcut for `pongo2.AsSafeValue`. var AsSafeValue = pongo2.AsSafeValue type tDjangoAssetLoader struct { rootDir string fs fs.FS } // Abs calculates the path to a given template. Whenever a path must be resolved // due to an import from another template, the base equals the parent template's path. func (l *tDjangoAssetLoader) Abs(base, name string) string { if stdPath.IsAbs(name) { return name } return stdPath.Join(l.rootDir, name) } // Get returns an io.Reader where the template's content can be read from. func (l *tDjangoAssetLoader) Get(path string) (io.Reader, error) { if stdPath.IsAbs(path) { path = path[1:] } res, err := asset(l.fs, path) if err != nil { return nil, err } return bytes.NewReader(res), nil } // DjangoEngine contains the django view engine structure. type DjangoEngine struct { fs fs.FS // files configuration rootDir string extension string reload bool // rmu sync.RWMutex // locks for filters, globals and `ExecuteWiter` when `reload` is true. // filters for pongo2, map[name of the filter] the filter function . The filters are auto register filters map[string]FilterFunction // globals share context fields between templates. globals map[string]any Set *pongo2.TemplateSet templateCache map[string]*pongo2.Template } var ( _ Engine = (*DjangoEngine)(nil) _ EngineFuncer = (*DjangoEngine)(nil) ) // Django creates and returns a new django view engine. // The given "extension" MUST begin with a dot. // // Usage: // Django("./views", ".html") or // Django(iris.Dir("./views"), ".html") or // Django(embed.FS, ".html") or Django(AssetFile(), ".html") for embedded data. func Django(fs any, extension string) *DjangoEngine { s := &DjangoEngine{ fs: getFS(fs), rootDir: "/", extension: extension, globals: make(map[string]any), filters: make(map[string]FilterFunction), templateCache: make(map[string]*pongo2.Template), } return s } // RootDir sets the directory to be used as a starting point // to load templates from the provided file system. func (s *DjangoEngine) RootDir(root string) *DjangoEngine { if s.fs != nil && root != "" && root != "/" && root != "." && root != s.rootDir { sub, err := fs.Sub(s.fs, s.rootDir) if err != nil { panic(err) } s.fs = sub // here so the "middleware" can work. } s.rootDir = filepath.ToSlash(root) return s } // Name returns the django engine's name. func (s *DjangoEngine) Name() string { return "Django" } // Ext returns the file extension which this view engine is responsible to render. // If the filename extension on ExecuteWriter is empty then this is appended. func (s *DjangoEngine) Ext() string { return s.extension } // Reload if set to true the templates are reloading on each render, // use it when you're in development and you're boring of restarting // the whole app when you edit a template file. // // Note that if `true` is passed then only one `View -> ExecuteWriter` will be render each time, // no concurrent access across clients, use it only on development status. // It's good to be used side by side with the https://github.com/kataras/rizla reloader for go source files. func (s *DjangoEngine) Reload(developmentMode bool) *DjangoEngine { s.reload = developmentMode return s } // AddFunc adds the function to the template's Globals. // It is legal to overwrite elements of the default actions: // - url func(routeName string, args ...string) string // - urlpath func(routeName string, args ...string) string // - render func(fullPartialName string) (template.HTML, error). func (s *DjangoEngine) AddFunc(funcName string, funcBody any) { s.rmu.Lock() s.globals[funcName] = funcBody s.rmu.Unlock() } // AddFilter registers a new filter. If there's already a filter with the same // name, RegisterFilter will panic. You usually want to call this // function in the filter's init() function: // http://golang.org/doc/effective_go.html#init // // Same as `RegisterFilter`. func (s *DjangoEngine) AddFilter(filterName string, filterBody FilterFunction) *DjangoEngine { return s.registerFilter(filterName, filterBody) } // RegisterFilter registers a new filter. If there's already a filter with the same // name, RegisterFilter will panic. You usually want to call this // function in the filter's init() function: // http://golang.org/doc/effective_go.html#init // // See http://www.florian-schlachter.de/post/pongo2/ for more about // writing filters and tags. func (s *DjangoEngine) RegisterFilter(filterName string, filterBody FilterFunction) *DjangoEngine { return s.registerFilter(filterName, filterBody) } func (s *DjangoEngine) registerFilter(filterName string, fn FilterFunction) *DjangoEngine { pongo2.RegisterFilter(filterName, fn) return s } // RegisterTag registers a new tag. You usually want to call this // function in the tag's init() function: // http://golang.org/doc/effective_go.html#init // // See http://www.florian-schlachter.de/post/pongo2/ for more about // writing filters and tags. func (s *DjangoEngine) RegisterTag(tagName string, fn TagParser) error { return pongo2.RegisterTag(tagName, fn) } // Load parses the templates to the engine. // It is responsible to add the necessary global functions. // // Returns an error if something bad happens, user is responsible to catch it. func (s *DjangoEngine) Load() error { // If only custom templates should be loaded. if (s.fs == nil || context.IsNoOpFS(s.fs)) && len(s.templateCache) > 0 { return nil } rootDirName := getRootDirName(s.fs) return walk(s.fs, "", func(path string, info os.FileInfo, err error) error { if err != nil { return err } if info == nil || info.IsDir() { return nil } if s.extension != "" { if !strings.HasSuffix(path, s.extension) { return nil } } if s.rootDir == rootDirName { path = strings.TrimPrefix(path, rootDirName) path = strings.TrimPrefix(path, "/") } contents, err := asset(s.fs, path) if err != nil { return err } return s.ParseTemplate(path, contents) }) } // ParseTemplate adds a custom template from text. // This parser does not support funcs per template. Use the `AddFunc` instead. func (s *DjangoEngine) ParseTemplate(name string, contents []byte) error { s.rmu.Lock() defer s.rmu.Unlock() s.initSet() name = strings.TrimPrefix(name, "/") tmpl, err := s.Set.FromBytes(contents) if err == nil { s.templateCache[name] = tmpl } return err } func (s *DjangoEngine) initSet() { // protected by the caller. if s.Set == nil { s.Set = pongo2.NewSet("", &tDjangoAssetLoader{fs: s.fs, rootDir: s.rootDir}) s.Set.Globals = getPongoContext(s.globals) } } // getPongoContext returns the pongo2.Context from map[string]any or from pongo2.Context, used internaly func getPongoContext(templateData any) pongo2.Context { if templateData == nil { return nil } switch data := templateData.(type) { case pongo2.Context: return data case context.Map: return pongo2.Context(data) default: // if struct, convert it to map[string]any if structs.IsStruct(data) { return pongo2.Context(structs.Map(data)) } panic("django: template data: should be a map or struct") } } func (s *DjangoEngine) fromCache(relativeName string) *pongo2.Template { if s.reload { s.rmu.RLock() defer s.rmu.RUnlock() } if tmpl, ok := s.templateCache[relativeName]; ok { return tmpl } return nil } // ExecuteWriter executes a templates and write its results to the w writer // layout here is useless. func (s *DjangoEngine) ExecuteWriter(w io.Writer, filename string, _ string, bindingData any) error { // re-parse the templates if reload is enabled. if s.reload { if err := s.Load(); err != nil { return err } } if tmpl := s.fromCache(filename); tmpl != nil { return tmpl.ExecuteWriter(getPongoContext(bindingData), w) } return ErrNotExist{Name: filename, IsLayout: false, Data: bindingData} } ================================================ FILE: view/fs.go ================================================ package view import ( "fmt" "io/fs" "path/filepath" "github.com/kataras/iris/v12/context" ) // walk recursively in "fileSystem" descends "root" path, calling "walkFn". func walk(fileSystem fs.FS, root string, walkFn filepath.WalkFunc) error { if root != "" && root != "/" && root != "." { sub, err := fs.Sub(fileSystem, root) if err != nil { return err } fileSystem = sub } if root == "" { root = "." } return fs.WalkDir(fileSystem, root, func(path string, d fs.DirEntry, err error) error { if err != nil { return fmt.Errorf("walk: %s: %w", path, err) } info, err := d.Info() if err != nil { if err != filepath.SkipDir { return fmt.Errorf("walk stat: %s: %w", path, err) } return nil } if info.IsDir() { return nil } walkFnErr := walkFn(path, info, err) if walkFnErr != nil { return fmt.Errorf("walk: walkFn: %w", walkFnErr) } return nil }) } func asset(fileSystem fs.FS, name string) ([]byte, error) { data, err := fs.ReadFile(fileSystem, name) if err != nil { return nil, fmt.Errorf("asset: read file: %w", err) } return data, nil } func getFS(fsOrDir any) fs.FS { return context.ResolveFS(fsOrDir) } func getRootDirName(fileSystem fs.FS) string { rootDirFile, err := fileSystem.Open(".") if err == nil { rootDirStat, err := rootDirFile.Stat() if err == nil { return rootDirStat.Name() } } return "" } ================================================ FILE: view/handlebars.go ================================================ package view import ( "fmt" "html/template" "io" "io/fs" "os" "path/filepath" "strings" "sync" "github.com/kataras/iris/v12/context" "github.com/mailgun/raymond/v2" ) // HandlebarsEngine contains the handlebars view engine structure. type HandlebarsEngine struct { fs fs.FS // files configuration rootDir string extension string // Not used anymore. // assetFn func(name string) ([]byte, error) // for embedded, in combination with directory & extension // namesFn func() []string // for embedded, in combination with directory & extension reload bool // if true, each time the ExecuteWriter is called the templates will be reloaded. // parser configuration layout string rmu sync.RWMutex funcs template.FuncMap templateCache map[string]*raymond.Template } var ( _ Engine = (*HandlebarsEngine)(nil) _ EngineFuncer = (*HandlebarsEngine)(nil) ) // Handlebars creates and returns a new handlebars view engine. // The given "extension" MUST begin with a dot. // // Usage: // Handlebars("./views", ".html") or // Handlebars(iris.Dir("./views"), ".html") or // Handlebars(embed.FS, ".html") or Handlebars(AssetFile(), ".html") for embedded data. func Handlebars(fs any, extension string) *HandlebarsEngine { s := &HandlebarsEngine{ fs: getFS(fs), rootDir: "/", extension: extension, templateCache: make(map[string]*raymond.Template), funcs: make(template.FuncMap), // global } // register the render helper here raymond.RegisterHelper("render", func(partial string, binding any) raymond.SafeString { contents, err := s.executeTemplateBuf(partial, binding) if err != nil { return raymond.SafeString("template with name: " + partial + " couldn't not be found.") } return raymond.SafeString(contents) }) return s } // RootDir sets the directory to be used as a starting point // to load templates from the provided file system. func (s *HandlebarsEngine) RootDir(root string) *HandlebarsEngine { if s.fs != nil && root != "" && root != "/" && root != "." && root != s.rootDir { sub, err := fs.Sub(s.fs, s.rootDir) if err != nil { panic(err) } s.fs = sub // here so the "middleware" can work. } s.rootDir = filepath.ToSlash(root) return s } // Name returns the handlebars engine's name. func (s *HandlebarsEngine) Name() string { return "Handlebars" } // Ext returns the file extension which this view engine is responsible to render. // If the filename extension on ExecuteWriter is empty then this is appended. func (s *HandlebarsEngine) Ext() string { return s.extension } // Reload if set to true the templates are reloading on each render, // use it when you're in development and you're boring of restarting // the whole app when you edit a template file. // // Note that if `true` is passed then only one `View -> ExecuteWriter` will be render each time, // no concurrent access across clients, use it only on development status. // It's good to be used side by side with the https://github.com/kataras/rizla reloader for go source files. func (s *HandlebarsEngine) Reload(developmentMode bool) *HandlebarsEngine { s.reload = developmentMode return s } // Layout sets the layout template file which should use // the {{ yield . }} func to yield the main template file // and optionally {{partial/partial_r/render . }} to render // other template files like headers and footers. func (s *HandlebarsEngine) Layout(layoutFile string) *HandlebarsEngine { s.layout = layoutFile return s } // AddFunc adds a function to the templates. // It is legal to overwrite elements of the default actions: // - url func(routeName string, args ...string) string // - urlpath func(routeName string, args ...string) string // - render func(fullPartialName string) (raymond.HTML, error). func (s *HandlebarsEngine) AddFunc(funcName string, funcBody any) { s.rmu.Lock() s.funcs[funcName] = funcBody s.rmu.Unlock() } // AddGlobalFunc registers a global template function for all Handlebars view engines. func (s *HandlebarsEngine) AddGlobalFunc(funcName string, funcBody any) { s.rmu.Lock() raymond.RegisterHelper(funcName, funcBody) s.rmu.Unlock() } // Load parses the templates to the engine. // It is responsible to add the necessary global functions. // // Returns an error if something bad happens, user is responsible to catch it. func (s *HandlebarsEngine) Load() error { // If only custom templates should be loaded. if (s.fs == nil || context.IsNoOpFS(s.fs)) && len(s.templateCache) > 0 { return nil } rootDirName := getRootDirName(s.fs) return walk(s.fs, "", func(path string, info os.FileInfo, _ error) error { if info == nil || info.IsDir() { return nil } if s.extension != "" { if !strings.HasSuffix(path, s.extension) { return nil } } if s.rootDir == rootDirName { path = strings.TrimPrefix(path, rootDirName) path = strings.TrimPrefix(path, "/") } contents, err := asset(s.fs, path) if err != nil { return err } return s.ParseTemplate(path, string(contents), nil) }) } // ParseTemplate adds a custom template from text. func (s *HandlebarsEngine) ParseTemplate(name string, contents string, funcs template.FuncMap) error { s.rmu.Lock() defer s.rmu.Unlock() name = strings.TrimPrefix(name, "/") tmpl, err := raymond.Parse(contents) if err == nil { // Add functions for this template. for k, v := range s.funcs { tmpl.RegisterHelper(k, v) } for k, v := range funcs { tmpl.RegisterHelper(k, v) } s.templateCache[name] = tmpl } return err } func (s *HandlebarsEngine) fromCache(relativeName string) *raymond.Template { if s.reload { s.rmu.RLock() defer s.rmu.RUnlock() } if tmpl, ok := s.templateCache[relativeName]; ok { return tmpl } return nil } func (s *HandlebarsEngine) executeTemplateBuf(name string, binding any) (string, error) { if tmpl := s.fromCache(name); tmpl != nil { return tmpl.Exec(binding) } return "", nil } // ExecuteWriter executes a template and writes its result to the w writer. func (s *HandlebarsEngine) ExecuteWriter(w io.Writer, filename string, layout string, bindingData any) error { // re-parse the templates if reload is enabled. if s.reload { if err := s.Load(); err != nil { return err } } isLayout := false layout = getLayout(layout, s.layout) renderFilename := filename if layout != "" { isLayout = true renderFilename = layout // the render becomes the layout, and the name is the partial. } if tmpl := s.fromCache(renderFilename); tmpl != nil { binding := bindingData if isLayout { var context map[string]any if m, is := binding.(map[string]any); is { // handlebars accepts maps, context = m } else { return fmt.Errorf("please provide a map[string]any type as the binding instead of the %#v", binding) } contents, err := s.executeTemplateBuf(filename, binding) if err != nil { return err } if context == nil { context = make(map[string]any, 1) } // I'm implemented the {{ yield . }} as with the rest of template engines, so this is not inneed for iris, but the user can do that manually if want // there is no performance cost: raymond.RegisterPartialTemplate(name, tmpl) context["yield"] = raymond.SafeString(contents) } res, err := tmpl.Exec(binding) if err != nil { return err } _, err = fmt.Fprint(w, res) return err } return ErrNotExist{ Name: fmt.Sprintf("%s (file: %s)", renderFilename, filename), IsLayout: false, Data: bindingData, } } ================================================ FILE: view/html.go ================================================ package view import ( "bytes" "fmt" "html/template" "io" "io/fs" "os" "path/filepath" "strings" "sync" "github.com/kataras/iris/v12/context" ) // HTMLEngine contains the html view engine structure. type HTMLEngine struct { name string // the view engine's name, can be HTML, Ace or Pug. // the file system to load from. fs fs.FS // files configuration rootDir string extension string // if true, each time the ExecuteWriter is called the templates will be reloaded, // each ExecuteWriter waits to be finished before writing to a new one. reload bool // parser configuration options []string // (text) template options left string right string layout string rmu sync.RWMutex // locks for layoutFuncs and funcs layoutFuncs template.FuncMap funcs template.FuncMap // middleware func(name string, contents []byte) (string, error) onLoad func() onLoaded func() Templates *template.Template customCache []customTmp // required to load them again if reload is true. bufPool *sync.Pool // } type customTmp struct { name string contents []byte funcs template.FuncMap } var ( _ Engine = (*HTMLEngine)(nil) _ EngineFuncer = (*HTMLEngine)(nil) ) // HTML creates and returns a new html view engine. // The html engine used like the "html/template" standard go package // but with a lot of extra features. // The given "extension" MUST begin with a dot. // // Usage: // HTML("./views", ".html") or // HTML(iris.Dir("./views"), ".html") or // HTML(embed.FS, ".html") or HTML(AssetFile(), ".html") for embedded data or // HTML("","").ParseTemplate("hello", `[]byte("hello {{.Name}}")`, nil) for custom template parsing only. func HTML(dirOrFS any, extension string) *HTMLEngine { s := &HTMLEngine{ name: "HTML", fs: getFS(dirOrFS), rootDir: "/", extension: extension, reload: false, left: "{{", right: "}}", layout: "", layoutFuncs: template.FuncMap{ "yield": func(binding any) template.HTML { return template.HTML("") }, }, funcs: make(template.FuncMap), bufPool: &sync.Pool{New: func() any { return new(bytes.Buffer) }}, } return s } // RootDir sets the directory to be used as a starting point // to load templates from the provided file system. func (s *HTMLEngine) RootDir(root string) *HTMLEngine { if s.fs != nil && root != "" && root != "/" && root != "." && root != s.rootDir { sub, err := fs.Sub(s.fs, root) if err != nil { panic(err) } s.fs = sub // here so the "middleware" can work. } s.rootDir = filepath.ToSlash(root) return s } // FS change templates DIR func (s *HTMLEngine) FS(dirOrFS any) *HTMLEngine { s.fs = getFS(dirOrFS) return s } // Name returns the engine's name. func (s *HTMLEngine) Name() string { return s.name } // Ext returns the file extension which this view engine is responsible to render. // If the filename extension on ExecuteWriter is empty then this is appended. func (s *HTMLEngine) Ext() string { return s.extension } // Reload if set to true the templates are reloading on each render, // use it when you're in development and you're boring of restarting // the whole app when you edit a template file. // // Note that if `true` is passed then only one `View -> ExecuteWriter` will be render each time, // no concurrent access across clients, use it only on development status. // It's good to be used side by side with the https://github.com/kataras/rizla reloader for go source files. func (s *HTMLEngine) Reload(developmentMode bool) *HTMLEngine { s.reload = developmentMode return s } // Option sets options for the template. Options are described by // strings, either a simple string or "key=value". There can be at // most one equals sign in an option string. If the option string // is unrecognized or otherwise invalid, Option panics. // // Known options: // // missingkey: Control the behavior during execution if a map is // indexed with a key that is not present in the map. // // "missingkey=default" or "missingkey=invalid" // The default behavior: Do nothing and continue execution. // If printed, the result of the index operation is the string // "". // "missingkey=zero" // The operation returns the zero value for the map type's element. // "missingkey=error" // Execution stops immediately with an error. func (s *HTMLEngine) Option(opt ...string) *HTMLEngine { s.rmu.Lock() s.options = append(s.options, opt...) s.rmu.Unlock() return s } // Delims sets the action delimiters to the specified strings, to be used in // templates. An empty delimiter stands for the // corresponding default: {{ or }}. func (s *HTMLEngine) Delims(left, right string) *HTMLEngine { s.left, s.right = left, right return s } // Layout sets the layout template file which inside should use // the {{ yield . }} func to yield the main template file // and optionally {{partial/partial_r/render . }} to render other template files like headers and footers // // The 'tmplLayoutFile' is a relative path of the templates base directory, // for the template file with its extension. // // Example: HTML("./templates", ".html").Layout("layouts/mainLayout.html") // // // mainLayout.html is inside: "./templates/layouts/". // // Note: Layout can be changed for a specific call // action with the option: "layout" on the iris' context.Render function. func (s *HTMLEngine) Layout(layoutFile string) *HTMLEngine { s.layout = layoutFile return s } // AddLayoutFunc adds the function to the template's layout-only function map. // It is legal to overwrite elements of the default layout actions: // - yield func() (template.HTML, error) // - current func() (string, error) // - partial func(partialName string) (template.HTML, error) // - partial_r func(partialName string) (template.HTML, error) // - render func(fullPartialName string) (template.HTML, error). func (s *HTMLEngine) AddLayoutFunc(funcName string, funcBody any) *HTMLEngine { s.rmu.Lock() s.layoutFuncs[funcName] = funcBody s.rmu.Unlock() return s } // AddFunc adds the function to the template's function map. // It is legal to overwrite elements of the default actions: // - url func(routeName string, args ...string) string // - urlpath func(routeName string, args ...string) string // - render func(fullPartialName string) (template.HTML, error). // - tr func(lang, key string, args ...any) string func (s *HTMLEngine) AddFunc(funcName string, funcBody any) { s.rmu.Lock() s.funcs[funcName] = funcBody s.rmu.Unlock() } // SetFuncs overrides the template funcs with the given "funcMap". func (s *HTMLEngine) SetFuncs(funcMap template.FuncMap) *HTMLEngine { s.rmu.Lock() s.funcs = funcMap s.rmu.Unlock() return s } // Funcs adds the elements of the argument map to the template's function map. // It is legal to overwrite elements of the map. The return // value is the template, so calls can be chained. func (s *HTMLEngine) Funcs(funcMap template.FuncMap) *HTMLEngine { s.rmu.Lock() for k, v := range funcMap { s.funcs[k] = v } s.rmu.Unlock() return s } // Load parses the templates to the engine. // It's also responsible to add the necessary global functions. // // Returns an error if something bad happens, caller is responsible to handle that. func (s *HTMLEngine) Load() error { s.rmu.Lock() defer s.rmu.Unlock() return s.load() } func (s *HTMLEngine) load() error { if s.onLoad != nil { s.onLoad() } if err := s.reloadCustomTemplates(); err != nil { return err } // If only custom templates should be loaded. if (s.fs == nil || context.IsNoOpFS(s.fs)) && len(s.Templates.DefinedTemplates()) > 0 { return nil } rootDirName := getRootDirName(s.fs) err := walk(s.fs, "", func(path string, info os.FileInfo, err error) error { if err != nil { return err } if info == nil || info.IsDir() { return nil } if s.extension != "" { if !strings.HasSuffix(path, s.extension) { return nil } } if s.rootDir == rootDirName { path = strings.TrimPrefix(path, rootDirName) path = strings.TrimPrefix(path, "/") } buf, err := asset(s.fs, path) if err != nil { return fmt.Errorf("%s: %w", path, err) } return s.parseTemplate(path, buf, nil) }) if s.onLoaded != nil { s.onLoaded() } if err != nil { return err } if s.Templates == nil { return fmt.Errorf("no templates found") } return nil } func (s *HTMLEngine) reloadCustomTemplates() error { for _, tmpl := range s.customCache { if err := s.parseTemplate(tmpl.name, tmpl.contents, tmpl.funcs); err != nil { return err } } return nil } // ParseTemplate adds a custom template to the root template. func (s *HTMLEngine) ParseTemplate(name string, contents []byte, funcs template.FuncMap) (err error) { s.rmu.Lock() defer s.rmu.Unlock() s.customCache = append(s.customCache, customTmp{ name: name, contents: contents, funcs: funcs, }) return s.parseTemplate(name, contents, funcs) } func (s *HTMLEngine) parseTemplate(name string, contents []byte, funcs template.FuncMap) (err error) { s.initRootTmpl() name = strings.TrimPrefix(name, "/") tmpl := s.Templates.New(name) // tmpl.Option("missingkey=error") tmpl.Option(s.options...) var text string if s.middleware != nil { text, err = s.middleware(name, contents) if err != nil { return } } else { text = string(contents) } tmpl.Funcs(s.getBuiltinFuncs(name)).Funcs(s.funcs) if strings.Contains(name, "layout") { tmpl.Funcs(s.layoutFuncs) } if len(funcs) > 0 { tmpl.Funcs(funcs) // custom for this template. } _, err = tmpl.Parse(text) return } func (s *HTMLEngine) initRootTmpl() { // protected by the caller. if s.Templates == nil { // the root template should be the same, // no matter how many reloads as the // following unexported fields cannot be modified. // However, on reload they should be cleared otherwise we get an error. s.Templates = template.New(s.rootDir) s.Templates.Delims(s.left, s.right) } } func (s *HTMLEngine) executeTemplateBuf(name string, binding any) (string, error) { buf := s.bufPool.Get().(*bytes.Buffer) buf.Reset() err := s.Templates.ExecuteTemplate(buf, name, binding) result := buf.String() s.bufPool.Put(buf) return result, err } func (s *HTMLEngine) getBuiltinRuntimeLayoutFuncs(name string) template.FuncMap { funcs := template.FuncMap{ "yield": func(binding any) (template.HTML, error) { result, err := s.executeTemplateBuf(name, binding) // Return safe HTML here since we are rendering our own template. return template.HTML(result), err }, } return funcs } func (s *HTMLEngine) getBuiltinFuncs(name string) template.FuncMap { funcs := template.FuncMap{ "part": func(partName string, binding any) (template.HTML, error) { nameTemp := strings.ReplaceAll(name, s.extension, "") fullPartName := fmt.Sprintf("%s-%s", nameTemp, partName) result, err := s.executeTemplateBuf(fullPartName, binding) if err != nil { return "", nil } return template.HTML(result), err }, "current": func() (string, error) { return name, nil }, "partial": func(partialName string, binding any) (template.HTML, error) { fullPartialName := fmt.Sprintf("%s-%s", partialName, name) if s.Templates.Lookup(fullPartialName) != nil { result, err := s.executeTemplateBuf(fullPartialName, binding) return template.HTML(result), err } return "", nil }, // partial related to current page, // it would be easier for adding pages' style/script inline // for example when using partial_r '.script' in layout.html // templates/users/index.html would load templates/users/index.script.html "partial_r": func(partialName string, binding any) (template.HTML, error) { ext := filepath.Ext(name) root := name[:len(name)-len(ext)] fullPartialName := fmt.Sprintf("%s%s%s", root, partialName, ext) if s.Templates.Lookup(fullPartialName) != nil { result, err := s.executeTemplateBuf(fullPartialName, binding) return template.HTML(result), err } return "", nil }, "render": func(fullPartialName string, binding any) (template.HTML, error) { result, err := s.executeTemplateBuf(fullPartialName, binding) return template.HTML(result), err }, } return funcs } // ExecuteWriter executes a template and writes its result to the w writer. func (s *HTMLEngine) ExecuteWriter(w io.Writer, name string, layout string, bindingData any) error { // re-parse the templates if reload is enabled. if s.reload { s.rmu.Lock() defer s.rmu.Unlock() s.Templates = nil // we lose the templates parsed manually, so store them when it's called // in order for load to take care of them too. if err := s.load(); err != nil { return err } } if layout = getLayout(layout, s.layout); layout != "" { lt := s.Templates.Lookup(layout) if lt == nil { return ErrNotExist{Name: layout, IsLayout: true, Data: bindingData} } return lt.Funcs(s.getBuiltinRuntimeLayoutFuncs(name)).Execute(w, bindingData) } t := s.Templates.Lookup(name) if t == nil { return ErrNotExist{Name: name, IsLayout: false, Data: bindingData} } return t.Execute(w, bindingData) } ================================================ FILE: view/jet.go ================================================ package view import ( "fmt" "io" "io/fs" "os" "path/filepath" "reflect" "strings" "sync" "github.com/kataras/iris/v12/context" "github.com/CloudyKit/jet/v6" ) const jetEngineName = "jet" // JetEngine is the jet template parser's view engine. type JetEngine struct { fs fs.FS rootDir string extension string left, right string loader jet.Loader developmentMode bool // The Set is the `*jet.Set`, exported to offer any custom capabilities that jet users may want. // Available after `Load`. Set *jet.Set mu sync.Mutex // Note that global vars and functions are set in a single spot on the jet parser. // If AddFunc or AddVar called before `Load` then these will be set here to be used via `Load` and clear. vars map[string]any jetDataContextKey string } var ( _ Engine = (*JetEngine)(nil) _ EngineFuncer = (*JetEngine)(nil) ) // jet library does not export or give us any option to modify them via Set // (unless we parse the files by ourselves but this is not a smart choice). var jetExtensions = [...]string{ ".html.jet", ".jet.html", ".jet", } // Jet creates and returns a new jet view engine. // The given "extension" MUST begin with a dot. // // Usage: // Jet("./views", ".jet") or // Jet(iris.Dir("./views"), ".jet") or // Jet(embed.FS, ".jet") or Jet(AssetFile(), ".jet") for embedded data. func Jet(dirOrFS any, extension string) *JetEngine { extOK := false for _, ext := range jetExtensions { if ext == extension { extOK = true break } } if !extOK { panic(fmt.Sprintf("%s extension is not a valid jet engine extension[%s]", extension, strings.Join(jetExtensions[0:], ", "))) } s := &JetEngine{ fs: getFS(dirOrFS), rootDir: "/", extension: extension, loader: &jetLoader{fs: getFS(dirOrFS)}, jetDataContextKey: "_jet", } return s } // String returns the name of this view engine, the "jet". func (s *JetEngine) String() string { return jetEngineName } // RootDir sets the directory to be used as a starting point // to load templates from the provided file system. func (s *JetEngine) RootDir(root string) *JetEngine { if s.fs != nil && root != "" && root != "/" && root != "." && root != s.rootDir { sub, err := fs.Sub(s.fs, s.rootDir) if err != nil { panic(err) } s.fs = sub } s.rootDir = filepath.ToSlash(root) return s } // Name returns the jet engine's name. func (s *JetEngine) Name() string { return "Jet" } // Ext should return the final file extension which this view engine is responsible to render. // If the filename extension on ExecuteWriter is empty then this is appended. func (s *JetEngine) Ext() string { return s.extension } // Delims sets the action delimiters to the specified strings, to be used in // templates. An empty delimiter stands for the // corresponding default: {{ or }}. // Should act before `Load` or `iris.Application#RegisterView`. func (s *JetEngine) Delims(left, right string) *JetEngine { s.left = left s.right = right return s } // JetArguments is a type alias of `jet.Arguments`, // can be used on `AddFunc$funcBody`. type JetArguments = jet.Arguments // AddFunc should adds a global function to the jet template set. func (s *JetEngine) AddFunc(funcName string, funcBody any) { // if something like "urlpath" is registered. if generalFunc, ok := funcBody.(func(string, ...any) string); ok { // jet, unlike others does not accept a func(string, ...any) string, // instead it wants: // func(JetArguments) reflect.Value. s.AddVar(funcName, jet.Func(func(args JetArguments) reflect.Value { n := args.NumOfArguments() if n == 0 { // no input, don't execute the function, panic instead. panic(funcName + " expects one or more input arguments") } firstInput := args.Get(0).String() if n == 1 { // if only the first argument is given. return reflect.ValueOf(generalFunc(firstInput)) } // if has variadic. variadicN := n - 1 variadicInputs := make([]any, variadicN) // except the first one. for i := 0; i < variadicN; i++ { variadicInputs[i] = args.Get(i + 1).Interface() } return reflect.ValueOf(generalFunc(firstInput, variadicInputs...)) })) return } if jetFunc, ok := funcBody.(jet.Func); !ok { alternativeJetFunc, ok := funcBody.(func(JetArguments) reflect.Value) if !ok { panic(fmt.Sprintf("JetEngine.AddFunc: funcBody argument is not a type of func(JetArguments) reflect.Value. Got %T instead", funcBody)) } s.AddVar(funcName, jet.Func(alternativeJetFunc)) } else { s.AddVar(funcName, jetFunc) } } // AddVar adds a global variable to the jet template set. func (s *JetEngine) AddVar(key string, value any) { if s.Set != nil { s.Set.AddGlobal(key, value) } else { if s.vars == nil { s.vars = make(map[string]any) } s.vars[key] = value } } // Reload if setted to true the templates are reloading on each render, // use it when you're in development and you're boring of restarting // the whole app when you edit a template file. // // Note that if `true` is passed then only one `View -> ExecuteWriter` will be render each time, // not safe concurrent access across clients, use it only on development state. func (s *JetEngine) Reload(developmentMode bool) *JetEngine { s.developmentMode = developmentMode return s } // SetLoader can be used when the caller wants to use something like // multi.Loader or httpfs.Loader. func (s *JetEngine) SetLoader(loader jet.Loader) *JetEngine { s.loader = loader return s } type jetLoader struct { fs fs.FS } var _ jet.Loader = (*jetLoader)(nil) // Open opens a file from file system. func (l *jetLoader) Open(name string) (io.ReadCloser, error) { name = strings.TrimPrefix(name, "/") return l.fs.Open(name) } // Exists checks if the template name exists by walking the list of template paths. func (l *jetLoader) Exists(name string) bool { name = strings.TrimPrefix(name, "/") _, err := l.fs.Open(name) return err == nil } // Load should load the templates from a physical system directory or by an embedded one (assets/go-bindata). func (s *JetEngine) Load() error { rootDirName := getRootDirName(s.fs) return walk(s.fs, "", func(path string, info os.FileInfo, err error) error { if err != nil { return err } if info == nil || info.IsDir() { return nil } if s.extension != "" { if !strings.HasSuffix(path, s.extension) { return nil } } if s.rootDir == rootDirName { path = strings.TrimPrefix(path, rootDirName) path = strings.TrimPrefix(path, "/") } buf, err := asset(s.fs, path) if err != nil { return fmt.Errorf("%s: %w", path, err) } return s.ParseTemplate(path, string(buf)) }) } // ParseTemplate accepts a name and contnets to parse and cache a template. // This parser does not support funcs per template. Use the `AddFunc` instead. func (s *JetEngine) ParseTemplate(name string, contents string) error { s.initSet() _, err := s.Set.Parse(name, contents) return err } func (s *JetEngine) initSet() { s.mu.Lock() if s.Set == nil { var opts = []jet.Option{ jet.WithDelims(s.left, s.right), } if s.developmentMode && !context.IsNoOpFS(s.fs) { // this check is made to avoid jet's fs lookup on noOp fs (nil passed by the developer). // This can be produced when nil fs passed // and only `ParseTemplate` is used. opts = append(opts, jet.InDevelopmentMode()) } s.Set = jet.NewSet(s.loader, opts...) for key, value := range s.vars { s.Set.AddGlobal(key, value) } } s.mu.Unlock() } type ( // JetRuntimeVars is a type alias for `jet.VarMap`. // Can be used at `AddJetRuntimeVars/JetEngine.AddRuntimeVars` // to set a runtime variable ${name} to the executing template. JetRuntimeVars = jet.VarMap // JetRuntime is a type alias of `jet.Runtime`, // can be used on RuntimeVariable input function. JetRuntime = jet.Runtime ) // JetRuntimeVarsContextKey is the Iris Context key to keep any custom jet runtime variables. // See `AddJetRuntimeVars` package-level function and `JetEngine.AddRuntimeVars` method. const JetRuntimeVarsContextKey = "iris.jetvarmap" // AddJetRuntimeVars sets or inserts runtime jet variables through the Iris Context. // This gives the ability to add runtime variables from different handlers in the request chain, // something that the jet template parser does not offer at all. // // Usage: view.AddJetRuntimeVars(ctx, view.JetRuntimeVars{...}). // See `JetEngine.AddRuntimeVars` too. func AddJetRuntimeVars(ctx *context.Context, jetVarMap JetRuntimeVars) { if v := ctx.Values().Get(JetRuntimeVarsContextKey); v != nil { if vars, ok := v.(JetRuntimeVars); ok { for key, value := range jetVarMap { vars[key] = value } return } } ctx.Values().Set(JetRuntimeVarsContextKey, jetVarMap) } // AddRuntimeVars sets or inserts runtime jet variables through the Iris Context. // This gives the ability to add runtime variables from different handlers in the request chain, // something that the jet template parser does not offer at all. // // Usage: view.AddJetRuntimeVars(ctx, view.JetRuntimeVars{...}). // See `view.AddJetRuntimeVars` if package-level access is more meanful to the code flow. func (s *JetEngine) AddRuntimeVars(ctx *context.Context, vars JetRuntimeVars) { AddJetRuntimeVars(ctx, vars) } // ExecuteWriter should execute a template by its filename with an optional layout and bindingData. func (s *JetEngine) ExecuteWriter(w io.Writer, filename string, layout string, bindingData any) error { tmpl, err := s.Set.GetTemplate(filename) if err != nil { return err } var vars JetRuntimeVars if ctx, ok := w.(*context.Context); ok { runtimeVars := ctx.Values().Get(JetRuntimeVarsContextKey) if runtimeVars != nil { if jetVars, ok := runtimeVars.(JetRuntimeVars); ok { vars = jetVars } } if viewContextData := ctx.GetViewData(); len(viewContextData) > 0 { // fix #1876 if vars == nil { vars = make(JetRuntimeVars) } for k, v := range viewContextData { val, ok := v.(reflect.Value) if !ok { val = reflect.ValueOf(v) } vars[k] = val } } if v := ctx.Values().Get(s.jetDataContextKey); v != nil { if bindingData == nil { // if bindingData is nil, try to fill them by context key (a middleware can set data). bindingData = v } else if m, ok := bindingData.(context.Map); ok { // else if bindingData are passed to App/Context.View // and it's map try to fill with the new values passed from a middleware. if mv, ok := v.(context.Map); ok { for key, value := range mv { m[key] = value } } } } } if bindingData == nil { return tmpl.Execute(w, vars, nil) } if vars == nil { vars = make(JetRuntimeVars) } /* fixed on jet v4.0.0, so no need of this: if m, ok := bindingData.(context.Map); ok { var jetData any for k, v := range m { if k == s.jetDataContextKey { jetData = v continue } if value, ok := v.(reflect.Value); ok { vars[k] = value } else { vars[k] = reflect.ValueOf(v) } } if jetData != nil { bindingData = jetData } }*/ return tmpl.Execute(w, vars, bindingData) } ================================================ FILE: view/pug.go ================================================ package view import ( "bytes" "os" "github.com/Joker/jade" ) // Pug (or Jade) returns a new pug view engine. // It shares the same exactly logic with the // html view engine, it uses the same exactly configuration. // The given "extension" MUST begin with a dot. // // Read more about the Jade Go Parser: https://github.com/Joker/jade // // Usage: // Pug("./views", ".pug") or // Pug(iris.Dir("./views"), ".pug") or // Pug(embed.FS, ".pug") or Pug(AssetFile(), ".pug") for embedded data. // // Examples: // https://github.com/kataras/iris/tree/main/_examples/view/template_pug_0 // https://github.com/kataras/iris/tree/main/_examples/view/template_pug_1 // https://github.com/kataras/iris/tree/main/_examples/view/template_pug_2 // https://github.com/kataras/iris/tree/main/_examples/view/template_pug_3 func Pug(fs any, extension string) *HTMLEngine { s := HTML(fs, extension) s.name = "Pug" s.middleware = func(name string, text []byte) (contents string, err error) { jade.ReadFunc = func(filename string) ([]byte, error) { return asset(s.fs, filename) } tmpl := jade.New(name) exec, err := tmpl.Parse(text) if err != nil { return } b := new(bytes.Buffer) exec.WriteIn(b) jade.ReadFunc = os.ReadFile // reset to original. return b.String(), nil } return s } ================================================ FILE: view/view.go ================================================ package view import ( "fmt" "html/template" "io" "strings" "github.com/kataras/iris/v12/context" "github.com/kataras/golog" ) type ( // Engine is the interface for a compatible Iris view engine. // It's an alias of context.ViewEngine. Engine = context.ViewEngine // EngineFuncer is the interface for a compatible Iris view engine // which accepts builtin framework functions such as url, urlpath and tr. // It's an alias of context.ViewEngineFuncer. EngineFuncer = context.ViewEngineFuncer ) // ErrNotExist reports whether a template was not found in the parsed templates tree. type ErrNotExist = context.ErrViewNotExist // View is just a wrapper on top of the registered template engine. type View struct{ Engine } // Register registers a view engine. func (v *View) Register(e Engine) { if v.Engine != nil { golog.Warnf("Engine already exists, replacing the old %q with the new one %q", v.Engine.Name(), e.Name()) } v.Engine = e } // Registered reports whether an engine was registered. func (v *View) Registered() bool { return v.Engine != nil } func (v *View) ensureTemplateName(s string) string { if s == "" || s == NoLayout { return s } s = strings.TrimPrefix(s, "/") if ext := v.Engine.Ext(); ext != "" { if !strings.HasSuffix(s, ext) { return s + ext } } return s } // ExecuteWriter calls the correct view Engine's ExecuteWriter func func (v *View) ExecuteWriter(w io.Writer, filename string, layout string, bindingData any) error { filename = v.ensureTemplateName(filename) layout = v.ensureTemplateName(layout) return v.Engine.ExecuteWriter(w, filename, layout, bindingData) } // AddFunc adds a function to all registered engines. // Each template engine that supports functions has its own AddFunc too. func (v *View) AddFunc(funcName string, funcBody any) { if !v.Registered() { return } if e, ok := v.Engine.(EngineFuncer); ok { e.AddFunc(funcName, funcBody) } } // Funcs registers a template func map to the registered view engine(s). func (v *View) Funcs(m template.FuncMap) *View { if !v.Registered() { return v } if e, ok := v.Engine.(EngineFuncer); ok { for k, v := range m { e.AddFunc(k, v) } } return v } // Load compiles all the registered engines. func (v *View) Load() error { if !v.Registered() { return fmt.Errorf("no engine was registered") } return v.Engine.Load() } // NoLayout disables the configuration's layout for a specific execution. const NoLayout = "iris.nolayout" // returns empty if it's no layout or empty layout and empty configuration's layout. func getLayout(layout string, globalLayout string) string { if layout == NoLayout { return "" } if layout == "" && globalLayout != "" { return globalLayout } return layout } ================================================ FILE: websocket/aliases.go ================================================ package websocket import ( "github.com/kataras/neffos" "github.com/kataras/neffos/gobwas" "github.com/kataras/neffos/gorilla" "github.com/kataras/neffos/stackexchange/redis" ) type ( // Dialer is the definition type of a dialer, gorilla or gobwas or custom. // It is the second parameter of the `Dial` function. Dialer = neffos.Dialer // GorillaDialerOptions is just an alias for the `gobwas/ws.Dialer` struct type. GorillaDialerOptions = gorilla.Options // GobwasDialerOptions is just an alias for the `gorilla/websocket.Dialer` struct type. GobwasDialerOptions = gobwas.Options // GobwasHeader is an alias to the adapter that allows the use of `http.Header` as // handshake headers. GobwasHeader = gobwas.Header // Conn describes the main websocket connection's functionality. // Its `Connection` will return a new `NSConn` instance. // Each connection can connect to one or more declared namespaces. // Each `NSConn` can join to multiple rooms. Conn = neffos.Conn // NSConn describes a connection connected to a specific namespace, // it emits with the `Message.Namespace` filled and it can join to multiple rooms. // A single `Conn` can be connected to one or more namespaces, // each connected namespace is described by this structure. NSConn = neffos.NSConn // Room describes a connected connection to a room, // emits messages with the `Message.Room` filled to the specific room // and `Message.Namespace` to the underline `NSConn`'s namespace. Room = neffos.Room // CloseError can be used to send and close a remote connection in the event callback's return statement. CloseError = neffos.CloseError // MessageHandlerFunc is the definition type of the events' callback. // Its error can be written to the other side on specific events, // i.e on `OnNamespaceConnect` it will abort a remote namespace connection. // See examples for more. MessageHandlerFunc = neffos.MessageHandlerFunc // ConnHandler is the interface which namespaces and events can be retrieved through. ConnHandler = neffos.ConnHandler // Events completes the `ConnHandler` interface. // It is a map which its key is the event name // and its value the event's callback. // // Events type completes the `ConnHandler` itself therefore, // can be used as standalone value on the `New` and `Dial` functions // to register events on empty namespace as well. // // See `Namespaces`, `New` and `Dial` too. Events = neffos.Events // Namespaces completes the `ConnHandler` interface. // Can be used to register one or more namespaces on the `New` and `Dial` functions. // The key is the namespace literal and the value is the `Events`, // a map with event names and their callbacks. // // See `WithTimeout`, `New` and `Dial` too. Namespaces = neffos.Namespaces // WithTimeout completes the `ConnHandler` interface. // Can be used to register namespaces and events or just events on an empty namespace // with Read and Write timeouts. // // See `New` and `Dial`. WithTimeout = neffos.WithTimeout // Struct completes the `ConnHandler` interface. // It uses a structure to register a specific namespace and its events. Struct = neffos.Struct // StructInjector can be used to customize the value creation that can is used on serving events. StructInjector = neffos.StructInjector // The Message is the structure which describes the incoming and outcoming data. // Emitter's "body" argument is the `Message.Body` field. // Emitter's return non-nil error is the `Message.Err` field. // If native message sent then the `Message.Body` is filled with the body and // when incoming native message then the `Message.Event` is the `OnNativeMessage`, // native messages are allowed only when an empty namespace("") and its `OnNativeMessage` callback are present. Message = neffos.Message // StackExchange is an optional interface // that can be used to change the way neffos // sends messages to its clients, i.e // communication between multiple neffos servers. // // See `NewRedisStackExchange` to create a new redis StackExchange. StackExchange = neffos.StackExchange // RedisStackExchange is a `neffos.StackExchange` for redis. RedisStackExchange = redis.StackExchange // RedisConfig is used on the `NewRedisStackExchange` package-level function. // Can be used to customize the redis client dialer. RedisConfig = redis.Config ) ================================================ FILE: websocket/websocket.go ================================================ package websocket import ( "net/http" "github.com/kataras/iris/v12/context" "github.com/kataras/neffos" "github.com/kataras/neffos/gobwas" "github.com/kataras/neffos/gorilla" "github.com/kataras/neffos/stackexchange/nats" "github.com/kataras/neffos/stackexchange/redis" ) var ( // EnableDebug enables debug mode for websocket module, // for MVC this is done automatically // when the app's logger level is set to "debug". EnableDebug = neffos.EnableDebug // GorillaUpgrader is an upgrader type for the gorilla/websocket subprotocol implementation. // Should be used on `New` to construct the websocket server. GorillaUpgrader = gorilla.Upgrader // GobwasUpgrader is an upgrader type for the gobwas/ws subprotocol implementation. // Should be used on `New` to construct the websocket server. GobwasUpgrader = gobwas.Upgrader // DefaultGorillaUpgrader is a gorilla/websocket Upgrader with all fields set to the default values. DefaultGorillaUpgrader = gorilla.DefaultUpgrader // DefaultGobwasUpgrader is a gobwas/ws Upgrader with all fields set to the default values. DefaultGobwasUpgrader = gobwas.DefaultUpgrader // New constructs and returns a new websocket server. // Listens to incoming connections automatically, no further action is required from the caller. // The second parameter is the "connHandler", it can be // filled as `Namespaces`, `Events` or `WithTimeout`, same namespaces and events can be used on the client-side as well, // Use the `Conn#IsClient` on any event callback to determinate if it's a client-side connection or a server-side one. // // See examples for more. New = neffos.New // DefaultIDGenerator returns a universal unique identifier for a new connection. // It's the default `IDGenerator` if missing. DefaultIDGenerator = func(ctx *context.Context) string { return neffos.DefaultIDGenerator(ctx.ResponseWriter(), ctx.Request()) } // NewRedisStackExchange returns a new redis StackExchange. // The "channel" input argument is the channel prefix for publish and subscribe. NewRedisStackExchange = redis.NewStackExchange // NewNatsStackExchange returns a new nats StackExchange. // The "url" input argument is the connection string of your nats server. // The second variadic input argument can be used to use custom `nats.Option`s // such as authentication and more nats servers addresses. NewNatsStackExchange = nats.NewStackExchange // WithNatsOptions can be used as the second input argument of `NewNatsStackExchange` // to declare a struct-based configuration for the nats server(s). WithNatsOptions = nats.With // GorillaDialer is a `Dialer` type for the gorilla/websocket subprotocol implementation. // Should be used on `Dial` to create a new client/client-side connection. GorillaDialer = gorilla.Dialer // GobwasDialer is a `Dialer` type for the gobwas/ws subprotocol implementation. // Should be used on `Dial` to create a new client/client-side connection. GobwasDialer = gobwas.Dialer // DefaultGorillaDialer is a gorilla/websocket dialer with all fields set to the default values. DefaultGorillaDialer = gorilla.DefaultDialer // DefaultGobwasDialer is a gobwas/ws dialer with all fields set to the default values. DefaultGobwasDialer = gobwas.DefaultDialer // Dial establishes a new websocket client connection. // Context "ctx" is used for handshake timeout. // Dialer "dial" can be either `GorillaDialer` or `GobwasDialer`, // custom dialers can be used as well when complete the `Socket` and `Dialer` interfaces for valid client. // URL "url" is the endpoint of the websocket server, i.e "ws://localhost:8080/echo". // The last parameter, and the most important one is the "connHandler", it can be // filled as `Namespaces`, `Events` or `WithTimeout`, same namespaces and events can be used on the server-side as well. // // See examples for more. Dial = neffos.Dial // IsTryingToReconnect reports whether the returning "err" from the `Server#Upgrade` // is from a client that was trying to reconnect to the websocket server. // // Look the `Conn#WasReconnected` and `Conn#ReconnectTries` too. IsTryingToReconnect = neffos.IsTryingToReconnect // NewStruct returns the `Struct` Conn Handler based on ptr value. NewStruct = neffos.NewStruct // JoinConnHandlers combines two or more ConnHandlers as one. JoinConnHandlers = neffos.JoinConnHandlers // OnNamespaceConnect is the event name which its callback is fired right before namespace connect, // if non-nil error then the remote connection's `Conn.Connect` will fail and send that error text. // Connection is not ready to emit data to the namespace. OnNamespaceConnect = neffos.OnNamespaceConnect // OnNamespaceConnected is the event name which its callback is fired after namespace successfully connected. // Connection is ready to emit data back to the namespace. OnNamespaceConnected = neffos.OnNamespaceConnected // OnNamespaceDisconnect is the event name which its callback is fired when // remote namespace disconnection or local namespace disconnection is happening. // For server-side connections the reply matters, so if error returned then the client-side cannot disconnect yet, // for client-side the return value does not matter. OnNamespaceDisconnect = neffos.OnNamespaceDisconnect // if allowed to connect then it's allowed to disconnect as well. // OnRoomJoin is the event name which its callback is fired right before room join. OnRoomJoin = neffos.OnRoomJoin // able to check if allowed to join. // OnRoomJoined is the event name which its callback is fired after the connection has successfully joined to a room. OnRoomJoined = neffos.OnRoomJoined // able to broadcast messages to room. // OnRoomLeave is the event name which its callback is fired right before room leave. OnRoomLeave = neffos.OnRoomLeave // able to broadcast bye-bye messages to room. // OnRoomLeft is the event name which its callback is fired after the connection has successfully left from a room. OnRoomLeft = neffos.OnRoomLeft // if allowed to join to a room, then its allowed to leave from it. // OnAnyEvent is the event name which its callback is fired when incoming message's event is not declared to the ConnHandler(`Events` or `Namespaces`). OnAnyEvent = neffos.OnAnyEvent // when event no match. // OnNativeMessage is fired on incoming native/raw websocket messages. // If this event defined then an incoming message can pass the check (it's an invalid message format) // with just the Message's Body filled, the Event is "OnNativeMessage" and IsNative always true. // This event should be defined under an empty namespace in order this to work. OnNativeMessage = neffos.OnNativeMessage // IsSystemEvent reports whether the "event" is a system event, // OnNamespaceConnect, OnNamespaceConnected, OnNamespaceDisconnect, // OnRoomJoin, OnRoomJoined, OnRoomLeave and OnRoomLeft. IsSystemEvent = neffos.IsSystemEvent // Reply is a special type of custom error which sends a message back to the other side // with the exact same incoming Message's Namespace (and Room if specified) // except its body which would be the given "body". Reply = neffos.Reply // Marshal marshals the "v" value and returns a Message's Body. // The "v" value's serialized value can be customized by implementing a `Marshal() ([]byte, error) ` method, // otherwise the default one will be used instead ( see `SetDefaultMarshaler` and `SetDefaultUnmarshaler`). // Errors are pushed to the result, use the object's Marshal method to catch those when necessary. Marshal = neffos.Marshal ) // SetDefaultMarshaler changes the default json marshaler. // See `Marshal` package-level function and `Message.Unmarshal` method for more. func SetDefaultMarshaler(fn func(v any) ([]byte, error)) { neffos.DefaultMarshaler = fn } // SetDefaultUnmarshaler changes the default json unmarshaler. // See `Message.Unmarshal` method and package-level `Marshal` function for more. func SetDefaultUnmarshaler(fn func(data []byte, v any) error) { neffos.DefaultUnmarshaler = fn } // IDGenerator is an iris-specific IDGenerator for new connections. type IDGenerator func(*context.Context) string func wrapIDGenerator(idGen IDGenerator) func(ctx *context.Context) neffos.IDGenerator { return func(ctx *context.Context) neffos.IDGenerator { return func(w http.ResponseWriter, r *http.Request) string { return idGen(ctx) } } } // Handler returns an Iris handler to be served in a route of an Iris application. // Accepts the neffos websocket server as its first input argument // and optionally an Iris-specific `IDGenerator` as its second one. // // This SHOULD be the last handler in the route's chain as it hijacks the connection and the context. func Handler(s *neffos.Server, idGenerator ...IDGenerator) context.Handler { idGen := DefaultIDGenerator if len(idGenerator) > 0 { idGen = idGenerator[0] } if cb := s.OnDisconnect; cb != nil { s.OnDisconnect = func(c *neffos.Conn) { cb(c) manualReleaseWithoutResp(GetContext(c)) } } else { s.OnDisconnect = func(c *neffos.Conn) { manualReleaseWithoutResp(GetContext(c)) } } return func(ctx *context.Context) { if ctx.IsStopped() { // let the framework release it as always; // socket was not created so disconnect event will not called and the // DisablePoolRelease was not even called yet. return } Upgrade(ctx, idGen, s) } } // Upgrade upgrades the request and returns a new websocket Conn. // Use `Handler` for higher-level implementation instead. func Upgrade(ctx *context.Context, idGen IDGenerator, s *neffos.Server) *neffos.Conn { /* Do NOT return the error as it is captured on the OnUpgradeError listener, the end-developer should not be able to write to this http client directly. */ ctx.DisablePoolRelease() conn, upgradeErr := s.Upgrade(ctx.ResponseWriter(), ctx.Request(), func(socket neffos.Socket) neffos.Socket { return &socketWrapper{ Socket: socket, ctx: ctx, } }, wrapIDGenerator(idGen)(ctx)) if upgradeErr != nil { manualReleaseWithoutResp(ctx) } return conn } func manualReleaseWithoutResp(ctx *context.Context) { ctx.ResponseWriter().EndResponse() // relases the response writer (common, recorder & compress). ctx.Application().GetContextPool().ReleaseLight(ctx) // just releases the context. } type socketWrapper struct { neffos.Socket ctx *context.Context } // GetContext returns the Iris Context from a websocket connection. // // Note that writing to the client connection through this Context is not allowed // from a websocket event because the connection is hijacked. // If used, you are limited for read-only access of the request e.g. read the request headers. func GetContext(c *neffos.Conn) *context.Context { if sw, ok := c.Socket().(*socketWrapper); ok { return sw.ctx } return nil } ================================================ FILE: x/client/client.go ================================================ package client import ( "bytes" "context" "encoding/json" "errors" "fmt" "io" "mime/multipart" "net/http" "net/url" "os" "strconv" "strings" "golang.org/x/time/rate" ) // A Client is an HTTP client. Initialize with the New package-level function. type Client struct { opts []Option // keep for clones. HTTPClient *http.Client // BaseURL prepends to all requests. BaseURL string // A list of persistent request options. PersistentRequestOptions []RequestOption // Optional rate limiter instance initialized by the RateLimit method. rateLimiter *rate.Limiter // Optional handlers that are being fired before and after each new request. requestHandlers []RequestHandler // store it here for future use. keepAlive bool } // New returns a new Iris HTTP Client. // Available options: // - BaseURL // - Timeout // - PersistentRequestOptions // - RateLimit // // Look the Client.Do/JSON/... methods to send requests and // ReadXXX methods to read responses. // // The default content type to send and receive data is JSON. func New(opts ...Option) *Client { c := &Client{ opts: opts, HTTPClient: &http.Client{}, PersistentRequestOptions: defaultRequestOptions, requestHandlers: defaultRequestHandlers, } for _, opt := range c.opts { // c.opts in order to make with `NoOption` work. opt(c) } if transport, ok := c.HTTPClient.Transport.(*http.Transport); ok { c.keepAlive = !transport.DisableKeepAlives } return c } // NoOption is a helper function that clears the previous options in the chain. // See `Client.Clone` method. var NoOption = func(c *Client) { c.opts = make([]Option, 0) /* clear previous options */ } // Clone returns a new Client with the same options as the original. // If you want to override the options from the base "c" Client, // use the `NoOption` variable as the 1st argument. func (c *Client) Clone(opts ...Option) *Client { return New(append(c.opts, opts...)...) } // RegisterRequestHandler registers one or more request handlers // to be ran before and after of each new request. // // Request handler's BeginRequest method run after each request constructed // and right before sent to the server. // // Request handler's EndRequest method run after response each received // and right before methods return back to the caller. // // Any request handlers MUST be set right after the Client's initialization. func (c *Client) RegisterRequestHandler(reqHandlers ...RequestHandler) { reqHandlersToRegister := make([]RequestHandler, 0, len(reqHandlers)) for _, h := range reqHandlers { if h == nil { continue } reqHandlersToRegister = append(reqHandlersToRegister, h) } c.requestHandlers = append(c.requestHandlers, reqHandlersToRegister...) } func (c *Client) emitBeginRequest(ctx context.Context, req *http.Request) error { if len(c.requestHandlers) == 0 { return nil } for _, h := range c.requestHandlers { if hErr := h.BeginRequest(ctx, req); hErr != nil { return hErr } } return nil } func (c *Client) emitEndRequest(ctx context.Context, resp *http.Response, err error) error { if len(c.requestHandlers) == 0 { return nil } for _, h := range c.requestHandlers { if hErr := h.EndRequest(ctx, resp, err); hErr != nil { return hErr } } return err } // RequestOption declares the type of option one can pass // to the Do methods(JSON, Form, ReadJSON...). // Request options run before request constructed. type RequestOption = func(*http.Request) error // We always add the following request headers, unless they're removed by custom ones. var defaultRequestOptions = []RequestOption{ RequestHeader(false, acceptKey, contentTypeJSON), } // RequestHeader adds or sets (if overridePrev is true) a header to the request. func RequestHeader(overridePrev bool, key string, values ...string) RequestOption { key = http.CanonicalHeaderKey(key) return func(req *http.Request) error { if overridePrev { // upsert. req.Header[key] = values } else { // just insert. req.Header[key] = append(req.Header[key], values...) } return nil } } // RequestAuthorization sets an Authorization request header. // Note that we could do the same with a Transport RoundDrip too. func RequestAuthorization(value string) RequestOption { return RequestHeader(true, "Authorization", value) } // RequestAuthorizationBearer sets an Authorization: Bearer $token request header. func RequestAuthorizationBearer(accessToken string) RequestOption { headerValue := "Bearer " + accessToken return RequestAuthorization(headerValue) } // RequestQuery adds a set of URL query parameters to the request. func RequestQuery(query url.Values) RequestOption { return func(req *http.Request) error { q := req.URL.Query() for k, v := range query { q[k] = v } req.URL.RawQuery = q.Encode() return nil } } // RequestParam sets a single URL query parameter to the request. func RequestParam(key string, values ...string) RequestOption { return RequestQuery(url.Values{ key: values, }) } // Do sends an HTTP request and returns an HTTP response. // // The payload can be: // - io.Reader // - raw []byte // - JSON raw message // - string // - struct (JSON). // // If method is empty then it defaults to "GET". // The final variadic, optional input argument sets // the custom request options to use before the request. // // Any HTTP returned error will be of type APIError // or a timeout error if the given context was canceled. func (c *Client) Do(ctx context.Context, method, urlpath string, payload any, opts ...RequestOption) (*http.Response, error) { if ctx == nil { ctx = context.Background() } if c.rateLimiter != nil { if err := c.rateLimiter.Wait(ctx); err != nil { return nil, err } } // Method defaults to GET. if method == "" { method = http.MethodGet } // Find the payload, if any. var body io.Reader if payload != nil { switch v := payload.(type) { case io.Reader: body = v case []byte: body = bytes.NewBuffer(v) case json.RawMessage: body = bytes.NewBuffer(v) case string: body = strings.NewReader(v) case url.Values: body = strings.NewReader(v.Encode()) default: w := new(bytes.Buffer) // We assume it's a struct, we wont make use of reflection to find out though. err := json.NewEncoder(w).Encode(v) if err != nil { return nil, err } body = w } } if c.BaseURL != "" { urlpath = c.BaseURL + urlpath // note that we don't do any special checks here, the caller is responsible. } // Initialize the request. req, err := http.NewRequestWithContext(ctx, method, urlpath, body) if err != nil { return nil, err } // We separate the error for the default options for now. for i, opt := range c.PersistentRequestOptions { if opt == nil { continue } if err = opt(req); err != nil { return nil, fmt.Errorf("client.Do: default request option[%d]: %w", i, err) } } // Apply any custom request options (e.g. content type, accept headers, query...) for _, opt := range opts { if opt == nil { continue } if err = opt(req); err != nil { return nil, err } } if err = c.emitBeginRequest(ctx, req); err != nil { return nil, err } // Caller is responsible for closing the response body. // Also note that the gzip compression is handled automatically nowadays. resp, respErr := c.HTTPClient.Do(req) if err = c.emitEndRequest(ctx, resp, respErr); err != nil { return nil, err } return resp, respErr } // DrainResponseBody drains response body and close it, allowing the transport to reuse TCP connections. // It's automatically called on Client.ReadXXX methods on the end. func (c *Client) DrainResponseBody(resp *http.Response) { _, _ = io.Copy(io.Discard, resp.Body) resp.Body.Close() } const ( acceptKey = "Accept" contentTypeKey = "Content-Type" contentLengthKey = "Content-Length" contentTypePlainText = "plain/text" contentTypeJSON = "application/json" contentTypeFormURLEncoded = "application/x-www-form-urlencoded" ) // JSON writes data as JSON to the server. func (c *Client) JSON(ctx context.Context, method, urlpath string, payload any, opts ...RequestOption) (*http.Response, error) { opts = append(opts, RequestHeader(true, contentTypeKey, contentTypeJSON)) return c.Do(ctx, method, urlpath, payload, opts...) } // JSON writes form data to the server. func (c *Client) Form(ctx context.Context, method, urlpath string, formValues url.Values, opts ...RequestOption) (*http.Response, error) { payload := formValues.Encode() opts = append(opts, RequestHeader(true, contentTypeKey, contentTypeFormURLEncoded), RequestHeader(true, contentLengthKey, strconv.Itoa(len(payload))), ) return c.Do(ctx, method, urlpath, payload, opts...) } // Uploader holds the necessary information for upload requests. // // Look the Client.NewUploader method. type Uploader struct { client *Client body *bytes.Buffer Writer *multipart.Writer } // AddFileSource adds a form field to the uploader with the given key. func (u *Uploader) AddField(key, value string) error { f, err := u.Writer.CreateFormField(key) if err != nil { return err } _, err = io.Copy(f, strings.NewReader(value)) return err } // AddFileSource adds a form file to the uploader with the given key. func (u *Uploader) AddFileSource(key, filename string, source io.Reader) error { f, err := u.Writer.CreateFormFile(key, filename) if err != nil { return err } _, err = io.Copy(f, source) return err } // AddFile adds a local form file to the uploader with the given key. func (u *Uploader) AddFile(key, filename string) error { source, err := os.Open(filename) if err != nil { return err } defer source.Close() return u.AddFileSource(key, filename, source) } // Uploads sends local data to the server. func (u *Uploader) Upload(ctx context.Context, method, urlpath string, opts ...RequestOption) (*http.Response, error) { err := u.Writer.Close() if err != nil { return nil, err } payload := bytes.NewReader(u.body.Bytes()) opts = append(opts, RequestHeader(true, contentTypeKey, u.Writer.FormDataContentType())) return u.client.Do(ctx, method, urlpath, payload, opts...) } // NewUploader returns a structure which is responsible for sending // file and form data to the server. func (c *Client) NewUploader() *Uploader { body := new(bytes.Buffer) writer := multipart.NewWriter(body) return &Uploader{ client: c, body: body, Writer: writer, } } // ReadJSON binds "dest" to the response's body. // After this call, the response body reader is closed. func (c *Client) ReadJSON(ctx context.Context, dest any, method, urlpath string, payload any, opts ...RequestOption) error { if payload != nil { opts = append(opts, RequestHeader(true, contentTypeKey, contentTypeJSON)) } resp, err := c.Do(ctx, method, urlpath, payload, opts...) if err != nil { return err } defer c.DrainResponseBody(resp) if resp.StatusCode >= http.StatusBadRequest { return ExtractError(resp) } // DBUG // b, _ := io.ReadAll(resp.Body) // println(string(b)) // return json.Unmarshal(b, &dest) if dest != nil { return json.NewDecoder(resp.Body).Decode(&dest) } return json.NewDecoder(resp.Body).Decode(&dest) } // ReadPlain like ReadJSON but it accepts a pointer to a string or byte slice or integer // and it reads the body as plain text. func (c *Client) ReadPlain(ctx context.Context, dest any, method, urlpath string, payload any, opts ...RequestOption) error { resp, err := c.Do(ctx, method, urlpath, payload, opts...) if err != nil { return err } defer c.DrainResponseBody(resp) if resp.StatusCode >= http.StatusBadRequest { return ExtractError(resp) } body, err := io.ReadAll(resp.Body) if err != nil { return err } switch ptr := dest.(type) { case *[]byte: *ptr = body return nil case *string: *ptr = string(body) return nil case *int: *ptr, err = strconv.Atoi(string(body)) return err default: return fmt.Errorf("unsupported response body type: %T", ptr) } } // GetPlainUnquote reads the response body as raw text and tries to unquote it, // useful when the remote server sends a single key as a value but due to backend mistake // it sends it as JSON (quoted) instead of plain text. func (c *Client) GetPlainUnquote(ctx context.Context, method, urlpath string, payload any, opts ...RequestOption) (string, error) { var bodyStr string if err := c.ReadPlain(ctx, &bodyStr, method, urlpath, payload, opts...); err != nil { return "", err } s, err := strconv.Unquote(bodyStr) if err == nil { bodyStr = s } return bodyStr, nil } // WriteTo reads the response and then copies its data to the "dest" writer. // If the "dest" is a type of HTTP response writer then it writes the // content-type and content-length of the original request. // // Returns the amount of bytes written to "dest". func (c *Client) WriteTo(ctx context.Context, dest io.Writer, method, urlpath string, payload any, opts ...RequestOption) (int64, error) { if payload != nil { opts = append(opts, RequestHeader(true, contentTypeKey, contentTypeJSON)) } resp, err := c.Do(ctx, method, urlpath, payload, opts...) if err != nil { return 0, err } defer resp.Body.Close() if w, ok := dest.(http.ResponseWriter); ok { // Copy the content type and content-length. w.Header().Set("Content-Type", resp.Header.Get("Content-Type")) if resp.ContentLength > 0 { w.Header().Set("Content-Length", strconv.FormatInt(resp.ContentLength, 10)) } } return io.Copy(dest, resp.Body) } // BindResponse consumes the response's body and binds the result to the "dest" pointer, // closing the response's body is up to the caller. // // The "dest" will be binded based on the response's content type header. // Note that this is strict in order to catch bad actioners fast, // e.g. it wont try to read plain text if not specified on // the response headers and the dest is a *string. func BindResponse(resp *http.Response, dest any) (err error) { contentType := trimHeader(resp.Header.Get(contentTypeKey)) switch contentType { case contentTypeJSON: // the most common scenario on successful responses. return json.NewDecoder(resp.Body).Decode(&dest) case contentTypePlainText: b, err := io.ReadAll(resp.Body) if err != nil { return err } switch v := dest.(type) { case *string: *v = string(b) case *[]byte: *v = b default: return fmt.Errorf("plain text response should accept a *string or a *[]byte") } default: acceptContentType := trimHeader(resp.Request.Header.Get(acceptKey)) msg := "" if acceptContentType == contentType { // Here we make a special case, if the content type // was explicitly set by the request but we cannot handle it. msg = fmt.Sprintf("current implementation can not handle the received (and accepted) mime type: %s", contentType) } else { msg = fmt.Sprintf("unexpected mime type received: %s", contentType) } err = errors.New(msg) } return } func trimHeader(v string) string { for i, char := range v { if char == ' ' || char == ';' { return v[:i] } } return v } ================================================ FILE: x/client/client_test.go ================================================ package client import ( "context" "encoding/json" "net/http" "net/http/httptest" "reflect" "testing" ) var defaultCtx = context.Background() type testValue struct { Firstname string `json:"firstname"` } func TestClientJSON(t *testing.T) { expectedJSON := testValue{Firstname: "Makis"} app := http.NewServeMux() app.HandleFunc("/send", sendJSON(t, expectedJSON)) var irisGotJSON testValue app.HandleFunc("/read", readJSON(t, &irisGotJSON, &expectedJSON)) srv := httptest.NewServer(app) client := New(BaseURL(srv.URL)) // Test ReadJSON (read from server). var got testValue if err := client.ReadJSON(defaultCtx, &got, http.MethodGet, "/send", nil); err != nil { t.Fatal(err) } // Test JSON (send to server). resp, err := client.JSON(defaultCtx, http.MethodPost, "/read", expectedJSON) if err != nil { t.Fatal(err) } client.DrainResponseBody(resp) } func sendJSON(t *testing.T, v any) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") if err := json.NewEncoder(w).Encode(v); err != nil { t.Fatal(err) } } } func readJSON(t *testing.T, ptr any, expected any) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if err := json.NewDecoder(r.Body).Decode(ptr); err != nil { t.Fatal(err) } if !reflect.DeepEqual(ptr, expected) { t.Fatalf("expected to read json: %#+v but got: %#+v", ptr, expected) } } } ================================================ FILE: x/client/error.go ================================================ package client import ( "encoding/json" "io" "net/http" "strings" ) // APIError errors that may return from the Client. type APIError struct { Response *http.Response Body json.RawMessage // may be any []byte, response body is closed at this point. } // Error implements the standard error type. func (e APIError) Error() string { var b strings.Builder if e.Response != nil { b.WriteString(e.Response.Request.URL.String()) b.WriteByte(':') b.WriteByte(' ') b.WriteString(http.StatusText(e.Response.StatusCode)) b.WriteByte(' ') b.WriteByte('(') b.WriteString(e.Response.Status) b.WriteByte(')') if len(e.Body) > 0 { b.WriteByte(':') b.WriteByte(' ') b.Write(e.Body) } } return b.String() } // ExtractError returns the response wrapped inside an APIError. func ExtractError(resp *http.Response) APIError { body, _ := io.ReadAll(resp.Body) return APIError{ Response: resp, Body: body, } } // GetError reports whether the given "err" is an APIError. func GetError(err error) (APIError, bool) { if err == nil { return APIError{}, false } apiErr, ok := err.(APIError) if !ok { return APIError{}, false } return apiErr, true } // DecodeError binds a json error to the "destPtr". func DecodeError(err error, destPtr any) error { apiErr, ok := GetError(err) if !ok { return err } return json.Unmarshal(apiErr.Body, destPtr) } // GetErrorCode reads an error, which should be a type of APIError, // and returns its status code. // If the given "err" is nil or is not an APIError it returns 200, // acting as we have no error. func GetErrorCode(err error) int { apiErr, ok := GetError(err) if !ok { return http.StatusOK } return apiErr.Response.StatusCode } ================================================ FILE: x/client/handler_transport.go ================================================ package client import ( "bytes" "fmt" "io" "net/http" "net/http/httptest" ) // See "Handler" client option. type handlerTransport struct { handler http.Handler } // RoundTrip completes the http.RoundTripper interface. // It can be used to test calls to a server's handler. func (t *handlerTransport) RoundTrip(req *http.Request) (*http.Response, error) { reqCopy := *req if reqCopy.Proto == "" { reqCopy.Proto = fmt.Sprintf("HTTP/%d.%d", reqCopy.ProtoMajor, reqCopy.ProtoMinor) } if reqCopy.Body != nil { if reqCopy.ContentLength == -1 { reqCopy.TransferEncoding = []string{"chunked"} } } else { reqCopy.Body = io.NopCloser(bytes.NewReader(nil)) } if reqCopy.RequestURI == "" { reqCopy.RequestURI = reqCopy.URL.RequestURI() } recorder := httptest.NewRecorder() t.handler.ServeHTTP(recorder, &reqCopy) resp := http.Response{ Request: &reqCopy, StatusCode: recorder.Code, Status: http.StatusText(recorder.Code), Header: recorder.Result().Header, } if recorder.Flushed { resp.TransferEncoding = []string{"chunked"} } if recorder.Body != nil { resp.Body = io.NopCloser(recorder.Body) } return &resp, nil } ================================================ FILE: x/client/option.go ================================================ package client import ( "context" "net/http" "net/http/httputil" "time" "github.com/kataras/golog" "golang.org/x/time/rate" ) // All the builtin client options should live here, for easy discovery. type Option = func(*Client) // BaseURL registers the base URL of this client. // All of its methods will prepend this url. func BaseURL(uri string) Option { return func(c *Client) { c.BaseURL = uri } } // Timeout specifies a time limit for requests made by this // Client. The timeout includes connection time, any // redirects, and reading the response body. // A Timeout of zero means no timeout. // // Defaults to 15 seconds. func Timeout(d time.Duration) Option { return func(c *Client) { c.HTTPClient.Timeout = d } } // Handler specifies an iris.Application or any http.Handler // instance which can be tested using this Client. // // It registers a custom HTTP client transport // which allows "fake calls" to the "h" server. Use it for testing. func Handler(h http.Handler) Option { return func(c *Client) { c.HTTPClient.Transport = new(handlerTransport) } } // PersistentRequestOptions adds one or more persistent request options // that all requests made by this Client will respect. func PersistentRequestOptions(reqOpts ...RequestOption) Option { return func(c *Client) { c.PersistentRequestOptions = append(c.PersistentRequestOptions, reqOpts...) } } // RateLimit configures the rate limit for requests. // // Defaults to zero which disables rate limiting. func RateLimit(requestsPerSecond int) Option { return func(c *Client) { c.rateLimiter = rate.NewLimiter(rate.Limit(requestsPerSecond), requestsPerSecond) } } // Debug enables the client's debug logger. // It fires right before request is created // and right after a response from the server is received. // // Example Output for request: // // [DBUG] 2022/03/01 21:54 Iris HTTP Client: POST / HTTP/1.1 // Host: 127.0.0.1:50948 // User-Agent: Go-http-client/1.1 // Content-Length: 22 // Accept: application/json // Content-Type: application/json // Accept-Encoding: gzip // // {"firstname":"Makis"} // // Example Output for response: // // [DBUG] 2022/03/01 21:54 Iris HTTP Client: HTTP/1.1 200 OK // Content-Length: 27 // Content-Type: application/json; charset=utf-8 // Date: Tue, 01 Mar 2022 19:54:03 GMT // // { // "firstname": "Makis" // } func Debug(c *Client) { handler := &debugRequestHandler{ logger: golog.Child("Iris HTTP Client: ").SetLevel("debug"), } c.requestHandlers = append(c.requestHandlers, handler) } type debugRequestHandler struct { logger *golog.Logger } func (h *debugRequestHandler) BeginRequest(ctx context.Context, req *http.Request) error { dump, err := httputil.DumpRequestOut(req, true) if err != nil { return err } h.logger.Debug(string(dump)) return nil } func (h *debugRequestHandler) EndRequest(ctx context.Context, resp *http.Response, err error) error { if err != nil { h.logger.Debugf("%s: %s: ERR: %s", resp.Request.Method, resp.Request.URL.String(), err.Error()) } else { dump, err := httputil.DumpResponse(resp, true) if err != nil { return err } h.logger.Debug(string(dump)) } return err } ================================================ FILE: x/client/request_handler.go ================================================ package client import ( "context" "net/http" "sync" ) // RequestHandler can be set to each Client instance and it should be // responsible to handle the begin and end states of each request. // Its BeginRequest fires right before the client talks to the server // and its EndRequest fires right after the client receives a response from the server. // If one of them return a non-nil error then the execution of client will stop and return that error. type RequestHandler interface { BeginRequest(context.Context, *http.Request) error EndRequest(context.Context, *http.Response, error) error } var ( defaultRequestHandlers []RequestHandler mu sync.Mutex ) // RegisterRequestHandler registers one or more request handlers // to be ran before and after of each request on all newly created Iris HTTP Clients. // Useful for Iris HTTP Client 3rd-party libraries // e.g. on init register a custom request-response lifecycle logging. func RegisterRequestHandler(reqHandlers ...RequestHandler) { mu.Lock() defaultRequestHandlers = append(defaultRequestHandlers, reqHandlers...) mu.Unlock() } ================================================ FILE: x/errors/aliases.go ================================================ package errors import ( "errors" "fmt" ) var ( // Is is an alias of the standard errors.Is function. Is = errors.Is // As is an alias of the standard errors.As function. As = errors.As // New is an alias of the standard errors.New function. New = errors.New // Unwrap is an alias of the standard errors.Unwrap function. Unwrap = errors.Unwrap // Join is an alias of the standard errors.Join function. Join = errors.Join ) func sprintf(format string, args ...any) string { if len(args) > 0 { return fmt.Sprintf(format, args...) } return format } ================================================ FILE: x/errors/context_error_handler.go ================================================ package errors import "github.com/kataras/iris/v12/context" // DefaultContextErrorHandler returns a context error handler // which calls the HandleError on any incoming error when // a rich rest response failed to be written to the client. // Register it on Application.SetContextErrorHandler method. var DefaultContextErrorHandler context.ErrorHandler = new(jsonErrorHandler) type jsonErrorHandler struct{} // HandleContextError completes the context.ErrorHandler interface. It's fired on // rich rest response failures. func (e *jsonErrorHandler) HandleContextError(ctx *context.Context, err error) { HandleError(ctx, err) } ================================================ FILE: x/errors/errors.go ================================================ package errors import ( "encoding/json" "errors" "fmt" "io" "log/slog" "net/http" "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/x/client" ) // LogErrorFunc is an alias of a function type which accepts the Iris request context and an error // and it's fired whenever an error should be logged. // // See "OnErrorLog" variable to change the way an error is logged, // by default the error is logged using the Application's Logger's Error method. type LogErrorFunc = func(ctx *context.Context, err error) // LogError can be modified to customize the way an error is logged to the server (most common: internal server errors, database errors et.c.). // Can be used to customize the error logging, e.g. using Sentry (cloud-based error console). var LogError LogErrorFunc = func(ctx *context.Context, err error) { if ctx == nil { slog.Error(err.Error()) return } ctx.Application().Logger().Error(err) } // SkipCanceled is a package-level setting which by default // skips the logging of a canceled response or operation. // See the "Context.IsCanceled()" method and "iris.IsCanceled()" function // that decide if the error is caused by a canceled operation. // // Change of this setting MUST be done on initialization of the program. var SkipCanceled = true type ( // ErrorCodeName is a custom string type represents canonical error names. // // It contains functionality for safe and easy error populating. // See its "Message", "Details", "Data" and "Log" methods. ErrorCodeName string // ErrorCode represents the JSON form ErrorCode of the Error. ErrorCode struct { CanonicalName ErrorCodeName `json:"canonical_name" yaml:"CanonicalName"` Status int `json:"status" yaml:"Status"` } ) // A read-only map of valid http error codes. var errorCodeMap = make(map[ErrorCodeName]ErrorCode) // Deprecated: Use Register instead. var E = Register // Register registers a custom HTTP Error and returns its canonical name for future use. // The method "New" is reserved and was kept as it is for compatibility // with the standard errors package, therefore the "Register" name was chosen instead. // The key stroke "e" is near and accessible while typing the "errors" word // so developers may find it easy to use. // // See "RegisterErrorCode" and "RegisterErrorCodeMap" for alternatives. // // Example: // // var ( // NotFound = errors.Register("NOT_FOUND", http.StatusNotFound) // ) // ... // NotFound.Details(ctx, "resource not found", "user with id: %q was not found", userID) // // This method MUST be called on initialization, before HTTP server starts as // the internal map is not protected by mutex. func Register(httpErrorCanonicalName string, httpStatusCode int) ErrorCodeName { canonicalName := ErrorCodeName(httpErrorCanonicalName) RegisterErrorCode(canonicalName, httpStatusCode) return canonicalName } // RegisterErrorCode registers a custom HTTP Error. // // This method MUST be called on initialization, before HTTP server starts as // the internal map is not protected by mutex. func RegisterErrorCode(canonicalName ErrorCodeName, httpStatusCode int) { errorCodeMap[canonicalName] = ErrorCode{ CanonicalName: canonicalName, Status: httpStatusCode, } } // RegisterErrorCodeMap registers one or more custom HTTP Errors. // // This method MUST be called on initialization, before HTTP server starts as // the internal map is not protected by mutex. func RegisterErrorCodeMap(errorMap map[ErrorCodeName]int) { if len(errorMap) == 0 { return } for canonicalName, httpStatusCode := range errorMap { RegisterErrorCode(canonicalName, httpStatusCode) } } // List of default error codes a server should follow and send back to the client. var ( Cancelled ErrorCodeName = Register("CANCELLED", context.StatusTokenRequired) Unknown ErrorCodeName = Register("UNKNOWN", http.StatusInternalServerError) InvalidArgument ErrorCodeName = Register("INVALID_ARGUMENT", http.StatusBadRequest) DeadlineExceeded ErrorCodeName = Register("DEADLINE_EXCEEDED", http.StatusGatewayTimeout) NotFound ErrorCodeName = Register("NOT_FOUND", http.StatusNotFound) AlreadyExists ErrorCodeName = Register("ALREADY_EXISTS", http.StatusConflict) PermissionDenied ErrorCodeName = Register("PERMISSION_DENIED", http.StatusForbidden) Unauthenticated ErrorCodeName = Register("UNAUTHENTICATED", http.StatusUnauthorized) ResourceExhausted ErrorCodeName = Register("RESOURCE_EXHAUSTED", http.StatusTooManyRequests) FailedPrecondition ErrorCodeName = Register("FAILED_PRECONDITION", http.StatusBadRequest) Aborted ErrorCodeName = Register("ABORTED", http.StatusConflict) OutOfRange ErrorCodeName = Register("OUT_OF_RANGE", http.StatusBadRequest) Unimplemented ErrorCodeName = Register("UNIMPLEMENTED", http.StatusNotImplemented) Internal ErrorCodeName = Register("INTERNAL", http.StatusInternalServerError) Unavailable ErrorCodeName = Register("UNAVAILABLE", http.StatusServiceUnavailable) DataLoss ErrorCodeName = Register("DATA_LOSS", http.StatusInternalServerError) ) // errorFuncCodeMap is a read-only map of error code names and their error functions. // See HandleError package-level function. var errorFuncCodeMap = make(map[ErrorCodeName][]func(error) error) // HandleError handles an error by sending it to the client // based on the registered error code names and their error functions. // Returns true if the error was handled, otherwise false. // If the given "err" is nil then it returns false. // If the given "err" is a type of validation error then it sends it to the client // using the "Validation" method. // If the given "err" is a type of client.APIError then it sends it to the client // using the "HandleAPIError" function. // // See ErrorCodeName.MapErrorFunc and MapErrors methods too. func HandleError(ctx *context.Context, err error) bool { if err == nil { return false } if ctx.IsStopped() { return false } for errorCodeName, errorFuncs := range errorFuncCodeMap { for _, errorFunc := range errorFuncs { if errToSend := errorFunc(err); errToSend != nil { errorCodeName.Err(ctx, errToSend) return true } } } // Unwrap and collect the errors slice so the error result doesn't contain the ErrorCodeName type // and fire the error status code and title based on this error code name itself. var asErrCode ErrorCodeName if As(err, &asErrCode) { if unwrapJoined, ok := err.(joinedErrors); ok { errs := unwrapJoined.Unwrap() errsToKeep := make([]error, 0, len(errs)-1) for _, src := range errs { if _, isErrorCodeName := src.(ErrorCodeName); !isErrorCodeName { errsToKeep = append(errsToKeep, src) } } if len(errsToKeep) > 0 { err = errors.Join(errsToKeep...) } } asErrCode.Err(ctx, err) return true } if handleJSONError(ctx, err) { return true } if vErr, ok := err.(ValidationError); ok { if vErr == nil { return false // consider as not error for any case, this should never happen. } InvalidArgument.Validation(ctx, vErr) return true } if vErrs, ok := err.(ValidationErrors); ok { if len(vErrs) == 0 { return false // consider as not error for any case, this should never happen. } InvalidArgument.Validation(ctx, vErrs...) return true } if apiErr, ok := client.GetError(err); ok { handleAPIError(ctx, apiErr) return true } Internal.LogErr(ctx, err) return true } // Error returns an empty string, it is only declared as a method of ErrorCodeName type in order // to be a compatible error to be joined within other errors: // // err = fmt.Errorf("%w%w", errors.InvalidArgument, err) OR // err = errors.InvalidArgument.Wrap(err) func (e ErrorCodeName) Error() string { return "" } type joinedErrors interface{ Unwrap() []error } // Wrap wraps the given error with this ErrorCodeName. // It calls the standard errors.Join package-level function. // See HandleError function for more. func (e ErrorCodeName) Wrap(err error) error { return errors.Join(e, err) } // MapErrorFunc registers a function which will validate the incoming error and // return the same error or overriden in order to be sent to the client, wrapped by this ErrorCodeName "e". // // This method MUST be called on initialization, before HTTP server starts as // the internal map is not protected by mutex. // // Example Code: // // errors.InvalidArgument.MapErrorFunc(func(err error) error { // stripeErr, ok := err.(*stripe.Error) // if !ok { // return nil // } // // return &errors.Error{ // Message: stripeErr.Msg, // Details: stripeErr.DocURL, // } // }) func (e ErrorCodeName) MapErrorFunc(fn func(error) error) { errorFuncCodeMap[e] = append(errorFuncCodeMap[e], fn) } // MapError registers one or more errors which will be sent to the client wrapped by this "e" ErrorCodeName // when the incoming error matches to at least one of the given "targets" one. // // This method MUST be called on initialization, before HTTP server starts as // the internal map is not protected by mutex. func (e ErrorCodeName) MapErrors(targets ...error) { e.MapErrorFunc(func(err error) error { for _, target := range targets { if Is(err, target) { return err } } return nil }) } // Message sends an error with a simple message to the client. func (e ErrorCodeName) Message(ctx *context.Context, msg string) { fail(ctx, e, msg, "", nil, nil) } // Details sends an error with a message and details to the client. func (e ErrorCodeName) Details(ctx *context.Context, msg, details string) { fail(ctx, e, msg, msg, nil, nil) } // Data sends an error with a message and json data to the client. func (e ErrorCodeName) Data(ctx *context.Context, msg string, data any) { fail(ctx, e, msg, "", nil, data) } // DataWithDetails sends an error with a message, details and json data to the client. func (e ErrorCodeName) DataWithDetails(ctx *context.Context, msg, details string, data any) { fail(ctx, e, msg, details, nil, data) } // Validation sends an error which renders the invalid fields to the client. func (e ErrorCodeName) Validation(ctx *context.Context, validationErrors ...ValidationError) { e.validation(ctx, validationErrors) } func (e ErrorCodeName) validation(ctx *context.Context, validationErrors any) { fail(ctx, e, "validation failure", "fields were invalid", validationErrors, nil) } // Err sends the error's text as a message to the client. // In exception, if the given "err" is a type of validation error // then the Validation method is called instead. func (e ErrorCodeName) Err(ctx *context.Context, err error) { if err == nil { return } if vErr, ok := err.(ValidationError); ok { if vErr == nil { return // consider as not error for any case, this should never happen. } e.Validation(ctx, vErr) } if vErrs, ok := err.(ValidationErrors); ok { if len(vErrs) == 0 { return // consider as not error for any case, this should never happen. } e.Validation(ctx, vErrs...) } // If it's already an Error type then send it directly. if httpErr, ok := err.(*Error); ok { if errorCode, ok := errorCodeMap[e]; ok { httpErr.ErrorCode = errorCode ctx.StopWithJSON(errorCode.Status, httpErr) // here we override the fail function and send the error as it is. return } } e.Message(ctx, err.Error()) } // Log sends an error of "format" and optional "args" to the client and prints that // error using the "LogError" package-level function, which can be customized. // // See "LogErr" too. func (e ErrorCodeName) Log(ctx *context.Context, format string, args ...any) { if SkipCanceled { if ctx.IsCanceled() { return } for _, arg := range args { if err, ok := arg.(error); ok { if context.IsErrCanceled(err) { return } } } } err := fmt.Errorf(format, args...) e.LogErr(ctx, err) } // LogErr sends the given "err" as message to the client and prints that // error to using the "LogError" package-level function, which can be customized. func (e ErrorCodeName) LogErr(ctx *context.Context, err error) { if SkipCanceled && (ctx.IsCanceled() || context.IsErrCanceled(err)) { return } LogError(ctx, err) e.Message(ctx, "server error") } // HandleAPIError handles remote server errors. // Optionally, use it when you write your server's HTTP clients using the the /x/client package. // When the HTTP Client sends data to a remote server but that remote server // failed to accept the request as expected, then the error will be proxied // to this server's end-client. // // When the given "err" is not a type of client.APIError then // the error will be sent using the "Internal.LogErr" method which sends // HTTP internal server error to the end-client and // prints the "err" using the "LogError" package-level function. func HandleAPIError(ctx *context.Context, err error) { // Error expected and came from the external server, // save its body so we can forward it to the end-client. if apiErr, ok := client.GetError(err); ok { handleAPIError(ctx, apiErr) return } Internal.LogErr(ctx, err) } func handleAPIError(ctx *context.Context, apiErr client.APIError) { // Error expected and came from the external server, // save its body so we can forward it to the end-client. statusCode := apiErr.Response.StatusCode if statusCode >= 400 && statusCode < 500 { InvalidArgument.DataWithDetails(ctx, "remote server error", "invalid client request", apiErr.Body) } else { Internal.Data(ctx, "remote server error", apiErr.Body) } // Unavailable.DataWithDetails(ctx, "remote server error", "unavailable", apiErr.Body) } func handleJSONError(ctx *context.Context, err error) bool { if err == nil { return false } messageText := "unable to parse request body" wireError := InvalidArgument if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) { wireError.Details(ctx, messageText, "empty body") return true } var syntaxErr *json.SyntaxError if errors.As(err, &syntaxErr) { wireError.Details(ctx, messageText, fmt.Sprintf("json: syntax error at byte offset %d", syntaxErr.Offset)) return true } var unmarshalErr *json.UnmarshalTypeError if errors.As(err, &unmarshalErr) { wireError.Details(ctx, messageText, unmarshalErr.Error()) return true } var unsupportedValueErr *json.UnsupportedValueError if errors.As(err, &unsupportedValueErr) { wireError.Details(ctx, messageText, err.Error()) return true } var unsupportedTypeErr *json.UnsupportedTypeError if errors.As(err, &unsupportedTypeErr) { wireError.Details(ctx, messageText, err.Error()) return true } var invalidUnmarshalErr *json.InvalidUnmarshalError if errors.As(err, &invalidUnmarshalErr) { wireError.Details(ctx, messageText, err.Error()) return true } return false } var ( // ErrUnexpected is the HTTP error which sent to the client // when server fails to send an error, it's a fallback error. // The server fails to send an error on two cases: // 1. when the provided error code name is not registered (the error value is the ErrUnexpectedErrorCode) // 2. when the error contains data but cannot be encoded to json (the value of the error is the result error of json.Marshal). ErrUnexpected = Register("UNEXPECTED_ERROR", http.StatusInternalServerError) // ErrUnexpectedErrorCode is the error which logged // when the given error code name is not registered. ErrUnexpectedErrorCode = New("unexpected error code name") ) // Error represents the JSON form of "http wire errors". // // Examples can be found at: // // https://github.com/kataras/iris/tree/main/_examples/routing/http-wire-errors. type Error struct { ErrorCode ErrorCode `json:"http_error_code" yaml:"HTTPErrorCode"` Message string `json:"message,omitempty" yaml:"Message"` Details string `json:"details,omitempty" yaml:"Details"` Validation any `json:"validation,omitempty" yaml:"Validation,omitempty"` Data json.RawMessage `json:"data,omitempty" yaml:"Data,omitempty"` // any other custom json data. } // Error method completes the error interface. It just returns the canonical name, status code, message and details. func (err *Error) Error() string { if err.Message == "" { err.Message = "" } if err.Details == "" { err.Details = "" } if err.ErrorCode.CanonicalName == "" { err.ErrorCode.CanonicalName = ErrUnexpected } if err.ErrorCode.Status <= 0 { err.ErrorCode.Status = http.StatusInternalServerError } return sprintf("iris http wire error: canonical name: %s, http status code: %d, message: %s, details: %s", err.ErrorCode.CanonicalName, err.ErrorCode.Status, err.Message, err.Details) } func fail(ctx *context.Context, codeName ErrorCodeName, msg, details string, validationErrors any, dataValue any) { errorCode, ok := errorCodeMap[codeName] if !ok { // This SHOULD NEVER happen, all ErrorCodeNames MUST be registered. LogError(ctx, ErrUnexpectedErrorCode) fail(ctx, ErrUnexpected, msg, details, validationErrors, dataValue) return } var data json.RawMessage if dataValue != nil { switch v := dataValue.(type) { case json.RawMessage: data = v case []byte: data = v case error: if msg == "" { msg = v.Error() } else if details == "" { details = v.Error() } else { data = json.RawMessage(v.Error()) } default: b, err := json.Marshal(v) if err != nil { LogError(ctx, err) fail(ctx, ErrUnexpected, err.Error(), "", nil, nil) return } data = b } } err := Error{ ErrorCode: errorCode, Message: msg, Details: details, Data: data, Validation: validationErrors, } // ctx.SetErr(&err) ctx.StopWithJSON(errorCode.Status, err) } ================================================ FILE: x/errors/handlers.go ================================================ package errors import ( stdContext "context" "encoding/json" "errors" "fmt" "io" "math" "net/http" "reflect" "github.com/kataras/iris/v12/context" recovery "github.com/kataras/iris/v12/middleware/recover" "github.com/kataras/iris/v12/x/pagination" "golang.org/x/exp/constraints" ) func init() { context.SetHandlerName("iris/x/errors.RecoveryHandler.*", "iris.errors.recover") } // RecoveryHandler is a middleware which recovers from panics and sends an appropriate error response // to the logger and the client. func RecoveryHandler(ctx *context.Context) { defer func() { if err := recovery.PanicRecoveryError(ctx, recover()); err != nil { Internal.LogErr(ctx, err) ctx.StopExecution() } }() ctx.Next() } // Handle handles a generic response and error from a service call and sends a JSON response to the client. // It returns a boolean value indicating whether the handle was successful or not. // If the error is not nil, it calls HandleError to send an appropriate error response to the client. func Handle(ctx *context.Context, resp any, err error) bool { if HandleError(ctx, err) { return false } ctx.StatusCode(http.StatusOK) if resp != nil { if jsonErr := ctx.JSON(resp); jsonErr != nil { // this returns the original error as it's, even if it's handled by the HandleJSONError function. if unsupportedValueErr, ok := jsonErr.(*json.UnsupportedValueError); ok { if unsupportedValueErr.Str == "NaN" { if nanErr := checkNaN(resp); nanErr != nil { newErr := fmt.Errorf("unable to parse response body: json: unspported value: %w", nanErr) LogError(ctx, newErr) return false } } } return false } } return true } // checkNaN checks if any exported field in the struct is NaN and returns an error if it is. func checkNaN(v any) error { val := reflect.ValueOf(v) return checkNaNRecursive(val, val.Type().Name()) } func checkNaNRecursive(val reflect.Value, path string) error { if val.Kind() == reflect.Ptr { if val.IsNil() { return nil } val = val.Elem() } switch val.Kind() { case reflect.Struct: for i := 0; i < val.NumField(); i++ { field := val.Field(i) fieldType := val.Type().Field(i) fieldPath := fmt.Sprintf("%s.%s", path, fieldType.Name) if err := checkNaNRecursive(field, fieldPath); err != nil { return err } } case reflect.Slice: for i := 0; i < val.Len(); i++ { elem := val.Index(i) elemPath := fmt.Sprintf("%s[%d]", path, i) if err := checkNaNRecursive(elem, elemPath); err != nil { return err } } case reflect.Float64: if math.IsNaN(val.Float()) { return fmt.Errorf("NaN value found in field: %s", path) } } return nil } // IDPayload is a simple struct which describes a json id value. type IDPayload[T string | int] struct { ID T `json:"id"` } // HandleCreate handles a create operation and sends a JSON response with the created resource to the client. // It returns a boolean value indicating whether the handle was successful or not. // // If the "respOrID" response is not nil, it sets the status code to 201 (Created) and sends the response as a JSON payload, // however if the given "respOrID" is a string or an int, it sends the response as a JSON payload of {"id": resp}. // If the "err" error is not nil, it calls HandleError to send an appropriate error response to the client. // It sets the status code to 201 (Created) and sends any response as a JSON payload, func HandleCreate(ctx *context.Context, respOrID any, err error) bool { if HandleError(ctx, err) { return false } ctx.StatusCode(http.StatusCreated) if respOrID != nil { switch responseValue := respOrID.(type) { case string: if ctx.JSON(IDPayload[string]{ID: responseValue}) != nil { return false } case int: if ctx.JSON(IDPayload[int]{ID: responseValue}) != nil { return false } default: if ctx.JSON(responseValue) != nil { return false } } } return true } // HandleUpdate handles an update operation and sends a status code to the client. // It returns a boolean value indicating whether the handle was successful or not. // If the error is not nil, it calls HandleError to send an appropriate error response to the client. // If the updated value is true, it sets the status code to 204 (No Content). // If the updated value is false, it sets the status code to 304 (Not Modified). func HandleUpdate(ctx *context.Context, updated bool, err error) bool { if HandleError(ctx, err) { return false } if updated { ctx.StatusCode(http.StatusNoContent) } else { ctx.StatusCode(http.StatusNotModified) } return true } // HandleDelete handles a delete operation and sends a status code to the client. // If the error is not nil, it calls HandleError to send an appropriate error response to the client. // If the deleted value is true, it sets the status code to 204 (No Content). // If the deleted value is false, it sets the status code to 304 (Not Modified). func HandleDelete(ctx *context.Context, deleted bool, err error) bool { return HandleUpdate(ctx, deleted, err) } // HandleDelete handles a delete operation and sends a status code to the client. // If the error is not nil, it calls HandleError to send an appropriate error response to the client. // It sets the status code to 204 (No Content). func HandleDeleteNoContent(ctx *context.Context, err error) bool { return HandleUpdate(ctx, true, err) } // ResponseFunc is a function which takes a context and a generic type T and returns a generic type R and an error. // It is used to bind a request payload to a generic type T and call a service function with it. type ResponseFunc[T, R any] interface { func(stdContext.Context, T) (R, error) } // ResponseOnlyErrorFunc is a function which takes a context and a generic type T and returns an error. // It is used to bind a request payload to a generic type T and call a service function with it. // It is used for functions which do not return a response. type ResponseOnlyErrorFunc[T any] interface { func(stdContext.Context, T) error } // ContextRequestFunc is a function which takes a context and a generic type T and returns an error. // It is used to validate the context before calling a service function. // // See Validation package-level function. type ContextRequestFunc[T any] func(*context.Context, T) error const contextRequestHandlerFuncKey = "iris.errors.ContextRequestHandler" // Validation adds a context validator function to the context. // It returns a middleware which can be used to validate the context before calling a service function. // It panics if the given validators are empty or nil. // // Example: // // r.Post("/", Validation(validateCreateRequest), createHandler(service)) // // func validateCreateRequest(ctx iris.Context, r *CreateRequest) error { // return validation.Join( // validation.String("fullname", r.Fullname).NotEmpty().Fullname().Length(3, 50), // validation.Number("age", r.Age).InRange(18, 130), // validation.Slice("hobbies", r.Hobbies).Length(1, 10), // ) // } func Validation[T any](validators ...ContextRequestFunc[T]) context.Handler { if len(validators) == 0 { return nil } validator := joinContextRequestFuncs(validators) return func(ctx *context.Context) { ctx.Values().Set(contextRequestHandlerFuncKey, validator) ctx.Next() } } func joinContextRequestFuncs[T any](requestHandlerFuncs []ContextRequestFunc[T]) ContextRequestFunc[T] { if len(requestHandlerFuncs) == 0 || requestHandlerFuncs[0] == nil { panic("at least one context request handler function is required") } if len(requestHandlerFuncs) == 1 { return requestHandlerFuncs[0] } return func(ctx *context.Context, req T) error { for _, handler := range requestHandlerFuncs { if handler == nil { continue } if err := handler(ctx, req); err != nil { return err } } return nil } } // RequestHandler is an interface which can be implemented by a request payload struct // in order to validate the context before calling a service function. type RequestHandler interface { HandleRequest(*context.Context) error } func validateRequest[T any](ctx *context.Context, req T) bool { var err error // Always run the request's validator first, // so dynamic validators can be customized per path and method. if contextRequestHandler, ok := any(&req).(RequestHandler); ok { err = contextRequestHandler.HandleRequest(ctx) } if err == nil { if v := ctx.Values().Get(contextRequestHandlerFuncKey); v != nil { if contextRequestHandlerFunc, ok := v.(ContextRequestFunc[T]); ok && contextRequestHandlerFunc != nil { err = contextRequestHandlerFunc(ctx, req) } else if contextRequestHandlerFunc, ok := v.(ContextRequestFunc[*T]); ok && contextRequestHandlerFunc != nil { // or a pointer of T. err = contextRequestHandlerFunc(ctx, &req) } } } return err == nil || !HandleError(ctx, err) } // ResponseHandler is an interface which can be implemented by a request payload struct // in order to handle a response before sending it to the client. type ResponseHandler[R any] interface { HandleResponse(ctx *context.Context, response *R) error } // ContextResponseFunc is a function which takes a context, a generic type T and a generic type R and returns an error. type ContextResponseFunc[T, R any] func(*context.Context, T, *R) error const contextResponseHandlerFuncKey = "iris.errors.ContextResponseHandler" func validateResponse[T, R any](ctx *context.Context, req T, resp *R) bool { var err error if contextResponseHandler, ok := any(&req).(ResponseHandler[R]); ok { err = contextResponseHandler.HandleResponse(ctx, resp) } if err == nil { if v := ctx.Values().Get(contextResponseHandlerFuncKey); v != nil { if contextResponseHandlerFunc, ok := v.(ContextResponseFunc[T, R]); ok && contextResponseHandlerFunc != nil { err = contextResponseHandlerFunc(ctx, req, resp) } else if contextResponseHandlerFunc, ok := v.(ContextResponseFunc[*T, R]); ok && contextResponseHandlerFunc != nil { err = contextResponseHandlerFunc(ctx, &req, resp) } } } return err == nil || !HandleError(ctx, err) } // Intercept adds a context response handler function to the context. // It returns a middleware which can be used to intercept the response before sending it to the client. // // Example Code: // // app.Post("/", errors.Intercept(func(ctx iris.Context, req *CreateRequest, resp *CreateResponse) error{ ... }), errors.CreateHandler(service.Create)) func Intercept[T, R any](responseHandlers ...ContextResponseFunc[T, R]) context.Handler { if len(responseHandlers) == 0 { return nil } responseHandler := joinContextResponseFuncs(responseHandlers) return func(ctx *context.Context) { ctx.Values().Set(contextResponseHandlerFuncKey, responseHandler) ctx.Next() } } func joinContextResponseFuncs[T, R any](responseHandlerFuncs []ContextResponseFunc[T, R]) ContextResponseFunc[T, R] { if len(responseHandlerFuncs) == 0 || responseHandlerFuncs[0] == nil { panic("at least one context response handler function is required") } if len(responseHandlerFuncs) == 1 { return responseHandlerFuncs[0] } return func(ctx *context.Context, req T, resp *R) error { for _, handler := range responseHandlerFuncs { if handler == nil { continue } if err := handler(ctx, req, resp); err != nil { return err } } return nil } } func bindResponse[T, R any, F ResponseFunc[T, R]](ctx *context.Context, fn F, fnInput ...T) (R, bool) { var req T switch len(fnInput) { case 0: var ok bool req, ok = ReadPayload[T](ctx) if !ok { var resp R return resp, false } case 1: req = fnInput[0] default: panic("invalid number of arguments") } if !validateRequest(ctx, req) { var resp R return resp, false } resp, err := fn(ctx, req) if err == nil { if !validateResponse(ctx, req, &resp) { return resp, false } } return resp, !HandleError(ctx, err) } // OK handles a generic response and error from a service call and sends a JSON response to the client. // It returns a boolean value indicating whether the handle was successful or not. // If the error is not nil, it calls HandleError to send an appropriate error response to the client. // It sets the status code to 200 (OK) and sends any response as a JSON payload. // // Useful for Get/List/Fetch operations. func OK[T, R any, F ResponseFunc[T, R]](ctx *context.Context, fn F, fnInput ...T) bool { // or Fetch. resp, ok := bindResponse(ctx, fn, fnInput...) if !ok { return false } return Handle(ctx, resp, nil) } // HandlerInputFunc is a function which takes a context and returns a generic type T. // It is used to call a service function with a generic type T. // It is used for functions which do not bind a request payload. // It is used for XHandler functions. // Developers can design their own HandlerInputFunc functions and use them with the XHandler functions. // To make a value required, stop the context execution through the context.StopExecution function and fire an error // or just use one of the [InvalidArgument].X methods. // // See PathParam, Query and Value package-level helpers too. type HandlerInputFunc[T any] interface { func(ctx *context.Context) T } // GetRequestInputs returns a slice of generic type T from a slice of HandlerInputFunc[T]. // It is exported so end-developers can use it to get the inputs from custom HandlerInputFunc[T] functions. func GetRequestInputs[T any, I HandlerInputFunc[T]](ctx *context.Context, fnInputFunc []I) ([]T, bool) { inputs := make([]T, 0, len(fnInputFunc)) for _, callIn := range fnInputFunc { if callIn == nil { continue } input := callIn(ctx) if ctx.IsStopped() { // if the input is required and it's not provided, then the context is stopped. return nil, false } inputs = append(inputs, input) } return inputs, true } // PathParam returns a HandlerInputFunc which reads a path parameter from the context and returns it as a generic type T. // It is used for XHandler functions. func PathParam[T any, I HandlerInputFunc[T]](paramName string) I { return func(ctx *context.Context) T { paramValue := ctx.Params().Store.Get(paramName) if paramValue == nil { var t T return t } return paramValue.(T) } } // Value returns a HandlerInputFunc which returns a generic type T. // It is used for XHandler functions. func Value[T any, I HandlerInputFunc[T]](value T) I { return func(ctx *context.Context) T { return value } } // Query returns a HandlerInputFunc which reads a URL query from the context and returns it as a generic type T. // It is used for XHandler functions. func Query[T any, I HandlerInputFunc[T]]() I { return func(ctx *context.Context) T { value, ok := ReadQuery[T](ctx) if !ok { var t T return t } return value } } // Handler handles a generic response and error from a service call and sends a JSON response to the client with status code of 200. // // See OK package-level function for more. func Handler[T, R any, F ResponseFunc[T, R], I HandlerInputFunc[T]](fn F, fnInput ...I) context.Handler { return func(ctx *context.Context) { inputs, ok := GetRequestInputs(ctx, fnInput) if !ok { return } OK(ctx, fn, inputs...) } } // ListResponseFunc is a function which takes a context, // a pagination.ListOptions and a generic type T and returns a slice []R, total count of the items and an error. // // It's used on the List function. type ListResponseFunc[T, R any, C constraints.Integer | constraints.Float] interface { func(stdContext.Context, pagination.ListOptions, T /* filter options */) ([]R, C, error) } // List handles a generic response and error from a service paginated call and sends a JSON response to the client. // It returns a boolean value indicating whether the handle was successful or not. // If the error is not nil, it calls HandleError to send an appropriate error response to the client. // It reads the pagination.ListOptions from the URL Query and any filter options of generic T from the request body. // It sets the status code to 200 (OK) and sends a *pagination.List[R] response as a JSON payload. func List[T, R any, C constraints.Integer | constraints.Float, F ListResponseFunc[T, R, C]](ctx *context.Context, fn F, fnInput ...T) bool { listOpts, filter, ok := ReadPaginationOptions[T](ctx) if !ok { return false } if !validateRequest(ctx, filter) { return false } items, totalCount, err := fn(ctx, listOpts, filter) if err != nil { HandleError(ctx, err) return false } resp := pagination.NewList(items, int64(totalCount), filter, listOpts) if !validateResponse(ctx, filter, resp) { return false } return Handle(ctx, resp, err) } // ListHandler handles a generic response and error from a service paginated call and sends a JSON response to the client. // // See List package-level function for more. func ListHandler[T, R any, C constraints.Integer | constraints.Float, F ListResponseFunc[T, R, C], I HandlerInputFunc[T]](fn F, fnInput ...I) context.Handler { return func(ctx *context.Context) { inputs, ok := GetRequestInputs(ctx, fnInput) if !ok { return } List(ctx, fn, inputs...) } } // Create handles a create operation and sends a JSON response with the created resource to the client. // It returns a boolean value indicating whether the handle was successful or not. // If the error is not nil, it calls HandleError to send an appropriate error response to the client. // It sets the status code to 201 (Created) and sends any response as a JSON payload // note that if the response is a string, then it sends an {"id": resp} JSON payload). // // Useful for Insert operations. func Create[T, R any, F ResponseFunc[T, R]](ctx *context.Context, fn F, fnInput ...T) bool { resp, ok := bindResponse(ctx, fn, fnInput...) if !ok { return false } return HandleCreate(ctx, resp, nil) } // CreateHandler handles a create operation and sends a JSON response with the created resource to the client with status code of 201. // // See Create package-level function for more. func CreateHandler[T, R any, F ResponseFunc[T, R], I HandlerInputFunc[T]](fn F, fnInput ...I) context.Handler { return func(ctx *context.Context) { inputs, ok := GetRequestInputs(ctx, fnInput) if !ok { return } Create(ctx, fn, inputs...) } } // NoContent handles a generic response and error from a service call and sends a JSON response to the client. // It returns a boolean value indicating whether the handle was successful or not. // If the error is not nil, it calls HandleError to send an appropriate error response to the client. // It sets the status code to 204 (No Content). // // Useful for Update and Deletion operations. func NoContent[T any, F ResponseOnlyErrorFunc[T]](ctx *context.Context, fn F, fnInput ...T) bool { toFn := func(c stdContext.Context, req T) (bool, error) { return true, fn(ctx, req) } return NoContentOrNotModified(ctx, toFn, fnInput...) } // NoContentHandler handles a generic response and error from a service call and sends a JSON response to the client with status code of 204. // // See NoContent package-level function for more. func NoContentHandler[T any, F ResponseOnlyErrorFunc[T], I HandlerInputFunc[T]](fn F, fnInput ...I) context.Handler { return func(ctx *context.Context) { inputs, ok := GetRequestInputs(ctx, fnInput) if !ok { return } NoContent(ctx, fn, inputs...) } } // NoContent handles a generic response and error from a service call and sends a JSON response to the client. // It returns a boolean value indicating whether the handle was successful or not. // If the error is not nil, it calls HandleError to send an appropriate error response to the client. // If the response is true, it sets the status code to 204 (No Content). // If the response is false, it sets the status code to 304 (Not Modified). // // Useful for Update and Deletion operations. func NoContentOrNotModified[T any, F ResponseFunc[T, bool]](ctx *context.Context, fn F, fnInput ...T) bool { resp, ok := bindResponse(ctx, fn, fnInput...) if !ok { return false } return HandleUpdate(ctx, bool(resp), nil) } // NoContentOrNotModifiedHandler handles a generic response and error from a service call and sends a JSON response to the client with status code of 204 or 304. // // See NoContentOrNotModified package-level function for more. func NoContentOrNotModifiedHandler[T any, F ResponseFunc[T, bool], I HandlerInputFunc[T]](fn F, fnInput ...I) context.Handler { return func(ctx *context.Context) { inputs, ok := GetRequestInputs(ctx, fnInput) if !ok { return } NoContentOrNotModified(ctx, fn, inputs...) } } // ReadPayload reads a JSON payload from the context and returns it as a generic type T. // It also returns a boolean value indicating whether the read was successful or not. // If the read fails, it sends an appropriate error response to the client. func ReadPayload[T any](ctx *context.Context) (T, bool) { var payload T err := ctx.ReadJSON(&payload) if err != nil { if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) { InvalidArgument.Details(ctx, "unable to parse body", "empty body") return payload, false } HandleError(ctx, err) return payload, false } return payload, true } // ReadQuery reads URL query values from the context and returns it as a generic type T. // It also returns a boolean value indicating whether the read was successful or not. // If the read fails, it sends an appropriate error response to the client. func ReadQuery[T any](ctx *context.Context) (T, bool) { var payload T err := ctx.ReadQuery(&payload) if err != nil { HandleError(ctx, err) return payload, false } return payload, true } // ReadPaginationOptions reads the ListOptions from the URL Query and // any filter options of generic T from the request body. func ReadPaginationOptions[T /* T is FilterOptions */ any](ctx *context.Context) (pagination.ListOptions, T, bool) { list, ok := ReadQuery[pagination.ListOptions](ctx) if !ok { var t T return list, t, false } filter, ok := ReadPayload[T](ctx) if !ok { var t T return list, t, false } return list, filter, true } ================================================ FILE: x/errors/path_parameter_type_error_handler.go ================================================ package errors import ( "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/macro/handler" ) // DefaultPathParameterTypeErrorHandler registers an error handler for macro path type parameter. // Register it with Application.Macros().SetErrorHandler(DefaultPathParameterTypeErrorHandler). var DefaultPathParameterTypeErrorHandler handler.ParamErrorHandler = func(ctx *context.Context, paramIndex int, err error) { param := ctx.Params().GetEntryAt(paramIndex) // key, value fields. InvalidArgument.DataWithDetails(ctx, "invalid path parameter", err.Error(), param) } ================================================ FILE: x/errors/validation/error.go ================================================ package validation import ( "fmt" "github.com/kataras/iris/v12/x/errors" ) // FieldError describes a field validation error. // It completes the errors.ValidationError interface. type FieldError[T any] struct { Field string `json:"field"` Value T `json:"value"` Reason string `json:"reason"` } // Field returns a new validation error. // // Use its Func method to add validations over this field. func Field[T any](field string, value T) *FieldError[T] { return &FieldError[T]{Field: field, Value: value} } // Error completes the standard error interface. func (e *FieldError[T]) Error() string { return fmt.Sprintf("field %q got invalid value of %v: reason: %s", e.Field, e.Value, e.Reason) } // GetField returns the field name. func (e *FieldError[T]) GetField() string { return e.Field } // GetValue returns the value of the field. func (e *FieldError[T]) GetValue() any { return e.Value } // GetReason returns the reason of the validation error. func (e *FieldError[T]) GetReason() string { return e.Reason } // IsZero reports whether the error is nil or has an empty reason. func (e *FieldError[T]) IsZero() bool { return e == nil || e.Reason == "" } func (e *FieldError[T]) joinReason(reason string) { if reason == "" { return } if e.Reason == "" { e.Reason = reason } else { e.Reason += ", " + reason } } // Func accepts a variadic number of functions which accept the value of the field // and return a string message if the value is invalid. // It joins the reasons into one. func (e *FieldError[T]) Func(fns ...func(value T) string) *FieldError[T] { for _, fn := range fns { e.joinReason(fn(e.Value)) } return e } // Join joins the given validation errors into one. func Join(errs ...errors.ValidationError) error { // note that here we return the standard error type instead of the errors.ValidationError in order to make the error nil instead of ValidationErrors(nil) on empty slice. if len(errs) == 0 { return nil } joinedErrs := make(errors.ValidationErrors, 0, len(errs)) for _, err := range errs { if err == nil || err.GetReason() == "" { continue } joinedErrs = append(joinedErrs, err) } if len(joinedErrs) == 0 { return nil } return joinedErrs } ================================================ FILE: x/errors/validation/number.go ================================================ package validation import ( "fmt" "golang.org/x/exp/constraints" ) // NumberType is a type constraint that accepts any numeric type. type NumberType interface { constraints.Integer | constraints.Float } // NumberError describes a number field validation error. type NumberError[T NumberType] struct{ *FieldError[T] } // Number returns a new number validation error. func Number[T NumberType](field string, value T) *NumberError[T] { return &NumberError[T]{Field(field, value)} } // Positive adds an error if the value is not positive. func (e *NumberError[T]) Positive() *NumberError[T] { e.Func(Positive) return e } // Negative adds an error if the value is not negative. func (e *NumberError[T]) Negative() *NumberError[T] { e.Func(Negative) return e } // Zero reports whether the value is zero. func (e *NumberError[T]) Zero() *NumberError[T] { e.Func(Zero) return e } // NonZero adds an error if the value is zero. func (e *NumberError[T]) NonZero() *NumberError[T] { e.Func(NonZero) return e } // InRange adds an error if the value is not in the range. func (e *NumberError[T]) InRange(min, max T) *NumberError[T] { e.Func(InRange(min, max)) return e } // Positive accepts any numeric type and // returns a message if the value is not positive. func Positive[T NumberType](n T) string { if n <= 0 { return "must be positive" } return "" } // Negative accepts any numeric type and returns a message if the value is not negative. func Negative[T NumberType](n T) string { if n >= 0 { return "must be negative" } return "" } // Zero accepts any numeric type and returns a message if the value is not zero. func Zero[T NumberType](n T) string { if n != 0 { return "must be zero" } return "" } // NonZero accepts any numeric type and returns a message if the value is not zero. func NonZero[T NumberType](n T) string { if n == 0 { return "must not be zero" } return "" } // InRange accepts any numeric type and returns a message if the value is not in the range. func InRange[T NumberType](min, max T) func(T) string { return func(n T) string { if n < min || n > max { return "must be in range of " + FormatRange(min, max) } return "" } } // FormatRange returns a string representation of a range of values, such as "[1, 10]". // It uses a type constraint NumberValue, which means that the parameters must be numeric types // that support comparison and formatting operations. func FormatRange[T NumberType](min, max T) string { return fmt.Sprintf("[%v, %v]", min, max) } ================================================ FILE: x/errors/validation/slice.go ================================================ package validation import "fmt" // SliceType is a type constraint that accepts any slice type. type SliceType[T any] interface { ~[]T } // SliceError describes a slice field validation error. type SliceError[T any, V SliceType[T]] struct{ *FieldError[V] } // Slice returns a new slice validation error. func Slice[T any, V SliceType[T]](field string, value V) *SliceError[T, V] { return &SliceError[T, V]{Field(field, value)} } // NotEmpty adds an error if the slice is empty. func (e *SliceError[T, V]) NotEmpty() *SliceError[T, V] { e.Func(NotEmptySlice) return e } // Length adds an error if the slice length is not in the given range. func (e *SliceError[T, V]) Length(min, max int) *SliceError[T, V] { e.Func(SliceLength[T, V](min, max)) return e } // NotEmptySlice accepts any slice and returns a message if the value is empty. func NotEmptySlice[T any, V SliceType[T]](s V) string { if len(s) == 0 { return "must not be empty" } return "" } // SliceLength accepts any slice and returns a message if the length is not in the given range. func SliceLength[T any, V SliceType[T]](min, max int) func(s V) string { return func(s V) string { n := len(s) if min == max { if n != min { return fmt.Sprintf("must be %d elements", min) } return "" } if n < min || n > max { return fmt.Sprintf("must be between %d and %d elements", min, max) } return "" } } ================================================ FILE: x/errors/validation/string.go ================================================ package validation import ( "fmt" "strings" ) // StringError describes a string field validation error. type StringError struct{ *FieldError[string] } // String returns a new string validation error. func String(field string, value string) *StringError { return &StringError{Field(field, value)} } // NotEmpty adds an error if the string is empty. func (e *StringError) NotEmpty() *StringError { e.Func(NotEmpty) return e } // Fullname adds an error if the string is not a full name. func (e *StringError) Fullname() *StringError { e.Func(Fullname) return e } // Length adds an error if the string length is not in the given range. func (e *StringError) Length(min, max int) *StringError { e.Func(StringLength(min, max)) return e } // NotEmpty accepts any string and returns a message if the value is empty. func NotEmpty(s string) string { if s == "" { return "must not be empty" } return "" } // Fullname accepts any string and returns a message if the value is not a full name. func Fullname(s string) string { if len(strings.Split(s, " ")) < 2 { return "must contain first and last name" } return "" } // StringLength accepts any string and returns a message if the length is not in the given range. func StringLength(min, max int) func(s string) string { return func(s string) string { n := len(s) if min == max { if n != min { return fmt.Sprintf("must be %d characters", min) } } if n < min || n > max { return fmt.Sprintf("must be between %d and %d characters", min, max) } return "" } } ================================================ FILE: x/errors/validation_error.go ================================================ package errors import ( "strconv" "strings" ) // ValidationError is an interface which IF // it custom error types completes, then // it can by mapped to a validation error. // // A validation error(s) can be given by ErrorCodeName's Validation or Err methods. type ValidationError interface { error GetField() string GetValue() any GetReason() string } type ValidationErrors []ValidationError func (errs ValidationErrors) Error() string { var buf strings.Builder for i, err := range errs { buf.WriteByte('[') buf.WriteString(strconv.Itoa(i)) buf.WriteByte(']') buf.WriteByte(' ') buf.WriteString(err.Error()) if i < len(errs)-1 { buf.WriteByte(',') buf.WriteByte(' ') } } return buf.String() } ================================================ FILE: x/jsonx/day_time.go ================================================ package jsonx import ( "fmt" "strconv" "strings" "time" ) const ( // DayTimeLayout holds the time layout for the the format of "hour:minute:second", hour can be 15, meaning 3 PM. DayTimeLayout = "15:04:05" ) // DayTime describes a time compatible with DayTimeLayout. type DayTime time.Time // ParseDayTime reads from "s" and returns the DayTime time. func ParseDayTime(s string) (DayTime, error) { if s == "" || s == "null" { return DayTime{}, nil } tt, err := time.Parse(DayTimeLayout, s) if err != nil { return DayTime{}, err } return DayTime(tt), nil } // UnmarshalJSON parses the "b" into DayTime time. func (t *DayTime) UnmarshalJSON(b []byte) error { if len(b) == 0 { return nil } s := strings.Trim(string(b), `"`) tt, err := ParseDayTime(s) if err != nil { return err } *t = tt return nil } // MarshalJSON writes a quoted string in the DayTime time format. func (t DayTime) MarshalJSON() ([]byte, error) { if s := t.String(); s != "" { s = strconv.Quote(s) return []byte(s), nil } return nullLiteral, nil // Note: if the front-end wants an empty string instead I must change that. } // ToTime returns the unwrapped *t to time.Time. func (t *DayTime) ToTime() time.Time { tt := time.Time(*t) return tt } // IsZero reports whether "t" is zero time. func (t DayTime) IsZero() bool { return time.Time(t).IsZero() } // String returns the text representation of the "t" using the DayTime time layout. func (t DayTime) String() string { tt := t.ToTime() if tt.IsZero() { return "" } return tt.Format(DayTimeLayout) } // Scan completes the sql driver.Scanner interface. func (t *DayTime) Scan(src any) error { switch v := src.(type) { case time.Time: // type was set to timestamp if v.IsZero() { return nil // don't set zero, ignore it. } *t = DayTime(v) case string: tt, err := ParseDayTime(v) if err != nil { return err } *t = tt case nil: *t = DayTime(time.Time{}) default: return fmt.Errorf("DayTime: unknown type of: %T", v) } return nil } ================================================ FILE: x/jsonx/day_time_test.go ================================================ package jsonx import ( "encoding/json" "testing" "time" ) func TestDayTime(t *testing.T) { tests := []struct { rawData string }{ { rawData: `{"start": "8:33:00", "end": "15:00:42", "nothing": null, "empty": ""}`, }, { rawData: `{"start": "8:33:00", "end": "15:00:42", "nothing": null, "empty": ""}`, }, } for _, tt := range tests { v := struct { Start DayTime `json:"start"` End DayTime `json:"end"` Nothing DayTime `json:"nothing"` Empty DayTime `json:"empty"` }{} err := json.Unmarshal([]byte(tt.rawData), &v) if err != nil { t.Fatal(err) } if !v.Nothing.IsZero() { t.Fatalf("expected 'nothing' to be zero but got: %v", v.Nothing) } if !v.Empty.IsZero() { t.Fatalf("expected 'empty' to be zero but got: %v", v.Empty) } loc := time.UTC if expected, got := time.Date(0, time.January, 1, 8, 33, 0, 0, loc), v.Start.ToTime(); expected != got { t.Fatalf("expected 'start' to be: %v but got: %v", expected, got) } if expected, got := time.Date(0, time.January, 1, 15, 0, 42, 0, loc), v.End.ToTime(); expected != got { t.Fatalf("expected 'start' to be: %v but got: %v", expected, got) } } } ================================================ FILE: x/jsonx/duration.go ================================================ package jsonx import ( "database/sql/driver" "encoding/json" "errors" "math" "time" ) // Duration is a JSON representation of the standard Duration type, until Go version 2 supports it under the hoods. type Duration time.Duration func (d Duration) MarshalJSON() ([]byte, error) { return json.Marshal(time.Duration(d).String()) } func (d *Duration) UnmarshalJSON(b []byte) error { var v any if err := json.Unmarshal(b, &v); err != nil { return err } switch value := v.(type) { case float64: *d = Duration(value) return nil case string: v, err := time.ParseDuration(value) if err != nil { return err } *d = Duration(v) return nil default: return errors.New("invalid duration") } } func (d Duration) ToDuration() time.Duration { return time.Duration(d) } func (d Duration) Value() (driver.Value, error) { return int64(d), nil } // Set sets the value of duration in nanoseconds. func (d *Duration) Set(v float64) { if math.IsNaN(v) { return } *d = Duration(v) } ================================================ FILE: x/jsonx/exampler.go ================================================ package jsonx // Exampler is an interface used by testing to generate examples. type Exampler interface { ListExamples() any } ================================================ FILE: x/jsonx/iso8601.go ================================================ package jsonx import ( "database/sql/driver" "errors" "fmt" "strconv" "strings" "time" // To load all system and embedded locations by name: // _ "time/tzdata" // OR build with: -tags timetzdata ) var fixedEastUTCLocations = make(map[int]*time.Location) // RegisterFixedLocation should be called on initialization of the program. // It registers a fixed location to the time parser. // // E.g. for input of 2023-02-04T09:48:14+03:00 to result a time string of 2023-02-04 09:48:14 +0300 EEST // you have to RegisterFixedLocation("EEST", 10800) otherwise it will result to: 2023-02-04 09:48:14 +0300 +0300. func RegisterFixedLocation(name string, secondsFromUTC int) { loc := time.FixedZone(name, secondsFromUTC) fixedEastUTCLocations[secondsFromUTC] = loc } func init() { RegisterFixedLocation("EEST", 3*60*60) // + 3 hours. RegisterFixedLocation("UTC", 0) } const ( // ISO8601Layout holds the time layout for the the javascript iso time. // Read more at: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString. ISO8601Layout = "2006-01-02T15:04:05" // ISO8601LayoutWithTimezone same as ISO8601Layout but with the timezone suffix. ISO8601LayoutWithTimezone = "2006-01-02T15:04:05Z" // To match Go’s standard time layout that pads zeroes for microseconds, you can use the format 2006-01-02T15:04:05.000000Z07:00. // This layout uses 0s instead of 9s for the fractional second part, which ensures that the microseconds are // always represented with six digits, padding with leading zeroes if necessary. // ISO8601ZUTCOffsetLayoutWithMicroseconds = "2006-01-02T15:04:05.000000Z07:00" // ISO8601ZUTCOffsetLayoutWithMicroseconds ISO 8601 format, with full time and zone with UTC offset. // Example: 2022-08-10T03:21:00.000000+03:00, 2023-02-04T09:48:14+00:00, 2022-08-09T00:00:00.000000. ISO8601ZUTCOffsetLayoutWithMicroseconds = "2006-01-02T15:04:05.999999Z07:00" // ISO8601ZUTCOffsetLayoutWithoutMicroseconds ISO 8601 format, with full time and zone with UTC offset without microsecond precision. ISO8601ZUTCOffsetLayoutWithoutMicroseconds = "2006-01-02T15:04:05Z07:00" /* The difference between the two time layouts "2006-01-02T15:04:05Z07:00" and "2006-01-02T15:04:05-07:00" is the presence of the Z character: "2006-01-02T15:04:05Z07:00": The Z indicates that the time is in UTC (Coordinated Universal Time) if there’s no offset specified. When an offset is present, as in +03:00, it indicates the time is in a timezone that is 3 hours ahead of UTC. The Z is combined with the offset (07:00), which can be positive or negative to represent the timezone difference from UTC. "2006-01-02T15:04:05-07:00": This layout does not have the Z character and directly uses the offset (-07:00). It’s more straightforward and indicates that the time is in a timezone that is 7 hours behind UTC. In summary, the Z in the first layout serves as a placeholder for UTC and is used when the time might be in UTC or might have an offset. The second layout is used when you’re directly specifying the offset without any reference to UTC. Both layouts can parse the timestamp "2024-04-08T04:47:10+03:00" correctly, as they include placeholders for the timezone offset. */ // ISO8601UnconventionalOffsetLayout is the layout for the unconventional offset. // Custom offset layout, e.g., 2024-05-21T18:06:07.000000-04:01:19. ISO8601UnconventionalOffsetLayout = "2006-01-02T15:04:05.000000" ) // ISO8601 describes a time compatible with javascript time format. type ISO8601 time.Time var _ Exampler = (*ISO8601)(nil) // ParseISO8601 reads from "s" and returns the ISO8601 time. // // The function supports the following formats: // - 2024-01-02T15:04:05.999999Z // - 2024-01-02T15:04:05+07:00 // - 2024-04-08T08:05:04.830140+00:00 // - 2024-01-02T15:04:05Z // - 2024-04-08T08:05:04.830140 // - 2024-01-02T15:04:05 // - 2024-05-21T18:06:07.000000-04:01:19 func ParseISO8601(s string) (ISO8601, error) { if s == "" || s == "null" { return ISO8601{}, nil } var ( tt time.Time err error ) /* // Check if the string contains a timezone offset after the 'T' character. hasOffset := strings.Contains(s, "Z") || (strings.Index(s, "+") > strings.Index(s, "T")) || (strings.Index(s, "-") > strings.Index(s, "T")) switch { case strings.HasSuffix(s, "Z"): tt, err = time.Parse(ISO8601LayoutWithTimezone, s) case hasOffset && strings.Contains(s, "."): tt, err = time.Parse(ISO8601ZUTCOffsetLayoutWithMicroseconds, s) case hasOffset: tt, err = parseWithOffset(s) default: tt, err = time.Parse(ISO8601Layout, s) } if err != nil { return ISO8601{}, fmt.Errorf("ISO8601: %w", err) } return ISO8601(tt), nil */ if idx := strings.LastIndexFunc(s, startUTCOffsetIndexFunc); idx > 18 { // should have some distance, with and without milliseconds length := parseSignedOffset(s[idx:]) // Check if the offset is unconventional, e.g., -04:01:19 if offset := s[idx:]; isUnconventionalOffset(offset) { mainPart := s[:idx] tt, err = time.Parse("2006-01-02T15:04:05.000000", mainPart) if err != nil { return ISO8601{}, fmt.Errorf("ISO8601: %w", err) } adjustedTime, parseErr := adjustForUnconventionalOffset(tt, offset) if parseErr != nil { return ISO8601{}, fmt.Errorf("ISO8601: %w", parseErr) } return ISO8601(adjustedTime), nil } if idx+1 > idx+length || len(s) <= idx+length+1 { return ISO8601{}, fmt.Errorf("ISO8601: invalid timezone format: %s", s[idx:]) } offsetText := s[idx+1 : idx+length] offset, parseErr := strconv.Atoi(offsetText) if parseErr != nil { return ISO8601{}, fmt.Errorf("ISO8601: %w", parseErr) } // E.g. offset of +0300 is returned as 10800 which is - (3 * 60 * 60). secondsEastUTC := offset * 60 * 60 // fmt.Printf("parsing %s with offset %s, secondsEastUTC: %d, using time layout: %s\n", s, offsetText, secondsEastUTC, ISO8601ZUTCOffsetLayoutWithMicroseconds) if loc, ok := fixedEastUTCLocations[secondsEastUTC]; ok { // Specific (fixed) zone. if strings.Contains(s, ".") { tt, err = time.ParseInLocation(ISO8601ZUTCOffsetLayoutWithMicroseconds, s, loc) } else { tt, err = time.ParseInLocation(ISO8601ZUTCOffsetLayoutWithoutMicroseconds, s, loc) } } else { // Local or UTC. if strings.Contains(s, ".") { tt, err = time.Parse(ISO8601ZUTCOffsetLayoutWithMicroseconds, s) } else { tt, err = time.Parse(ISO8601ZUTCOffsetLayoutWithoutMicroseconds, s) } } } else if s[len(s)-1] == 'Z' { tt, err = time.Parse(ISO8601LayoutWithTimezone, s) } else { tt, err = time.Parse(ISO8601Layout, s) } if err != nil { return ISO8601{}, fmt.Errorf("ISO8601: %w", err) } return ISO8601(tt), nil } func parseWithOffset(s string) (time.Time, error) { idx := strings.LastIndexFunc(s, startUTCOffsetIndexFunc) if idx == -1 { return time.Time{}, fmt.Errorf("ISO8601: missing timezone offset") } offsetText := s[idx:] secondsEastUTC, err := parseOffsetToSeconds(offsetText) if err != nil { return time.Time{}, err } loc, ok := fixedEastUTCLocations[secondsEastUTC] if !ok { loc = time.FixedZone("", secondsEastUTC) } return time.ParseInLocation(ISO8601ZUTCOffsetLayoutWithoutMicroseconds, s, loc) } func parseOffsetToSeconds(offsetText string) (int, error) { if len(offsetText) < 6 { return 0, fmt.Errorf("ISO8601: invalid timezone offset length: %s", offsetText) } sign := offsetText[0] if sign != '-' && sign != '+' { return 0, fmt.Errorf("ISO8601: invalid timezone offset sign: %c", sign) } hours, err := strconv.Atoi(offsetText[1:3]) if err != nil { return 0, fmt.Errorf("ISO8601: %w", err) } minutes, err := strconv.Atoi(offsetText[4:6]) if err != nil { return 0, fmt.Errorf("ISO8601: %w", err) } secondsEastUTC := (hours*60 + minutes) * 60 if sign == '-' { secondsEastUTC = -secondsEastUTC } return secondsEastUTC, nil } func isUnconventionalOffset(offset string) bool { parts := strings.Split(offset, ":") return len(parts) == 3 } func adjustForUnconventionalOffset(t time.Time, offset string) (time.Time, error) { sign := 1 if offset[0] == '-' { sign = -1 } offset = offset[1:] offsetParts := strings.Split(offset, ":") if len(offsetParts) != 3 { return time.Time{}, fmt.Errorf("invalid offset format: %s", offset) } hours, err := strconv.Atoi(offsetParts[0]) if err != nil { return time.Time{}, fmt.Errorf("error parsing offset hours: %s: %w", offset, err) } if hours > 24 { return time.Time{}, fmt.Errorf("invalid offset hours: %d: %s", hours, offset) } minutes, err := strconv.Atoi(offsetParts[1]) if err != nil { return time.Time{}, fmt.Errorf("error parsing offset minutes: %s: %w", offset, err) } if minutes > 60 { return time.Time{}, fmt.Errorf("invalid offset minutes: %d: %s", minutes, offset) } seconds, err := strconv.Atoi(offsetParts[2]) if err != nil { return time.Time{}, fmt.Errorf("error parsing offset seconds: %s: %w", offset, err) } if seconds > 60 { return time.Time{}, fmt.Errorf("invalid offset seconds: %d: %s", seconds, offset) } totalOffset := time.Duration(sign) * (time.Duration(hours)*time.Hour + time.Duration(minutes)*time.Minute + time.Duration(seconds)*time.Second) return t.Add(-totalOffset), nil } // UnmarshalJSON parses the "b" into ISO8601 time. func (t *ISO8601) UnmarshalJSON(b []byte) error { if len(b) == 0 { return nil } s := strings.Trim(string(b), `"`) tt, err := ParseISO8601(s) if err != nil { return err } *t = tt return nil } // MarshalJSON writes a quoted string in the ISO8601 time format. func (t ISO8601) MarshalJSON() ([]byte, error) { if s := t.String(); s != "" { s = strconv.Quote(s) return []byte(s), nil } return nullLiteral, nil // Note: if the front-end wants an empty string instead I must change that. } // Examples returns a list of example values. func (t ISO8601) ListExamples() any { return []string{ "2024-01-02T15:04:05.999999Z", "2024-01-02T15:04:05+07:00", "2024-04-08T08:05:04.830140+00:00", "2024-01-02T15:04:05Z", "2024-04-08T08:05:04.830140", "2024-01-02T15:04:05", } } // ToTime returns the unwrapped *t to time.Time. func (t ISO8601) ToTime() time.Time { return time.Time(t) } // IsZero reports whether "t" is zero time. // It completes the pg.Zeroer interface. func (t ISO8601) IsZero() bool { return time.Time(t).IsZero() } // After reports whether the time instant "t" is after "u". func (t ISO8601) After(u ISO8601) bool { return t.ToTime().After(u.ToTime()) } // Equal reports whether the time instant "t" is equal to "u". func (t ISO8601) Equal(u ISO8601) bool { return t.ToTime().Equal(u.ToTime()) } // Add returns the time "t" with the duration added. func (t ISO8601) Add(d time.Duration) ISO8601 { return ISO8601(t.ToTime().Add(d)) } // Sub returns the duration between "t" and "u". func (t ISO8601) Sub(u ISO8601) time.Duration { return t.ToTime().Sub(u.ToTime()) } // String returns the text representation of the "t" using the ISO8601 time layout. func (t ISO8601) String() string { tt := t.ToTime() if tt.IsZero() { return "" } return tt.Format(ISO8601Layout) } // To24Hour returns the 24-hour representation of the time. func (t ISO8601) To24Hour() string { tt := t.ToTime() if tt.IsZero() { return "" } return tt.Format("15:04") } // ToSimpleDate converts the current ISO8601 "t" to SimpleDate. func (t ISO8601) ToSimpleDate() SimpleDate { return SimpleDateFromTime(t.ToTime()) } // ToSimpleDateIn converts the current ISO8601 "t" to SimpleDate in specific location. func (t ISO8601) ToSimpleDateIn(in *time.Location) SimpleDate { if in == nil { in = time.UTC } return SimpleDateFromTime(t.ToTime().In(in)) } // ToDayTime converts the current ISO8601 "t" to DayTime. func (t ISO8601) ToDayTime() DayTime { return DayTime(t.ToTime()) } // Value returns the database value of time.Time. func (t ISO8601) Value() (driver.Value, error) { return time.Time(t), nil } // Scan completes the sql driver.Scanner interface. func (t *ISO8601) Scan(src any) error { switch v := src.(type) { case time.Time: // type was set to timestamp if v.IsZero() { return nil // don't set zero, ignore it. } *t = ISO8601(v) case string: tt, err := ParseISO8601(v) if err != nil { return err } *t = tt case []byte: return t.Scan(string(v)) case nil: *t = ISO8601(time.Time{}) default: return fmt.Errorf("ISO8601: unknown type of: %T", v) } return nil } // parseSignedOffset parses a signed timezone offset (e.g. "+03" or "-04"). // The function checks for a signed number in the range -23 through +23 excluding zero. // Returns length of the found offset string or 0 otherwise. // // Language internal function. func parseSignedOffset(value string) int { sign := value[0] if sign != '-' && sign != '+' { return 0 } x, rem, err := leadingInt(value[1:]) // fail if nothing consumed by leadingInt if err != nil || value[1:] == rem { return 0 } if x > 23 { return 0 } return len(value) - len(rem) } var errLeadingInt = errors.New("ISO8601: time: bad [0-9]*") // never printed. // leadingInt consumes the leading [0-9]* from s. // // Language internal function. func leadingInt(s string) (x uint64, rem string, err error) { i := 0 for ; i < len(s); i++ { c := s[i] if c < '0' || c > '9' { break } if x > 1<<63/10 { // overflow return 0, "", errLeadingInt } x = x*10 + uint64(c) - '0' if x > 1<<63 { // overflow return 0, "", errLeadingInt } } return x, s[i:], nil } func startUTCOffsetIndexFunc(char rune) bool { return char == '+' || char == '-' } ================================================ FILE: x/jsonx/iso8601_test.go ================================================ package jsonx import ( "encoding/json" "testing" "time" ) func TestParseISO8601(t *testing.T) { tests := []struct { name string input string want ISO8601 wantErr bool }{ { name: "Timestamp with microseconds", input: "2024-01-02T15:04:05.999999Z", want: ISO8601(time.Date(2024, 01, 02, 15, 04, 05, 999999*1000, time.UTC)), wantErr: false, }, { name: "Timestamp with timezone but no microseconds", input: "2024-01-02T15:04:05+07:00", want: ISO8601(time.Date(2024, 01, 02, 15, 04, 05, 0, time.FixedZone("", 7*3600))), wantErr: false, }, { name: "Timestamp with timezone of UTC with microseconds", input: "2024-04-08T08:05:04.830140+00:00", // time.Date function interprets the nanosecond parameter. The time.Date function expects the nanosecond parameter to be the entire nanosecond part of the time, not just the microsecond part. // When we pass 830140 as the nanosecond argument, Go interprets this as 830140 nanoseconds, // which is equivalent to 000830140 microseconds (padded with leading zeros to fill the nanosecond precision). // This is why we see 2024-04-08 08:05:04.00083014 +0000 UTC as the output. // To correctly represent 830140 microseconds, we need to convert it to nanoseconds by multiplying by 1000 (or set the value to 830140000). want: ISO8601(time.Date(2024, 04, 8, 8, 05, 04, 830140*1000, time.UTC)), wantErr: false, }, { name: "Timestamp with timezone but no microseconds (2)", input: "2024-04-08T04:47:10+03:00", want: ISO8601(time.Date(2024, 04, 8, 4, 47, 10, 0, time.FixedZone("", 3*3600))), wantErr: false, }, { name: "Timestamp with Zulu time", input: "2024-01-02T15:04:05Z", want: ISO8601(time.Date(2024, 01, 02, 15, 04, 05, 0, time.UTC)), wantErr: false, }, { name: "Timestamp with Zulu time with microseconds", input: "2024-04-08T08:05:04.830140", want: ISO8601(time.Date(2024, 04, 8, 8, 05, 04, 830140*1000, time.UTC)), wantErr: false, }, { name: "Basic ISO8601 layout", input: "2024-01-02T15:04:05", want: ISO8601(time.Date(2024, 01, 02, 15, 04, 05, 0, time.UTC)), wantErr: false, }, { name: "Invalid format", input: "2024-01-02", want: ISO8601{}, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := ParseISO8601(tt.input) if (err != nil) != tt.wantErr { t.Errorf("ParseISO8601() error = %v, wantErr %v", err, tt.wantErr) return } if !tt.wantErr && !time.Time(got).Equal(time.Time(tt.want)) { t.Errorf("ParseISO8601() = %v (%s), want %v (%s)", got, got.ToTime().String(), tt.want, tt.want.ToTime().String()) } }) } } func TestISO8601(t *testing.T) { data := `{"start": "2021-08-20T10:05:01", "end": "2021-12-01T17:05:06", "nothing": null, "empty": ""}` v := struct { Start ISO8601 `json:"start"` End ISO8601 `json:"end"` Nothing ISO8601 `json:"nothing"` Empty ISO8601 `json:"empty"` }{} err := json.Unmarshal([]byte(data), &v) if err != nil { t.Fatal(err) } if !v.Nothing.IsZero() { t.Fatalf("expected 'nothing' to be zero but got: %v", v.Nothing) } if !v.Empty.IsZero() { t.Fatalf("expected 'empty' to be zero but got: %v", v.Empty) } loc := time.UTC if expected, got := time.Date(2021, time.August, 20, 10, 5, 1, 0, loc), v.Start.ToTime(); expected != got { t.Fatalf("expected 'start' to be: %v but got: %v", expected, got) } if expected, got := time.Date(2021, time.December, 1, 17, 5, 6, 0, loc), v.End.ToTime(); expected != got { t.Fatalf("expected 'start' to be: %v but got: %v", expected, got) } } func TestISO8601WithZoneUTCOffset(t *testing.T) { data := `{"start": "2022-08-10T03:21:00.000000+03:00", "end": "2022-08-10T09:49:00.000000+03:00", "nothing": null, "empty": ""}` v := struct { Start ISO8601 `json:"start"` End ISO8601 `json:"end"` Nothing ISO8601 `json:"nothing"` Empty ISO8601 `json:"empty"` }{} err := json.Unmarshal([]byte(data), &v) if err != nil { t.Fatalf("unmarshal: %v", err) } // t.Logf("Start: %s, location: %s\n", v.Start.String(), v.Start.ToTime().Location().String()) if !v.Nothing.IsZero() { t.Fatalf("expected 'nothing' to be zero but got: %v", v.Nothing) } if !v.Empty.IsZero() { t.Fatalf("expected 'empty' to be zero but got: %v", v.Empty) } loc := time.FixedZone("EEST", 10800) if expected, got := time.Date(2022, time.August, 10, 3, 21, 0, 0, loc).String(), v.Start.ToTime().String(); expected != got { t.Fatalf("expected 'start' string to be: %v but got: %v", expected, got) } if expected, got := time.Date(2022, time.August, 10, 9, 49, 0, 0, loc).String(), v.End.ToTime().String(); expected != got { t.Fatalf("expected 'end' string to be: %v but got: %v", expected, got) } if expected, got := time.Date(2022, time.August, 10, 3, 21, 0, 0, loc), v.Start.ToTime().In(loc); expected != got { t.Fatalf("expected 'start' to be: %v but got: %v", expected, got) } if expected, got := time.Date(2022, time.August, 10, 9, 49, 0, 0, loc), v.End.ToTime().In(loc); expected != got { t.Fatalf("expected 'end' to be: %v but got: %v", expected, got) } } func TestISO8601WithZoneUTCOffsetWithoutMilliseconds(t *testing.T) { data := `{"start": "2023-02-04T09:48:14+00:00", "end": "2023-02-05T00:03:16+00:00", "nothing": null, "empty": ""}` v := struct { Start ISO8601 `json:"start"` End ISO8601 `json:"end"` Nothing ISO8601 `json:"nothing"` Empty ISO8601 `json:"empty"` }{} err := json.Unmarshal([]byte(data), &v) if err != nil { t.Fatalf("unmarshal: %v", err) } // t.Logf("Start: %s, location: %s\n", v.Start.String(), v.Start.ToTime().Location().String()) if !v.Nothing.IsZero() { t.Fatalf("expected 'nothing' to be zero but got: %v", v.Nothing) } if !v.Empty.IsZero() { t.Fatalf("expected 'empty' to be zero but got: %v", v.Empty) } loc := time.FixedZone("UTC", 0) if expected, got := time.Date(2023, time.February, 04, 9, 48, 14, 0, loc).String(), v.Start.ToTime().String(); expected != got { t.Fatalf("expected 'start' string to be: %v but got: %v", expected, got) } if expected, got := time.Date(2023, time.February, 05, 0, 3, 16, 0, loc).String(), v.End.ToTime().String(); expected != got { t.Fatalf("expected 'end' string to be: %v but got: %v", expected, got) } if expected, got := time.Date(2023, time.February, 04, 9, 48, 14, 0, loc), v.Start.ToTime().In(loc); expected != got { t.Fatalf("expected 'start' to be: %v but got: %v", expected, got) } if expected, got := time.Date(2023, time.February, 05, 0, 3, 16, 0, loc), v.End.ToTime().In(loc); expected != got { t.Fatalf("expected 'end' to be: %v but got: %v", expected, got) } } func TestParseISO8601_StandardLayouts(t *testing.T) { tests := []struct { input string expected time.Time hasError bool }{ { input: "2024-05-21T18:06:07Z", expected: time.Date(2024, 5, 21, 18, 6, 7, 0, time.UTC), hasError: false, }, { input: "2024-05-21T18:06:07-04:00", expected: time.Date(2024, 5, 21, 22, 6, 7, 0, time.UTC), hasError: false, }, { input: "2024-05-21T18:06:07", expected: time.Date(2024, 5, 21, 18, 6, 7, 0, time.UTC), // no time local. hasError: false, }, } for _, tt := range tests { t.Run(tt.input, func(t *testing.T) { parsedTime, err := ParseISO8601(tt.input) if (err != nil) != tt.hasError { t.Errorf("ParseISO8601() error = %v, wantErr %v", err, tt.hasError) return } if !tt.hasError && !parsedTime.ToTime().Equal(tt.expected) { t.Errorf("ParseISO8601() = %v, want %v", parsedTime, tt.expected) } }) } } func TestParseISO8601_UnconventionalOffset(t *testing.T) { tests := []struct { input string expected time.Time hasError bool }{ { input: "2024-05-21T18:06:07.000000-04:01:19", expected: time.Date(2024, 5, 21, 22, 7, 26, 0, time.UTC), hasError: false, }, { input: "2024-12-31T23:59:59.000000-00:00:59", expected: time.Date(2025, 1, 1, 0, 0, 58, 0, time.UTC), hasError: false, }, { input: "2024-05-21T18:06:07.000000+03:30:15", expected: time.Date(2024, 5, 21, 14, 35, 52, 0, time.UTC), hasError: false, }, { input: "2024-05-21T18:06:07.000000-24:00:00", expected: time.Date(2024, 5, 22, 18, 6, 7, 0, time.UTC), hasError: false, }, { input: "2024-05-21T18:06:07.000000-04:61:19", // Invalid minute part in offset expected: time.Time{}, hasError: true, }, } for _, tt := range tests { t.Run(tt.input, func(t *testing.T) { parsedTime, err := ParseISO8601(tt.input) if (err != nil) != tt.hasError { t.Errorf("ParseISO8601() error = %v, wantErr %v", err, tt.hasError) return } if !tt.hasError && !parsedTime.ToTime().Equal(tt.expected) { t.Errorf("ParseISO8601() = %v, want %v", parsedTime, tt.expected) } }) } } func TestISO8601WithMicroseconds(t *testing.T) { data := `{"start": "2025-08-20T08:06:42.611522", "end": "2025-12-31T23:59:59.999999", "nothing": null, "empty": ""}` v := struct { Start ISO8601 `json:"start"` End ISO8601 `json:"end"` Nothing ISO8601 `json:"nothing"` Empty ISO8601 `json:"empty"` }{} err := json.Unmarshal([]byte(data), &v) if err != nil { t.Fatalf("unmarshal: %v", err) } if !v.Nothing.IsZero() { t.Fatalf("expected 'nothing' to be zero but got: %v", v.Nothing) } if !v.Empty.IsZero() { t.Fatalf("expected 'empty' to be zero but got: %v", v.Empty) } loc := time.UTC // Test the specific timestamp requested: 2025-08-20T08:06:42.611522 // 611522 microseconds = 611522000 nanoseconds if expected, got := time.Date(2025, time.August, 20, 8, 6, 42, 611522000, loc), v.Start.ToTime(); expected != got { t.Fatalf("expected 'start' to be: %v but got: %v", expected, got) } // Test another timestamp with maximum microseconds if expected, got := time.Date(2025, time.December, 31, 23, 59, 59, 999999000, loc), v.End.ToTime(); expected != got { t.Fatalf("expected 'end' to be: %v but got: %v", expected, got) } // Verify the string format excludes microseconds (as per ISO8601Layout) if expected, got := "2025-08-20T08:06:42", v.Start.String(); expected != got { t.Fatalf("expected 'start' string to be: %v but got: %v", expected, got) } } ================================================ FILE: x/jsonx/jsonx.go ================================================ package jsonx import ( "bytes" "errors" ) var ( quoteLiteral = '"' emptyQuoteBytes = []byte(`""`) nullLiteral = []byte("null") // ErrInvalid is returned when the value is invalid. ErrInvalid = errors.New("invalid") ) func isNull(b []byte) bool { return len(b) == 0 || bytes.Equal(b, nullLiteral) } func trimQuotesFunc(r rune) bool { return r == quoteLiteral } func trimQuotes(b []byte) []byte { return bytes.TrimFunc(b, trimQuotesFunc) } ================================================ FILE: x/jsonx/kitchen_time.go ================================================ package jsonx import ( "fmt" "strconv" "strings" "time" ) // KitchenTimeLayout represents the "3:04 PM" Go time format, similar to time.Kitchen. const KitchenTimeLayout = "3:04 PM" // KitchenTime holds a json "3:04 PM" time. type KitchenTime time.Time var ErrParseKitchenTimeColon = fmt.Errorf("parse kitchen time: missing ':' character") func parseKitchenTime(s string) (KitchenTime, error) { // Remove any second,millisecond variable (probably given by postgres 00:00:00.000000). // required(00:00)remove(:00.000000) firstIndex := strings.IndexByte(s, ':') if firstIndex == -1 { return KitchenTime{}, ErrParseKitchenTimeColon } else { nextIndex := strings.LastIndexByte(s, ':') spaceIdx := strings.LastIndexByte(s, ' ') if nextIndex > firstIndex && spaceIdx > 0 { tmp := s[0:nextIndex] s = tmp + s[spaceIdx:] } } tt, err := time.Parse(KitchenTimeLayout, s) if err != nil { return KitchenTime{}, err } return KitchenTime(tt), nil } // ParseKitchenTime reads from "s" and returns the KitchenTime time. func ParseKitchenTime(s string) (KitchenTime, error) { if s == "" || s == "null" { return KitchenTime{}, nil } return parseKitchenTime(s) } // UnmarshalJSON binds the json "data" to "t" with the `KitchenTimeLayout`. func (t *KitchenTime) UnmarshalJSON(data []byte) error { if t == nil { return fmt.Errorf("kitchen time: dest is nil") } if isNull(data) { return nil } data = trimQuotes(data) if len(data) == 0 { return nil } tt, err := parseKitchenTime(string(data)) if err != nil { return err } *t = KitchenTime(tt) return nil } // MarshalJSON returns the json representation of the "t". func (t KitchenTime) MarshalJSON() ([]byte, error) { if s := t.String(); s != "" { s = strconv.Quote(s) return []byte(s), nil } return emptyQuoteBytes, nil } // IsZero reports whether "t" is zero time. // It completes the pg.Zeroer interface. func (t KitchenTime) IsZero() bool { return t.Value().IsZero() } // Value returns the standard time type. func (t KitchenTime) Value() time.Time { return time.Time(t) } // String returns the text representation of the date // formatted based on the `KitchenTimeLayout`. // If date is zero it returns an empty string. func (t KitchenTime) String() string { tt := t.Value() if tt.IsZero() { return "" } return tt.Format(KitchenTimeLayout) } // Scan completes the pg and native sql driver.Scanner interface // reading functionality of a custom type. func (t *KitchenTime) Scan(src any) error { switch v := src.(type) { case time.Time: // type was set to timestamp. if v.IsZero() { return nil // don't set zero, ignore it. } *t = KitchenTime(v) case string: // type was set to time, input example: 10:00:00.000000 d, err := ParseTimeNotationDuration(v) if err != nil { return fmt.Errorf("kitchen time: convert to time notation first: %w", err) } s := kitchenTimeStringFromDuration(d.ToDuration()) *t, err = ParseKitchenTime(s) return err case int64: // timestamp with integer. u := time.Unix(v/1000, v%1000) s := kitchenTimeStringFromHourAndMinute(u.Hour(), u.Minute()) tt, err := ParseKitchenTime(s) if err != nil { return err } *t = tt case nil: *t = KitchenTime(time.Time{}) default: return fmt.Errorf("KitchenTime: unknown type of: %T", v) } return nil } func kitchenTimeStringFromDuration(dt time.Duration) string { hour := int(dt.Hours()) minute := 0 if totalMins := dt.Minutes(); totalMins > 0 { minute := int(totalMins / 60) if minute < 0 { minute = 0 } } return kitchenTimeStringFromHourAndMinute(hour, minute) } func kitchenTimeStringFromHourAndMinute(hour, minute int) string { ampm := "AM" if hour/12 == 1 { ampm = "PM" } th := hour % 12 hh := strconv.Itoa(th) if th < 10 { hh = "0" + hh } tm := minute mm := strconv.Itoa(tm) if tm < 10 { mm = "0" + mm } return hh + ":" + mm + " " + ampm } ================================================ FILE: x/jsonx/kitchen_time_test.go ================================================ package jsonx import ( "encoding/json" "testing" "time" ) func TestJSONKitchenTime(t *testing.T) { tests := []struct { rawData string }{ { rawData: `{"start": "8:33 AM", "end": "3:04 PM", "nothing": null, "empty": ""}`, }, { rawData: `{"start": "08:33 AM", "end": "03:04 PM", "nothing": null, "empty": ""}`, }, { rawData: `{"start": "08:33:00.000000 AM", "end": "03:04 PM", "nothing": null, "empty": ""}`, }, } for _, tt := range tests { v := struct { Start KitchenTime `json:"start"` End KitchenTime `json:"end"` Nothing KitchenTime `json:"nothing"` Empty KitchenTime `json:"empty"` }{} err := json.Unmarshal([]byte(tt.rawData), &v) if err != nil { t.Fatal(err) } if !v.Nothing.IsZero() { t.Fatalf("expected 'nothing' to be zero but got: %v", v.Nothing) } if !v.Empty.IsZero() { t.Fatalf("expected 'empty' to be zero but got: %v", v.Empty) } loc := time.UTC if expected, got := time.Date(0, time.January, 1, 8, 33, 0, 0, loc), v.Start.Value(); expected != got { t.Fatalf("expected 'start' to be: %v but got: %v", expected, got) } if expected, got := time.Date(0, time.January, 1, 15, 4, 0, 0, loc), v.End.Value(); expected != got { t.Fatalf("expected 'start' to be: %v but got: %v", expected, got) } } } ================================================ FILE: x/jsonx/season.go ================================================ package jsonx import ( "fmt" "strconv" "strings" "time" ) // GetSeasonByDate returns the season based on the given date. func GetSeasonByDate(date SimpleDate) Season { month := date.ToTime().Month() switch month { case time.December, time.January, time.February: return Winter case time.March, time.April, time.May: return Spring case time.June, time.July, time.August: return Summer case time.September, time.October, time.November: return Autumn default: return 0 // Should never happen. } } // Season is a bitmask of seasons // Winter, Spring, Summer, Autumn. // It is used to represent the seasons of a year. // It is a bitmask so that multiple seasons can be set at once. // It has methods to check if a season is set, add a season, // remove a season, and get a string representation of the seasons. type Season int const ( // December, January, February. Winter Season = 1 << iota // 1 << 0 = 1 // March, April, May. Spring // 1 << 1 = 2 // June, July, August. Summer // 1 << 2 = 4 // September, October, November. Autumn // 1 << 3 = 8 // AllSeasons is a bitmask of all seasons. // It's the number 15 because it's the sum of all seasons. AllSeasons = Winter | Spring | Summer | Autumn // 1 + 2 + 4 + 8 = 15 ) // IsValid checks if the season is valid. func (s Season) IsValid() bool { return s&AllSeasons == s } // Is checks if the season is set in the bitmask. func (s Season) Is(season Season) bool { return s&season != 0 } // Add adds a season to the bitmask. func (s *Season) Add(season Season) { *s |= season } // Remove removes a season from the bitmask. func (s *Season) Remove(season Season) { *s &= ^season } // String returns the string representation of the season(s). func (s Season) String() string { var seasons []string if s.Is(Winter) { seasons = append(seasons, "Winter") } if s.Is(Spring) { seasons = append(seasons, "Spring") } if s.Is(Summer) { seasons = append(seasons, "Summer") } if s.Is(Autumn) { seasons = append(seasons, "Autumn") } if len(seasons) == 0 { return "None" } return strings.Join(seasons, ", ") } // MarshalJSON marshals the season to JSON. // It marshals the season as a string. func (s *Season) UnmarshalJSON(data []byte) error { if isNull(data) { return nil } data = trimQuotes(data) if len(data) == 0 { return nil } str := string(data) constantAsInt, err := strconv.Atoi(str) if err != nil { return err } if constantAsInt == 0 { // if 0 is passed, it means All seasons. constantAsInt = int(AllSeasons) } *s = Season(constantAsInt) if !s.IsValid() { return fmt.Errorf("%w: season: %s", ErrInvalid, str) } return nil } ================================================ FILE: x/jsonx/simple_date.go ================================================ package jsonx import ( "database/sql/driver" "encoding/json" "fmt" "strconv" "time" "github.com/kataras/iris/v12/x/timex" ) const ( // SimpleDateLayout represents the "year-month-day" Go time format. SimpleDateLayout = "2006-01-02" simpleDateLayoutPostgres = "2006-1-2" ) // SimpleDate holds a json "year-month-day" time. type SimpleDate time.Time var _ Exampler = (*SimpleDate)(nil) // SimpleDateFromTime accepts a "t" Time and returns // a SimpleDate. If format fails, it returns the zero value of time.Time. func SimpleDateFromTime(t time.Time) SimpleDate { date, _ := ParseSimpleDate(t.Format(SimpleDateLayout)) return date } // ParseSimpleDate reads from "s" and returns the SimpleDate time. // // The function supports the following formats: // - "2024-01-01" // - "2024-1-1" func ParseSimpleDate(s string) (SimpleDate, error) { if s == "" || s == "null" { return SimpleDate{}, nil } var ( tt time.Time err error ) tt, err = time.Parse(SimpleDateLayout, s) if err != nil { // After v5.0.0-alpha.3 of pgx this is coming as "1993-1-1" instead of the stored // value "1993-01-01". var err2 error tt, err2 = time.Parse(simpleDateLayoutPostgres, s) if err2 != nil { return SimpleDate{}, fmt.Errorf("%s: %w", err2.Error(), err) } } return SimpleDate(tt), nil } // UnmarshalJSON binds the json "data" to "t" with the `SimpleDateLayout`. func (t *SimpleDate) UnmarshalJSON(data []byte) error { if isNull(data) { return nil } data = trimQuotes(data) dataStr := string(data) if len(dataStr) == 0 { return nil // do not allow empty "" on simple dates. } tt, err := time.Parse(SimpleDateLayout, dataStr) if err != nil { return err } *t = SimpleDate(tt) return nil } // MarshalJSON returns the json representation of the "t". func (t SimpleDate) MarshalJSON() ([]byte, error) { if s := t.String(); s != "" { s = strconv.Quote(s) return []byte(s), nil } return emptyQuoteBytes, nil } // Examples returns a list of example values. func (t SimpleDate) ListExamples() any { return []string{ "2024-01-01", "2024-1-1", } } // IsZero reports whether "t" is zero time. // It completes the pg.Zeroer interface. func (t SimpleDate) IsZero() bool { return t.ToTime().IsZero() } // Add returns the date of "t" plus "d". func (t SimpleDate) Add(d time.Duration) SimpleDate { return SimpleDateFromTime(t.ToTime().Add(d)) } // CountPastDays returns the count of days between "t" and "pastDate". func (t SimpleDate) CountPastDays(pastDate SimpleDate) int { t1, t2 := t.ToTime(), pastDate.ToTime() return int(t1.Sub(t2).Hours() / 24) } // Equal reports back if "t" and "d" equals to the same date. func (t SimpleDate) Equal(d SimpleDate) bool { return t.String() == d.String() } // After reports whether the time instant t is after u. func (t SimpleDate) After(d2 SimpleDate) bool { t1, t2 := t.ToTime(), d2.ToTime() return t1.Truncate(24 * time.Hour).After(t2.Truncate(24 * time.Hour)) } // Before reports whether the time instant t is before u. func (t SimpleDate) Before(d2 SimpleDate) bool { t1, t2 := t.ToTime(), d2.ToTime() return t1.Truncate(24 * time.Hour).Before(t2.Truncate(24 * time.Hour)) // OR: compare year and year's day. } // ToTime returns the standard time type. func (t SimpleDate) ToTime() time.Time { return time.Time(t) } // Value completes the pg and native sql driver.Valuer interface. func (t SimpleDate) Value() (driver.Value, error) { return t.String(), nil } // String returns the text representation of the date // formatted based on the `SimpleDateLayout`. // If date is zero it returns an empty string. func (t SimpleDate) String() string { tt := t.ToTime() if tt.IsZero() { return "" } return tt.Format(SimpleDateLayout) } // Scan completes the pg and native sql driver.Scanner interface // reading functionality of a custom type. func (t *SimpleDate) Scan(src any) error { switch v := src.(type) { case time.Time: // type was set to timestamp if v.IsZero() { return nil // don't set zero, ignore it. } *t = SimpleDate(v) case string: tt, err := ParseSimpleDate(v) if err != nil { return err } *t = tt case nil: *t = SimpleDate(time.Time{}) default: return fmt.Errorf("SimpleDate: unknown type of: %T", v) } return nil } // Slice of SimpleDate. type SimpleDates []SimpleDate // First returns the first element of the date slice. func (t SimpleDates) First() SimpleDate { if len(t) == 0 { return SimpleDate{} } return t[0] } // Last returns the last element of the date slice. func (t SimpleDates) Last() SimpleDate { if len(t) == 0 { return SimpleDate{} } return t[len(t)-1] } // DateStrings returns a slice of string representation of the dates. func (t SimpleDates) DateStrings() []string { list := make([]string, 0, len(t)) for _, d := range t { list = append(list, d.String()) } return list } // Scan completes the pg and native sql driver.Scanner interface. func (t *SimpleDates) Scan(src any) error { if src == nil { return nil } var data []byte switch v := src.(type) { case []byte: data = v case string: data = []byte(v) default: return fmt.Errorf("simple dates: scan: invalid type of: %T", src) } err := json.Unmarshal(data, t) return err } // Value completes the pg and native sql driver.Valuer interface. func (t SimpleDates) Value() (driver.Value, error) { if len(t) == 0 { return nil, nil } b, err := json.Marshal(t) return b, err } // Contains reports if the "date" exists inside "t". func (t SimpleDates) Contains(date SimpleDate) bool { for _, v := range t { if v.Equal(date) { return true } } return false } // DateRangeType is the type of the date range. type DateRangeType string const ( // DayRange is the date range type of a day. DayRange DateRangeType = "day" // MonthRange is the date range type of a month. MonthRange DateRangeType = "month" // WeekRange is the date range type of a week. WeekRange DateRangeType = "week" // YearRange is the date range type of a year. YearRange DateRangeType = "year" ) // GetSimpleDateRange returns a slice of SimpleDate between "start" and "end" pf "date" // based on given "typ" (WeekRange, MonthRange...). // // Example Code: // date := jsonx.SimpleDateFromTime(time.Now()) // dates := jsonx.GetSimpleDateRange(date, jsonx.WeekRange, time.Monday, time.Sunday) func GetSimpleDateRange(date SimpleDate, typ DateRangeType, startWeekday, endWeekday time.Weekday) SimpleDates { var dates []time.Time switch typ { case WeekRange: dates = timex.GetWeekdays(date.ToTime(), startWeekday, endWeekday) case MonthRange: dates = timex.GetMonthDays(date.ToTime()) default: panic(fmt.Sprintf("invalid DateRangeType given: %s", typ)) } simpleDates := make(SimpleDates, len(dates)) for i, t := range dates { simpleDates[i] = SimpleDateFromTime(t) } return simpleDates } ================================================ FILE: x/jsonx/simple_date_test.go ================================================ package jsonx import ( "encoding/json" "testing" "time" ) func TestJSONSimpleDate(t *testing.T) { data := `{"start": "2021-08-20", "end": "2021-12-01", "nothing": null, "empty": ""}` v := struct { Start SimpleDate `json:"start"` End SimpleDate `json:"end"` Nothing SimpleDate `json:"nothing"` Empty SimpleDate `json:"empty"` }{} err := json.Unmarshal([]byte(data), &v) if err != nil { t.Fatal(err) } if !v.Nothing.IsZero() { t.Fatalf("expected 'nothing' to be zero but got: %v", v.Nothing) } if !v.Empty.IsZero() { t.Fatalf("expected 'empty' to be zero but got: %v", v.Empty) } loc := time.UTC if expected, got := time.Date(2021, time.August, 20, 0, 0, 0, 0, loc), v.Start.ToTime(); expected != got { t.Fatalf("expected 'start' to be: %v but got: %v", expected, got) } if expected, got := time.Date(2021, time.December, 1, 0, 0, 0, 0, loc), v.End.ToTime(); expected != got { t.Fatalf("expected 'start' to be: %v but got: %v", expected, got) } } func TestSimpleDateAterBefore(t *testing.T) { d1, d2 := SimpleDateFromTime(time.Now()), SimpleDateFromTime(time.Now().AddDate(0, 0, 1)) if d1.After(d2) { t.Fatalf("[after] expected d1 to be before d2") } if !d1.Before(d2) { t.Fatalf("[before] expected d1 to be before d2") } if d2.Before(d1) { t.Fatalf("[after] expected d2 to be after d1") } if !d2.After(d1) { t.Fatalf("[after] expected d2 to be after d1") } } func TestCountPastDays(t *testing.T) { tests := []struct { AfterDate SimpleDate BeforeDate SimpleDate ExpectedPastDays int }{ { AfterDate: mustParseSimpleDate("2023-01-01"), BeforeDate: mustParseSimpleDate("2022-12-31"), ExpectedPastDays: 1, }, { AfterDate: mustParseSimpleDate("2023-01-01"), BeforeDate: mustParseSimpleDate("2022-01-01"), ExpectedPastDays: 365, }, } for i, tt := range tests { if expected, got := tt.ExpectedPastDays, tt.AfterDate.CountPastDays(tt.BeforeDate); expected != got { t.Fatalf("[%d] expected past days count: %d but got: %d", i, expected, got) } } } func mustParseSimpleDate(s string) SimpleDate { d, err := ParseSimpleDate(s) if err != nil { panic(err) } return d } ================================================ FILE: x/jsonx/time_notation.go ================================================ package jsonx import ( "bytes" "database/sql/driver" "encoding/json" "errors" "fmt" "math" "strconv" "strings" "time" ) // TimeNotationDuration is a JSON representation of the standard Duration type in 00:00:00 (hour, minute seconds). type TimeNotationDuration time.Duration func fmtDuration(d time.Duration) string { d = d.Round(time.Minute) h := d / time.Hour d -= h * time.Hour m := d / time.Minute return fmt.Sprintf("%02d:%02d:00", h, m) // Manos doesn't care about the seconds. } // ParseTimeNotationDuration parses a string to a time notation duration (00:00:00) hours:minutes:seconds. func ParseTimeNotationDuration(s string) (TimeNotationDuration, error) { entries := strings.SplitN(s, ":", 3) if len(entries) < 3 { return TimeNotationDuration(0), fmt.Errorf("invalid duration format: expected hours:minutes:seconds (e.g. 01:05:00) but got: %s", s) } hours, err := strconv.Atoi(entries[0]) if err != nil { return TimeNotationDuration(0), err } minutes, err := strconv.Atoi(entries[1]) if err != nil { return TimeNotationDuration(0), err } // remove any .0000 secondsStr := strings.TrimSuffix(entries[2], ".000000") seconds, err := strconv.Atoi(secondsStr) if err != nil { return TimeNotationDuration(0), err } format := fmt.Sprintf("%02dh%02dm%02ds", hours, minutes, seconds) v, err := time.ParseDuration(format) if err != nil { return TimeNotationDuration(0), err } return TimeNotationDuration(v), nil } func (d TimeNotationDuration) MarshalJSON() ([]byte, error) { v := d.ToDuration() format := fmtDuration(v) return []byte(strconv.Quote(format)), nil } func (d *TimeNotationDuration) UnmarshalJSON(b []byte) error { if len(b) == 0 || bytes.Equal(b, nullLiteral) || bytes.Equal(b, emptyQuoteBytes) { // if null or empty don't throw an error. return nil } var v any if err := json.Unmarshal(b, &v); err != nil { return err } switch value := v.(type) { case float64: *d = TimeNotationDuration(value) return nil case string: dv, err := ParseTimeNotationDuration(value) if err != nil { return err } *d = dv return nil default: return errors.New("invalid duration") } } func (d TimeNotationDuration) ToDuration() time.Duration { return time.Duration(d) } func (d TimeNotationDuration) Value() (driver.Value, error) { return d.ToDuration(), nil } // Set sets the value of duration in nanoseconds. func (d *TimeNotationDuration) Set(v float64) { if math.IsNaN(v) { return } *d = TimeNotationDuration(v) } ================================================ FILE: x/mathx/round.go ================================================ package mathx import "math" // Round rounds the "input" on "roundOn" (e.g. 0.5) on "places" digits. func Round(input float64, roundOn float64, places float64) float64 { pow := math.Pow(10, places) digit := pow * input _, div := math.Modf(digit) if div >= roundOn { return math.Ceil(digit) / pow } return math.Floor(digit) / pow } // RoundUp rounds up the "input" up to "places" digits. func RoundUp(input float64, places float64) float64 { pow := math.Pow(10, places) return math.Ceil(pow*input) / pow } // RoundDown rounds down the "input" up to "places" digits. func RoundDown(input float64, places float64) float64 { pow := math.Pow(10, places) return math.Floor(pow*input) / pow } // RoundToInteger rounds the given float64 to an integer. func RoundToInteger(x float64) int { // If the number is 1.1 round it to the previous integer (1), if >= 1.11 round it to the next one (2). t := math.Trunc(x) odd := math.Remainder(t, 2) != 0 d := math.Abs(x - t) d = Round(d, 0.5, 2) // round to 2 decimals so we can easily check 0.11 and 0.1 and 0.2. if d > 0.1 || (d == 0.2 && odd) { // fmt.Printf("%f-%f -> %f. Is > 0.1 -> %v \n", x, t, d, d > 0.1) t = t + math.Copysign(1, x) return int(t) } return int(t) } ================================================ FILE: x/pagination/pagination.go ================================================ //go:build go1.18 // +build go1.18 /* Until go version 2, we can't really apply the type alias feature on a generic type or function, so keep it separated on x/pagination. import "github.com/kataras/iris/v12/context" type ListResponse[T any] = context.ListResponse[T] OR type ListResponse = context.ListResponse doesn't work. The only workable thing for generic aliases is when you know the type e.g. type ListResponse = context.ListResponse[any] but that doesn't fit us. */ package pagination import ( "math" "net/http" "strconv" ) var ( // MaxSize defines the max size of items to display. MaxSize = 100000 // DefaultSize defines the default size when ListOptions.Size is zero. DefaultSize = MaxSize ) // ListOptions is the list request object which should be provided by the client through // URL Query. Then the server passes that options to a database query, // including any custom filters may be given from the request body and, // then the server responds back with a `Context.JSON(NewList(...))` response based // on the database query's results. type ListOptions struct { // Current page number. // If Page > 0 then: // Limit = DefaultLimit // Offset = DefaultLimit * Page // If Page == 0 then no actual data is return, // internally we must check for this value // because in postgres LIMIT 0 returns the columns but with an empty set. Page int `json:"page" url:"page"` // The elements to get, this modifies the LIMIT clause, // this Size can't be higher than the MaxSize. // If Size is zero then size is set to DefaultSize. Size int `json:"size" url:"size"` } // GetLimit returns the LIMIT value of a query. func (opts ListOptions) GetLimit() int { if opts.Size > 0 && opts.Size < MaxSize { return opts.Size } return DefaultSize } // GetLimit returns the OFFSET value of a query. func (opts ListOptions) GetOffset() int { if opts.Page > 1 { return (opts.Page - 1) * opts.GetLimit() } return 0 } // GetCurrentPage returns the Page or 1. func (opts ListOptions) GetCurrentPage() int { current := opts.Page if current == 0 { current = 1 } return current } // GetNextPage returns the next page, current page + 1. func (opts ListOptions) GetNextPage() int { return opts.GetCurrentPage() + 1 } // Bind binds the ListOptions values to a request value. // It should be used as an x/client.RequestOption to fire requests // on a server that supports pagination. func (opts ListOptions) Bind(r *http.Request) error { page := strconv.Itoa(opts.GetCurrentPage()) size := strconv.Itoa(opts.GetLimit()) q := r.URL.Query() q.Set("page", page) q.Set("size", size) return nil } // List is the http response of a server handler which should render // items with pagination support. type List[T any] struct { CurrentPage int `json:"current_page"` // the current page. PageSize int `json:"page_size"` // the total amount of the entities return. TotalPages int `json:"total_pages"` // the total number of pages based on page, size and total count. TotalItems int64 `json:"total_items"` // the total number of rows. HasNextPage bool `json:"has_next_page"` // true if more data can be fetched, depending on the current page * page size and total pages. Filter any `json:"filter"` // if any filter data. Items []T `json:"items"` // Items is empty array if no objects returned. Do NOT modify from outside. } // NewList returns a new List response which holds // the current page, page size, total pages, total items count, any custom filter // and the items array. // // Example Code: // // import "github.com/kataras/iris/v12/x/pagination" // ...more code // // type User struct { // Firstname string `json:"firstname"` // Lastname string `json:"lastname"` // } // // type ExtraUser struct { // User // ExtraData string // } // // func main() { // users := []User{ // {"Gerasimos", "Maropoulos"}, // {"Efi", "Kwfidou"}, // } // // t := pagination.NewList(users, 100, nil, pagination.ListOptions{ // Page: 1, // Size: 50, // }) // // // Optionally, transform a T list of objects to a V list of objects. // v, err := pagination.TransformList(t, func(u User) (ExtraUser, error) { // return ExtraUser{ // User: u, // ExtraData: "test extra data", // }, nil // }) // if err != nil { panic(err) } // // paginationJSON, err := json.MarshalIndent(v, "", " ") // if err!=nil { panic(err) } // fmt.Println(paginationJSON) // } func NewList[T any](items []T, totalCount int64, filter any, opts ListOptions) *List[T] { pageSize := opts.GetLimit() n := len(items) if n == 0 || pageSize <= 0 { return &List[T]{ CurrentPage: 1, PageSize: 0, TotalItems: 0, TotalPages: 0, Filter: filter, Items: make([]T, 0), } } numberOfPages := int(roundUp(float64(totalCount)/float64(pageSize), 0)) if numberOfPages <= 0 { numberOfPages = 1 } var hasNextPage bool currentPage := opts.GetCurrentPage() if totalCount == 0 { currentPage = 1 } if n > 0 { hasNextPage = currentPage < numberOfPages } return &List[T]{ CurrentPage: currentPage, PageSize: n, TotalPages: numberOfPages, TotalItems: totalCount, HasNextPage: hasNextPage, Filter: filter, Items: items, } } // TransformList accepts a List response and converts to a list of V items. // T => from // V => to // // Example Code: // // listOfUsers := pagination.NewList(...) // newListOfExtraUsers, err := pagination.TransformList(listOfUsers, func(u User) (ExtraUser, error) { // return ExtraUser{ // User: u, // ExtraData: "test extra data", // }, nil // }) func TransformList[T any, V any](list *List[T], transform func(T) (V, error)) (*List[V], error) { if list == nil { return &List[V]{ CurrentPage: 1, PageSize: 0, TotalItems: 0, TotalPages: 0, Filter: nil, Items: make([]V, 0), }, nil } items := list.Items toItems := make([]V, 0, len(items)) for _, fromItem := range items { toItem, err := transform(fromItem) if err != nil { return nil, err } toItems = append(toItems, toItem) } newList := &List[V]{ CurrentPage: list.CurrentPage, PageSize: list.PageSize, TotalItems: list.TotalItems, TotalPages: list.TotalPages, Filter: list.Filter, Items: toItems, } return newList, nil } func roundUp(input float64, places float64) float64 { pow := math.Pow(10, places) return math.Ceil(pow*input) / pow } ================================================ FILE: x/reflex/error.go ================================================ package reflex import "reflect" // IsError reports whether "typ" is an error type. func IsError(typ interface{ Implements(reflect.Type) bool }) bool { return typ.Implements(ErrTyp) } ================================================ FILE: x/reflex/func.go ================================================ package reflex import "reflect" // IsFunc reports whether the "kindable" is a type of function. func IsFunc(typ interface{ Kind() reflect.Kind }) bool { return typ.Kind() == reflect.Func } // FuncParam holds the properties of function input or output. type FuncParam struct { Index int Type reflect.Type } // LookupInputs returns the index and type of each function's input argument. // Panics if "fn" is not a type of Func. func LookupInputs(fn reflect.Type) []FuncParam { n := fn.NumIn() params := make([]FuncParam, 0, n) for i := 0; i < n; i++ { in := fn.In(i) params = append(params, FuncParam{ Index: i, Type: in, }) } return params } // LookupOutputs returns the index and type of each function's output argument. // Panics if "fn" is not a type of Func. func LookupOutputs(fn reflect.Type) []FuncParam { n := fn.NumOut() params := make([]FuncParam, 0, n) for i := 0; i < n; i++ { out := fn.Out(i) params = append(params, FuncParam{ Index: i, Type: out, }) } return params } ================================================ FILE: x/reflex/reflex.go ================================================ package reflex import "reflect" // IndirectType returns the value of a pointer-type "typ". // If "IndirectType" is a pointer, array, chan, map or slice it returns its Elem, // otherwise returns the "typ" as it is. func IndirectType(typ reflect.Type) reflect.Type { switch typ.Kind() { case reflect.Ptr, reflect.Array, reflect.Chan, reflect.Map, reflect.Slice: return typ.Elem() } return typ } // IndirectValue returns the element type (e.g. if pointer of *User it will return the User type). func IndirectValue(val reflect.Value) reflect.Value { return reflect.Indirect(val) } ================================================ FILE: x/reflex/struct.go ================================================ package reflex import "reflect" // LookupFields returns a slice of all fields containing a struct field // of the given "fieldTag" of the "typ" struct. The fields returned // are flatted and reclusive over fields with value of struct. // Panics if "typ" is not a type of Struct. func LookupFields(typ reflect.Type, fieldTag string) []reflect.StructField { fields := lookupFields(typ, fieldTag, nil) return fields[0:len(fields):len(fields)] } func lookupFields(typ reflect.Type, fieldTag string, parentIndex []int) []reflect.StructField { n := typ.NumField() fields := make([]reflect.StructField, 0, n) checkTag := fieldTag != "" for i := 0; i < n; i++ { field := typ.Field(i) if field.PkgPath != "" { // skip unexported fields. continue } if checkTag { if v := field.Tag.Get(fieldTag); v == "" || v == "-" { // Skip fields that don't contain the 'fieldTag' tag or has '-'. continue } } fieldType := IndirectType(field.Type) if fieldType.Kind() == reflect.Struct { // It's a struct inside a struct and it's not time, flat it. if fieldType != TimeType { structFields := lookupFields(fieldType, fieldTag, append(parentIndex, i)) if nn := len(structFields); nn > 0 { fields = append(fields, structFields...) continue } } } index := []int{i} if len(parentIndex) > 0 { index = append(parentIndex, i) } tmp := make([]int, len(index)) copy(tmp, index) field.Index = tmp fields = append(fields, field) } return fields } // LookupUnderlineValueType returns the underline type of "v". func LookupUnderlineValueType(v reflect.Value) (reflect.Value, reflect.Type) { typ := v.Type() for typ.Kind() == reflect.Ptr { typ = typ.Elem() v = reflect.New(typ).Elem() } return v, typ } ================================================ FILE: x/reflex/types.go ================================================ package reflex import ( "encoding/json" "fmt" "net" "reflect" "time" ) // Common reflect types for go standard data types. var ( StringType = reflect.TypeOf("") BytesType = reflect.TypeOf([]byte{}) IntType = reflect.TypeOf(int(0)) Int16Type = reflect.TypeOf(int16(0)) Int32Type = reflect.TypeOf(int32(0)) Int64Type = reflect.TypeOf(int64(0)) Float32Type = reflect.TypeOf(float32(0)) Float64Type = reflect.TypeOf(float64(0)) TimeType = reflect.TypeOf(time.Time{}) IpTyp = reflect.TypeOf(net.IP{}) JSONNumberTyp = reflect.TypeOf(json.Number("")) StringerTyp = reflect.TypeOf((*fmt.Stringer)(nil)).Elem() ArrayIntegerTyp = reflect.TypeOf([]int{}) ArrayStringTyp = reflect.TypeOf([]string{}) DoubleArrayIntegerTyp = reflect.TypeOf([][]int{}) DoubleArrayStringTyp = reflect.TypeOf([][]string{}) ErrTyp = reflect.TypeOf((*error)(nil)).Elem() ) ================================================ FILE: x/reflex/zero.go ================================================ package reflex import ( "encoding/json" "net" ) // Zeroer can be implemented by custom types // to report whether its current value is zero. // Standard Time also implements that. type Zeroer interface { IsZero() bool } // IsZero reports whether "v" is zero value or no. // The given "v" value can complete the Zeroer interface // which can be used to customize the behavior for each type of "v". func IsZero(v any) bool { switch t := v.(type) { case Zeroer: // completes the time.Time as well. return t.IsZero() case string: return t == "" case int: return t == 0 case int8: return t == 0 case int16: return t == 0 case int32: return t == 0 case int64: return t == 0 case uint: return t == 0 case uint8: return t == 0 case uint16: return t == 0 case uint32: return t == 0 case uint64: return t == 0 case float32: return t == 0 case float64: return t == 0 case bool: return !t case []int: return len(t) == 0 case []string: return len(t) == 0 case [][]int: return len(t) == 0 case [][]string: return len(t) == 0 case json.Number: return t.String() == "" case net.IP: return len(t) == 0 default: return false } } ================================================ FILE: x/sqlx/sqlx.go ================================================ package sqlx import ( "context" "database/sql" "fmt" "reflect" "strings" "unsafe" "github.com/kataras/iris/v12/x/reflex" ) type ( // Schema holds the row definitions. Schema struct { Name string Rows map[reflect.Type]*Row ColumnNameFunc ColumnNameFunc AutoCloseRows bool } // Row holds the column definitions and the struct type & name. Row struct { Schema string // e.g. public Name string // e.g. users. Must set to a custom one if the select query contains AS names. StructType reflect.Type Columns map[string]*Column // e.g. "id":{"id", 0, [0]} } // Column holds the database column name and other properties extracted by a struct's field. Column struct { Name string Index int FieldIndex []int } ) // NewSchema returns a new Schema. Use its Register() method to cache // a structure value so Bind() can fill all struct's fields based on a query. func NewSchema() *Schema { return &Schema{ Name: "public", Rows: make(map[reflect.Type]*Row), ColumnNameFunc: snakeCase, AutoCloseRows: true, } } // DefaultSchema initializes a common Schema. var DefaultSchema = NewSchema() // Register caches a struct value to the default schema. func Register(tableName string, value any) *Schema { return DefaultSchema.Register(tableName, value) } // Query is a shortcut of executing a query and bind the result to "dst". func Query(ctx context.Context, db *sql.DB, dst any, query string, args ...any) error { return DefaultSchema.Query(ctx, db, dst, query, args...) } // Bind sets "dst" to the result of "src" and reports any errors. func Bind(dst any, src *sql.Rows) error { return DefaultSchema.Bind(dst, src) } // Register caches a struct value to the schema. func (s *Schema) Register(tableName string, value any) *Schema { typ := reflect.TypeOf(value) for typ.Kind() == reflect.Ptr { typ = typ.Elem() } if tableName == "" { // convert to a human name, e.g. sqlx.Food -> food. typeName := typ.String() if idx := strings.LastIndexByte(typeName, '.'); idx > 0 && len(typeName) > idx { typeName = typeName[idx+1:] } tableName = snakeCase(typeName) } columns, err := convertStructToColumns(typ, s.ColumnNameFunc) if err != nil { panic(fmt.Sprintf("sqlx: register: %q: %s", reflect.TypeOf(value).String(), err.Error())) } s.Rows[typ] = &Row{ Schema: s.Name, Name: tableName, StructType: typ, Columns: columns, } return s } // Query is a shortcut of executing a query and bind the result to "dst". func (s *Schema) Query(ctx context.Context, db *sql.DB, dst any, query string, args ...any) error { rows, err := db.QueryContext(ctx, query, args...) if err != nil { return err } if !s.AutoCloseRows { // if not close on bind, we must close it here. defer rows.Close() } err = s.Bind(dst, rows) return err } // Bind sets "dst" to the result of "src" and reports any errors. func (s *Schema) Bind(dst any, src *sql.Rows) error { typ := reflect.TypeOf(dst) if typ.Kind() != reflect.Ptr { return fmt.Errorf("sqlx: bind: destination not a pointer") } typ = typ.Elem() originalKind := typ.Kind() if typ.Kind() == reflect.Slice { typ = typ.Elem() } r, ok := s.Rows[typ] if !ok { return fmt.Errorf("sqlx: bind: unregistered type: %q", typ.String()) } columnTypes, err := src.ColumnTypes() if err != nil { return fmt.Errorf("sqlx: bind: table: %q: %w", r.Name, err) } if expected, got := len(r.Columns), len(columnTypes); expected != got { return fmt.Errorf("sqlx: bind: table: %q: unexpected number of result columns: %d: expected: %d", r.Name, got, expected) } val := reflex.IndirectValue(reflect.ValueOf(dst)) if s.AutoCloseRows { defer src.Close() } switch originalKind { case reflect.Struct: if src.Next() { if err = r.bindSingle(typ, val, columnTypes, src); err != nil { return err } } else { return sql.ErrNoRows } return src.Err() case reflect.Slice: for src.Next() { elem := reflect.New(typ).Elem() if err = r.bindSingle(typ, elem, columnTypes, src); err != nil { return err } val = reflect.Append(val, elem) } if err = src.Err(); err != nil { return err } reflect.ValueOf(dst).Elem().Set(val) return nil default: return fmt.Errorf("sqlx: bind: table: %q: unexpected destination kind: %q", r.Name, typ.Kind().String()) } } func (r *Row) bindSingle(typ reflect.Type, val reflect.Value, columnTypes []*sql.ColumnType, scanner interface{ Scan(...any) error }) error { fieldPtrs, err := r.lookupStructFieldPtrs(typ, val, columnTypes) if err != nil { return fmt.Errorf("sqlx: bind: table: %q: %w", r.Name, err) } return scanner.Scan(fieldPtrs...) } func (r *Row) lookupStructFieldPtrs(typ reflect.Type, val reflect.Value, columnTypes []*sql.ColumnType) ([]any, error) { fieldPtrs := make([]any, 0, len(columnTypes)) for _, columnType := range columnTypes { columnName := columnType.Name() tableColumn, ok := r.Columns[columnName] if !ok { continue } // TODO: when go 1.18 released, replace with that: /* tableColumnField, err := val.FieldByIndexErr(tableColumn.FieldIndex) if err != nil { return nil, fmt.Errorf("column: %q: %w", tableColumn.Name, err) } */ tableColumnField := val.FieldByIndex(tableColumn.FieldIndex) tableColumnFieldType := tableColumnField.Type() fieldPtr := reflect.NewAt(tableColumnFieldType, unsafe.Pointer(tableColumnField.UnsafeAddr())).Elem().Addr().Interface() fieldPtrs = append(fieldPtrs, fieldPtr) } return fieldPtrs, nil } ================================================ FILE: x/sqlx/sqlx_test.go ================================================ package sqlx /* import ( "reflect" "testing" sqlmock "github.com/DATA-DOG/go-sqlmock" ) type food struct { ID string Name string Presenter bool `db:"-"` } func TestTableBind(t *testing.T) { Register("foods", food{}) db, mock, err := sqlmock.New() if err != nil { t.Fatal(err) } mock.ExpectQuery("SELECT .* FROM foods WHERE id = ?"). WithArgs("42"). WillReturnRows(sqlmock.NewRows([]string{"id", "name"}). AddRow("42", "banana"). AddRow("43", "broccoli")) rows, err := db.Query("SELECT .* FROM foods WHERE id = ? LIMIT 1", "42") if err != nil { t.Fatal(err) } var f food err = Bind(&f, rows) if err != nil { t.Fatal(err) } expectedSingle := food{"42", "banana", false} if !reflect.DeepEqual(f, expectedSingle) { t.Fatalf("expected value: %#+v but got: %#+v", expectedSingle, f) } mock.ExpectQuery("SELECT .* FROM foods"). WillReturnRows(sqlmock.NewRows([]string{"id", "name"}). AddRow("42", "banana"). AddRow("43", "broccoli"). AddRow("44", "chicken")) rows, err = db.Query("SELECT .* FROM foods") if err != nil { t.Fatal(err) } var foods []food err = Bind(&foods, rows) if err != nil { t.Fatal(err) } expectedMany := []food{ {"42", "banana", false}, {"43", "broccoli", false}, {"44", "chicken", false}, } for i := range foods { if !reflect.DeepEqual(foods[i], expectedMany[i]) { t.Fatalf("[%d] expected: %#+v but got: %#+v", i, expectedMany[i], foods[i]) } } } */ ================================================ FILE: x/sqlx/struct_row.go ================================================ package sqlx import ( "fmt" "reflect" "strings" "github.com/kataras/iris/v12/x/reflex" ) // DefaultTag is the default struct field tag. var DefaultTag = "db" // ColumnNameFunc is the function which converts a struct field name to a database column name. type ColumnNameFunc = func(string) string func convertStructToColumns(typ reflect.Type, nameFunc ColumnNameFunc) (map[string]*Column, error) { if kind := typ.Kind(); kind != reflect.Struct { return nil, fmt.Errorf("convert struct: invalid type: expected a struct value but got: %q", kind.String()) } // Retrieve only fields valid for database. fields := reflex.LookupFields(typ, "") columns := make(map[string]*Column, len(fields)) for i, field := range fields { column, ok, err := convertStructFieldToColumn(field, DefaultTag, nameFunc) if !ok { continue } if err != nil { return nil, fmt.Errorf("convert struct: field name: %q: %w", field.Name, err) } column.Index = i columns[column.Name] = column } return columns, nil } func convertStructFieldToColumn(field reflect.StructField, optionalTag string, nameFunc ColumnNameFunc) (*Column, bool, error) { c := &Column{ Name: nameFunc(field.Name), FieldIndex: field.Index, } fieldTag, ok := field.Tag.Lookup(optionalTag) if ok { if fieldTag == "-" { return nil, false, nil } if err := parseOptions(fieldTag, c); err != nil { return nil, false, err } } return c, true, nil } func parseOptions(fieldTag string, c *Column) error { options := strings.Split(fieldTag, ",") for _, opt := range options { if opt == "" { continue // skip empty. } var key, value string kv := strings.Split(opt, "=") // When more options come to play. switch len(kv) { case 2: key = kv[0] value = kv[1] case 1: c.Name = kv[0] return nil default: return fmt.Errorf("option: %s: expected key value separated by '='", opt) } switch key { case "name": c.Name = value default: return fmt.Errorf("unexpected tag option: %s", key) } } return nil } ================================================ FILE: x/sqlx/util.go ================================================ package sqlx import "strings" // snakeCase converts a given string to a friendly snake case, e.g. // - userId to user_id // - ID to id // - ProviderAPIKey to provider_api_key // - Option to option func snakeCase(camel string) string { var ( b strings.Builder prevWasUpper bool ) for i, c := range camel { if isUppercase(c) { // it's upper. if b.Len() > 0 && !prevWasUpper { // it's not the first and the previous was not uppercased too (e.g "ID"). b.WriteRune('_') } else { // check for XxxAPIKey, it should be written as xxx_api_key. next := i + 1 if next > 1 && len(camel)-1 > next { if !isUppercase(rune(camel[next])) { b.WriteRune('_') } } } b.WriteRune(c - 'A' + 'a') // write its lowercase version. prevWasUpper = true } else { b.WriteRune(c) // write it as it is, it's already lowercased. prevWasUpper = false } } return b.String() } func isUppercase(c rune) bool { return 'A' <= c && c <= 'Z' } ================================================ FILE: x/timex/weekday.go ================================================ package timex import "time" // RangeDate returns a function which returns a time // between "start" and "end". When the iteration finishes // the returned time is zero. func RangeDate(start, end time.Time) func() time.Time { y, m, d := start.Date() start = time.Date(y, m, d, 0, 0, 0, 0, start.Location()) y, m, d = end.Date() end = time.Date(y, m, d, 0, 0, 0, 0, end.Location()) return func() time.Time { if start.After(end) { return time.Time{} } date := start start = start.AddDate(0, 0, 1) return date } } type DateRangeType string const ( DayRange DateRangeType = "day" MonthRange DateRangeType = "month" WeekRange DateRangeType = "week" YearRange DateRangeType = "year" ) // Between returns the dates from "start" to "end". func Between(start, end time.Time) []time.Time { var dates []time.Time for df := RangeDate(start, end); ; { d := df() if d.IsZero() { break } dates = append(dates, d) } return dates } // Backwards returns a list of dates between "end" and -n (years, months, weeks or days). func Backwards(typ DateRangeType, end time.Time, n int) []time.Time { var start time.Time switch typ { case DayRange: n = n - 1 // -7 should be -6 to get the week from today. start = end.AddDate(0, 0, -n) case WeekRange: // 7 should be 6 to get the week. start = end.AddDate(0, 0, -n*6) case MonthRange: start = end.AddDate(0, -n, 0) case YearRange: start = end.AddDate(-n, 0, 0) } return Between(start, end) } // BackwardsN returns the dates from back to "n" years, months, weeks or days from today. func BackwardsN(typ DateRangeType, n int) []time.Time { end := time.Now() return Backwards(typ, end, n) } // BackwardsToMonday returns the dates between "end" (including "end") // until the previous monday of the current week (including monday). func BackwardsToMonday(end time.Time) []time.Time { dates := []time.Time{end} for end.Weekday() != time.Monday { end = end.AddDate(0, 0, -1) dates = append(dates, end) } return dates } // GetWeekDate returns the date of the given weekday (monday, tuesday, etc.) of the current week. func GetWeekDate(now time.Time, weekday, start, end time.Weekday) time.Time { dates := GetWeekdays(now, start, end) for _, d := range dates { if d.Weekday() == weekday { return d } } return time.Time{} } // GetWeekStart returns the date of the first week day (startWeekday) of the current now's week. func GetWeekStart(now time.Time, startWeekday time.Weekday) time.Time { offset := (int(startWeekday) - int(now.Weekday()) - 7) % 7 date := now.Add(time.Duration(offset*24) * time.Hour) return date } // GetWeekEnd returns the date of the last week day (endWeekday) of the current now's week. func GetWeekEnd(now time.Time, endWeekday time.Weekday) time.Time { offset := (int(endWeekday) - int(now.Weekday()) + 7) % 7 date := now.Add(time.Duration(offset*24) * time.Hour) return date } // GetWeekdays returns the range between "startWeekday" and "endWeekday" of the current week. func GetWeekdays(now time.Time, startWeekday, endWeekday time.Weekday) (dates []time.Time) { return Between(GetWeekStart(now, startWeekday), GetWeekEnd(now, endWeekday)) } // GetMonthStart returns the date of the first month day of the current now's month. func GetMonthStart(now time.Time) time.Time { return time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()) } // GetMonthEnd returns the date of the last month day of the current now's month. func GetMonthEnd(now time.Time) time.Time { now = now.UTC() // Add one month to the current date and subtract one day return time.Date(now.Year(), now.Month()+1, 0, 0, 0, 0, 0, now.Location()) } // GetMonthDays returns the range between first and last days the current month. func GetMonthDays(now time.Time) (dates []time.Time) { return Between(GetMonthStart(now), GetMonthEnd(now)) } // GetYearStart returns the date of the first year of the current now's year. func GetYearStart(now time.Time) time.Time { return time.Date(now.Year(), 1, 1, 0, 0, 0, 0, now.Location()) } ================================================ FILE: x/timex/weekday_test.go ================================================ package timex import ( "fmt" "testing" "time" ) const ISO8601Layout = "2006-01-02T15:04:05" func TestMonthAndYearStart(t *testing.T) { now, err := time.Parse(ISO8601Layout, "2021-04-21T00:00:00") if err != nil { t.Fatal(err) } startMonthDate := GetMonthStart(now) if expected, got := "2021-04-01 00:00:00 +0000 UTC", startMonthDate.String(); expected != got { t.Logf("start of the current month: expected: %s but got: %s", expected, got) } startYearDate := GetYearStart(now) if expected, got := "2021-01-01 00:00:00 +0000 UTC", startYearDate.String(); expected != got { t.Logf("start of the current year: expected: %s but got: %s", expected, got) } } func TestGetWeekEnd(t *testing.T) { var tests = []struct { End time.Weekday Dates []string ExpectedDateEnd string }{ { // 1. Test sunday as end. End: time.Sunday, Dates: []string{ "2022-01-17T00:00:00", // 1. "2022-01-18T00:00:00", // 2. "2022-01-19T00:00:00", // 3. "2022-01-20T00:00:00", // 4. "2022-01-21T00:00:00", // 5. "2022-01-22T00:00:00", // 6. "2022-01-23T00:00:00", // 7. }, ExpectedDateEnd: "2022-01-23T00:00:00", }, { // 1. Test saturday as end. End: time.Saturday, Dates: []string{ "2022-01-23T00:00:00", // Sunday. "2022-01-24T00:00:00", "2022-01-25T00:00:00", "2022-01-26T00:00:00", "2022-01-27T00:00:00", "2022-01-28T00:00:00", "2022-01-29T00:00:00", }, ExpectedDateEnd: "2022-01-29T00:00:00", }, } for i := range tests { tt := tests[i] t.Run(fmt.Sprintf("%s[%d]", t.Name(), i+1), func(t *testing.T) { for j, date := range tt.Dates { now, err := time.Parse(ISO8601Layout, date) if err != nil { t.Fatal(err) } weekEndDate := GetWeekEnd(now, tt.End) if got := weekEndDate.Format(ISO8601Layout); got != tt.ExpectedDateEnd { t.Fatalf("[%d] expected week end date: %s but got: %s ", j+1, tt.ExpectedDateEnd, got) } } }) } } func TestGetWeekDate(t *testing.T) { now, err := time.Parse(ISO8601Layout, "2022-02-10T00:00:00") if err != nil { t.Fatal(err) } var tests = []struct { Now time.Time Start time.Weekday End time.Weekday Weekday time.Weekday ExpectedDate string }{ { Now: now, Start: time.Monday, End: time.Sunday, Weekday: time.Monday, ExpectedDate: "2022-02-07T00:00:00", }, { Now: now, Start: time.Monday, End: time.Sunday, Weekday: time.Tuesday, ExpectedDate: "2022-02-08T00:00:00", }, { Now: now, Start: time.Monday, End: time.Sunday, Weekday: time.Wednesday, ExpectedDate: "2022-02-09T00:00:00", }, { Now: now, Start: time.Monday, End: time.Sunday, Weekday: time.Thursday, ExpectedDate: "2022-02-10T00:00:00", }, { Now: now, Start: time.Monday, End: time.Sunday, Weekday: time.Friday, ExpectedDate: "2022-02-11T00:00:00", }, { Now: now, Start: time.Monday, End: time.Sunday, Weekday: time.Saturday, ExpectedDate: "2022-02-12T00:00:00", }, { Now: now, Start: time.Monday, End: time.Sunday, Weekday: time.Sunday, ExpectedDate: "2022-02-13T00:00:00", }, // Test sunday to saturday. { Now: now, Start: time.Sunday, End: time.Saturday, Weekday: time.Wednesday, ExpectedDate: "2022-02-09T00:00:00", }, } for i := range tests { tt := tests[i] t.Run(fmt.Sprintf("%s[%s]", t.Name(), tt.Weekday.String()), func(t *testing.T) { weekDate := GetWeekDate(tt.Now, tt.Weekday, tt.Start, tt.End) if got := weekDate.Format(ISO8601Layout); got != tt.ExpectedDate { t.Fatalf("[%d] expected week date: %s but got: %s ", i+1, tt.ExpectedDate, got) } }) } } func TestGetMonthDays(t *testing.T) { now, err := time.Parse(ISO8601Layout, "2023-12-12T00:00:00") if err != nil { t.Fatal(err) } dates := GetMonthDays(now) expectedDates := []string{ "2023-12-01 00:00:00 +0000 UTC", "2023-12-02 00:00:00 +0000 UTC", "2023-12-03 00:00:00 +0000 UTC", "2023-12-04 00:00:00 +0000 UTC", "2023-12-05 00:00:00 +0000 UTC", "2023-12-06 00:00:00 +0000 UTC", "2023-12-07 00:00:00 +0000 UTC", "2023-12-08 00:00:00 +0000 UTC", "2023-12-09 00:00:00 +0000 UTC", "2023-12-10 00:00:00 +0000 UTC", "2023-12-11 00:00:00 +0000 UTC", "2023-12-12 00:00:00 +0000 UTC", "2023-12-13 00:00:00 +0000 UTC", "2023-12-14 00:00:00 +0000 UTC", "2023-12-15 00:00:00 +0000 UTC", "2023-12-16 00:00:00 +0000 UTC", "2023-12-17 00:00:00 +0000 UTC", "2023-12-18 00:00:00 +0000 UTC", "2023-12-19 00:00:00 +0000 UTC", "2023-12-20 00:00:00 +0000 UTC", "2023-12-21 00:00:00 +0000 UTC", "2023-12-22 00:00:00 +0000 UTC", "2023-12-23 00:00:00 +0000 UTC", "2023-12-24 00:00:00 +0000 UTC", "2023-12-25 00:00:00 +0000 UTC", "2023-12-26 00:00:00 +0000 UTC", "2023-12-27 00:00:00 +0000 UTC", "2023-12-28 00:00:00 +0000 UTC", "2023-12-29 00:00:00 +0000 UTC", "2023-12-30 00:00:00 +0000 UTC", "2023-12-31 00:00:00 +0000 UTC", } for i, d := range dates { if expectedDates[i] != d.String() { t.Fatalf("expected: %s but got: %s", expectedDates[i], d.String()) } } } func TestGetWeekdays(t *testing.T) { var tests = []struct { Date string ExpectedDates []string }{ { Date: "2021-02-04T00:00:00", ExpectedDates: []string{ "2021-02-01 00:00:00 +0000 UTC", "2021-02-02 00:00:00 +0000 UTC", "2021-02-03 00:00:00 +0000 UTC", "2021-02-04 00:00:00 +0000 UTC", "2021-02-05 00:00:00 +0000 UTC", "2021-02-06 00:00:00 +0000 UTC", "2021-02-07 00:00:00 +0000 UTC", }, }, { // It's monday. Date: "2022-01-17T00:00:00", ExpectedDates: []string{ "2022-01-17 00:00:00 +0000 UTC", "2022-01-18 00:00:00 +0000 UTC", "2022-01-19 00:00:00 +0000 UTC", "2022-01-20 00:00:00 +0000 UTC", "2022-01-21 00:00:00 +0000 UTC", "2022-01-22 00:00:00 +0000 UTC", "2022-01-23 00:00:00 +0000 UTC", }, }, { // Test all other days by order. Date: "2022-01-18T00:00:00", ExpectedDates: []string{ "2022-01-17 00:00:00 +0000 UTC", "2022-01-18 00:00:00 +0000 UTC", "2022-01-19 00:00:00 +0000 UTC", "2022-01-20 00:00:00 +0000 UTC", "2022-01-21 00:00:00 +0000 UTC", "2022-01-22 00:00:00 +0000 UTC", "2022-01-23 00:00:00 +0000 UTC", }, }, { Date: "2022-01-19T00:00:00", ExpectedDates: []string{ "2022-01-17 00:00:00 +0000 UTC", "2022-01-18 00:00:00 +0000 UTC", "2022-01-19 00:00:00 +0000 UTC", "2022-01-20 00:00:00 +0000 UTC", "2022-01-21 00:00:00 +0000 UTC", "2022-01-22 00:00:00 +0000 UTC", "2022-01-23 00:00:00 +0000 UTC", }, }, { Date: "2022-01-20T00:00:00", ExpectedDates: []string{ "2022-01-17 00:00:00 +0000 UTC", "2022-01-18 00:00:00 +0000 UTC", "2022-01-19 00:00:00 +0000 UTC", "2022-01-20 00:00:00 +0000 UTC", "2022-01-21 00:00:00 +0000 UTC", "2022-01-22 00:00:00 +0000 UTC", "2022-01-23 00:00:00 +0000 UTC", }, }, { Date: "2022-01-21T00:00:00", ExpectedDates: []string{ "2022-01-17 00:00:00 +0000 UTC", "2022-01-18 00:00:00 +0000 UTC", "2022-01-19 00:00:00 +0000 UTC", "2022-01-20 00:00:00 +0000 UTC", "2022-01-21 00:00:00 +0000 UTC", "2022-01-22 00:00:00 +0000 UTC", "2022-01-23 00:00:00 +0000 UTC", }, }, { Date: "2022-01-22T00:00:00", ExpectedDates: []string{ "2022-01-17 00:00:00 +0000 UTC", "2022-01-18 00:00:00 +0000 UTC", "2022-01-19 00:00:00 +0000 UTC", "2022-01-20 00:00:00 +0000 UTC", "2022-01-21 00:00:00 +0000 UTC", "2022-01-22 00:00:00 +0000 UTC", "2022-01-23 00:00:00 +0000 UTC", }, }, { // Sunday. Date: "2022-01-23T00:00:00", ExpectedDates: []string{ "2022-01-17 00:00:00 +0000 UTC", "2022-01-18 00:00:00 +0000 UTC", "2022-01-19 00:00:00 +0000 UTC", "2022-01-20 00:00:00 +0000 UTC", "2022-01-21 00:00:00 +0000 UTC", "2022-01-22 00:00:00 +0000 UTC", "2022-01-23 00:00:00 +0000 UTC", }, }, { // Test 1st Jenuary (Saturday) . Date: "2022-01-01T00:00:00", ExpectedDates: []string{ "2021-12-27 00:00:00 +0000 UTC", // monday. "2021-12-28 00:00:00 +0000 UTC", "2021-12-29 00:00:00 +0000 UTC", "2021-12-30 00:00:00 +0000 UTC", "2021-12-31 00:00:00 +0000 UTC", "2022-01-01 00:00:00 +0000 UTC", "2022-01-02 00:00:00 +0000 UTC", // sunday. }, }, } for i := range tests { tt := tests[i] t.Run(fmt.Sprintf("%s[%d]", t.Name(), i+1), func(t *testing.T) { now, err := time.Parse(ISO8601Layout, tt.Date) if err != nil { t.Fatal(err) } dates := GetWeekdays(now, time.Monday, time.Sunday) checkDates(t, "", tt.ExpectedDates, dates) }) } // t.Logf("[%s] Current day of the week: %s", now.String(), now.Weekday().String()) } func TestBackwardsToMonday(t *testing.T) { end, err := time.Parse(ISO8601Layout, "2021-04-05T00:00:00") if err != nil { t.Fatal(err) } expected := []string{ "2021-04-05 00:00:00 +0000 UTC", } // Test when when today is monday. dates := BackwardsToMonday(end) checkDates(t, "", expected, dates) // Test when today is tuesday. end, err = time.Parse(ISO8601Layout, "2021-04-06T00:00:00") if err != nil { t.Fatal(err) } expected = []string{ "2021-04-06 00:00:00 +0000 UTC", "2021-04-05 00:00:00 +0000 UTC", } dates = BackwardsToMonday(end) checkDates(t, "", expected, dates) // Test when today is thursday. end, err = time.Parse(ISO8601Layout, "2021-04-08T00:00:00") if err != nil { t.Fatal(err) } expected = []string{ "2021-04-08 00:00:00 +0000 UTC", "2021-04-07 00:00:00 +0000 UTC", "2021-04-06 00:00:00 +0000 UTC", "2021-04-05 00:00:00 +0000 UTC", } dates = BackwardsToMonday(end) checkDates(t, "", expected, dates) // Test when today is sunday. end, err = time.Parse(ISO8601Layout, "2021-04-10T00:00:00") if err != nil { t.Fatal(err) } expected = []string{ "2021-04-10 00:00:00 +0000 UTC", "2021-04-09 00:00:00 +0000 UTC", "2021-04-08 00:00:00 +0000 UTC", "2021-04-07 00:00:00 +0000 UTC", "2021-04-06 00:00:00 +0000 UTC", "2021-04-05 00:00:00 +0000 UTC", } dates = BackwardsToMonday(end) checkDates(t, "", expected, dates) } func checkDates(t *testing.T, typ DateRangeType, expected []string, dates []time.Time) { t.Helper() t.Logf("[%s] length of days: %d", typ, len(dates)) if expectedLength, gotLength := len(expected), len(dates); expectedLength != gotLength { t.Logf("[%s] expected days length: %d but got: %d", typ, expectedLength, gotLength) if gotLength > expectedLength { t.Logf("Got %d extra date(s), list of all dates we've got:", gotLength-expectedLength) for i, gotDate := range dates { t.Logf("[%d] %s ", i, gotDate.String()) } } t.FailNow() } for i, date := range dates { // t.Logf("[%d] %s", i, date.String()) if expected, got := expected[i], date.String(); expected != got { t.Fatalf("[%d] [%s] expected date: %s but got: %s", i, typ, expected, got) } } } func TestBetweenAndBackwardsN(t *testing.T) { start, err := time.Parse(ISO8601Layout, "2021-03-26T00:00:00") if err != nil { t.Fatal(err) } end, err := time.Parse(ISO8601Layout, "2021-04-01T00:00:00") if err != nil { t.Fatal(err) } expected := []string{ "2021-03-26 00:00:00 +0000 UTC", "2021-03-27 00:00:00 +0000 UTC", "2021-03-28 00:00:00 +0000 UTC", "2021-03-29 00:00:00 +0000 UTC", "2021-03-30 00:00:00 +0000 UTC", "2021-03-31 00:00:00 +0000 UTC", "2021-04-01 00:00:00 +0000 UTC", } dates := Between(start, end) checkDates(t, "", expected, dates) dates = Backwards(DayRange, end, 7) checkDates(t, DayRange, expected, dates) dates = Backwards(WeekRange, end, 1) checkDates(t, WeekRange, expected, dates) dates = Backwards(MonthRange, end, 2) expectedMonthDates := []string{ "2021-02-01 00:00:00 +0000 UTC", "2021-02-02 00:00:00 +0000 UTC", "2021-02-03 00:00:00 +0000 UTC", "2021-02-04 00:00:00 +0000 UTC", "2021-02-05 00:00:00 +0000 UTC", "2021-02-06 00:00:00 +0000 UTC", "2021-02-07 00:00:00 +0000 UTC", "2021-02-08 00:00:00 +0000 UTC", "2021-02-09 00:00:00 +0000 UTC", "2021-02-10 00:00:00 +0000 UTC", "2021-02-11 00:00:00 +0000 UTC", "2021-02-12 00:00:00 +0000 UTC", "2021-02-13 00:00:00 +0000 UTC", "2021-02-14 00:00:00 +0000 UTC", "2021-02-15 00:00:00 +0000 UTC", "2021-02-16 00:00:00 +0000 UTC", "2021-02-17 00:00:00 +0000 UTC", "2021-02-18 00:00:00 +0000 UTC", "2021-02-19 00:00:00 +0000 UTC", "2021-02-20 00:00:00 +0000 UTC", "2021-02-21 00:00:00 +0000 UTC", "2021-02-22 00:00:00 +0000 UTC", "2021-02-23 00:00:00 +0000 UTC", "2021-02-24 00:00:00 +0000 UTC", "2021-02-25 00:00:00 +0000 UTC", "2021-02-26 00:00:00 +0000 UTC", "2021-02-27 00:00:00 +0000 UTC", "2021-02-28 00:00:00 +0000 UTC", "2021-03-01 00:00:00 +0000 UTC", "2021-03-02 00:00:00 +0000 UTC", "2021-03-03 00:00:00 +0000 UTC", "2021-03-04 00:00:00 +0000 UTC", "2021-03-05 00:00:00 +0000 UTC", "2021-03-06 00:00:00 +0000 UTC", "2021-03-07 00:00:00 +0000 UTC", "2021-03-08 00:00:00 +0000 UTC", "2021-03-09 00:00:00 +0000 UTC", "2021-03-10 00:00:00 +0000 UTC", "2021-03-11 00:00:00 +0000 UTC", "2021-03-12 00:00:00 +0000 UTC", "2021-03-13 00:00:00 +0000 UTC", "2021-03-14 00:00:00 +0000 UTC", "2021-03-15 00:00:00 +0000 UTC", "2021-03-16 00:00:00 +0000 UTC", "2021-03-17 00:00:00 +0000 UTC", "2021-03-18 00:00:00 +0000 UTC", "2021-03-19 00:00:00 +0000 UTC", "2021-03-20 00:00:00 +0000 UTC", "2021-03-21 00:00:00 +0000 UTC", "2021-03-22 00:00:00 +0000 UTC", "2021-03-23 00:00:00 +0000 UTC", "2021-03-24 00:00:00 +0000 UTC", "2021-03-25 00:00:00 +0000 UTC", "2021-03-26 00:00:00 +0000 UTC", "2021-03-27 00:00:00 +0000 UTC", "2021-03-28 00:00:00 +0000 UTC", "2021-03-29 00:00:00 +0000 UTC", "2021-03-30 00:00:00 +0000 UTC", "2021-03-31 00:00:00 +0000 UTC", "2021-04-01 00:00:00 +0000 UTC", } checkDates(t, MonthRange, expectedMonthDates, dates) }