reorganization of _examples and add some new examples such as iris+groupcache+mysql+docker

Former-commit-id: ed635ee95de7160cde11eaabc0c1dcb0e460a620
This commit is contained in:
Gerasimos (Makis) Maropoulos 2020-06-07 15:26:06 +03:00
parent 9fdcb4c7fb
commit ed45c77be5
328 changed files with 4262 additions and 41621 deletions

View File

@ -159,7 +159,7 @@ Prior to this version the `iris.Context` was the only one dependency that has be
| `float, float32, float64`, | |
| `bool`, | |
| `slice` | [Path Parameter](https://github.com/kataras/iris/wiki/Routing-path-parameter-types) |
| Struct | [Request Body](https://github.com/kataras/iris/tree/master/_examples/http_request) of `JSON`, `XML`, `YAML`, `Form`, `URL Query`, `Protobuf`, `MsgPack` |
| Struct | [Request Body](https://github.com/kataras/iris/tree/master/_examples/request-body) of `JSON`, `XML`, `YAML`, `Form`, `URL Query`, `Protobuf`, `MsgPack` |
Here is a preview of what the new Hero handlers look like:
@ -393,7 +393,7 @@ Other Improvements:
- 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(interface{}) 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/http_request/read-json-struct-validation) example was updated.
- New `app.Validator { Struct(interface{}) 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 <T> 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.
@ -415,7 +415,7 @@ New Package-level Variables:
New Context Methods:
- `Context.GzipReader(enable bool)` method and `iris.GzipReader` middleware to enable future request read body calls to decompress data using gzip, [example](_examples/http_request/read-gzip).
- `Context.GzipReader(enable bool)` method and `iris.GzipReader` middleware to enable future request read body calls to decompress data using gzip, [example](_examples/request-body/read-gzip).
- `Context.RegisterDependency(v interface{})` and `Context.RemoveDependency(typ reflect.Type)` to register/remove struct dependencies on serve-time through a middleware.
- `Context.SetID(id interface{})` and `Context.GetID() interface{}` added to register a custom unique indetifier to the Context, if necessary.
- `Context.GetDomain() string` returns the domain.
@ -476,7 +476,6 @@ Implement **new** `SetRegisterRule(iris.RouteOverride, RouteSkip, RouteError)` t
New Examples:
- [_examples/Docker](_examples/Docker)
- [_examples/routing/route-register-rule](_examples/routing/route-register-rule)
# We, 05 February 2020 | v12.1.6
@ -570,10 +569,10 @@ Navigate through: https://github.com/kataras/iris/wiki/Sitemap for more.
## New Examples
2. [_examples/i18n](_examples/i18n)
1. [_examples/sitemap](_examples/sitemap)
3. [_examples/desktop-app/blink](_examples/desktop-app/blink)
4. [_examples/desktop-app/lorca](_examples/desktop-app/lorca)
5. [_examples/desktop-app/webview](_examples/desktop-app/webview)
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
@ -603,7 +602,7 @@ The iris-contrib/middleare and examples are updated to use the new `github.com/k
# 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/kataras/iris/tree/master/_examples/experimental-handlers)
- Add and update all [experimental handlers](https://github.com/iris-contrib/middleware)
- New `XMLMap` function which wraps a `map[string]interface{}` 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.
@ -657,7 +656,7 @@ Commit log: https://github.com/kataras/iris/compare/v11.2.3...v11.2.4
- [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/master/_examples/http_request/read-yaml)
- [Context.ReadYAML](https://github.com/kataras/iris/tree/master/_examples/request-body/read-yaml)
- Fixes https://github.com/kataras/neffos/issues/1#issuecomment-515698536
# We, 24 July 2019 | v11.2.2

View File

@ -108,7 +108,7 @@ Registro de commits: https://github.com/kataras/iris/compare/v11.2.3...v11.2.4
- [Nueva característica: Manejar diferentes tipos de parámetros en la misma ruta](https://github.com/kataras/iris/issues/1315)
- [Nueva característica: Negociación de contenido](https://github.com/kataras/iris/issues/1319)
- [Context.ReadYAML](https://github.com/kataras/iris/tree/master/_examples/http_request/read-yaml)
- [Context.ReadYAML](https://github.com/kataras/iris/tree/master/_examples/request-body/read-yaml)
- Ajustes https://github.com/kataras/neffos/issues/1#issuecomment-515698536
# Miércoles, 24 de julio 2019 | v11.2.2

View File

@ -1,31 +1,30 @@
# Table of Contents
* Tutorials
* [Dockerize](tutorial/docker)
* [Caddy](tutorial/caddy)
* [MongoDB](tutorial/mongodb)
* [Dropzone.js](tutorial/dropzonejs)
* [URL Shortener](tutorial/url-shortener/main.go)
* [Online Visitors](tutorial/online-visitors/main.go)
* [REST API for Apache Kafka](tutorial/api-for-apache-kafka)
* [Vue.js Todo (MVC)](tutorial/vuejs-todo-mvc)
* [gRPC (MVC)](mvc/grpc-compatible)
* HTTP Listening
* [HOST:PORT](http-listening/listen-addr/main.go)
* [Public Test Domain](http-listening/listen-addr-public/main.go)
* [UNIX socket file](http-listening/listen-unix/main.go)
* [TLS](http-listening/listen-tls/main.go)
* [Letsencrypt (Automatic Certifications)](http-listening/listen-letsencrypt/main.go)
* [Graceful Shutdown](http-listening/graceful-shutdown/default-notifier/main.go)
* [Notify on shutdown](http-listening/notify-on-shutdown/main.go)
* [REST API for Apache Kafka](kafka-api)
* [URL Shortener](url-shortener)
* [Dropzone.js](dropzonejs)
* [Caddy](caddy)
* Database
* [MySQL, Groupcache & Docker](database/mysql)
* [MongoDB](database/mongodb)
* [Xorm](database/orm/xorm/main.go)
* [Gorm](database/orm/gorm/main.go)
* 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)
* [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-listening/custom-listener/main.go)
* [SO_REUSEPORT for unix systems](http-listening/custom-listener/unix-reuseport/main.go)
* [Common net.Listener](http-server/custom-listener/main.go)
* [SO_REUSEPORT for unix systems](http-server/custom-listener/unix-reuseport/main.go)
* Custom HTTP Server
* [Pass a custom Server](http-listening/custom-httpserver/easy-way/main.go)
* [Use Iris as a single http.Handler](http-listening/custom-httpserver/std-way/main.go)
* [Multi Instances](http-listening/custom-httpserver/multi/main.go)
* [HTTP/3 Quic](http-listening/http3-quic)
* [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)
* Configuration
* [Functional](configuration/functional/main.go)
* [Configuration Struct](configuration/from-configuration-structure/main.go)
@ -56,19 +55,27 @@
* Custom Context
* [Method Overriding](routing/custom-context/method-overriding/main.go)
* [New Implementation](routing/custom-context/new-implementation/main.go)
* Subdomains
* [Single](subdomains/single/main.go)
* [Multi](subdomains/multi/main.go)
* [Wildcard](subdomains/wildcard/main.go)
* [WWW](subdomains/www/main.go)
* [Redirection](subdomains/redirect/main.go)
* API Versioning
* [How it works](https://github.com/kataras/iris/wiki/API-versioning)
* [Example](versioning/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)
* [Redirection](routing/subdomains/redirect/main.go)
* [HTTP Method Override](https://github.com/kataras/iris/blob/master/middleware/methodoverride/methodoverride_test.go)
* [API Versioning](routing/versioning/main.go)
* [Sitemap](routing/sitemap/main.go)
* Logging
* [Request Logger](logging/request-logger/main.go)
* [Log Requests to a File](logging/request-logger/request-logger-file/main.go)
* [Log Requests to a JSON File](logging/request-logger/request-logger-file-json/main.go)
* [Application File Logger](logging/file-logger/main.go)
* [Application JSON Logger](logging/json-logger/main.go)
* API Documentation
* [yaag](apidoc/yaag/main.go)
* Testing
* [Example](testing/httptest/main_test.go)
* [Yaag](apidoc/yaag/main.go)
* [Swagger](https://github.com/iris-contrib/swagger/tree/master/example)
* [Testing](testing/httptest/main_test.go)
* [Recovery](recover/main.go)
* [Profiling](pprof/main.go)
* File Server
* [Favicon](file-server/favicon/main.go)
* [Basic](file-server/basic/main.go)
@ -79,6 +86,8 @@
* [Basic SPA](file-server/single-page-application/basic/main.go)
* [Embedded Single Page Application](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)
* View
* [Overview](view/overview/main.go)
* [Basic](view/template_html_0/main.go)
@ -93,51 +102,51 @@
* [Pug: `Actions`](view/template_pug_1)
* [Pug: `Includes`](view/template_pug_2)
* [Pug: `Extends`](view/template_pug_3)
* [Jet Template](/view/template_jet_0)
* [Jet Template](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)
* [Jet 'urlpath' tmpl func](view/template_jet_2)
* [Jet Template Funcs from Struct](view/template_jet_3)
* Third-Parties
* [Render `valyala/quicktemplate` templates](http_responsewriter/quicktemplate)
* [Render `shiyanhui/hero` templates](http_responsewriter/herotemplate)
* [Render `valyala/quicktemplate` templates](view/quicktemplate)
* [Render `shiyanhui/hero` templates](view/herotemplate)
* [Request ID](https://github.com/kataras/iris/blob/master/middleware/requestid/requestid_test.go)
* [Request Rate Limit](request-ratelimit/main.go)
* [Request Referrer](request-referrer/main.go)
* [Webassembly](webassembly/basic/main.go)
* Request Body
* [Bind JSON](http_request/read-json/main.go)
* * [Struct Validation](http_request/read-json-struct-validation/main.go)
* [Bind XML](http_request/read-xml/main.go)
* [Bind MsgPack](http_request/read-msgpack/main.go)
* [Bind YAML](http_request/read-yaml/main.go)
* [Bind Form](http_request/read-form/main.go)
* [Bind Query](http_request/read-query/main.go)
* [Bind Body](http_request/read-body/main.go)
* [Bind Custom per type](http_request/read-custom-per-type/main.go)
* [Bind Custom via Unmarshaler](http_request/read-custom-via-unmarshaler/main.go)
* [Bind Many times](http_request/read-many/main.go)
* [Read/Bind Gzip compressed data](http_request/read-gzip/main.go)
* [Upload/Read File](http_request/upload-file/main.go)
* [Upload multiple Files](http_request/upload-files/main.go)
* [Extract Referrer](http_request/extract-referer/main.go)
* [Bind JSON](request-body/read-json/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)
* [Bind Query](request-body/read-query/main.go)
* [Bind Body](request-body/read-body/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)
* [Read/Bind Gzip compressed data](request-body/read-gzip/main.go)
* Response Writer
* [Content Negotiation](http_responsewriter/content-negotiation)
* [Text, Markdown, YAML, HTML, JSON, JSONP, Msgpack, XML and Binary](http_responsewriter/write-rest/main.go)
* [Write Gzip](http_responsewriter/write-gzip/main.go)
* [Stream Writer](http_responsewriter/stream-writer/main.go)
* [Transactions](http_responsewriter/transactions/main.go)
* [SSE](http_responsewriter/sse/main.go)
* [SSE (third-party package usage for server sent events)](http_responsewriter/sse-third-party/main.go)
* [Webassembly](webassembly/basic/main.go)
* Cache
* [Simple](cache/simple/main.go)
* [Client-Side (304)](cache/client-side/main.go)
* [Content Negotiation](response-writer/content-negotiation)
* [Text, Markdown, YAML, HTML, JSON, JSONP, Msgpack, XML and Binary](response-writer/write-rest/main.go)
* [Write Gzip](response-writer/write-gzip/main.go)
* [Stream Writer](response-writer/stream-writer/main.go)
* [Transactions](response-writer/transactions/main.go)
* [SSE](response-writer/sse/main.go)
* [SSE (third-party package usage for server sent events)](response-writer/sse-third-party/main.go)
* Cache
* [Simple](response-writer/simple/main.go)
* [Client-Side (304)](response-writer/client-side/main.go)
* Localization and Internationalization
* [i18n](i18n/main.go)
* Sitemaps
* [Sitemap](sitemap/main.go)
* Authentication
* [Basic Authentication](authentication/basicauth/main.go)
* [JWT](miscellaneous/jwt/main.go)
* [JWT (community edition)](experimental-handlers/jwt/main.go)
* [OAUth2](authentication/oauth2/main.go)
* [Manage Permissions](permissions/main.go)
* Authentication, Authorization & Bot Detection
* [Basic Authentication](auth/basicauth/main.go)
* [JWT](auth/jwt/main.go)
* [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)
@ -160,6 +169,7 @@
* [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)
@ -167,9 +177,9 @@
* [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)
* MVC
* [Overview - Repository and Service layers](mvc/overview)
* [Login - Repository and Service layers](mvc/login)
* [Hello world](mvc/hello-world/main.go)
* [Basic](mvc/basic/main.go)
* [Wildcard](mvc/basic/wildcard/main.go)
@ -179,27 +189,13 @@
* [Authenticated Controller](mvc/authenticated-controller/main.go)
* [Websocket Controller](mvc/websocket)
* [Register Middleware](mvc/middleware)
* Object-Relational Mapping
* [Using `go-xorm/xorm` (Mysql, MyMysql, Postgres, Tidb, SQLite, MsSql, MsSql, Oracle)](orm/xorm/main.go)
* [Using `jinzhu/gorm`](orm/gorm/main.go)
* Project Structure
* [Bootstrapper](structuring/bootstrap)
* [MVC with Repository and Service layer Overview](structuring/mvc-plus-repository-and-service-layers)
* [Login (MVC with Single Responsibility package)](structuring/login-mvc-single-responsibility-package)
* [Login (MVC with Datamodels, Datasource, Repository and Service layer)](structuring/login-mvc)
* [gRPC](mvc/grpc-compatible)
* [Login (Repository and Service layers)](mvc/login)
* [Login (Single Responsibility)](mvc/login-mvc-single-responsibility)
* [Vue.js Todo App](mvc/vuejs-todo-mvc)
* [Bootstrapper](bootstrap)
* Desktop Applications
* [The blink package](desktop-app/blink)
* [The lorca package](desktop-app/lorca)
* [The webview package](desktop-app/webview)
* Middlewares (Builtin)
* [JWT](miscellaneous/jwt/main.go)
* [Rate Limit](miscellaneous/ratelimit/main.go)
* [HTTP Method Override](https://github.com/kataras/iris/blob/master/middleware/methodoverride/methodoverride_test.go)
* [Request Logger](http_request/request-logger/main.go)
* [Log Requests to a File](http_request/request-logger/request-logger-file/main.go)
* [Recovery](miscellaneous/recover/main.go)
* [Profiling (pprof)](miscellaneous/pprof/main.go)
* [Internal Application File Logger](miscellaneous/file-logger/main.go)
* [Google reCAPTCHA](miscellaneous/recaptcha/main.go)
* [hCaptcha](miscellaneous/hcaptcha/main.go)
* [The blink package](desktop/blink)
* [The lorca package](desktop/lorca)
* [The webview package](desktop/webview)
* Middlewares [(Community)](https://github.com/iris-contrib/middleware)

View File

@ -0,0 +1,3 @@
# Swagger 2.0
Visit https://github.com/iris-contrib/swagger instead.

View File

@ -19,7 +19,7 @@ func main() {
// or defaults to "secret" and "itsa16bytesecret" respectfully.
//
// Use the `jwt.New` instead for more flexibility, if necessary.
j := jwt.DefaultHMAC(15*time.Minute, "secret", "itsa16bytesecret")
j := jwt.HMAC(15*time.Minute, "secret", "itsa16bytesecret")
app := iris.New()
app.Logger().SetLevel("debug")
@ -68,7 +68,7 @@ func main() {
/*
func default_RSA_Example() {
j := jwt.DefaultRSA(1 * time.Minute)
j := jwt.RSA(15*time.Minute)
}
Same as:
@ -115,7 +115,7 @@ func hmac_Example() {
/*
func load_From_File_With_Password_Example() {
b, err := ioutil.ReadFile("./private_rsa.pem")
b, err := ioutil.ReadFile("./rsa_password_protected.key")
if err != nil {
panic(err)
}

View File

@ -1,7 +0,0 @@
# Authentication
- [Basic Authentication](basicauth/main.go)
- [OAUth2](oauth2/main.go)
- [JWT](https://github.com/kataras/iris/tree/master/_examples/miscellaneous/jwt)
- [JWT (community edition)](https://github.com/iris-contrib/middleware/blob/master/jwt)
- [Sessions](https://github.com/kataras/iris/tree/master/_examples/sessions)

View File

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

View File

@ -1,9 +1,9 @@
package main
import (
"github.com/kataras/iris/v12/_examples/structuring/bootstrap/bootstrap"
"github.com/kataras/iris/v12/_examples/structuring/bootstrap/middleware/identity"
"github.com/kataras/iris/v12/_examples/structuring/bootstrap/routes"
"github.com/kataras/iris/v12/_examples/bootstrap/bootstrap"
"github.com/kataras/iris/v12/_examples/bootstrap/middleware/identity"
"github.com/kataras/iris/v12/_examples/bootstrap/routes"
)
func newApp() *bootstrap.Bootstrapper {

View File

@ -14,11 +14,11 @@ func TestApp(t *testing.T) {
// test our routes
e.GET("/").Expect().Status(httptest.StatusOK)
e.GET("/follower/42").Expect().Status(httptest.StatusOK).
Body().Equal("from /follower/{id:long} with ID: 42")
Body().Equal("from /follower/{id:int64} with ID: 42")
e.GET("/following/52").Expect().Status(httptest.StatusOK).
Body().Equal("from /following/{id:long} with ID: 52")
Body().Equal("from /following/{id:int64} with ID: 52")
e.GET("/like/64").Expect().Status(httptest.StatusOK).
Body().Equal("from /like/{id:long} with ID: 64")
Body().Equal("from /like/{id:int64} with ID: 64")
// test not found
e.GET("/notfound").Expect().Status(httptest.StatusNotFound)

View File

@ -5,7 +5,7 @@ import (
"github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/_examples/structuring/bootstrap/bootstrap"
"github.com/kataras/iris/v12/_examples/bootstrap/bootstrap"
)
// New returns a new handler which adds some headers and view data

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,13 @@
package routes
import (
"github.com/kataras/iris/v12/_examples/bootstrap/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)
}

View File

@ -4,15 +4,15 @@ The `Caddyfile` shows how you can use caddy to listen on ports 80 & 443 and sit
## Running our two web servers
1. Go to `$GOPATH/src/github.com/kataras/iris/_examples/tutorial/caddy/server1`
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/tutorial/caddy/server2`
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/tutorial/caddy` in this case
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`

View File

@ -35,7 +35,7 @@ func (c *UserController) Get() string {
// User is our test User model, nothing tremendous here.
type User struct{ ID int64 }
// GetBy handles GET /user/42, equal to .Get("/user/{id:long}")
// 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}

View File

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 92 KiB

View File

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 69 KiB

View File

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 102 KiB

View File

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 83 KiB

View File

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

View File

@ -0,0 +1,17 @@
# 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"]

View File

@ -7,11 +7,17 @@ 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
$ go get -u go.mongodb.org/mongo-driver/...
$ go get -u github.com/joho/godotenv
$ docker-compose up
```
### Manually
```sh
# .env file contents
@ -22,7 +28,7 @@ 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 ◽ Port=8080
> 2019/01/28 05:17:59 ◽ DSN=mongodb://localhost:27017
> Now listening on: http://localhost:8080
```

View File

@ -1,8 +1,8 @@
package storeapi
import (
"github.com/kataras/iris/v12/_examples/tutorial/mongodb/httputil"
"github.com/kataras/iris/v12/_examples/tutorial/mongodb/store"
"myapp/httputil"
"myapp/store"
"github.com/kataras/iris/v12"
)

View File

@ -0,0 +1,18 @@
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

View File

@ -22,6 +22,9 @@ var (
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.
@ -34,28 +37,31 @@ func parse() {
// 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/master/_examples/tutorials/mongodb/README.md")
fmt.Fprintln(os.Stderr, "https://github.com/kataras/iris/blob/master/_examples/database/mongodb/README.md")
os.Exit(-1)
}
log.Printf("Loading environment variables from file: %s\n", envFileName)
// 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 i := range envFiles {
if filepath.Ext(envFiles[i]) == "" {
envFiles[i] += ".env"
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))
}
}
}
if err := godotenv.Load(envFiles...); err != nil {
panic(fmt.Sprintf("error loading environment variables from [%s]: %v", envFileName, err))
}
envMap, _ := godotenv.Read(envFiles...)
for k, v := range envMap {
log.Printf("◽ %s=%s\n", k, v)
}
// envMap, _ := godotenv.Read(envFiles...)
// for k, v := range envMap {
// log.Printf("◽ %s=%s\n", k, v)
// }
parse()
}
@ -69,3 +75,11 @@ func getDefault(key string, def string) string {
return value
}
func fileExists(filename string) bool {
info, err := os.Stat(filename)
if os.IsNotExist(err) {
return false
}
return !info.IsDir()
}

View File

@ -0,0 +1,9 @@
module myapp
go 1.14
require (
github.com/joho/godotenv v1.3.0
github.com/kataras/iris/v12 v12.2.0
go.mongodb.org/mongo-driver v1.3.4
)

View File

@ -11,11 +11,11 @@ import (
"os"
// APIs
storeapi "github.com/kataras/iris/v12/_examples/tutorial/mongodb/api/store"
storeapi "myapp/api/store"
//
"github.com/kataras/iris/v12/_examples/tutorial/mongodb/env"
"github.com/kataras/iris/v12/_examples/tutorial/mongodb/store"
"myapp/env"
"myapp/store"
"github.com/kataras/iris/v12"
@ -30,7 +30,7 @@ func init() {
flagset := flag.CommandLine
flagset.StringVar(&envFileName, "env", envFileName, "the env file which web app will use to extract its environment variables")
flag.CommandLine.Parse(os.Args[1:])
flagset.Parse(os.Args[1:])
env.Load(envFileName)
}

View File

@ -0,0 +1,17 @@
# 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"]

View File

@ -0,0 +1,146 @@
# 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 | - | - |
| 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 | - | - | - |
| DELETE | /category/{id} | Deletes a Category | - | - | Token |
| GET | /category/{id}/products | Lists all Products from a Category | offset, limit, order | - | - |
| 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 | - | - |
| 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) | - | - | - |
| 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
```
### Install (Manually)
Run `go build` or `go run 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/myapp.sql](migration/myapp.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/dgrijalva/jwt-go (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)

View File

@ -0,0 +1,97 @@
// 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) {
j := jwt.HMAC(15*time.Minute, secret)
r.Use(requestid.New())
r.Use(verifyToken(j))
// 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.
r.Get("/token", writeToken(j))
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(j *jwt.JWT) iris.Handler {
return func(ctx iris.Context) {
claims := jwt.Claims{
Issuer: "https://iris-go.com",
Audience: jwt.Audience{requestid.Get(ctx)},
}
j.WriteToken(ctx, claims)
}
}
func verifyToken(j *jwt.JWT) iris.Handler {
return func(ctx iris.Context) {
// Allow all GET.
if ctx.Method() == iris.MethodGet {
ctx.Next()
return
}
j.Verify(ctx)
}
}

View File

@ -0,0 +1,251 @@
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 interface{} `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]interface{}
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 {
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 {
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})
}
}

View File

@ -0,0 +1,25 @@
package api
import (
"log"
"github.com/kataras/iris/v12"
)
const debug = true
func debugf(format string, args ...interface{}) {
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"))
}

View File

@ -0,0 +1,60 @@
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 ...interface{}) 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 == "")
}

View File

@ -0,0 +1,173 @@
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]interface{}
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)
}

View File

@ -0,0 +1,120 @@
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 interface{}, id int64) error
List(ctx context.Context, dest interface{}, 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 interface{}
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))
}

View File

@ -0,0 +1,32 @@
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

View File

@ -0,0 +1,89 @@
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...]
}
)*/

View File

@ -0,0 +1,95 @@
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...]
}
)
*/

View File

@ -0,0 +1,9 @@
module myapp
go 1.14
require (
github.com/go-sql-driver/mysql v1.5.0
github.com/kataras/iris/v12 v12.2.0
github.com/mailgun/groupcache/v2 v2.1.0
)

View File

@ -0,0 +1,44 @@
package main
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
}

View File

@ -0,0 +1,5 @@
{
"title": "computer-internet",
"position": 2,
"image_url": "https://bp.pstatic.gr/public/dist/images/1mOPxYtw1k.webp"
}

View File

@ -0,0 +1,31 @@
[{
"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"
}]

View File

@ -0,0 +1,6 @@
{
"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"
}

View File

@ -0,0 +1,3 @@
{
"title": "computers-technology"
}

View File

@ -0,0 +1,484 @@
{
"info": {
"_postman_id": "d3a2fdf6-9ebd-4e85-827d-385592a71fd6",
"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 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODk2MzkzNjd9.cYohwgUpe-Z7ac0LPpz4Adi5QXJmtwD1ZRpXrMUMPN0",
"type": "text"
}
],
"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": [],
"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": [],
"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"
}
],
"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"
}
],
"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"
}
],
"body": {
"mode": "raw",
"raw": "{\r\n \"title\": \"computers-technology\"\r\n}"
},
"url": {
"raw": "http://localhost:8080/category/1",
"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": [],
"url": {
"raw": "http://localhost:8080/category/1/products?offset=0&limit=30&by=price&order=asc",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
"category",
"3",
"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"
}
],
"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/1/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": [],
"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": [],
"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"
}
],
"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"
}
],
"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"
}
],
"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"
}
],
"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": []
}
],
"protocolProfileBehavior": {}
}

