Add two examples for folder structuring as requested at https://github.com/kataras/iris/issues/748
Former-commit-id: 27c97d005d9cbd2309587b11fc9e2bab85870502
|
@ -76,6 +76,7 @@ Help this project to continue deliver awesome and unique features with the highe
|
||||||
* [Installation](#-installation)
|
* [Installation](#-installation)
|
||||||
* [Latest changes](https://github.com/kataras/iris/blob/master/HISTORY.md#we-27-september-2017--v843)
|
* [Latest changes](https://github.com/kataras/iris/blob/master/HISTORY.md#we-27-september-2017--v843)
|
||||||
* [Learn](#-learn)
|
* [Learn](#-learn)
|
||||||
|
* [Structuring](_examples/#structuring)
|
||||||
* [HTTP Listening](_examples/#http-listening)
|
* [HTTP Listening](_examples/#http-listening)
|
||||||
* [Configuration](_examples/#configuration)
|
* [Configuration](_examples/#configuration)
|
||||||
* [Routing, Grouping, Dynamic Path Parameters, "Macros" and Custom Context](_examples/#routing-grouping-dynamic-path-parameters-macros-and-custom-context)
|
* [Routing, Grouping, Dynamic Path Parameters, "Macros" and Custom Context](_examples/#routing-grouping-dynamic-path-parameters-macros-and-custom-context)
|
||||||
|
|
|
@ -14,6 +14,15 @@ It doesn't always contain the "best ways" but it does cover each important featu
|
||||||
- [Tutorial: URL Shortener using BoltDB](https://medium.com/@kataras/a-url-shortener-service-using-go-iris-and-bolt-4182f0b00ae7)
|
- [Tutorial: URL Shortener using BoltDB](https://medium.com/@kataras/a-url-shortener-service-using-go-iris-and-bolt-4182f0b00ae7)
|
||||||
- [Tutorial: How to turn your Android Device into a fully featured Web Server (**MUST**)](https://twitter.com/ThePracticalDev/status/892022594031017988)
|
- [Tutorial: How to turn your Android Device into a fully featured Web Server (**MUST**)](https://twitter.com/ThePracticalDev/status/892022594031017988)
|
||||||
|
|
||||||
|
### Structuring
|
||||||
|
|
||||||
|
Nothing stops you from using your favorite folder structure. Iris is a low level web framework, it has got MVC support but it doesn't limit your folder structure, this is your choice.
|
||||||
|
|
||||||
|
Structuring is always depends on your needs. We can't tell you how to design your own application for sure but you're free to take a closer look to the examples below; you may find something useful that you can borrow for your app
|
||||||
|
|
||||||
|
- [Example 1](mvc/login)
|
||||||
|
- [Example 2](structuring/mvc)
|
||||||
|
|
||||||
### HTTP Listening
|
### HTTP Listening
|
||||||
|
|
||||||
- [Common, with address](http-listening/listen-addr/main.go)
|
- [Common, with address](http-listening/listen-addr/main.go)
|
||||||
|
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 15 KiB |
144
_examples/structuring/mvc/app/app.go
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/securecookie"
|
||||||
|
|
||||||
|
"github.com/kataras/iris"
|
||||||
|
"github.com/kataras/iris/middleware/logger"
|
||||||
|
"github.com/kataras/iris/middleware/recover"
|
||||||
|
"github.com/kataras/iris/sessions"
|
||||||
|
|
||||||
|
"github.com/kataras/iris/_examples/structuring/mvc/app/controllers/follower"
|
||||||
|
"github.com/kataras/iris/_examples/structuring/mvc/app/controllers/following"
|
||||||
|
"github.com/kataras/iris/_examples/structuring/mvc/app/controllers/index"
|
||||||
|
"github.com/kataras/iris/_examples/structuring/mvc/app/controllers/like"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Application is our application wrapper and bootstrapper, keeps our settings.
|
||||||
|
type Application struct {
|
||||||
|
*iris.Application
|
||||||
|
|
||||||
|
Name string
|
||||||
|
Owner string
|
||||||
|
SpawnDate time.Time
|
||||||
|
|
||||||
|
Sessions *sessions.Sessions
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewApplication returns a new named Application.
|
||||||
|
func NewApplication(name, owner string) *Application {
|
||||||
|
return &Application{
|
||||||
|
Name: name,
|
||||||
|
Owner: owner,
|
||||||
|
Application: iris.New(),
|
||||||
|
SpawnDate: time.Now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// begin sends the app's identification info.
|
||||||
|
func (app *Application) begin(ctx iris.Context) {
|
||||||
|
// response headers
|
||||||
|
ctx.Header("App-Name", app.Name)
|
||||||
|
ctx.Header("App-Owner", app.Owner)
|
||||||
|
ctx.Header("App-Since", time.Since(app.SpawnDate).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", app.Name)
|
||||||
|
ctx.ViewData("AppOwner", app.Owner)
|
||||||
|
ctx.Next()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupViews loads the templates.
|
||||||
|
func (app *Application) SetupViews(viewsDir string) {
|
||||||
|
app.RegisterView(iris.HTML(viewsDir, ".html").Layout("shared/layout.html"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupSessions initializes the sessions, optionally.
|
||||||
|
func (app *Application) SetupSessions(expires time.Duration, cookieHashKey, cookieBlockKey []byte) {
|
||||||
|
app.Sessions = sessions.New(sessions.Config{
|
||||||
|
Cookie: "SECRET_SESS_COOKIE_" + app.Name,
|
||||||
|
Expires: expires,
|
||||||
|
Encoding: securecookie.New(cookieHashKey, cookieBlockKey),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupErrorHandlers prepares the http error handlers (>=400).
|
||||||
|
// Remember that error handlers in Iris have their own middleware ecosystem
|
||||||
|
// so the route's middlewares are not running when an http error happened.
|
||||||
|
// So if we want a logger we have to re-create one, here we will customize that logger as well.
|
||||||
|
func (app *Application) SetupErrorHandlers() {
|
||||||
|
httpErrStatusLogger := logger.New(logger.Config{
|
||||||
|
Status: true,
|
||||||
|
IP: true,
|
||||||
|
Method: true,
|
||||||
|
Path: true,
|
||||||
|
MessageContextKey: "message",
|
||||||
|
LogFunc: func(now time.Time, latency time.Duration,
|
||||||
|
status, ip, method, path string,
|
||||||
|
message interface{}) {
|
||||||
|
|
||||||
|
line := fmt.Sprintf("%v %4v %s %s %s", status, latency, ip, method, path)
|
||||||
|
|
||||||
|
if message != nil {
|
||||||
|
line += fmt.Sprintf(" %v", message)
|
||||||
|
}
|
||||||
|
app.Logger().Warn(line)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
app.OnAnyErrorCode(app.begin, httpErrStatusLogger, func(ctx iris.Context) {
|
||||||
|
err := iris.Map{
|
||||||
|
"app": app.Name,
|
||||||
|
"status": ctx.GetStatusCode(),
|
||||||
|
"message": ctx.Values().GetString("message"),
|
||||||
|
}
|
||||||
|
|
||||||
|
if jsonOutput, _ := ctx.URLParamBool("json"); jsonOutput {
|
||||||
|
ctx.JSON(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.ViewData("Err", err)
|
||||||
|
ctx.ViewData("Title", "Error")
|
||||||
|
ctx.View("shared/error.html")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupRouter registers the available routes from the "controllers" package.
|
||||||
|
func (app *Application) SetupRouter() {
|
||||||
|
app.Use(recover.New())
|
||||||
|
app.Use(app.begin)
|
||||||
|
app.Use(iris.Gzip)
|
||||||
|
|
||||||
|
app.Favicon("./public/favicon.ico")
|
||||||
|
app.StaticWeb("/public", "./public")
|
||||||
|
|
||||||
|
app.Use(logger.New())
|
||||||
|
|
||||||
|
app.Controller("/", new(index.Controller))
|
||||||
|
app.Controller("/follower", new(follower.Controller))
|
||||||
|
app.Controller("/following", new(following.Controller))
|
||||||
|
app.Controller("/like", new(like.Controller))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Instance is our global application bootstrap instance.
|
||||||
|
var Instance = NewApplication("My Awesome App", "kataras2006@hotmail.com")
|
||||||
|
|
||||||
|
// Boot starts our default instance appolication.
|
||||||
|
func Boot(runner iris.Runner, configurators ...iris.Configurator) {
|
||||||
|
Instance.SetupViews("./app/views")
|
||||||
|
Instance.SetupSessions(24*time.Hour,
|
||||||
|
[]byte("the-big-and-secret-fash-key-here"),
|
||||||
|
[]byte("lot-secret-of-characters-big-too"),
|
||||||
|
)
|
||||||
|
|
||||||
|
Instance.SetupErrorHandlers()
|
||||||
|
Instance.SetupRouter()
|
||||||
|
|
||||||
|
Instance.Run(runner, configurators...)
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
package follower
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/kataras/iris"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Controller struct {
|
||||||
|
iris.Controller
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) GetBy(id int64) {
|
||||||
|
c.Ctx.Writef("from "+c.Route().Path()+" with ID: %d", id)
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
package following
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/kataras/iris"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Controller struct {
|
||||||
|
iris.Controller
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) GetBy(id int64) {
|
||||||
|
c.Ctx.Writef("from "+c.Route().Path()+" with ID: %d", id)
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package index
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/kataras/iris"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Controller struct {
|
||||||
|
iris.Controller
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) Get() {
|
||||||
|
c.Data["Title"] = "Index"
|
||||||
|
c.Tmpl = "index.html"
|
||||||
|
}
|
13
_examples/structuring/mvc/app/controllers/like/controller.go
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
package like
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/kataras/iris"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Controller struct {
|
||||||
|
iris.Controller
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) GetBy(id int64) {
|
||||||
|
c.Ctx.Writef("from "+c.Route().Path()+" with ID: %d", id)
|
||||||
|
}
|
1
_examples/structuring/mvc/app/views/index.html
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<h1>Welcome!!</h1>
|
5
_examples/structuring/mvc/app/views/shared/error.html
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<h1 class="text-danger">Error.</h1>
|
||||||
|
<h2 class="text-danger">An error occurred while processing your request.</h2>
|
||||||
|
|
||||||
|
<h3>{{.Err.status}}</h3>
|
||||||
|
<h4>{{.Err.message}}</h4>
|
23
_examples/structuring/mvc/app/views/shared/layout.html
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico" />
|
||||||
|
<title>{{.Title}} - {{.AppName}}</title>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div>
|
||||||
|
<!-- Render the current template here -->
|
||||||
|
{{ yield }}
|
||||||
|
<hr />
|
||||||
|
<footer>
|
||||||
|
<p>© 2017 - {{.AppOwner}}</p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
15
_examples/structuring/mvc/main.go
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/kataras/iris"
|
||||||
|
|
||||||
|
"github.com/kataras/iris/_examples/structuring/mvc/app"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// http://localhost:8080
|
||||||
|
// http://localhost:8080/follower/42
|
||||||
|
// http://localhost:8080/following/42
|
||||||
|
// http://localhost:8080/like/42
|
||||||
|
app.Boot(iris.Addr(":8080"), iris.WithoutServerError(iris.ErrServerClosed), iris.WithoutVersionChecker)
|
||||||
|
}
|
BIN
_examples/structuring/mvc/public/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 15 KiB |
|
@ -117,7 +117,7 @@ func (w *GzipResponseWriter) Write(contents []byte) (int, error) {
|
||||||
func (w *GzipResponseWriter) Writef(format string, a ...interface{}) (n int, err error) {
|
func (w *GzipResponseWriter) Writef(format string, a ...interface{}) (n int, err error) {
|
||||||
n, err = fmt.Fprintf(w, format, a...)
|
n, err = fmt.Fprintf(w, format, a...)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
w.ResponseWriter.Header().Set(contentTextHeaderValue, "text/plain")
|
w.ResponseWriter.Header().Set(contentTypeHeaderKey, contentTextHeaderValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
|
@ -128,7 +128,7 @@ func (w *GzipResponseWriter) Writef(format string, a ...interface{}) (n int, err
|
||||||
func (w *GzipResponseWriter) WriteString(s string) (n int, err error) {
|
func (w *GzipResponseWriter) WriteString(s string) (n int, err error) {
|
||||||
n, err = w.Write([]byte(s))
|
n, err = w.Write([]byte(s))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
w.ResponseWriter.Header().Set(contentTextHeaderValue, "text/plain")
|
w.ResponseWriter.Header().Set(contentTypeHeaderKey, contentTextHeaderValue)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -164,7 +164,7 @@ func (w *GzipResponseWriter) WriteNow(contents []byte) (int, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
w.ResponseWriter.Header().Add(varyHeaderKey, "Accept-Encoding")
|
w.ResponseWriter.Header().Add(varyHeaderKey, "Accept-Encoding")
|
||||||
w.ResponseWriter.Header().Set(contentEncodingHeaderKey, "gzip")
|
w.ResponseWriter.Header().Add(contentEncodingHeaderKey, "gzip")
|
||||||
|
|
||||||
// if not `WriteNow` but "Content-Length" header
|
// if not `WriteNow` but "Content-Length" header
|
||||||
// is exists, then delete it before `.Write`
|
// is exists, then delete it before `.Write`
|
||||||
|
|
|
@ -11,24 +11,9 @@ const (
|
||||||
DefaultCookieName = "irissessionid"
|
DefaultCookieName = "irissessionid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
// Encoding is the Cookie Encoder/Decoder interface, which can be passed as configuration field
|
||||||
// Config is the configuration for sessions. Please review it well before using sessions.
|
// alternatively to the `Encode` and `Decode` fields.
|
||||||
Config struct {
|
type Encoding interface {
|
||||||
// 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 setted true.
|
|
||||||
//
|
|
||||||
// Note: The user should fill the Decode configuation field in order for this to work.
|
|
||||||
// Recommendation: You don't need this to be setted to true, just fill the Encode and Decode fields
|
|
||||||
// with a third-party library like secure cookie, example is provided at the _examples folder.
|
|
||||||
//
|
|
||||||
// Defaults to false
|
|
||||||
CookieSecureTLS bool
|
|
||||||
|
|
||||||
// Encode the cookie value if not nil.
|
// Encode the cookie value if not nil.
|
||||||
// Should accept as first argument the cookie name (config.Name)
|
// Should accept as first argument the cookie name (config.Name)
|
||||||
// as second argument the server's generated session id.
|
// as second argument the server's generated session id.
|
||||||
|
@ -39,7 +24,7 @@ type (
|
||||||
// You either need to provide exactly that amount or you derive the key from what you type in.
|
// You either need to provide exactly that amount or you derive the key from what you type in.
|
||||||
//
|
//
|
||||||
// Defaults to nil
|
// Defaults to nil
|
||||||
Encode func(cookieName string, value interface{}) (string, error)
|
Encode(cookieName string, value interface{}) (string, error)
|
||||||
// Decode the cookie value if not nil.
|
// Decode the cookie value if not nil.
|
||||||
// Should accept as first argument the cookie name (config.Name)
|
// Should accept as first argument the cookie name (config.Name)
|
||||||
// as second second accepts the client's cookie value (the encoded session id).
|
// as second second accepts the client's cookie value (the encoded session id).
|
||||||
|
@ -50,8 +35,54 @@ type (
|
||||||
// You either need to provide exactly that amount or you derive the key from what you type in.
|
// You either need to provide exactly that amount or you derive the key from what you type in.
|
||||||
//
|
//
|
||||||
// Defaults to nil
|
// Defaults to nil
|
||||||
|
Decode(cookieName string, cookieValue string, v interface{}) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type (
|
||||||
|
// Config is the configuration for sessions. Please review it well before using sessions.
|
||||||
|
Config struct {
|
||||||
|
// 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 setted true.
|
||||||
|
//
|
||||||
|
// Note: The user should fill the Decode configuation field in order for this to work.
|
||||||
|
// Recommendation: You don't need this to be setted to true, just fill the Encode and Decode fields
|
||||||
|
// with a third-party library like secure cookie, example is provided at the _examples folder.
|
||||||
|
//
|
||||||
|
// Defaults to false.
|
||||||
|
CookieSecureTLS bool
|
||||||
|
|
||||||
|
// Encode the cookie value if not nil.
|
||||||
|
// Should accept as first argument the cookie name (config.Cookie)
|
||||||
|
// as second argument the server's generated session id.
|
||||||
|
// Should return the new session id, if error the session id setted to empty which is invalid.
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
//
|
||||||
|
// Defaults to nil.
|
||||||
|
Encode func(cookieName string, value interface{}) (string, error)
|
||||||
|
// Decode the cookie value if not nil.
|
||||||
|
// Should accept as first argument the cookie name (config.Cookie)
|
||||||
|
// as second second accepts the client's cookie value (the encoded session id).
|
||||||
|
// 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.
|
||||||
|
//
|
||||||
|
// Defaults to nil.
|
||||||
Decode func(cookieName string, cookieValue string, v interface{}) error
|
Decode func(cookieName string, cookieValue string, v interface{}) error
|
||||||
|
|
||||||
|
// Encoding same as Encode and Decode but receives a single instance which
|
||||||
|
// completes the "CookieEncoder" interface, `Encode` and `Decode` functions.
|
||||||
|
Encoding Encoding
|
||||||
|
|
||||||
// Expires the duration of which the cookie must expires (created_time.Add(Expires)).
|
// 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.
|
// If you want to delete the cookie when the browser closes, set it to -1.
|
||||||
//
|
//
|
||||||
|
@ -59,7 +90,7 @@ type (
|
||||||
// -1 means when browser closes
|
// -1 means when browser closes
|
||||||
// > 0 is the time.Duration which the session cookies should expire.
|
// > 0 is the time.Duration which the session cookies should expire.
|
||||||
//
|
//
|
||||||
// Defaults to infinitive/unlimited life duration(0)
|
// Defaults to infinitive/unlimited life duration(0).
|
||||||
Expires time.Duration
|
Expires time.Duration
|
||||||
|
|
||||||
// SessionIDGenerator should returns a random session id.
|
// SessionIDGenerator should returns a random session id.
|
||||||
|
@ -69,7 +100,7 @@ type (
|
||||||
|
|
||||||
// DisableSubdomainPersistence set it to true in order dissallow your subdomains to have access to the session cookie
|
// DisableSubdomainPersistence set it to true in order dissallow your subdomains to have access to the session cookie
|
||||||
//
|
//
|
||||||
// Defaults to false
|
// Defaults to false.
|
||||||
DisableSubdomainPersistence bool
|
DisableSubdomainPersistence bool
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -88,5 +119,10 @@ func (c Config) Validate() Config {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if c.Encoding != nil {
|
||||||
|
c.Encode = c.Encoding.Encode
|
||||||
|
c.Decode = c.Encoding.Decode
|
||||||
|
}
|
||||||
|
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|