View File

@ -0,0 +1,7 @@
{
"title": "product-1",
"category_id": 3,
"image_url": "https://images.product1.png",
"price": 42.42,
"description": "a description for product-1"
}

View File

@ -0,0 +1,3 @@
{
"title": "product-19-new-title"
}

View File

@ -0,0 +1,8 @@
{
"id":19,
"title": "product-19",
"category_id": 3,
"image_url": "https://images.product19.png",
"price": 20,
"description": "a description for product-19"
}

View File

@ -0,0 +1,33 @@
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;

View File

@ -0,0 +1,74 @@
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]interface{}) (int, error) {
return s.Service.PartialUpdate(ctx, id, categoryUpdateSchema, attrs)
}

View File

@ -0,0 +1,42 @@
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)
}
}

View File

@ -0,0 +1,110 @@
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 []interface{}
)
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, []interface{}{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]interface{}) (int, error) {
return s.Service.PartialUpdate(ctx, id, productUpdateSchema, attrs)
}

View File

@ -0,0 +1,123 @@
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 interface{}, query string, args ...interface{}) 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 interface{}, query string, args ...interface{}) 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 ...interface{}) (sql.Result, error) {
return db.Conn.ExecContext(ctx, query, args...)
}

View File

@ -0,0 +1,243 @@
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 interface{}, 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 interface{}
}
// 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 interface{}) 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 []interface{}) {
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(...).
orderBy := q.Get("by") // e.g. price
return ListOptions{Offset: offset, Limit: limit, Order: order, OrderByColumn: orderBy}
}
// 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 interface{}, 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 opts.OrderByColumn == "" {
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]interface{}) (int, error) {
if len(schema) == 0 || len(attrs) == 0 {
return 0, nil
}
var (
keyLines []string
values []interface{}
)
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
}

View File

@ -0,0 +1,40 @@
package sql
import (
"context"
"database/sql"
)
// Database is an interface which a database(sql) should implement.
type Database interface {
Get(ctx context.Context, dest interface{}, q string, args ...interface{}) error
Select(ctx context.Context, dest interface{}, q string, args ...interface{}) error
Exec(ctx context.Context, q string, args ...interface{}) (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
}

View File

@ -0,0 +1,66 @@
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) (interface{}, 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)
}

View File

@ -1,13 +1,12 @@
package main
import (
"time"
"github.com/kataras/iris/v12"
"github.com/iris-contrib/middleware/jwt"
"github.com/kataras/iris/v12/middleware/jwt"
)
var secret = []byte("My Secret Key")
func main() {
app := iris.New()
app.ConfigureContainer(register)
@ -16,51 +15,36 @@ func main() {
}
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) (interface{}, error) {
return secret, nil
},
SigningMethod: jwt.SigningMethodHS256,
j := jwt.HMAC(15*time.Minute, "secret", "secretforencrypt")
api.RegisterDependency(func(ctx iris.Context) (claims userClaims) {
if err := j.VerifyToken(ctx, &claims); err != nil {
ctx.StopWithError(iris.StatusUnauthorized, err)
return
}
return
})
api.Get("/token", writeToken)
// This works as usually:
api.Get("/", j.Serve, verifiedPage)
// 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("/", verifiedWithBindedTokenPage)
api.Get("/authenticate", writeToken(j))
api.Get("/restricted", restrictedPage)
}
func writeToken() string {
token := jwt.NewTokenWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"foo": "bar",
type userClaims struct {
jwt.Claims
Username string
}
func writeToken(j *jwt.JWT) iris.Handler {
return func(ctx iris.Context) {
j.WriteToken(ctx, userClaims{
Claims: j.Expiry(jwt.Claims{Issuer: "an-issuer"}),
Username: "kataras",
})
tokenString, _ := token.SignedString(secret)
return tokenString
}
}
func verifiedPage() string {
return "This page can only be seen by verified clients"
}
func verifiedWithBindedTokenPage(token *jwt.Token) string {
// Token[foo] value: bar
return "Token[foo] value: " + token.Claims.(jwt.MapClaims)["foo"].(string)
func restrictedPage(claims userClaims) string {
// userClaims.Username: kataras
return "userClaims.Username: " + claims.Username
}

View File

@ -302,7 +302,7 @@ If you have done it successfully. Now go and upload some images and reload the u
- 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/master/_examples/tutorial/dropzonejs
- https://github.com/kataras/iris/tree/master/_examples/dropzonejs
## The end

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Some files were not shown because too many files have changed in this diff Show More