├── .gitignore ├── LICENSE ├── README.md ├── README_ja.md ├── core.go ├── core_test.go ├── go.mod ├── go.sum ├── middleware_builtin.go ├── middleware_builtin_test.go ├── plugin.go ├── request.go ├── request_test.go ├── routing.go ├── routing_classic.go ├── routing_common.go ├── routing_test.go ├── sample ├── appengine │ ├── .gitignore │ ├── app.yaml │ ├── main.go │ ├── misc_model.go │ ├── model_json.go │ ├── model_query.go │ ├── package.json │ ├── public │ │ └── index.html │ ├── run.sh │ └── todo_service.go ├── basic │ ├── main.go │ ├── model_json.go │ ├── public │ │ └── index.html │ └── run.sh ├── helloworld │ └── main.go ├── oauth2idp │ ├── .gitignore │ ├── README.md │ ├── main.go │ ├── model_json.go │ ├── osin_storage.go │ ├── package.json │ ├── public │ │ └── index.html │ ├── run.sh │ ├── swagger-ui-index.patch │ └── yarn.lock └── swagger │ ├── .gitignore │ ├── main.go │ ├── model_json.go │ ├── package.json │ ├── public │ └── index.html │ ├── run.sh │ └── yarn.lock ├── setup.sh ├── swagger ├── jsonschema_draft4.go ├── middleware.go ├── middleware_customreq_test.go ├── middleware_test.go ├── plugin.go ├── plugin_test.go ├── schema_test.go ├── swagger_model.go ├── swagger_model_test.go ├── validator.go └── validator_test.go ├── test.sh ├── test_utils.go ├── tools.go ├── url_rawpath.go ├── url_rawpath_go14.go ├── utils.go ├── utils_test.go └── v3 ├── core.go ├── core_test.go ├── go.mod ├── go.sum ├── middleware_builtin.go ├── middleware_builtin_test.go ├── plugin.go ├── request.go ├── request_test.go ├── routing.go ├── routing_classic.go ├── routing_common.go ├── routing_test.go ├── setup.sh ├── swagger ├── jsonschema_draft4.go ├── middleware.go ├── middleware_customreq_test.go ├── middleware_test.go ├── plugin.go ├── plugin_test.go ├── schema_test.go ├── swagger_model.go ├── swagger_model_test.go ├── validator.go └── validator_test.go ├── test.sh ├── test_utils.go ├── tools.go ├── url_rawpath.go ├── url_rawpath_go14.go ├── utils.go └── utils_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.iml 3 | 4 | build-cmd/ 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 tv-asahi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README_ja.md: -------------------------------------------------------------------------------- 1 | # ucon 2 | 3 | uconはMiddlewareとPluginによる柔軟な拡張が可能な、Golangのウェブアプリケーションフレームワークです。 4 | 5 | ## インストール 6 | 7 | ``` 8 | go get -u github.com/favclip/ucon 9 | ``` 10 | 11 | ## 使い方 12 | 13 | uconを始めるには、サーバーを起動するためのgoファイルが必要です。まずは`main.go`を作成し、次のようにmain関数を実装します。 14 | 15 | ```go 16 | package main 17 | 18 | import ( 19 | "net/http" 20 | 21 | "github.com/favclip/ucon/v3" 22 | ) 23 | 24 | func main() { 25 | ucon.Orthodox() 26 | 27 | ucon.HandleFunc("GET", "/", func(w http.ResponseWriter, r *http.Request) { 28 | w.Write([]byte("Hello World!")) 29 | }) 30 | 31 | ucon.ListenAndServe(":8080") 32 | } 33 | ``` 34 | 35 | 次に`go run`コマンドでサーバーを起動してみましょう。 36 | 37 | ``` 38 | go run main.go 39 | ``` 40 | 41 | Webブラウザで`localhost:8080`にアクセスすると、uconによって返された`Hello World!`という文字列が表示されるでしょう! 42 | 43 | もっとサンプルが見たい方は`/sample`ディレクトリの中にあるいくつかの例を見てみるといいかもしれません。 44 | 45 | ## 特徴 46 | 47 | * 標準の[net/http](https://golang.org/pkg/net/http/)との互換性 48 | * 柔軟なルーティング設定 49 | * Middlewareによるリクエストハンドラの拡張 50 | * 強力なDI(依存性注入)機構 51 | * Pluginによるサーバー機能の拡張 52 | * `Orthodox()`による標準的な機能の提供 53 | * テスト支援のための便利なユーティリティ 54 | * Google App Engineなどの様々なプラットフォームで利用可能 55 | * [Swagger(Open API Initiative)](https://openapis.org/)に対応する`swagger`Plugin 56 | 57 | ### サーバーの起動 58 | uconを使ったサーバーを起動するには`ucon.ListenAndServe`関数を実行します。 59 | この関数は引数がアドレスだけであるという点を除いて、[`http.ListenAndServe`](https://golang.org/pkg/net/http/#ListenAndServe)と完全に互換性があります。 60 | 61 | 既存のサーバー上でuconを使用する場合は[「既存のサーバーへの組み込み」](#既存のサーバーへの組み込み)を参照してください。 62 | 63 | ### ルーティング 64 | uconのルーティングは`Handle`関数、または`HandleFunc`関数によって設定されます。 65 | `HandleFunc`関数はリクエストハンドラとして関数オブジェクトを登録します。 66 | 関数だけでは表現できない複雑なリクエストハンドリングを行いたい場合は、[HandlerContainer](http://godoc.org/github.com/favclip/ucon#HandlerContainer)インタフェースを実装したオブジェクトを`Handle`関数で登録することもできます。 67 | 68 | uconが標準の`http`パッケージと違うのは、uconではルーティングの定義にHTTPのリクエストメソッドが必須であることです。 69 | (これは[Google Cloud Endpoints](https://cloud.google.com/endpoints/?hl=en)やSwaggerといったプラットフォームとの親和性を高める上で必要なアプローチでした。) 70 | 同じパスへのリクエストでも、リクエストメソッドが違う場合はそれぞれにリクエストハンドラが個別に必要です。 71 | ただし、ルーティング定義のリクエストメソッドをワイルドカード(`*`)に指定した場合はすべてのリクエストメソッドに対して有効なリクエストハンドラになります。 72 | 73 | * `HandleFunc("GET", "/a/", ...)`は、GETリクエストの`/a/b`や`/a/b/c`などにはマッチしますが、 74 | POSTリクエストの`/a/b`にはマッチしませんし、GETリクエストの`/a`にもマッチしません。 75 | * `HandleFunc("*", "/", ...)`は、すべてのリクエストにマッチします。 76 | * (1)`HandleFunc("GET", "/a", ...)`と(2)`HandleFunc("GET", "/a/", ...)`の2つがある場合、 77 | GETリクエストの`/a`は(1)にマッチしますが、`/a/b`は(2)にマッチします。 78 | * `HandleFunc("GET", "/users/{id}", ...)`は、`/users/1`や`/users/foo/bar`にマッチしますが、`/users`にはマッチしません。 79 | 80 | ### Middleware機能 81 | uconにおけるMiddlewareとは、サーバーがリクエストを受け取ってからリクエストハンドラに届けられるまでの間に実行されるプリプロセッサのことです。 82 | いくつかのMiddlewareがuconには標準で用意されており、`ucon.Orthodox()`を実行すると次のMiddlewareが読み込まれます。 83 | 84 | * [ResponseMapper](http://godoc.org/github.com/favclip/ucon#ResponseMapper) - リクエストハンドラの戻り値をJSONに変換する 85 | * [HTTPRWDI](http://godoc.org/github.com/favclip/ucon#HTTPRWDI) - `http.Request`と`http.ResponseWriter`のDIを行う 86 | * [NetContextDI](http://godoc.org/github.com/favclip/ucon#NetContextDI) - `context.Context`のDIを行う 87 | * [RequestObjectMapper](http://godoc.org/github.com/favclip/ucon#RequestObjectMapper) - リクエストに含まれるパラメータやデータをリクエストハンドラの引数にある型のオブジェクトに変換する 88 | 89 | もちろん独自のMiddlewareを作成することもできます。ここでは例としてリクエストを受け取るたびに標準出力にログを書き込むMiddlewareを作ってみましょう。 90 | 91 | Middlewareの実体は、`func(b *ucon.Bubble) error`で表現される関数です。 92 | `main.go`に次のような`Logger`関数を定義します。 93 | 94 | ```go 95 | func Logger(b *ucon.Bubble) error { 96 | fmt.Printf("Received: %s %s\n", b.R.Method, b.R.URL.String()) 97 | return b.Next() 98 | } 99 | ``` 100 | 101 | あとは`Logger`をMiddlewareとして登録するだけです。Middlewareを登録するには`ucon.Middleware()`関数を呼び出します。 102 | 103 | ```go 104 | package main 105 | 106 | import ( 107 | "fmt" 108 | "net/http" 109 | 110 | "github.com/favclip/ucon/v3" 111 | ) 112 | 113 | func main() { 114 | ucon.Orthodox() 115 | 116 | ucon.Middleware(Logger) 117 | 118 | ucon.HandleFunc("GET", "/", func(w http.ResponseWriter, r *http.Request) { 119 | w.Write([]byte("Hello World!")) 120 | }) 121 | 122 | ucon.ListenAndServe(":8080") 123 | } 124 | 125 | func Logger(b *ucon.Bubble) error { 126 | fmt.Printf("Received: %s %s\n", b.R.Method, b.R.URL.String()) 127 | return b.Next() 128 | } 129 | ``` 130 | 131 | これで`main.go`を実行してサーバーを立ち上げると、リクエストを受け取るたびにログを出力するようになりました。 132 | 133 | Middlewareに与えられる`Bubble`は、リクエストがサーバーに届いてから、適切なリクエストハンドラに到達までの間のデータの運搬を行っています。 134 | `Bubble.Next()`を呼び出すと、次のMiddlewareに処理が移り、すべてのMiddlewareが処理を終えたら`ucon.Handle`か`ucon.HandleFunc`で登録された 135 | リクエストハンドラの処理が実行されます。 136 | 137 | ### DI機能 138 | uconのDI機構はMiddlewareが`Bubble.Arguments`に値を格納することで解決しています。 139 | リクエストハンドラとして登録された関数の引数は`Bubble.ArgumentTypes`に格納されるため、 140 | Middlewareで`Bubble.ArgumentTypes`を見て、任意の型に対して値を与えることができます。 141 | 例えば、リクエストハンドラに`time.Time`型の引数を追加する際は、次のようなMiddlewareでDIを追加することができます。 142 | 143 | ```go 144 | package main 145 | 146 | import ( 147 | "fmt" 148 | "net/http" 149 | "reflect" 150 | "time" 151 | 152 | "github.com/favclip/ucon/v3" 153 | ) 154 | 155 | func main() { 156 | ucon.Orthodox() 157 | 158 | ucon.Middleware(NowInJST) 159 | 160 | ucon.HandleFunc("GET", "/", func(w http.ResponseWriter, r *http.Request, now time.Time) { 161 | w.Write([]byte( 162 | fmt.Sprintf("Hello World! : %s", now.Format("2006/01/02 15:04:05"))) 163 | ) 164 | }) 165 | 166 | ucon.ListenAndServe(":8080") 167 | } 168 | 169 | func NowInJST(b *ucon.Bubble) error { 170 | for idx, argT := range b.ArgumentTypes { 171 | if argT == reflect.TypeOf(time.Time{}) { 172 | b.Arguments[idx] = reflect.ValueOf(time.Now()) 173 | break 174 | } 175 | } 176 | return b.Next() 177 | } 178 | ``` 179 | 180 | ### Plugin機能 181 | uconにおけるPluginとは、ucon全体の機能を拡張するためのプリプロセッサです。 182 | Middlewareのようにリクエストが来るたびに実行されるのではなく、サーバーが起動する際に一度だけ実行されます。 183 | sampleの`swagger`は、具体的なPluginの使い方を知るための手助けになるでしょう。 184 | 185 | Pluginを実装するには、Pluginのインタフェースを実装した構造体のオブジェクトを`ucon.Plugin()`関数で登録します。 186 | 現在、uconでは以下のPluginインタフェースが提供されています。 187 | 188 | - [HandlersScannerPlugin](http://godoc.org/github.com/favclip/ucon#HandlersScannerPlugin) - uconに登録されたリクエストハンドラの一覧を取得できる 189 | 190 | Pluginには`*ServeMux`が引数で与えられるので、PluginによってリクエストハンドラやMiddlewareを追加することも可能です。 191 | 192 | ### テスト支援機能 193 | uconではuconを使ったアプリケーションの単体テストを行うのに便利なユーティリティも提供しています。 194 | 195 | #### [MakeMiddlewareTestBed](http://godoc.org/github.com/favclip/ucon#MakeMiddlewareTestBed) 196 | `MakeMiddlewareTestBed` はMiddlewareのテストを行うためのテストベッドを提供します。 197 | 例えば、`NetContextDI`Middlewareのテストは次のように記述されています。 198 | 199 | ```go 200 | func TestNetContextDI(t *testing.T) { 201 | b, _ := MakeMiddlewareTestBed(t, NetContextDI, func(c context.Context) { 202 | if c == nil { 203 | t.Errorf("unexpected: %v", c) 204 | } 205 | }, nil) 206 | err := b.Next() 207 | if err != nil { 208 | t.Fatal(err) 209 | } 210 | } 211 | ``` 212 | 213 | #### [MakeHandlerTestBed](http://godoc.org/github.com/favclip/ucon#MakeHandlerTestBed) 214 | `MakeHandlerTestBed`はリクエストハンドラのテストを行うためのテストベッドを提供します。 215 | この関数を呼び出す前にリクエストハンドラが登録されている必要があります。 216 | 217 | routing_test.goではこの関数を使って次のようなテストが記述されています。 218 | 219 | ```go 220 | func TestRouterServeHTTP1(t *testing.T) { 221 | DefaultMux = NewServeMux() 222 | Orthodox() 223 | 224 | HandleFunc("PUT", "/api/test/{id}", func(req *RequestOfRoutingInfoAddHandlers) (*ResponseOfRoutingInfoAddHandlers, error) { 225 | if v := req.ID; v != 1 { 226 | t.Errorf("unexpected: %v", v) 227 | } 228 | if v := req.Offset; v != 100 { 229 | t.Errorf("unexpected: %v", v) 230 | } 231 | if v := req.Text; v != "Hi!" { 232 | t.Errorf("unexpected: %v", v) 233 | } 234 | return &ResponseOfRoutingInfoAddHandlers{Text: req.Text + "!"}, nil 235 | }) 236 | 237 | DefaultMux.Prepare() 238 | 239 | resp := MakeHandlerTestBed(t, "PUT", "/api/test/1?offset=100", strings.NewReader("{\"text\":\"Hi!\"}")) 240 | 241 | if v := resp.StatusCode; v != 200 { 242 | t.Errorf("unexpected: %v", v) 243 | } 244 | 245 | body, err := ioutil.ReadAll(resp.Body) 246 | if err != nil { 247 | t.Fatal(err) 248 | } 249 | if v := string(body); v != "{\"text\":\"Hi!!\"}" { 250 | t.Errorf("unexpected: %v", v) 251 | } 252 | } 253 | ``` 254 | 255 | ### 既存のサーバーへの組み込み 256 | uconの`ServeMux`は`http.Handler`を実装しているので、`http.Handle`関数に渡すことで 257 | 既存のGolangのサーバーに簡単に組み込むことができます。 258 | ただし`Handle`関数に渡す前に、通常は`ucon.ListenAndServe`の内部で実行されている`ServeMux#Prepare`関数を明示的に実行する必要があります。 259 | 260 | デフォルトの`ServeMux`の参照は、`ucon.DefaultMux`で取得することができます。 261 | 262 | ```go 263 | func init() { 264 | ucon.Orthodox() 265 | 266 | ... 267 | 268 | ucon.DefaultMux.Prepare() 269 | http.Handle("/", ucon.DefaultMux) 270 | } 271 | ``` 272 | 273 | ## ライセンス 274 | MIT 275 | 276 | -------------------------------------------------------------------------------- /core.go: -------------------------------------------------------------------------------- 1 | package ucon 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "strings" 8 | ) 9 | 10 | // DefaultMux is the default ServeMux in ucon. 11 | var DefaultMux = NewServeMux() 12 | 13 | // NewServeMux allocates and returns a new ServeMux. 14 | func NewServeMux() *ServeMux { 15 | mux := &ServeMux{ 16 | router: &Router{}, 17 | } 18 | mux.router.mux = mux 19 | return mux 20 | } 21 | 22 | // ServeMux is an HTTP request multiplexer. 23 | type ServeMux struct { 24 | Debug bool 25 | router *Router 26 | middlewares []MiddlewareFunc 27 | plugins []*pluginContainer 28 | } 29 | 30 | // MiddlewareFunc is an adapter to hook middleware processing. 31 | // Middleware works with 1 request. 32 | type MiddlewareFunc func(b *Bubble) error 33 | 34 | // Middleware can append Middleware to ServeMux. 35 | func (m *ServeMux) Middleware(f MiddlewareFunc) { 36 | m.middlewares = append(m.middlewares, f) 37 | } 38 | 39 | // Plugin can append Plugin to ServeMux. 40 | func (m *ServeMux) Plugin(plugin interface{}) { 41 | p, ok := plugin.(*pluginContainer) 42 | if !ok { 43 | p = &pluginContainer{base: plugin} 44 | } 45 | p.check() 46 | m.plugins = append(m.plugins, p) 47 | } 48 | 49 | // Prepare the ServeMux. 50 | // Plugin is not show affect to anything. 51 | // This method is enabled plugins. 52 | func (m *ServeMux) Prepare() { 53 | for _, plugin := range m.plugins { 54 | used := false 55 | if sc := plugin.HandlersScanner(); sc != nil { 56 | err := sc.HandlersScannerProcess(m, m.router.handlers) 57 | if err != nil { 58 | panic(err) 59 | } 60 | used = true 61 | } 62 | if !used { 63 | panic(fmt.Sprintf("unused plugin: %#v", plugin)) 64 | } 65 | } 66 | } 67 | 68 | // ServeHTTP dispatches request to the handler. 69 | func (m *ServeMux) ServeHTTP(w http.ResponseWriter, r *http.Request) { 70 | // NOTE ucon内部のルーティングは一元的にこの関数から行う 71 | // Handlerを細分化しhttp.ServeMuxに登録すると、OPTIONSのhandleがうまくできなくなる 72 | // このため、Handlerはucon全体で1つとし、OPTIONSも通常のMethodと同じようにHandlerを設定し利用する 73 | // OPTIONSを適切にhandleするため、全てのHandlerに特殊なHookを入れるよりマシである 74 | 75 | m.router.ServeHTTP(w, r) 76 | } 77 | 78 | // ListenAndServe start accepts the client request. 79 | func (m *ServeMux) ListenAndServe(addr string) error { 80 | m.Prepare() 81 | 82 | server := &http.Server{Addr: addr, Handler: m} 83 | return server.ListenAndServe() 84 | } 85 | 86 | // Handle register the HandlerContainer for the given method & path to the ServeMux. 87 | func (m *ServeMux) Handle(method string, path string, hc HandlerContainer) { 88 | CheckFunction(hc.Handler()) 89 | 90 | pathTmpl := ParsePathTemplate(path) 91 | methods := strings.Split(strings.ToUpper(method), ",") 92 | for _, method := range methods { 93 | rd := &RouteDefinition{ 94 | Method: method, 95 | PathTemplate: pathTmpl, 96 | HandlerContainer: hc, 97 | } 98 | m.router.addRoute(rd) 99 | } 100 | } 101 | 102 | // HandleFunc register the handler function for the given method & path to the ServeMux. 103 | func (m *ServeMux) HandleFunc(method string, path string, h interface{}) { 104 | m.Handle(method, path, &handlerContainerImpl{ 105 | handler: h, 106 | Context: background, 107 | }) 108 | } 109 | 110 | func (m *ServeMux) newBubble(c context.Context, w http.ResponseWriter, r *http.Request, rd *RouteDefinition) (*Bubble, error) { 111 | b := &Bubble{ 112 | R: r, 113 | W: w, 114 | Context: c, 115 | RequestHandler: rd.HandlerContainer, 116 | } 117 | err := b.init(m) 118 | if err != nil { 119 | return nil, err 120 | } 121 | 122 | return b, nil 123 | } 124 | 125 | // HandlerContainer is handler function container. 126 | // and It has a ucon Context that make it possible communicate to Plugins. 127 | type HandlerContainer interface { 128 | Handler() interface{} 129 | Context 130 | } 131 | 132 | type handlerContainerImpl struct { 133 | handler interface{} 134 | Context 135 | } 136 | 137 | func (hc *handlerContainerImpl) Handler() interface{} { 138 | return hc.handler 139 | } 140 | 141 | // Orthodox middlewares enable to DefaultServeMux. 142 | func Orthodox() { 143 | DefaultMux.Middleware(ResponseMapper()) 144 | DefaultMux.Middleware(HTTPRWDI()) 145 | DefaultMux.Middleware(ContextDI()) 146 | DefaultMux.Middleware(RequestObjectMapper()) 147 | } 148 | 149 | // Middleware can append Middleware to ServeMux. 150 | func Middleware(f MiddlewareFunc) { 151 | DefaultMux.Middleware(f) 152 | } 153 | 154 | // Plugin can append Plugin to ServeMux. 155 | func Plugin(plugin interface{}) { 156 | DefaultMux.Plugin(plugin) 157 | } 158 | 159 | // ListenAndServe start accepts the client request. 160 | func ListenAndServe(addr string) { 161 | DefaultMux.ListenAndServe(addr) 162 | } 163 | 164 | // Handle register the HandlerContainer for the given method & path to the ServeMux. 165 | func Handle(method string, path string, hc HandlerContainer) { 166 | DefaultMux.Handle(method, path, hc) 167 | } 168 | 169 | // HandleFunc register the handler function for the given method & path to the ServeMux. 170 | func HandleFunc(method string, path string, h interface{}) { 171 | DefaultMux.HandleFunc(method, path, h) 172 | } 173 | -------------------------------------------------------------------------------- /core_test.go: -------------------------------------------------------------------------------- 1 | package ucon 2 | 3 | import "testing" 4 | 5 | func TestMiddleware(t *testing.T) { 6 | DefaultMux = NewServeMux() 7 | 8 | if v := len(DefaultMux.middlewares); v != 0 { 9 | t.Fatalf("unexpected: %v", v) 10 | } 11 | 12 | Middleware(func(b *Bubble) error { 13 | return nil 14 | }) 15 | 16 | if v := len(DefaultMux.middlewares); v != 1 { 17 | t.Fatalf("unexpected: %v", v) 18 | } 19 | } 20 | 21 | type TargetOfHandlersScannerPlugin struct { 22 | } 23 | 24 | func (obj *TargetOfHandlersScannerPlugin) HandlersScannerProcess(m *ServeMux, rds []*RouteDefinition) error { 25 | m.HandleFunc("GET", "/api/test/{id}", func() {}) 26 | 27 | return nil 28 | } 29 | 30 | func TestPluginWithPluginContainer(t *testing.T) { 31 | DefaultMux = NewServeMux() 32 | 33 | if v := len(DefaultMux.plugins); v != 0 { 34 | t.Fatalf("unexpected: %v", v) 35 | } 36 | 37 | Plugin(&pluginContainer{ 38 | base: &TargetOfHandlersScannerPlugin{}, 39 | }) 40 | 41 | if v := len(DefaultMux.plugins); v != 1 { 42 | t.Fatalf("unexpected: %v", v) 43 | } 44 | } 45 | 46 | func TestPluginWithoutPluginContainer(t *testing.T) { 47 | DefaultMux = NewServeMux() 48 | 49 | if v := len(DefaultMux.plugins); v != 0 { 50 | t.Fatalf("unexpected: %v", v) 51 | } 52 | 53 | Plugin(&TargetOfHandlersScannerPlugin{}) 54 | 55 | if v := len(DefaultMux.plugins); v != 1 { 56 | t.Fatalf("unexpected: %v", v) 57 | } 58 | } 59 | 60 | func TestPrepare(t *testing.T) { 61 | DefaultMux = NewServeMux() 62 | 63 | Plugin(&TargetOfHandlersScannerPlugin{}) 64 | 65 | if v := len(DefaultMux.router.handlers); v != 0 { 66 | t.Fatalf("unexpected: %v", v) 67 | } 68 | 69 | DefaultMux.Prepare() 70 | 71 | if v := len(DefaultMux.router.handlers); v != 1 { 72 | t.Fatalf("unexpected: %v", v) 73 | } 74 | } 75 | 76 | func TestHandle(t *testing.T) { 77 | DefaultMux = NewServeMux() 78 | 79 | if v := len(DefaultMux.router.handlers); v != 0 { 80 | t.Fatalf("unexpected: %v", v) 81 | } 82 | 83 | Handle("GET", "/api/test", &handlerContainerImpl{ 84 | handler: func() {}, 85 | Context: background, 86 | }) 87 | 88 | HandleFunc("GET", "/api/test/{id}", func() {}) 89 | HandleFunc("PUT", "/api/test/{id}", func() {}) 90 | 91 | if v := len(DefaultMux.router.handlers); v != 3 { 92 | t.Fatalf("unexpected: %v", v) 93 | } 94 | } 95 | 96 | func TestUconContextWithValue(t *testing.T) { 97 | var ctx Context = background 98 | if v := ctx.Value("a"); v != nil { 99 | t.Fatalf("unexpected: %v", v) 100 | } 101 | 102 | ctx = WithValue(ctx, "a", "b") 103 | 104 | if v := ctx.Value("a"); v.(string) != "b" { 105 | t.Fatalf("unexpected: %v", v) 106 | } 107 | 108 | ctx = WithValue(ctx, 1, 2) 109 | 110 | if v := ctx.Value("a"); v.(string) != "b" { 111 | t.Fatalf("unexpected: %v", v) 112 | } 113 | if v := ctx.Value(1); v.(int) != 2 { 114 | t.Fatalf("unexpected: %v", v) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/favclip/ucon 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/RangelReale/osin v1.0.1 7 | github.com/favclip/golidator v2.1.1+incompatible 8 | github.com/favclip/jwg v1.1.0 9 | github.com/favclip/qbg v1.1.1 10 | github.com/favclip/ucon/v3 v3.2.0 11 | github.com/google/uuid v1.1.0 // indirect 12 | github.com/k0kubun/pp v3.0.1+incompatible 13 | github.com/mjibson/goon v0.0.0-20180507203004-0c01b4bc4f49 14 | github.com/pborman/uuid v1.2.0 // indirect 15 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b 16 | golang.org/x/tools v0.0.0-20200408132156-9ee5ef7a2c0d 17 | google.golang.org/appengine v1.6.5 18 | ) 19 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/RangelReale/osin v1.0.1 h1:JcqBe8ljQq9WQJPtioXGxBWyIcfuVMw0BX6yJ9E4HKw= 2 | github.com/RangelReale/osin v1.0.1/go.mod h1:k/PH1SjZDitJDtK3zHm/XZRi+bRz6i3rhx9qE9p54CY= 3 | github.com/favclip/genbase v1.0.0 h1:nvoQl0lpkQi3yrSHIrlo0+qfuxv7ZFXpgXii/8CfphA= 4 | github.com/favclip/genbase v1.0.0/go.mod h1:woIlAaQeKArqr+4fq0587kpfuLsjhJV3DA7xGtEdK7Q= 5 | github.com/favclip/golidator v2.1.1+incompatible h1:ktb02g0J1vXnhB6LaTmffeROb5Wxacr6nJkw4cKs9/Q= 6 | github.com/favclip/golidator v2.1.1+incompatible/go.mod h1:ruXV1d2tSVPa7CCm99hW+DYn58ac/sM5eeL73+Y7k1w= 7 | github.com/favclip/jwg v1.1.0 h1:JCveviH6hJz0YiHqisF+5pNC4rIV52lUdLjEVwoiFUY= 8 | github.com/favclip/jwg v1.1.0/go.mod h1:pnVGpkY4wkjLNei7EW96N+nrtl5vX/n2mELPHQBH9Mk= 9 | github.com/favclip/qbg v1.1.1 h1:+XMzBTY7L2lvhrurHB+QYqzW0RZtRsNoSitpGnmbgYE= 10 | github.com/favclip/qbg v1.1.1/go.mod h1:MVeXoe5BQr3gFch7dOojqXoMdI+vOs81mGmfBy0vFPo= 11 | github.com/favclip/ucon/v3 v3.2.0 h1:P33wIvyIHNDJuXq3B75ySwiMg9WStF6zkpLwPTJF6ls= 12 | github.com/favclip/ucon/v3 v3.2.0/go.mod h1:aL7254Y43kWChblSpPFGl3l5OMMSAc9QrIDslQSmkF8= 13 | github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= 14 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 15 | github.com/google/uuid v1.0.0 h1:b4Gk+7WdP/d3HZH8EJsZpvV7EtDOgaZLtnaNGIu1adA= 16 | github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 17 | github.com/google/uuid v1.1.0 h1:Jf4mxPC/ziBnoPIdpQdPJ9OeiomAUHLvxmPRSPH9m4s= 18 | github.com/google/uuid v1.1.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 19 | github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 h1:uC1QfSlInpQF+M0ao65imhwqKnz3Q2z/d8PWZRMQvDM= 20 | github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= 21 | github.com/k0kubun/pp v3.0.1+incompatible h1:3tqvf7QgUnZ5tXO6pNAZlrvHgl6DvifjDrd9g2S9Z40= 22 | github.com/k0kubun/pp v3.0.1+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg= 23 | github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE= 24 | github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 25 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 26 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 27 | github.com/mjibson/goon v0.0.0-20180507203004-0c01b4bc4f49 h1:aRO4t5OVomiqba8g4Mipn30TeiONKDRa75vtmpEl5i4= 28 | github.com/mjibson/goon v0.0.0-20180507203004-0c01b4bc4f49/go.mod h1:ux0hcoHAAelrijanY+NcGfOWQwUpUadA7mOQMF6o1ro= 29 | github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g= 30 | github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= 31 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 32 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 33 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 34 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 35 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 36 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k= 37 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 38 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 39 | golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ= 40 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 41 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 42 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 43 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 44 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b h1:0mm1VjtFUOIlE1SbDlwjYaDxZVDP2S5ou6y0gSgXHu8= 45 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 46 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 47 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 48 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= 49 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 50 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 51 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 52 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8= 53 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 54 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 55 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 56 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 57 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 58 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 59 | golang.org/x/tools v0.0.0-20200408132156-9ee5ef7a2c0d h1:2DXIdtvIYvvWOcAOsX81FwOUBoQoMZhosWn7KjXEl94= 60 | golang.org/x/tools v0.0.0-20200408132156-9ee5ef7a2c0d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 61 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 62 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 63 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 64 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 65 | google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= 66 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 67 | -------------------------------------------------------------------------------- /plugin.go: -------------------------------------------------------------------------------- 1 | package ucon 2 | 3 | import "fmt" 4 | 5 | type pluginContainer struct { 6 | base interface{} 7 | } 8 | 9 | // HandlersScannerPlugin is an interface to make a plugin for scanning request handlers. 10 | type HandlersScannerPlugin interface { 11 | HandlersScannerProcess(m *ServeMux, rds []*RouteDefinition) error 12 | } 13 | 14 | func (p *pluginContainer) check() { 15 | if p.HandlersScanner() != nil { 16 | return 17 | } 18 | 19 | panic(fmt.Sprintf("unused plugin: %#v", p.base)) 20 | } 21 | 22 | // HandlersScanner returns itself if it implements HandlersScannerPlugin. 23 | func (p *pluginContainer) HandlersScanner() HandlersScannerPlugin { 24 | if v, ok := p.base.(HandlersScannerPlugin); ok { 25 | return v 26 | } 27 | 28 | return nil 29 | } 30 | 31 | type emptyCtx int 32 | 33 | var background = new(emptyCtx) 34 | 35 | func (*emptyCtx) Value(key interface{}) interface{} { 36 | return nil 37 | } 38 | 39 | // Context is a key-value store. 40 | type Context interface { 41 | Value(key interface{}) interface{} 42 | } 43 | 44 | // WithValue returns a new context containing the value. 45 | // Values contained by parent context are inherited. 46 | func WithValue(parent Context, key interface{}, val interface{}) Context { 47 | return &valueCtx{parent, key, val} 48 | } 49 | 50 | type valueCtx struct { 51 | Context 52 | key interface{} 53 | val interface{} 54 | } 55 | 56 | func (c *valueCtx) Value(key interface{}) interface{} { 57 | if c.key == key { 58 | return c.val 59 | } 60 | return c.Context.Value(key) 61 | } 62 | -------------------------------------------------------------------------------- /request.go: -------------------------------------------------------------------------------- 1 | package ucon 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "reflect" 9 | ) 10 | 11 | // ErrInvalidRequestHandler is the error that Bubble.RequestHandler is not a function. 12 | var ErrInvalidRequestHandler = errors.New("invalid request handler. not function") 13 | 14 | // ErrInvalidArgumentLength is the error that length of Bubble.Arguments does not match to RequestHandler arguments. 15 | var ErrInvalidArgumentLength = errors.New("invalid arguments") 16 | 17 | // ErrInvalidArgumentValue is the error that value in Bubble.Arguments is invalid. 18 | var ErrInvalidArgumentValue = errors.New("invalid argument value") 19 | 20 | // Bubble is a context of data processing that will be passed to a request handler at last. 21 | // The name `Bubble` means that the processing flow is a event-bubbling. 22 | // Processors, called `middleware`, are executed in order with same context, and at last the RequestHandler will be called. 23 | type Bubble struct { 24 | R *http.Request 25 | W http.ResponseWriter 26 | Context context.Context 27 | RequestHandler HandlerContainer 28 | 29 | Debug bool 30 | 31 | Handled bool 32 | ArgumentTypes []reflect.Type 33 | Arguments []reflect.Value 34 | Returns []reflect.Value 35 | 36 | queueIndex int 37 | mux *ServeMux 38 | } 39 | 40 | func (b *Bubble) checkHandlerType() error { 41 | if _, ok := b.RequestHandler.(HandlerContainer); ok { 42 | return nil 43 | } 44 | hv := reflect.ValueOf(b.RequestHandler) 45 | if hv.Type().Kind() == reflect.Func { 46 | return nil 47 | } 48 | 49 | return ErrInvalidRequestHandler 50 | } 51 | 52 | func (b *Bubble) handler() interface{} { 53 | if hv, ok := b.RequestHandler.(HandlerContainer); ok { 54 | return hv.Handler() 55 | } 56 | hv := reflect.ValueOf(b.RequestHandler) 57 | if hv.Type().Kind() == reflect.Func { 58 | return b.RequestHandler 59 | } 60 | 61 | return nil 62 | } 63 | 64 | func (b *Bubble) init(m *ServeMux) error { 65 | err := b.checkHandlerType() 66 | if err != nil { 67 | return err 68 | } 69 | 70 | hv := reflect.ValueOf(b.handler()) 71 | numIn := hv.Type().NumIn() 72 | b.ArgumentTypes = make([]reflect.Type, numIn) 73 | for i := 0; i < numIn; i++ { 74 | b.ArgumentTypes[i] = hv.Type().In(i) 75 | } 76 | b.Arguments = make([]reflect.Value, numIn) 77 | 78 | b.mux = m 79 | b.Debug = m.Debug 80 | 81 | return nil 82 | } 83 | 84 | // Next passes the bubble to next middleware. 85 | // If the bubble reaches at last, RequestHandler will be called. 86 | func (b *Bubble) Next() error { 87 | if b.queueIndex < len(b.mux.middlewares) { 88 | qi := b.queueIndex 89 | b.queueIndex++ 90 | m := b.mux.middlewares[qi] 91 | err := m(b) 92 | return err 93 | } 94 | 95 | return b.do() 96 | } 97 | 98 | func (b *Bubble) do() error { 99 | hv := reflect.ValueOf(b.handler()) 100 | 101 | if len(b.Arguments) != len(b.ArgumentTypes) || len(b.Arguments) != hv.Type().NumIn() { 102 | return ErrInvalidArgumentLength 103 | } 104 | for idx, arg := range b.Arguments { 105 | if !arg.IsValid() { 106 | fmt.Printf("ArgumentInvalid %d\n", idx) 107 | return ErrInvalidArgumentValue 108 | } 109 | if !arg.Type().AssignableTo(hv.Type().In(idx)) { 110 | fmt.Printf("TypeMismatch %d, %+v, %+v\n", idx, b.Arguments[idx], hv.Type().In(idx)) 111 | return ErrInvalidArgumentValue 112 | } 113 | } 114 | 115 | b.Returns = hv.Call(b.Arguments) 116 | 117 | b.Handled = true 118 | 119 | return nil 120 | } 121 | -------------------------------------------------------------------------------- /request_test.go: -------------------------------------------------------------------------------- 1 | package ucon 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func noopMiddleware(b *Bubble) error { 11 | return b.Next() 12 | } 13 | 14 | func TestBubbleInit(t *testing.T) { 15 | b, _ := MakeMiddlewareTestBed(t, noopMiddleware, func(ctx context.Context) error { 16 | return nil 17 | }, nil) 18 | 19 | if v := len(b.ArgumentTypes); v != 1 { 20 | t.Errorf("unexpected: %v", v) 21 | } 22 | if v := b.ArgumentTypes[0]; v != contextType { 23 | t.Errorf("unexpected: %v", v) 24 | } 25 | if v := len(b.Arguments); v != 1 { 26 | t.Errorf("unexpected: %v", v) 27 | } 28 | if v := len(b.Returns); v != 0 { 29 | t.Errorf("unexpected: %v", v) 30 | } 31 | } 32 | 33 | func TestBubbleDo(t *testing.T) { 34 | b, _ := MakeMiddlewareTestBed(t, noopMiddleware, func(ctx context.Context) error { 35 | return nil 36 | }, nil) 37 | 38 | b.Arguments = nil 39 | if v := b.Next(); v != ErrInvalidArgumentLength { 40 | t.Errorf("unexpected: %v", v) 41 | } 42 | 43 | b.Arguments = make([]reflect.Value, 1) 44 | b.Arguments[0] = reflect.Value{} 45 | if v := b.Next(); v != ErrInvalidArgumentValue { 46 | t.Errorf("unexpected: %v", v) 47 | } 48 | 49 | b.Arguments[0] = reflect.ValueOf(time.Time{}) 50 | if v := b.Next(); v != ErrInvalidArgumentValue { 51 | t.Errorf("unexpected: %v", v) 52 | } 53 | 54 | if v := b.Handled; v { 55 | t.Errorf("unexpected: %v", v) 56 | } 57 | 58 | b.Arguments[0] = reflect.ValueOf(context.Background()) 59 | if v := b.Next(); v != nil { 60 | t.Errorf("unexpected: %v", v) 61 | } 62 | 63 | if v := len(b.Returns); v != 1 { 64 | t.Errorf("unexpected: %v", v) 65 | } 66 | if v := b.Returns[0]; !v.IsNil() { 67 | t.Errorf("unexpected: %v", v) 68 | } 69 | if v := b.Handled; !v { 70 | t.Errorf("unexpected: %v", v) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /routing.go: -------------------------------------------------------------------------------- 1 | // +build go1.7 2 | 3 | package ucon 4 | 5 | import ( 6 | "context" 7 | "net/http" 8 | ) 9 | 10 | func getDefaultContext(r *http.Request) context.Context { 11 | return r.Context() 12 | } 13 | -------------------------------------------------------------------------------- /routing_classic.go: -------------------------------------------------------------------------------- 1 | // +build !go1.7 2 | 3 | package ucon 4 | 5 | import ( 6 | "context" 7 | "net/http" 8 | ) 9 | 10 | func getDefaultContext(r *http.Request) context.Context { 11 | return context.Background() 12 | } 13 | -------------------------------------------------------------------------------- /routing_common.go: -------------------------------------------------------------------------------- 1 | package ucon 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "net/url" 7 | "regexp" 8 | "strings" 9 | ) 10 | 11 | type methodMatchRate int 12 | 13 | const ( 14 | noMethodMatch methodMatchRate = iota 15 | starMethodMatch 16 | overloadMethodMatch 17 | exactMethodMatch 18 | ) 19 | 20 | const ( 21 | noPathMatch int = iota 22 | starPathMatch 23 | exactPathMatch 24 | ) 25 | 26 | // Router is a handler to pass requests to the best-matched route definition. 27 | // For the router decides the best route definition, there are 3 rules. 28 | // 29 | // 1. Methods must match. 30 | // a. If the method of request is `HEAD`, exceptionally `GET` definition is also allowed. 31 | // b. If the method of definition is `*`, the definition matches on all method. 32 | // 2. Paths must match as longer as possible. 33 | // a. The path of definition must match to the request path completely. 34 | // b. Select the longest match. 35 | // * Against Request[/api/foo/hi/comments/1], Definition[/api/foo/{bar}/] is stronger than Definition[/api/foo/]. 36 | // 3. If there are multiple options after 1 and 2 rules, select the earliest one which have been added to router. 37 | // 38 | type Router struct { 39 | mux *ServeMux 40 | handlers []*RouteDefinition 41 | } 42 | 43 | func (ro *Router) addRoute(rd *RouteDefinition) { 44 | ro.handlers = append(ro.handlers, rd) 45 | } 46 | 47 | // ServeHTTP routes a request to the handler and creates new bubble. 48 | func (ro *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) { 49 | rd := ro.pickupBestRouteDefinition(r) 50 | 51 | if rd == nil { 52 | http.NotFound(w, r) 53 | return 54 | } 55 | 56 | ctx := getDefaultContext(r) 57 | 58 | match, params := rd.PathTemplate.Match(encodedPathFromRequest(r)) 59 | if !match { 60 | http.Error(w, "[ucon] invalid handler picked", http.StatusInternalServerError) 61 | return 62 | } 63 | ctx = context.WithValue(ctx, PathParameterKey, params) 64 | 65 | b, err := ro.mux.newBubble(ctx, w, r, rd) 66 | if err != nil { 67 | http.Error(w, err.Error(), http.StatusInternalServerError) 68 | return 69 | } 70 | err = b.Next() 71 | if err != nil { 72 | http.Error(w, err.Error(), http.StatusInternalServerError) 73 | return 74 | } 75 | } 76 | 77 | func (ro *Router) pickupBestRouteDefinition(r *http.Request) *RouteDefinition { 78 | // NOTE 動作概要 79 | // 設定されているHandler群から適切なものを選択しそれにroutingする 80 | // "適切なもの"の選び方は以下の通り 81 | // 1. Methodが一致する 82 | // a. 指定MethodがHEADの場合、GETも探索する(明示的にHEADのものが優先される) 83 | // b. 指定Methodが*の場合、すべてのMethodに対して一致するものとする 84 | // 2. RequestPathが一致する 85 | // a. Handler側のパスは全長一致しなければならない 86 | // b. より長いパス長のものを優先する /api/foo/bar なら3節 という数え方 87 | // /api/foo/ と /api/foo/{bar}/ というHandlerがあったら、 /api/foo/hi/comments/1 は /api/foo/{bar}/ に割り当てられる 88 | // 3. 1,2での評価が最も高いものが複数ある場合は、より早くServeMuxに追加されたHandlerを選ぶ 89 | // 90 | // ルーティングのサンプル 91 | // Handler: OPTIONS / , POST /api/todo & Request: OPTIONS /api/todo -> OPTIONS / が選択される 92 | // Handler: * /api/todo/ , POST /api/todo/{id} & Request: GET /api/todo/1 -> * /api/todo/ が選択される 93 | 94 | var bestRoute *RouteDefinition 95 | var bestMethodMatchRate methodMatchRate 96 | var bestPathMatchRate int 97 | 98 | methodMatchRate := func(rd *RouteDefinition) methodMatchRate { 99 | if rd.Method == "*" { 100 | return starMethodMatch 101 | } 102 | 103 | if rd.Method == "GET" && r.Method == "HEAD" { 104 | return overloadMethodMatch 105 | } 106 | 107 | if rd.Method == r.Method { 108 | return exactMethodMatch 109 | } 110 | 111 | return noMethodMatch 112 | } 113 | 114 | pathMatchRate := func(rd *RouteDefinition) int { 115 | match, _ := rd.PathTemplate.Match(r.URL.Path) 116 | if !match { 117 | 118 | return noPathMatch 119 | } 120 | 121 | tempPathTokens := rd.PathTemplate.splittedPathTemplate 122 | reqPathTokens := strings.Split(r.URL.Path, "/") 123 | 124 | if len(reqPathTokens) < len(tempPathTokens) { 125 | // tempPath must not be longer than reqPath 126 | return noPathMatch 127 | } 128 | 129 | var rate int 130 | for i, token := range tempPathTokens { 131 | if i == 0 { 132 | // first token is always "" 133 | continue 134 | } 135 | if rd.PathTemplate.isVariables[i] { 136 | // variable token matches to everything. 137 | rate += exactPathMatch 138 | continue 139 | } 140 | if token == "" { 141 | // "/a/" can match to "/a/c", but it's weaker than exact match. 142 | rate += starPathMatch 143 | continue 144 | } 145 | if token == reqPathTokens[i] { 146 | rate += exactPathMatch 147 | } 148 | } 149 | 150 | return rate 151 | } 152 | 153 | for _, rd := range ro.handlers { 154 | mRate := methodMatchRate(rd) 155 | if mRate == noMethodMatch { 156 | continue 157 | } else if mRate < bestMethodMatchRate { 158 | continue 159 | } 160 | 161 | pRate := pathMatchRate(rd) 162 | if pRate == noPathMatch { 163 | continue 164 | } else if pRate < bestPathMatchRate { 165 | continue 166 | } 167 | 168 | if bestMethodMatchRate == mRate && bestPathMatchRate == pRate { 169 | continue 170 | } 171 | 172 | bestMethodMatchRate = mRate 173 | bestPathMatchRate = pRate 174 | bestRoute = rd 175 | } 176 | 177 | return bestRoute 178 | } 179 | 180 | // RouteDefinition is a definition of route handling. 181 | // If a request matches on both the method and the path, the handler runs. 182 | type RouteDefinition struct { 183 | Method string 184 | PathTemplate *PathTemplate 185 | HandlerContainer HandlerContainer 186 | } 187 | 188 | // PathTemplate is a path with parameters template. 189 | type PathTemplate struct { 190 | PathTemplate string 191 | httpHandlePath string 192 | isVariables []bool 193 | splittedPathTemplate []string 194 | PathParameters []string 195 | } 196 | 197 | // Match checks whether PathTemplate matches the request path. 198 | // If the path contains parameter templates, those key-value map is also returned. 199 | func (pt *PathTemplate) Match(requestPath string) (bool, map[string]string) { 200 | if pt.PathTemplate == pt.httpHandlePath { 201 | // TODO ざっくりした実装なので後でリファクタリングすること 202 | if strings.HasPrefix(requestPath, pt.httpHandlePath) { 203 | return true, nil 204 | } 205 | } 206 | 207 | requestPathSplitted := strings.Split(requestPath, "/") 208 | if requiredLen, requestLen := len(pt.splittedPathTemplate), len(requestPathSplitted); requiredLen < requestLen { 209 | // I want to match /js/index.js to / :) 210 | requestPathSplitted = requestPathSplitted[0:requiredLen] 211 | } else if requiredLen != requestLen { 212 | return false, nil 213 | } 214 | 215 | params := make(map[string]string) 216 | for idx, s := range pt.splittedPathTemplate { 217 | reqPart := requestPathSplitted[idx] 218 | if pt.isVariables[idx] { 219 | if reqPart == "" { 220 | return false, nil 221 | } 222 | v, err := url.QueryUnescape(reqPart) 223 | if err != nil { 224 | v = reqPart 225 | } 226 | params[s[1:len(s)-1]] = v 227 | } else if s != reqPart { 228 | return false, nil 229 | } else { 230 | // match 231 | } 232 | } 233 | 234 | return true, params 235 | } 236 | 237 | // ParsePathTemplate parses path string to PathTemplate. 238 | func ParsePathTemplate(pathTmpl string) *PathTemplate { 239 | tmpl := &PathTemplate{} 240 | tmpl.PathTemplate = pathTmpl 241 | vIndex := strings.Index(pathTmpl, "{") 242 | if vIndex == -1 { 243 | tmpl.httpHandlePath = pathTmpl 244 | } else { 245 | tmpl.httpHandlePath = pathTmpl[:vIndex] 246 | } 247 | 248 | tmpl.splittedPathTemplate = strings.Split(pathTmpl, "/") 249 | tmpl.isVariables = make([]bool, len(tmpl.splittedPathTemplate)) 250 | 251 | re := regexp.MustCompile("^\\{(.+)\\}$") 252 | for idx, param := range tmpl.splittedPathTemplate { 253 | if re.MatchString(param) { 254 | key := re.FindStringSubmatch(param)[1] 255 | tmpl.PathParameters = append(tmpl.PathParameters, key) 256 | tmpl.isVariables[idx] = true 257 | continue 258 | } 259 | } 260 | 261 | return tmpl 262 | } 263 | -------------------------------------------------------------------------------- /sample/appengine/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /sample/appengine/app.yaml: -------------------------------------------------------------------------------- 1 | application: gaego 2 | version: ucon 3 | runtime: go 4 | api_version: go1 5 | 6 | handlers: 7 | - url: /.* 8 | script: _go_app 9 | -------------------------------------------------------------------------------- /sample/appengine/main.go: -------------------------------------------------------------------------------- 1 | //go:generate jwg -output model_json.go -transcripttag swagger . 2 | //go:generate qbg -output model_query.go . 3 | 4 | package example_appengine 5 | 6 | import ( 7 | "net/http" 8 | "reflect" 9 | "strings" 10 | 11 | "github.com/favclip/ucon/v3" 12 | "github.com/favclip/ucon/v3/swagger" 13 | "google.golang.org/appengine" 14 | ) 15 | 16 | func init() { 17 | var _ ucon.HTTPErrorResponse = &HttpError{} 18 | 19 | ucon.Middleware(UseAppengineContext) 20 | ucon.Orthodox() 21 | ucon.Middleware(swagger.RequestValidator()) 22 | 23 | swPlugin := swagger.NewPlugin(&swagger.Options{ 24 | Object: &swagger.Object{ 25 | Info: &swagger.Info{ 26 | Title: "Todo list", 27 | Version: "v1", 28 | }, 29 | Schemes: []string{"http"}, 30 | }, 31 | DefinitionNameModifier: func(refT reflect.Type, defName string) string { 32 | if strings.HasSuffix(defName, "JSON") { 33 | return defName[:len(defName)-4] 34 | } 35 | return defName 36 | }, 37 | }) 38 | ucon.Plugin(swPlugin) 39 | 40 | ucon.HandleFunc("GET", "/swagger-ui/", func(w http.ResponseWriter, r *http.Request) { 41 | localPath := "./node_modules/swagger-ui/dist/" + r.URL.Path[len("/swagger-ui/"):] 42 | http.ServeFile(w, r, localPath) 43 | }) 44 | 45 | setupTodo(swPlugin) 46 | 47 | ucon.HandleFunc("GET", "/", func(w http.ResponseWriter, r *http.Request) { 48 | localPath := "./public/" + r.URL.Path[len("/"):] 49 | http.ServeFile(w, r, localPath) 50 | }) 51 | 52 | ucon.DefaultMux.Prepare() 53 | http.Handle("/", ucon.DefaultMux) 54 | } 55 | 56 | func UseAppengineContext(b *ucon.Bubble) error { 57 | if b.Context == nil { 58 | b.Context = appengine.NewContext(b.R) 59 | } else { 60 | b.Context = appengine.WithContext(b.Context, b.R) 61 | } 62 | b.R = b.R.WithContext(b.Context) 63 | 64 | return b.Next() 65 | } 66 | -------------------------------------------------------------------------------- /sample/appengine/misc_model.go: -------------------------------------------------------------------------------- 1 | package example_appengine 2 | 3 | type IntIDRequest struct { 4 | ID int64 `json:"id,string"` 5 | } 6 | -------------------------------------------------------------------------------- /sample/appengine/model_query.go: -------------------------------------------------------------------------------- 1 | // generated by qbg -output model_query.go .; DO NOT EDIT 2 | 3 | package example_appengine 4 | 5 | import ( 6 | "github.com/favclip/qbg/qbgutils" 7 | "google.golang.org/appengine/datastore" 8 | ) 9 | 10 | // TodoQueryBuilder build query for Todo. 11 | type TodoQueryBuilder struct { 12 | q *datastore.Query 13 | plugin qbgutils.Plugin 14 | ID *TodoQueryProperty 15 | Text *TodoQueryProperty 16 | Done *TodoQueryProperty 17 | UpdatedAt *TodoQueryProperty 18 | CreatedAt *TodoQueryProperty 19 | } 20 | 21 | // TodoQueryProperty has property information for TodoQueryBuilder. 22 | type TodoQueryProperty struct { 23 | bldr *TodoQueryBuilder 24 | name string 25 | } 26 | 27 | // NewTodoQueryBuilder create new TodoQueryBuilder. 28 | func NewTodoQueryBuilder() *TodoQueryBuilder { 29 | return NewTodoQueryBuilderWithKind("Todo") 30 | } 31 | 32 | // NewTodoQueryBuilderWithKind create new TodoQueryBuilder with specific kind. 33 | func NewTodoQueryBuilderWithKind(kind string) *TodoQueryBuilder { 34 | q := datastore.NewQuery(kind) 35 | bldr := &TodoQueryBuilder{q: q} 36 | bldr.ID = &TodoQueryProperty{ 37 | bldr: bldr, 38 | name: "__key__", 39 | } 40 | bldr.Text = &TodoQueryProperty{ 41 | bldr: bldr, 42 | name: "Text", 43 | } 44 | bldr.Done = &TodoQueryProperty{ 45 | bldr: bldr, 46 | name: "Done", 47 | } 48 | bldr.UpdatedAt = &TodoQueryProperty{ 49 | bldr: bldr, 50 | name: "UpdatedAt", 51 | } 52 | bldr.CreatedAt = &TodoQueryProperty{ 53 | bldr: bldr, 54 | name: "CreatedAt", 55 | } 56 | 57 | if plugger, ok := interface{}(bldr).(qbgutils.Plugger); ok { 58 | bldr.plugin = plugger.Plugin() 59 | bldr.plugin.Init("Todo") 60 | } 61 | 62 | return bldr 63 | } 64 | 65 | // Ancestor sets parent key to ancestor query. 66 | func (bldr *TodoQueryBuilder) Ancestor(parentKey *datastore.Key) *TodoQueryBuilder { 67 | bldr.q = bldr.q.Ancestor(parentKey) 68 | if bldr.plugin != nil { 69 | bldr.plugin.Ancestor(parentKey) 70 | } 71 | return bldr 72 | } 73 | 74 | // KeysOnly sets keys only option to query. 75 | func (bldr *TodoQueryBuilder) KeysOnly() *TodoQueryBuilder { 76 | bldr.q = bldr.q.KeysOnly() 77 | if bldr.plugin != nil { 78 | bldr.plugin.KeysOnly() 79 | } 80 | return bldr 81 | } 82 | 83 | // Start setup to query. 84 | func (bldr *TodoQueryBuilder) Start(cur datastore.Cursor) *TodoQueryBuilder { 85 | bldr.q = bldr.q.Start(cur) 86 | if bldr.plugin != nil { 87 | bldr.plugin.Start(cur) 88 | } 89 | return bldr 90 | } 91 | 92 | // Offset setup to query. 93 | func (bldr *TodoQueryBuilder) Offset(offset int) *TodoQueryBuilder { 94 | bldr.q = bldr.q.Offset(offset) 95 | if bldr.plugin != nil { 96 | bldr.plugin.Offset(offset) 97 | } 98 | return bldr 99 | } 100 | 101 | // Limit setup to query. 102 | func (bldr *TodoQueryBuilder) Limit(limit int) *TodoQueryBuilder { 103 | bldr.q = bldr.q.Limit(limit) 104 | if bldr.plugin != nil { 105 | bldr.plugin.Limit(limit) 106 | } 107 | return bldr 108 | } 109 | 110 | // Query returns *datastore.Query. 111 | func (bldr *TodoQueryBuilder) Query() *datastore.Query { 112 | return bldr.q 113 | } 114 | 115 | // Filter with op & value. 116 | func (p *TodoQueryProperty) Filter(op string, value interface{}) *TodoQueryBuilder { 117 | switch op { 118 | case "<=": 119 | p.LessThanOrEqual(value) 120 | case ">=": 121 | p.GreaterThanOrEqual(value) 122 | case "<": 123 | p.LessThan(value) 124 | case ">": 125 | p.GreaterThan(value) 126 | case "=": 127 | p.Equal(value) 128 | default: 129 | p.bldr.q = p.bldr.q.Filter(p.name+" "+op, value) // error raised by native query 130 | } 131 | if p.bldr.plugin != nil { 132 | p.bldr.plugin.Filter(p.name, op, value) 133 | } 134 | return p.bldr 135 | } 136 | 137 | // LessThanOrEqual filter with value. 138 | func (p *TodoQueryProperty) LessThanOrEqual(value interface{}) *TodoQueryBuilder { 139 | p.bldr.q = p.bldr.q.Filter(p.name+" <=", value) 140 | if p.bldr.plugin != nil { 141 | p.bldr.plugin.Filter(p.name, "<=", value) 142 | } 143 | return p.bldr 144 | } 145 | 146 | // GreaterThanOrEqual filter with value. 147 | func (p *TodoQueryProperty) GreaterThanOrEqual(value interface{}) *TodoQueryBuilder { 148 | p.bldr.q = p.bldr.q.Filter(p.name+" >=", value) 149 | if p.bldr.plugin != nil { 150 | p.bldr.plugin.Filter(p.name, ">=", value) 151 | } 152 | return p.bldr 153 | } 154 | 155 | // LessThan filter with value. 156 | func (p *TodoQueryProperty) LessThan(value interface{}) *TodoQueryBuilder { 157 | p.bldr.q = p.bldr.q.Filter(p.name+" <", value) 158 | if p.bldr.plugin != nil { 159 | p.bldr.plugin.Filter(p.name, "<", value) 160 | } 161 | return p.bldr 162 | } 163 | 164 | // GreaterThan filter with value. 165 | func (p *TodoQueryProperty) GreaterThan(value interface{}) *TodoQueryBuilder { 166 | p.bldr.q = p.bldr.q.Filter(p.name+" >", value) 167 | if p.bldr.plugin != nil { 168 | p.bldr.plugin.Filter(p.name, ">", value) 169 | } 170 | return p.bldr 171 | } 172 | 173 | // Equal filter with value. 174 | func (p *TodoQueryProperty) Equal(value interface{}) *TodoQueryBuilder { 175 | p.bldr.q = p.bldr.q.Filter(p.name+" =", value) 176 | if p.bldr.plugin != nil { 177 | p.bldr.plugin.Filter(p.name, "=", value) 178 | } 179 | return p.bldr 180 | } 181 | 182 | // Asc order. 183 | func (p *TodoQueryProperty) Asc() *TodoQueryBuilder { 184 | p.bldr.q = p.bldr.q.Order(p.name) 185 | if p.bldr.plugin != nil { 186 | p.bldr.plugin.Asc(p.name) 187 | } 188 | return p.bldr 189 | } 190 | 191 | // Desc order. 192 | func (p *TodoQueryProperty) Desc() *TodoQueryBuilder { 193 | p.bldr.q = p.bldr.q.Order("-" + p.name) 194 | if p.bldr.plugin != nil { 195 | p.bldr.plugin.Desc(p.name) 196 | } 197 | return p.bldr 198 | } 199 | -------------------------------------------------------------------------------- /sample/appengine/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ucon-example", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "", 6 | "main": "index.js", 7 | "scripts": { 8 | "start": "go run main.go model_json.go", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "author": "", 12 | "license": "MIT", 13 | "dependencies": { 14 | "swagger-ui": "^2.1.4" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /sample/appengine/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | TODO 4 | 5 | 6 |

TODO

7 |
8 | 9 |
10 | ...Loading 11 |
12 |
13 | 14 | 164 | 165 | -------------------------------------------------------------------------------- /sample/appengine/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -eu 2 | echo "open http://localhost:8080/" 3 | goapp serve . 4 | -------------------------------------------------------------------------------- /sample/appengine/todo_service.go: -------------------------------------------------------------------------------- 1 | package example_appengine 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/favclip/ucon/v3" 10 | "github.com/favclip/ucon/v3/swagger" 11 | "github.com/mjibson/goon" 12 | "google.golang.org/appengine" 13 | "google.golang.org/appengine/datastore" 14 | ) 15 | 16 | func setupTodo(swPlugin *swagger.Plugin) { 17 | s := &TodoService{} 18 | tag := swPlugin.AddTag(&swagger.Tag{Name: "TODO", Description: "TODO list"}) 19 | var hInfo *swagger.HandlerInfo 20 | 21 | hInfo = swagger.NewHandlerInfo(s.Get) 22 | ucon.Handle("GET", "/todo/{id}", hInfo) 23 | hInfo.Description, hInfo.Tags = "get todo entity", []string{tag.Name} 24 | 25 | hInfo = swagger.NewHandlerInfo(s.List) 26 | ucon.Handle("GET", "/todo", hInfo) 27 | hInfo.Description, hInfo.Tags = "get todo list", []string{tag.Name} 28 | 29 | hInfo = swagger.NewHandlerInfo(s.Insert) 30 | ucon.Handle("POST", "/todo", hInfo) 31 | hInfo.Description, hInfo.Tags = "post new todo entity", []string{tag.Name} 32 | 33 | hInfo = swagger.NewHandlerInfo(s.Update) 34 | ucon.Handle("PUT", "/todo/{id}", hInfo) 35 | hInfo.Description, hInfo.Tags = "update todo entity", []string{tag.Name} 36 | 37 | hInfo = swagger.NewHandlerInfo(s.Delete) 38 | ucon.Handle("DELETE", "/todo/{id}", hInfo) 39 | hInfo.Description, hInfo.Tags = "delete todo entity", []string{tag.Name} 40 | } 41 | 42 | type TodoService struct { 43 | } 44 | 45 | // +jwg 46 | // +qbg 47 | type Todo struct { 48 | ID int64 `datastore:"-" goon:"id"` 49 | Text string `swagger:",req"` 50 | Done bool 51 | UpdatedAt time.Time 52 | CreatedAt time.Time 53 | } 54 | 55 | func (todo *Todo) Load(ps []datastore.Property) error { 56 | if err := datastore.LoadStruct(todo, ps); err != nil { 57 | return err 58 | } 59 | 60 | return nil 61 | } 62 | 63 | func (todo *Todo) Save() ([]datastore.Property, error) { 64 | if todo.CreatedAt.IsZero() { 65 | todo.CreatedAt = time.Now() 66 | } 67 | todo.UpdatedAt = time.Now() 68 | 69 | ps, err := datastore.SaveStruct(todo) 70 | if err != nil { 71 | return nil, err 72 | } 73 | return ps, nil 74 | } 75 | 76 | type ListOpts struct { 77 | Offset int `json:"offset" swagger:",in=query"` 78 | Limit int `json:"limit" swagger:",in=query"` 79 | } 80 | 81 | type HttpError struct { 82 | Code int `json:"code"` 83 | Text string `json:"text"` 84 | } 85 | 86 | func (err *HttpError) Error() string { 87 | return fmt.Sprintf("status %d: %s", err.Code, err.Text) 88 | } 89 | 90 | func (err *HttpError) StatusCode() int { 91 | return err.Code 92 | } 93 | 94 | func (err *HttpError) ErrorMessage() interface{} { 95 | return err 96 | } 97 | 98 | func (s *TodoService) Get(c context.Context, req *IntIDRequest) (*TodoJSON, error) { 99 | if req.ID == 0 { 100 | return nil, &HttpError{http.StatusBadRequest, "ID is required"} 101 | } 102 | 103 | g := goon.FromContext(c) 104 | 105 | todo := &Todo{ID: req.ID} 106 | err := g.Get(todo) 107 | if err == datastore.ErrNoSuchEntity { 108 | return nil, &HttpError{http.StatusNotFound, fmt.Sprintf("ID: %d is not found", req.ID)} 109 | } else if err != nil { 110 | return nil, err 111 | } 112 | 113 | resp, err := NewTodoJSONBuilder().AddAll().Convert(todo) 114 | if err != nil { 115 | return nil, err 116 | } 117 | 118 | return resp, nil 119 | } 120 | 121 | func (s *TodoService) List(c context.Context, r *http.Request, opts *ListOpts) ([]*TodoJSON, error) { 122 | // ucon can return []*Xxx. but CloudEndpoints can't it. 123 | 124 | // ucon can take 3 args. but go-endpoints can't it. 125 | { 126 | c := appengine.NewContext(r) 127 | appengine.AppID(c) 128 | } 129 | 130 | g := goon.FromContext(c) 131 | 132 | qb := NewTodoQueryBuilder() 133 | if opts.Limit != 0 { 134 | qb.Limit(opts.Limit) 135 | } 136 | if opts.Offset != 0 { 137 | qb.Offset(opts.Offset) 138 | } 139 | qb.CreatedAt.Asc() 140 | q := qb.Query() 141 | 142 | var todoList []*Todo 143 | _, err := g.GetAll(q, &todoList) 144 | if err != nil { 145 | return nil, err 146 | } 147 | 148 | resp, err := NewTodoJSONBuilder().AddAll().ConvertList(todoList) 149 | if err != nil { 150 | return nil, err 151 | } 152 | 153 | return resp, nil 154 | } 155 | 156 | func (s *TodoService) Insert(c context.Context, req *TodoJSON) (*TodoJSON, error) { 157 | if req == nil { 158 | return nil, &HttpError{http.StatusBadRequest, "Payload is required"} 159 | } 160 | if req.ID != 0 { 161 | return nil, &HttpError{http.StatusBadRequest, "ID should be 0"} 162 | } 163 | 164 | todo, err := req.Convert() 165 | if err != nil { 166 | return nil, err 167 | } 168 | 169 | g := goon.FromContext(c) 170 | _, err = g.Put(todo) 171 | if err != nil { 172 | return nil, err 173 | } 174 | 175 | resp, err := NewTodoJSONBuilder().AddAll().Convert(todo) 176 | if err != nil { 177 | return nil, err 178 | } 179 | 180 | return resp, nil 181 | } 182 | 183 | func (s *TodoService) Update(c context.Context, req *TodoJSON) (*TodoJSON, error) { 184 | if req.ID == 0 { 185 | return nil, &HttpError{http.StatusBadRequest, "ID is required"} 186 | } 187 | 188 | todo, err := req.Convert() 189 | if err != nil { 190 | return nil, err 191 | } 192 | 193 | old := &Todo{ID: todo.ID} 194 | 195 | g := goon.FromContext(c) 196 | 197 | err = g.RunInTransaction(func(g *goon.Goon) error { 198 | err = g.Get(old) 199 | if err == datastore.ErrNoSuchEntity { 200 | return &HttpError{http.StatusNotFound, fmt.Sprintf("ID: %d is not found", req.ID)} 201 | } else if err != nil { 202 | return err 203 | } 204 | 205 | _, err = g.Put(todo) 206 | if err != nil { 207 | return err 208 | } 209 | 210 | return nil 211 | }, nil) 212 | if err != nil { 213 | return nil, err 214 | } 215 | 216 | resp, err := NewTodoJSONBuilder().AddAll().Convert(todo) 217 | if err != nil { 218 | return nil, err 219 | } 220 | 221 | return resp, nil 222 | } 223 | 224 | func (s *TodoService) Delete(c context.Context, req *IntIDRequest) (*TodoJSON, error) { 225 | if req.ID == 0 { 226 | return nil, &HttpError{http.StatusBadRequest, "ID is required"} 227 | } 228 | 229 | todo := &Todo{ID: req.ID} 230 | 231 | g := goon.FromContext(c) 232 | 233 | err := g.Get(todo) 234 | if err == datastore.ErrNoSuchEntity { 235 | return nil, &HttpError{http.StatusNotFound, fmt.Sprintf("ID: %d is not found", req.ID)} 236 | } else if err != nil { 237 | return nil, err 238 | } 239 | 240 | key := g.Key(todo) 241 | 242 | err = g.Delete(key) 243 | if err != nil { 244 | return nil, err 245 | } 246 | 247 | resp, err := NewTodoJSONBuilder().AddAll().Convert(todo) 248 | if err != nil { 249 | return nil, err 250 | } 251 | 252 | return resp, nil 253 | } 254 | -------------------------------------------------------------------------------- /sample/basic/main.go: -------------------------------------------------------------------------------- 1 | //go:generate jwg -output model_json.go . 2 | 3 | package main 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "net/http" 9 | "sync" 10 | "time" 11 | 12 | "github.com/favclip/ucon/v3" 13 | ) 14 | 15 | func main() { 16 | var _ ucon.HTTPErrorResponse = &HttpError{} 17 | 18 | ucon.Orthodox() 19 | 20 | ucon.Middleware(func(b *ucon.Bubble) error { 21 | // Do something before handler working... 22 | fmt.Printf("request coming! %s %s\n", b.R.Method, b.R.URL.String()) 23 | 24 | err := b.Next() 25 | if err != nil { 26 | b.W.Header().Add("X-Hi", "Woo X(") 27 | return err 28 | } 29 | 30 | // Do something after handler worked... 31 | if b.Handled { 32 | b.W.Header().Add("X-Hi", "Hi! ;)") 33 | } 34 | 35 | return nil 36 | }) 37 | 38 | s := &TodoService{} 39 | 40 | ucon.HandleFunc("GET", "/todo/{id}", s.Get) 41 | ucon.HandleFunc("GET", "/todo", s.List) 42 | ucon.HandleFunc("POST", "/todo", s.Insert) 43 | ucon.HandleFunc("PUT", "/todo/{id}", s.Update) 44 | ucon.HandleFunc("DELETE", "/todo/{id}", s.Delete) 45 | 46 | ucon.HandleFunc("GET", "/", func(w http.ResponseWriter, r *http.Request) { 47 | localPath := "./public/" + r.URL.Path[len("/"):] 48 | http.ServeFile(w, r, localPath) 49 | }) 50 | 51 | ucon.ListenAndServe(":8080") 52 | } 53 | 54 | type TodoService struct { 55 | m sync.Mutex 56 | id int64 57 | todoList []*Todo 58 | } 59 | 60 | type IntIDRequest struct { 61 | ID int64 `json:"id,string"` 62 | } 63 | 64 | // +jwg 65 | type Todo struct { 66 | ID int64 `json:",string"` 67 | Text string `` 68 | Done bool 69 | CreatedAt time.Time 70 | } 71 | 72 | type ListOpts struct { 73 | Offset int `json:"offset" swagger:",in=query"` 74 | Limit int `json:"limit" swagger:",in=query"` 75 | } 76 | 77 | type HttpError struct { 78 | Code int `json:"code"` 79 | Text string `json:"text"` 80 | } 81 | 82 | func (err *HttpError) Error() string { 83 | return fmt.Sprintf("status %d: %s", err.Code, err.Text) 84 | } 85 | 86 | func (err *HttpError) StatusCode() int { 87 | return err.Code 88 | } 89 | 90 | func (err *HttpError) ErrorMessage() interface{} { 91 | return err 92 | } 93 | 94 | func (s *TodoService) Get(c context.Context, req *IntIDRequest) (*TodoJSON, error) { 95 | if req.ID == 0 { 96 | return nil, &HttpError{http.StatusBadRequest, "ID is required"} 97 | } 98 | 99 | for _, todo := range s.todoList { 100 | if todo.ID == req.ID { 101 | resp, err := NewTodoJSONBuilder().AddAll().Convert(todo) 102 | if err != nil { 103 | return nil, err 104 | } 105 | 106 | return resp, nil 107 | } 108 | } 109 | 110 | return nil, &HttpError{http.StatusNotFound, fmt.Sprintf("ID: %d is not found", req.ID)} 111 | } 112 | 113 | func (s *TodoService) List(c context.Context, opts *ListOpts) ([]*TodoJSON, error) { 114 | lo := opts.Offset 115 | if len(s.todoList) < lo { 116 | lo = len(s.todoList) 117 | } 118 | hi := opts.Offset + opts.Limit 119 | if hi == 0 { 120 | hi = 10 121 | } 122 | if len(s.todoList) < hi { 123 | hi = len(s.todoList) 124 | } 125 | 126 | resp, err := NewTodoJSONBuilder().AddAll().ConvertList(s.todoList[lo:hi]) 127 | if err != nil { 128 | return nil, err 129 | } 130 | 131 | return resp, nil 132 | } 133 | 134 | func (s *TodoService) Insert(c context.Context, req *TodoJSON) (*TodoJSON, error) { 135 | if req == nil { 136 | return nil, &HttpError{http.StatusBadRequest, "Payload is required"} 137 | } 138 | if req.ID != 0 { 139 | return nil, &HttpError{http.StatusBadRequest, "ID should be 0"} 140 | } 141 | 142 | todo, err := req.Convert() 143 | if err != nil { 144 | return nil, err 145 | } 146 | 147 | s.m.Lock() 148 | defer s.m.Unlock() 149 | s.id++ 150 | todo.ID = s.id 151 | todo.CreatedAt = time.Now() 152 | 153 | s.todoList = append(s.todoList, todo) 154 | 155 | resp, err := NewTodoJSONBuilder().AddAll().Convert(todo) 156 | if err != nil { 157 | return nil, err 158 | } 159 | 160 | return resp, nil 161 | } 162 | 163 | func (s *TodoService) Update(c context.Context, req *TodoJSON) (*TodoJSON, error) { 164 | if req.ID == 0 { 165 | return nil, &HttpError{http.StatusBadRequest, "ID is required"} 166 | } 167 | 168 | s.m.Lock() 169 | defer s.m.Unlock() 170 | 171 | todo, err := req.Convert() 172 | if err != nil { 173 | return nil, err 174 | } 175 | 176 | var found bool 177 | for idx, t := range s.todoList { 178 | if t.ID == todo.ID { 179 | todo.CreatedAt = t.CreatedAt 180 | s.todoList[idx] = todo 181 | found = true 182 | break 183 | } 184 | } 185 | if !found { 186 | return nil, &HttpError{http.StatusNotFound, fmt.Sprintf("ID: %d is not found", req.ID)} 187 | } 188 | 189 | resp, err := NewTodoJSONBuilder().AddAll().Convert(todo) 190 | if err != nil { 191 | return nil, err 192 | } 193 | 194 | return resp, nil 195 | } 196 | 197 | func (s *TodoService) Delete(c context.Context, req *IntIDRequest) (*TodoJSON, error) { 198 | if req.ID == 0 { 199 | return nil, &HttpError{http.StatusBadRequest, "ID is required"} 200 | } 201 | 202 | s.m.Lock() 203 | defer s.m.Unlock() 204 | 205 | var removedTodo *Todo 206 | newList := make([]*Todo, 0, len(s.todoList)) 207 | for _, todo := range s.todoList { 208 | if todo.ID == req.ID { 209 | removedTodo = todo 210 | continue 211 | } 212 | newList = append(newList, todo) 213 | } 214 | if removedTodo == nil { 215 | return nil, &HttpError{http.StatusNotFound, fmt.Sprintf("ID: %d is not found", req.ID)} 216 | } 217 | s.todoList = newList 218 | 219 | resp, err := NewTodoJSONBuilder().AddAll().Convert(removedTodo) 220 | if err != nil { 221 | return nil, err 222 | } 223 | 224 | return resp, nil 225 | } 226 | -------------------------------------------------------------------------------- /sample/basic/model_json.go: -------------------------------------------------------------------------------- 1 | // generated by jwg -output model_json.go .; DO NOT EDIT 2 | 3 | package main 4 | 5 | import ( 6 | "encoding/json" 7 | "time" 8 | ) 9 | 10 | // TodoJSON is jsonized struct for Todo. 11 | type TodoJSON struct { 12 | ID int64 `json:"id,omitempty,string"` 13 | Text string `json:"text,omitempty"` 14 | Done bool `json:"done,omitempty"` 15 | CreatedAt time.Time `json:"createdAt,omitempty"` 16 | } 17 | 18 | // TodoJSONList is synonym about []*TodoJSON. 19 | type TodoJSONList []*TodoJSON 20 | 21 | // TodoPropertyEncoder is property encoder for [1]sJSON. 22 | type TodoPropertyEncoder func(src *Todo, dest *TodoJSON) error 23 | 24 | // TodoPropertyDecoder is property decoder for [1]sJSON. 25 | type TodoPropertyDecoder func(src *TodoJSON, dest *Todo) error 26 | 27 | // TodoPropertyInfo stores property information. 28 | type TodoPropertyInfo struct { 29 | fieldName string 30 | jsonName string 31 | Encoder TodoPropertyEncoder 32 | Decoder TodoPropertyDecoder 33 | } 34 | 35 | // FieldName returns struct field name of property. 36 | func (info *TodoPropertyInfo) FieldName() string { 37 | return info.fieldName 38 | } 39 | 40 | // JSONName returns json field name of property. 41 | func (info *TodoPropertyInfo) JSONName() string { 42 | return info.jsonName 43 | } 44 | 45 | // TodoJSONBuilder convert between Todo to TodoJSON mutually. 46 | type TodoJSONBuilder struct { 47 | _properties map[string]*TodoPropertyInfo 48 | _jsonPropertyMap map[string]*TodoPropertyInfo 49 | _structPropertyMap map[string]*TodoPropertyInfo 50 | ID *TodoPropertyInfo 51 | Text *TodoPropertyInfo 52 | Done *TodoPropertyInfo 53 | CreatedAt *TodoPropertyInfo 54 | } 55 | 56 | // NewTodoJSONBuilder make new TodoJSONBuilder. 57 | func NewTodoJSONBuilder() *TodoJSONBuilder { 58 | jb := &TodoJSONBuilder{ 59 | _properties: map[string]*TodoPropertyInfo{}, 60 | _jsonPropertyMap: map[string]*TodoPropertyInfo{}, 61 | _structPropertyMap: map[string]*TodoPropertyInfo{}, 62 | ID: &TodoPropertyInfo{ 63 | fieldName: "ID", 64 | jsonName: "id", 65 | Encoder: func(src *Todo, dest *TodoJSON) error { 66 | if src == nil { 67 | return nil 68 | } 69 | dest.ID = src.ID 70 | return nil 71 | }, 72 | Decoder: func(src *TodoJSON, dest *Todo) error { 73 | if src == nil { 74 | return nil 75 | } 76 | dest.ID = src.ID 77 | return nil 78 | }, 79 | }, 80 | Text: &TodoPropertyInfo{ 81 | fieldName: "Text", 82 | jsonName: "text", 83 | Encoder: func(src *Todo, dest *TodoJSON) error { 84 | if src == nil { 85 | return nil 86 | } 87 | dest.Text = src.Text 88 | return nil 89 | }, 90 | Decoder: func(src *TodoJSON, dest *Todo) error { 91 | if src == nil { 92 | return nil 93 | } 94 | dest.Text = src.Text 95 | return nil 96 | }, 97 | }, 98 | Done: &TodoPropertyInfo{ 99 | fieldName: "Done", 100 | jsonName: "done", 101 | Encoder: func(src *Todo, dest *TodoJSON) error { 102 | if src == nil { 103 | return nil 104 | } 105 | dest.Done = src.Done 106 | return nil 107 | }, 108 | Decoder: func(src *TodoJSON, dest *Todo) error { 109 | if src == nil { 110 | return nil 111 | } 112 | dest.Done = src.Done 113 | return nil 114 | }, 115 | }, 116 | CreatedAt: &TodoPropertyInfo{ 117 | fieldName: "CreatedAt", 118 | jsonName: "createdAt", 119 | Encoder: func(src *Todo, dest *TodoJSON) error { 120 | if src == nil { 121 | return nil 122 | } 123 | dest.CreatedAt = src.CreatedAt 124 | return nil 125 | }, 126 | Decoder: func(src *TodoJSON, dest *Todo) error { 127 | if src == nil { 128 | return nil 129 | } 130 | dest.CreatedAt = src.CreatedAt 131 | return nil 132 | }, 133 | }, 134 | } 135 | jb._structPropertyMap["ID"] = jb.ID 136 | jb._jsonPropertyMap["id"] = jb.ID 137 | jb._structPropertyMap["Text"] = jb.Text 138 | jb._jsonPropertyMap["text"] = jb.Text 139 | jb._structPropertyMap["Done"] = jb.Done 140 | jb._jsonPropertyMap["done"] = jb.Done 141 | jb._structPropertyMap["CreatedAt"] = jb.CreatedAt 142 | jb._jsonPropertyMap["createdAt"] = jb.CreatedAt 143 | return jb 144 | } 145 | 146 | // Properties returns all properties on TodoJSONBuilder. 147 | func (b *TodoJSONBuilder) Properties() []*TodoPropertyInfo { 148 | return []*TodoPropertyInfo{ 149 | b.ID, 150 | b.Text, 151 | b.Done, 152 | b.CreatedAt, 153 | } 154 | } 155 | 156 | // AddAll adds all property to TodoJSONBuilder. 157 | func (b *TodoJSONBuilder) AddAll() *TodoJSONBuilder { 158 | b._properties["ID"] = b.ID 159 | b._properties["Text"] = b.Text 160 | b._properties["Done"] = b.Done 161 | b._properties["CreatedAt"] = b.CreatedAt 162 | return b 163 | } 164 | 165 | // Add specified property to TodoJSONBuilder. 166 | func (b *TodoJSONBuilder) Add(infos ...*TodoPropertyInfo) *TodoJSONBuilder { 167 | for _, info := range infos { 168 | b._properties[info.fieldName] = info 169 | } 170 | return b 171 | } 172 | 173 | // AddByJSONNames add properties to TodoJSONBuilder by JSON property name. if name is not in the builder, it will ignore. 174 | func (b *TodoJSONBuilder) AddByJSONNames(names ...string) *TodoJSONBuilder { 175 | for _, name := range names { 176 | info := b._jsonPropertyMap[name] 177 | if info == nil { 178 | continue 179 | } 180 | b._properties[info.fieldName] = info 181 | } 182 | return b 183 | } 184 | 185 | // AddByNames add properties to TodoJSONBuilder by struct property name. if name is not in the builder, it will ignore. 186 | func (b *TodoJSONBuilder) AddByNames(names ...string) *TodoJSONBuilder { 187 | for _, name := range names { 188 | info := b._structPropertyMap[name] 189 | if info == nil { 190 | continue 191 | } 192 | b._properties[info.fieldName] = info 193 | } 194 | return b 195 | } 196 | 197 | // Remove specified property to TodoJSONBuilder. 198 | func (b *TodoJSONBuilder) Remove(infos ...*TodoPropertyInfo) *TodoJSONBuilder { 199 | for _, info := range infos { 200 | delete(b._properties, info.fieldName) 201 | } 202 | return b 203 | } 204 | 205 | // RemoveByJSONNames remove properties to TodoJSONBuilder by JSON property name. if name is not in the builder, it will ignore. 206 | func (b *TodoJSONBuilder) RemoveByJSONNames(names ...string) *TodoJSONBuilder { 207 | 208 | for _, name := range names { 209 | info := b._jsonPropertyMap[name] 210 | if info == nil { 211 | continue 212 | } 213 | delete(b._properties, info.fieldName) 214 | } 215 | return b 216 | } 217 | 218 | // RemoveByNames remove properties to TodoJSONBuilder by struct property name. if name is not in the builder, it will ignore. 219 | func (b *TodoJSONBuilder) RemoveByNames(names ...string) *TodoJSONBuilder { 220 | for _, name := range names { 221 | info := b._structPropertyMap[name] 222 | if info == nil { 223 | continue 224 | } 225 | delete(b._properties, info.fieldName) 226 | } 227 | return b 228 | } 229 | 230 | // Convert specified non-JSON object to JSON object. 231 | func (b *TodoJSONBuilder) Convert(orig *Todo) (*TodoJSON, error) { 232 | if orig == nil { 233 | return nil, nil 234 | } 235 | ret := &TodoJSON{} 236 | 237 | for _, info := range b._properties { 238 | if err := info.Encoder(orig, ret); err != nil { 239 | return nil, err 240 | } 241 | } 242 | 243 | return ret, nil 244 | } 245 | 246 | // ConvertList specified non-JSON slice to JSONList. 247 | func (b *TodoJSONBuilder) ConvertList(orig []*Todo) (TodoJSONList, error) { 248 | if orig == nil { 249 | return nil, nil 250 | } 251 | 252 | list := make(TodoJSONList, len(orig)) 253 | for idx, or := range orig { 254 | json, err := b.Convert(or) 255 | if err != nil { 256 | return nil, err 257 | } 258 | list[idx] = json 259 | } 260 | 261 | return list, nil 262 | } 263 | 264 | // Convert specified JSON object to non-JSON object. 265 | func (orig *TodoJSON) Convert() (*Todo, error) { 266 | ret := &Todo{} 267 | 268 | b := NewTodoJSONBuilder().AddAll() 269 | for _, info := range b._properties { 270 | if err := info.Decoder(orig, ret); err != nil { 271 | return nil, err 272 | } 273 | } 274 | 275 | return ret, nil 276 | } 277 | 278 | // Convert specified JSONList to non-JSON slice. 279 | func (jsonList TodoJSONList) Convert() ([]*Todo, error) { 280 | orig := ([]*TodoJSON)(jsonList) 281 | 282 | list := make([]*Todo, len(orig)) 283 | for idx, or := range orig { 284 | obj, err := or.Convert() 285 | if err != nil { 286 | return nil, err 287 | } 288 | list[idx] = obj 289 | } 290 | 291 | return list, nil 292 | } 293 | 294 | // Marshal non-JSON object to JSON string. 295 | func (b *TodoJSONBuilder) Marshal(orig *Todo) ([]byte, error) { 296 | ret, err := b.Convert(orig) 297 | if err != nil { 298 | return nil, err 299 | } 300 | return json.Marshal(ret) 301 | } 302 | -------------------------------------------------------------------------------- /sample/basic/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | TODO 4 | 5 | 6 |

TODO

7 |
8 | 9 |
10 | ...Loading 11 |
12 |
13 | 14 | 164 | 165 | -------------------------------------------------------------------------------- /sample/basic/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -eu 2 | echo "open http://localhost:8080/" 3 | go run *.go 4 | -------------------------------------------------------------------------------- /sample/helloworld/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "reflect" 7 | "time" 8 | 9 | "github.com/favclip/ucon/v3" 10 | ) 11 | 12 | func main() { 13 | ucon.Orthodox() 14 | 15 | ucon.Middleware(NowInJST) 16 | ucon.Middleware(Logger) 17 | 18 | ucon.HandleFunc("GET", "/", func(w http.ResponseWriter, r *http.Request, now time.Time) { 19 | w.Write([]byte(fmt.Sprintf("Hello World! : %s", now.Format("2006/01/02 15:04:05")))) 20 | }) 21 | 22 | ucon.HandleFunc("GET", "/a", func(w http.ResponseWriter, r *http.Request) { 23 | w.Write([]byte("/a")) 24 | }) 25 | // ucon.HandleFunc("GET", "/a/", func(w http.ResponseWriter, r *http.Request) { 26 | // w.Write([]byte("/a/")) 27 | // }) 28 | // ucon.HandleFunc("GET", "/a/b/", func(w http.ResponseWriter, r *http.Request) { 29 | // w.Write([]byte("/a/b")) 30 | // }) 31 | 32 | ucon.ListenAndServe(":8080") 33 | } 34 | 35 | func Logger(b *ucon.Bubble) error { 36 | fmt.Printf("Received: %s %s\n", b.R.Method, b.R.URL.String()) 37 | return b.Next() 38 | } 39 | 40 | func NowInJST(b *ucon.Bubble) error { 41 | for idx, argT := range b.ArgumentTypes { 42 | if argT == reflect.TypeOf(time.Time{}) { 43 | b.Arguments[idx] = reflect.ValueOf(time.Now()) 44 | break 45 | } 46 | } 47 | return b.Next() 48 | } 49 | -------------------------------------------------------------------------------- /sample/oauth2idp/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /sample/oauth2idp/README.md: -------------------------------------------------------------------------------- 1 | # OAuth2 provider with ucon 2 | 3 | ``` 4 | $ ./run.sh 5 | open http://localhost:8080/ 6 | ``` 7 | 8 | try it on swagger-ui! 9 | 10 | This example uses [github.com/RangelReale/osin](https://github.com/RangelReale/osin). 11 | However, anything can be used as long as the following requirements are satisfied. 12 | 13 | * It can handle oauth2 authentication. 14 | * It can get scopes from Authorization header with swagger.CheckSecurityRequirements middleware. 15 | -------------------------------------------------------------------------------- /sample/oauth2idp/model_json.go: -------------------------------------------------------------------------------- 1 | // generated by jwg -output model_json.go -transcripttag swagger .; DO NOT EDIT 2 | 3 | package main 4 | 5 | import ( 6 | "encoding/json" 7 | "time" 8 | ) 9 | 10 | // TodoJSON is jsonized struct for Todo. 11 | type TodoJSON struct { 12 | ID int64 `json:"id,omitempty,string"` 13 | Text string `json:"text,omitempty" swagger:",req"` 14 | Done bool `json:"done,omitempty"` 15 | CreatedAt time.Time `json:"createdAt,omitempty"` 16 | } 17 | 18 | // TodoJSONList is synonym about []*TodoJSON. 19 | type TodoJSONList []*TodoJSON 20 | 21 | // TodoPropertyEncoder is property encoder for [1]sJSON. 22 | type TodoPropertyEncoder func(src *Todo, dest *TodoJSON) error 23 | 24 | // TodoPropertyDecoder is property decoder for [1]sJSON. 25 | type TodoPropertyDecoder func(src *TodoJSON, dest *Todo) error 26 | 27 | // TodoPropertyInfo stores property information. 28 | type TodoPropertyInfo struct { 29 | fieldName string 30 | jsonName string 31 | Encoder TodoPropertyEncoder 32 | Decoder TodoPropertyDecoder 33 | } 34 | 35 | // FieldName returns struct field name of property. 36 | func (info *TodoPropertyInfo) FieldName() string { 37 | return info.fieldName 38 | } 39 | 40 | // JSONName returns json field name of property. 41 | func (info *TodoPropertyInfo) JSONName() string { 42 | return info.jsonName 43 | } 44 | 45 | // TodoJSONBuilder convert between Todo to TodoJSON mutually. 46 | type TodoJSONBuilder struct { 47 | _properties map[string]*TodoPropertyInfo 48 | _jsonPropertyMap map[string]*TodoPropertyInfo 49 | _structPropertyMap map[string]*TodoPropertyInfo 50 | ID *TodoPropertyInfo 51 | Text *TodoPropertyInfo 52 | Done *TodoPropertyInfo 53 | CreatedAt *TodoPropertyInfo 54 | } 55 | 56 | // NewTodoJSONBuilder make new TodoJSONBuilder. 57 | func NewTodoJSONBuilder() *TodoJSONBuilder { 58 | jb := &TodoJSONBuilder{ 59 | _properties: map[string]*TodoPropertyInfo{}, 60 | _jsonPropertyMap: map[string]*TodoPropertyInfo{}, 61 | _structPropertyMap: map[string]*TodoPropertyInfo{}, 62 | ID: &TodoPropertyInfo{ 63 | fieldName: "ID", 64 | jsonName: "id", 65 | Encoder: func(src *Todo, dest *TodoJSON) error { 66 | if src == nil { 67 | return nil 68 | } 69 | dest.ID = src.ID 70 | return nil 71 | }, 72 | Decoder: func(src *TodoJSON, dest *Todo) error { 73 | if src == nil { 74 | return nil 75 | } 76 | dest.ID = src.ID 77 | return nil 78 | }, 79 | }, 80 | Text: &TodoPropertyInfo{ 81 | fieldName: "Text", 82 | jsonName: "text", 83 | Encoder: func(src *Todo, dest *TodoJSON) error { 84 | if src == nil { 85 | return nil 86 | } 87 | dest.Text = src.Text 88 | return nil 89 | }, 90 | Decoder: func(src *TodoJSON, dest *Todo) error { 91 | if src == nil { 92 | return nil 93 | } 94 | dest.Text = src.Text 95 | return nil 96 | }, 97 | }, 98 | Done: &TodoPropertyInfo{ 99 | fieldName: "Done", 100 | jsonName: "done", 101 | Encoder: func(src *Todo, dest *TodoJSON) error { 102 | if src == nil { 103 | return nil 104 | } 105 | dest.Done = src.Done 106 | return nil 107 | }, 108 | Decoder: func(src *TodoJSON, dest *Todo) error { 109 | if src == nil { 110 | return nil 111 | } 112 | dest.Done = src.Done 113 | return nil 114 | }, 115 | }, 116 | CreatedAt: &TodoPropertyInfo{ 117 | fieldName: "CreatedAt", 118 | jsonName: "createdAt", 119 | Encoder: func(src *Todo, dest *TodoJSON) error { 120 | if src == nil { 121 | return nil 122 | } 123 | dest.CreatedAt = src.CreatedAt 124 | return nil 125 | }, 126 | Decoder: func(src *TodoJSON, dest *Todo) error { 127 | if src == nil { 128 | return nil 129 | } 130 | dest.CreatedAt = src.CreatedAt 131 | return nil 132 | }, 133 | }, 134 | } 135 | jb._structPropertyMap["ID"] = jb.ID 136 | jb._jsonPropertyMap["id"] = jb.ID 137 | jb._structPropertyMap["Text"] = jb.Text 138 | jb._jsonPropertyMap["text"] = jb.Text 139 | jb._structPropertyMap["Done"] = jb.Done 140 | jb._jsonPropertyMap["done"] = jb.Done 141 | jb._structPropertyMap["CreatedAt"] = jb.CreatedAt 142 | jb._jsonPropertyMap["createdAt"] = jb.CreatedAt 143 | return jb 144 | } 145 | 146 | // Properties returns all properties on TodoJSONBuilder. 147 | func (b *TodoJSONBuilder) Properties() []*TodoPropertyInfo { 148 | return []*TodoPropertyInfo{ 149 | b.ID, 150 | b.Text, 151 | b.Done, 152 | b.CreatedAt, 153 | } 154 | } 155 | 156 | // AddAll adds all property to TodoJSONBuilder. 157 | func (b *TodoJSONBuilder) AddAll() *TodoJSONBuilder { 158 | b._properties["ID"] = b.ID 159 | b._properties["Text"] = b.Text 160 | b._properties["Done"] = b.Done 161 | b._properties["CreatedAt"] = b.CreatedAt 162 | return b 163 | } 164 | 165 | // Add specified property to TodoJSONBuilder. 166 | func (b *TodoJSONBuilder) Add(infos ...*TodoPropertyInfo) *TodoJSONBuilder { 167 | for _, info := range infos { 168 | b._properties[info.fieldName] = info 169 | } 170 | return b 171 | } 172 | 173 | // AddByJSONNames add properties to TodoJSONBuilder by JSON property name. if name is not in the builder, it will ignore. 174 | func (b *TodoJSONBuilder) AddByJSONNames(names ...string) *TodoJSONBuilder { 175 | for _, name := range names { 176 | info := b._jsonPropertyMap[name] 177 | if info == nil { 178 | continue 179 | } 180 | b._properties[info.fieldName] = info 181 | } 182 | return b 183 | } 184 | 185 | // AddByNames add properties to TodoJSONBuilder by struct property name. if name is not in the builder, it will ignore. 186 | func (b *TodoJSONBuilder) AddByNames(names ...string) *TodoJSONBuilder { 187 | for _, name := range names { 188 | info := b._structPropertyMap[name] 189 | if info == nil { 190 | continue 191 | } 192 | b._properties[info.fieldName] = info 193 | } 194 | return b 195 | } 196 | 197 | // Remove specified property to TodoJSONBuilder. 198 | func (b *TodoJSONBuilder) Remove(infos ...*TodoPropertyInfo) *TodoJSONBuilder { 199 | for _, info := range infos { 200 | delete(b._properties, info.fieldName) 201 | } 202 | return b 203 | } 204 | 205 | // RemoveByJSONNames remove properties to TodoJSONBuilder by JSON property name. if name is not in the builder, it will ignore. 206 | func (b *TodoJSONBuilder) RemoveByJSONNames(names ...string) *TodoJSONBuilder { 207 | 208 | for _, name := range names { 209 | info := b._jsonPropertyMap[name] 210 | if info == nil { 211 | continue 212 | } 213 | delete(b._properties, info.fieldName) 214 | } 215 | return b 216 | } 217 | 218 | // RemoveByNames remove properties to TodoJSONBuilder by struct property name. if name is not in the builder, it will ignore. 219 | func (b *TodoJSONBuilder) RemoveByNames(names ...string) *TodoJSONBuilder { 220 | for _, name := range names { 221 | info := b._structPropertyMap[name] 222 | if info == nil { 223 | continue 224 | } 225 | delete(b._properties, info.fieldName) 226 | } 227 | return b 228 | } 229 | 230 | // Convert specified non-JSON object to JSON object. 231 | func (b *TodoJSONBuilder) Convert(orig *Todo) (*TodoJSON, error) { 232 | if orig == nil { 233 | return nil, nil 234 | } 235 | ret := &TodoJSON{} 236 | 237 | for _, info := range b._properties { 238 | if err := info.Encoder(orig, ret); err != nil { 239 | return nil, err 240 | } 241 | } 242 | 243 | return ret, nil 244 | } 245 | 246 | // ConvertList specified non-JSON slice to JSONList. 247 | func (b *TodoJSONBuilder) ConvertList(orig []*Todo) (TodoJSONList, error) { 248 | if orig == nil { 249 | return nil, nil 250 | } 251 | 252 | list := make(TodoJSONList, len(orig)) 253 | for idx, or := range orig { 254 | json, err := b.Convert(or) 255 | if err != nil { 256 | return nil, err 257 | } 258 | list[idx] = json 259 | } 260 | 261 | return list, nil 262 | } 263 | 264 | // Convert specified JSON object to non-JSON object. 265 | func (orig *TodoJSON) Convert() (*Todo, error) { 266 | ret := &Todo{} 267 | 268 | b := NewTodoJSONBuilder().AddAll() 269 | for _, info := range b._properties { 270 | if err := info.Decoder(orig, ret); err != nil { 271 | return nil, err 272 | } 273 | } 274 | 275 | return ret, nil 276 | } 277 | 278 | // Convert specified JSONList to non-JSON slice. 279 | func (jsonList TodoJSONList) Convert() ([]*Todo, error) { 280 | orig := ([]*TodoJSON)(jsonList) 281 | 282 | list := make([]*Todo, len(orig)) 283 | for idx, or := range orig { 284 | obj, err := or.Convert() 285 | if err != nil { 286 | return nil, err 287 | } 288 | list[idx] = obj 289 | } 290 | 291 | return list, nil 292 | } 293 | 294 | // Marshal non-JSON object to JSON string. 295 | func (b *TodoJSONBuilder) Marshal(orig *Todo) ([]byte, error) { 296 | ret, err := b.Convert(orig) 297 | if err != nil { 298 | return nil, err 299 | } 300 | return json.Marshal(ret) 301 | } 302 | -------------------------------------------------------------------------------- /sample/oauth2idp/osin_storage.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/RangelReale/osin" 5 | ) 6 | 7 | var _ osin.Storage = &OsinOnMemoryStorage{} 8 | 9 | type OsinOnMemoryStorage struct { 10 | clients map[string]osin.Client 11 | authorize map[string]*osin.AuthorizeData 12 | access map[string]*osin.AccessData 13 | refresh map[string]string 14 | } 15 | 16 | func (s *OsinOnMemoryStorage) Clone() osin.Storage { 17 | return s 18 | } 19 | 20 | func (s *OsinOnMemoryStorage) Close() { 21 | } 22 | 23 | func (s *OsinOnMemoryStorage) GetClient(id string) (osin.Client, error) { 24 | if c, ok := s.clients[id]; ok { 25 | return c, nil 26 | } 27 | return nil, osin.ErrNotFound 28 | } 29 | 30 | func (s *OsinOnMemoryStorage) SetClient(id string, client osin.Client) error { 31 | s.clients[id] = client 32 | return nil 33 | } 34 | 35 | func (s *OsinOnMemoryStorage) SaveAuthorize(data *osin.AuthorizeData) error { 36 | s.authorize[data.Code] = data 37 | return nil 38 | } 39 | 40 | func (s *OsinOnMemoryStorage) LoadAuthorize(code string) (*osin.AuthorizeData, error) { 41 | if d, ok := s.authorize[code]; ok { 42 | return d, nil 43 | } 44 | return nil, osin.ErrNotFound 45 | } 46 | 47 | func (s *OsinOnMemoryStorage) RemoveAuthorize(code string) error { 48 | delete(s.authorize, code) 49 | return nil 50 | } 51 | 52 | func (s *OsinOnMemoryStorage) SaveAccess(data *osin.AccessData) error { 53 | s.access[data.AccessToken] = data 54 | if data.RefreshToken != "" { 55 | s.refresh[data.RefreshToken] = data.AccessToken 56 | } 57 | return nil 58 | } 59 | 60 | func (s *OsinOnMemoryStorage) LoadAccess(code string) (*osin.AccessData, error) { 61 | if d, ok := s.access[code]; ok { 62 | return d, nil 63 | } 64 | return nil, osin.ErrNotFound 65 | } 66 | 67 | func (s *OsinOnMemoryStorage) RemoveAccess(code string) error { 68 | delete(s.access, code) 69 | return nil 70 | } 71 | 72 | func (s *OsinOnMemoryStorage) LoadRefresh(code string) (*osin.AccessData, error) { 73 | if d, ok := s.refresh[code]; ok { 74 | return s.LoadAccess(d) 75 | } 76 | return nil, osin.ErrNotFound 77 | } 78 | 79 | func (s *OsinOnMemoryStorage) RemoveRefresh(code string) error { 80 | delete(s.refresh, code) 81 | return nil 82 | } 83 | -------------------------------------------------------------------------------- /sample/oauth2idp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ucon-example", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "", 6 | "main": "index.js", 7 | "scripts": { 8 | "start": "go run *.go", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "author": "", 12 | "license": "MIT", 13 | "dependencies": { 14 | "swagger-ui-dist": "^3.9.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /sample/oauth2idp/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | TODO 4 | 5 | 6 |

TODO

7 |
8 |
9 | swagger-ui try with /api/swagger.json .
10 | client_id: 1234, client_secret: foobar 11 |
12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /sample/oauth2idp/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -eu 2 | 3 | set +e 4 | patch --unified --force node_modules/swagger-ui-dist/index.html < ./swagger-ui-index.patch 5 | set -e 6 | 7 | echo "open http://localhost:8080/" 8 | go run *.go 9 | -------------------------------------------------------------------------------- /sample/oauth2idp/swagger-ui-index.patch: -------------------------------------------------------------------------------- 1 | --- node_modules/swagger-ui-dist/index.html 2018-01-06 17:33:30.000000000 +0900 2 | +++ modified/index.html 2018-01-07 20:06:13.000000000 +0900 3 | @@ -74,7 +74,7 @@ 4 | 5 | // Build a system 6 | const ui = SwaggerUIBundle({ 7 | - url: "http://petstore.swagger.io/v2/swagger.json", 8 | + url: "/api/swagger.json", 9 | dom_id: '#swagger-ui', 10 | deepLinking: true, 11 | presets: [ 12 | @@ -84,7 +84,8 @@ 13 | plugins: [ 14 | SwaggerUIBundle.plugins.DownloadUrl 15 | ], 16 | - layout: "StandaloneLayout" 17 | + layout: "StandaloneLayout", 18 | + oauth2RedirectUrl: "http://localhost:8080/swagger-ui/oauth2-redirect.html" 19 | }) 20 | 21 | window.ui = ui 22 | -------------------------------------------------------------------------------- /sample/oauth2idp/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | swagger-ui-dist@^3.9.0: 6 | version "3.9.0" 7 | resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-3.9.0.tgz#7965d967a76cef8b455a8d0cd91380ff64acd913" 8 | -------------------------------------------------------------------------------- /sample/swagger/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /sample/swagger/main.go: -------------------------------------------------------------------------------- 1 | //go:generate jwg -output model_json.go -transcripttag swagger . 2 | 3 | package main 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "net/http" 9 | "reflect" 10 | "strings" 11 | "sync" 12 | "time" 13 | 14 | "github.com/favclip/ucon/v3" 15 | "github.com/favclip/ucon/v3/swagger" 16 | ) 17 | 18 | func main() { 19 | var _ ucon.HTTPErrorResponse = &HttpError{} 20 | 21 | ucon.Orthodox() 22 | ucon.Middleware(swagger.RequestValidator()) 23 | 24 | swPlugin := swagger.NewPlugin(&swagger.Options{ 25 | Object: &swagger.Object{ 26 | Info: &swagger.Info{ 27 | Title: "Todo list", 28 | Version: "v1", 29 | }, 30 | Schemes: []string{"http"}, 31 | }, 32 | DefinitionNameModifier: func(refT reflect.Type, defName string) string { 33 | if strings.HasSuffix(defName, "JSON") { 34 | return defName[:len(defName)-4] 35 | } 36 | return defName 37 | }, 38 | }) 39 | ucon.Plugin(swPlugin) 40 | 41 | ucon.HandleFunc("GET", "/swagger-ui/", func(w http.ResponseWriter, r *http.Request) { 42 | localPath := "./node_modules/swagger-ui-dist/" + r.URL.Path[len("/swagger-ui/"):] 43 | http.ServeFile(w, r, localPath) 44 | }) 45 | 46 | s := &TodoService{} 47 | tag := swPlugin.AddTag(&swagger.Tag{Name: "TODO", Description: "TODO list"}) 48 | var hc *swagger.HandlerInfo 49 | 50 | ucon.HandleFunc("GET", "/todo/{id}", s.Get) 51 | ucon.HandleFunc("GET", "/todo", s.List) 52 | ucon.HandleFunc("POST", "/todo", s.Insert) 53 | 54 | hc = swagger.NewHandlerInfo(s.Update) 55 | ucon.Handle("PUT", "/todo/{id}", hc) 56 | hc.Description, hc.Tags = "update todo entity", []string{tag.Name} 57 | 58 | ucon.HandleFunc("DELETE", "/todo/{id}", s.Delete) 59 | 60 | ucon.HandleFunc("GET", "/", func(w http.ResponseWriter, r *http.Request) { 61 | localPath := "./public/" + r.URL.Path[len("/"):] 62 | http.ServeFile(w, r, localPath) 63 | }) 64 | 65 | ucon.ListenAndServe(":8080") 66 | } 67 | 68 | type TodoService struct { 69 | m sync.Mutex 70 | id int64 71 | todoList []*Todo 72 | } 73 | 74 | type IntIDRequest struct { 75 | ID int64 `json:"id,string"` 76 | } 77 | 78 | // +jwg 79 | type Todo struct { 80 | ID int64 `json:",string"` 81 | Text string `swagger:",req"` 82 | Done bool 83 | CreatedAt time.Time 84 | } 85 | 86 | type ListOpts struct { 87 | Offset int `json:"offset" swagger:",in=query"` 88 | Limit int `json:"limit" swagger:",in=query"` 89 | } 90 | 91 | type HttpError struct { 92 | Code int `json:"code"` 93 | Text string `json:"text"` 94 | } 95 | 96 | func (err *HttpError) Error() string { 97 | return fmt.Sprintf("status %d: %s", err.Code, err.Text) 98 | } 99 | 100 | func (err *HttpError) StatusCode() int { 101 | return err.Code 102 | } 103 | 104 | func (err *HttpError) ErrorMessage() interface{} { 105 | return err 106 | } 107 | 108 | func (s *TodoService) Get(c context.Context, req *IntIDRequest) (*TodoJSON, error) { 109 | if req.ID == 0 { 110 | return nil, &HttpError{http.StatusBadRequest, "ID is required"} 111 | } 112 | 113 | for _, todo := range s.todoList { 114 | if todo.ID == req.ID { 115 | resp, err := NewTodoJSONBuilder().AddAll().Convert(todo) 116 | if err != nil { 117 | return nil, err 118 | } 119 | 120 | return resp, nil 121 | } 122 | } 123 | 124 | return nil, &HttpError{http.StatusNotFound, fmt.Sprintf("ID: %d is not found", req.ID)} 125 | } 126 | 127 | func (s *TodoService) List(c context.Context, opts *ListOpts) ([]*TodoJSON, error) { 128 | lo := opts.Offset 129 | if len(s.todoList) < lo { 130 | lo = len(s.todoList) 131 | } 132 | hi := opts.Offset + opts.Limit 133 | if hi == 0 { 134 | hi = 10 135 | } 136 | if len(s.todoList) < hi { 137 | hi = len(s.todoList) 138 | } 139 | 140 | resp, err := NewTodoJSONBuilder().AddAll().ConvertList(s.todoList[lo:hi]) 141 | if err != nil { 142 | return nil, err 143 | } 144 | 145 | return resp, nil 146 | } 147 | 148 | func (s *TodoService) Insert(c context.Context, req *TodoJSON) (*TodoJSON, error) { 149 | if req == nil { 150 | return nil, &HttpError{http.StatusBadRequest, "Payload is required"} 151 | } 152 | if req.ID != 0 { 153 | return nil, &HttpError{http.StatusBadRequest, "ID should be 0"} 154 | } 155 | 156 | todo, err := req.Convert() 157 | if err != nil { 158 | return nil, err 159 | } 160 | 161 | s.m.Lock() 162 | defer s.m.Unlock() 163 | s.id++ 164 | todo.ID = s.id 165 | todo.CreatedAt = time.Now() 166 | 167 | s.todoList = append(s.todoList, todo) 168 | 169 | resp, err := NewTodoJSONBuilder().AddAll().Convert(todo) 170 | if err != nil { 171 | return nil, err 172 | } 173 | 174 | return resp, nil 175 | } 176 | 177 | func (s *TodoService) Update(c context.Context, req *TodoJSON) (*TodoJSON, error) { 178 | if req.ID == 0 { 179 | return nil, &HttpError{http.StatusBadRequest, "ID is required"} 180 | } 181 | 182 | s.m.Lock() 183 | defer s.m.Unlock() 184 | 185 | todo, err := req.Convert() 186 | if err != nil { 187 | return nil, err 188 | } 189 | 190 | var found bool 191 | for idx, t := range s.todoList { 192 | if t.ID == todo.ID { 193 | todo.CreatedAt = t.CreatedAt 194 | s.todoList[idx] = todo 195 | found = true 196 | break 197 | } 198 | } 199 | if !found { 200 | return nil, &HttpError{http.StatusNotFound, fmt.Sprintf("ID: %d is not found", req.ID)} 201 | } 202 | 203 | resp, err := NewTodoJSONBuilder().AddAll().Convert(todo) 204 | if err != nil { 205 | return nil, err 206 | } 207 | 208 | return resp, nil 209 | } 210 | 211 | func (s *TodoService) Delete(c context.Context, req *IntIDRequest) (*TodoJSON, error) { 212 | if req.ID == 0 { 213 | return nil, &HttpError{http.StatusBadRequest, "ID is required"} 214 | } 215 | 216 | s.m.Lock() 217 | defer s.m.Unlock() 218 | 219 | var removedTodo *Todo 220 | newList := make([]*Todo, 0, len(s.todoList)) 221 | for _, todo := range s.todoList { 222 | if todo.ID == req.ID { 223 | removedTodo = todo 224 | continue 225 | } 226 | newList = append(newList, todo) 227 | } 228 | if removedTodo == nil { 229 | return nil, &HttpError{http.StatusNotFound, fmt.Sprintf("ID: %d is not found", req.ID)} 230 | } 231 | s.todoList = newList 232 | 233 | resp, err := NewTodoJSONBuilder().AddAll().Convert(removedTodo) 234 | if err != nil { 235 | return nil, err 236 | } 237 | 238 | return resp, nil 239 | } 240 | -------------------------------------------------------------------------------- /sample/swagger/model_json.go: -------------------------------------------------------------------------------- 1 | // generated by jwg -output model_json.go -transcripttag swagger .; DO NOT EDIT 2 | 3 | package main 4 | 5 | import ( 6 | "encoding/json" 7 | "time" 8 | ) 9 | 10 | // TodoJSON is jsonized struct for Todo. 11 | type TodoJSON struct { 12 | ID int64 `json:"id,omitempty,string"` 13 | Text string `json:"text,omitempty" swagger:",req"` 14 | Done bool `json:"done,omitempty"` 15 | CreatedAt time.Time `json:"createdAt,omitempty"` 16 | } 17 | 18 | // TodoJSONList is synonym about []*TodoJSON. 19 | type TodoJSONList []*TodoJSON 20 | 21 | // TodoPropertyEncoder is property encoder for [1]sJSON. 22 | type TodoPropertyEncoder func(src *Todo, dest *TodoJSON) error 23 | 24 | // TodoPropertyDecoder is property decoder for [1]sJSON. 25 | type TodoPropertyDecoder func(src *TodoJSON, dest *Todo) error 26 | 27 | // TodoPropertyInfo stores property information. 28 | type TodoPropertyInfo struct { 29 | fieldName string 30 | jsonName string 31 | Encoder TodoPropertyEncoder 32 | Decoder TodoPropertyDecoder 33 | } 34 | 35 | // FieldName returns struct field name of property. 36 | func (info *TodoPropertyInfo) FieldName() string { 37 | return info.fieldName 38 | } 39 | 40 | // JSONName returns json field name of property. 41 | func (info *TodoPropertyInfo) JSONName() string { 42 | return info.jsonName 43 | } 44 | 45 | // TodoJSONBuilder convert between Todo to TodoJSON mutually. 46 | type TodoJSONBuilder struct { 47 | _properties map[string]*TodoPropertyInfo 48 | _jsonPropertyMap map[string]*TodoPropertyInfo 49 | _structPropertyMap map[string]*TodoPropertyInfo 50 | ID *TodoPropertyInfo 51 | Text *TodoPropertyInfo 52 | Done *TodoPropertyInfo 53 | CreatedAt *TodoPropertyInfo 54 | } 55 | 56 | // NewTodoJSONBuilder make new TodoJSONBuilder. 57 | func NewTodoJSONBuilder() *TodoJSONBuilder { 58 | jb := &TodoJSONBuilder{ 59 | _properties: map[string]*TodoPropertyInfo{}, 60 | _jsonPropertyMap: map[string]*TodoPropertyInfo{}, 61 | _structPropertyMap: map[string]*TodoPropertyInfo{}, 62 | ID: &TodoPropertyInfo{ 63 | fieldName: "ID", 64 | jsonName: "id", 65 | Encoder: func(src *Todo, dest *TodoJSON) error { 66 | if src == nil { 67 | return nil 68 | } 69 | dest.ID = src.ID 70 | return nil 71 | }, 72 | Decoder: func(src *TodoJSON, dest *Todo) error { 73 | if src == nil { 74 | return nil 75 | } 76 | dest.ID = src.ID 77 | return nil 78 | }, 79 | }, 80 | Text: &TodoPropertyInfo{ 81 | fieldName: "Text", 82 | jsonName: "text", 83 | Encoder: func(src *Todo, dest *TodoJSON) error { 84 | if src == nil { 85 | return nil 86 | } 87 | dest.Text = src.Text 88 | return nil 89 | }, 90 | Decoder: func(src *TodoJSON, dest *Todo) error { 91 | if src == nil { 92 | return nil 93 | } 94 | dest.Text = src.Text 95 | return nil 96 | }, 97 | }, 98 | Done: &TodoPropertyInfo{ 99 | fieldName: "Done", 100 | jsonName: "done", 101 | Encoder: func(src *Todo, dest *TodoJSON) error { 102 | if src == nil { 103 | return nil 104 | } 105 | dest.Done = src.Done 106 | return nil 107 | }, 108 | Decoder: func(src *TodoJSON, dest *Todo) error { 109 | if src == nil { 110 | return nil 111 | } 112 | dest.Done = src.Done 113 | return nil 114 | }, 115 | }, 116 | CreatedAt: &TodoPropertyInfo{ 117 | fieldName: "CreatedAt", 118 | jsonName: "createdAt", 119 | Encoder: func(src *Todo, dest *TodoJSON) error { 120 | if src == nil { 121 | return nil 122 | } 123 | dest.CreatedAt = src.CreatedAt 124 | return nil 125 | }, 126 | Decoder: func(src *TodoJSON, dest *Todo) error { 127 | if src == nil { 128 | return nil 129 | } 130 | dest.CreatedAt = src.CreatedAt 131 | return nil 132 | }, 133 | }, 134 | } 135 | jb._structPropertyMap["ID"] = jb.ID 136 | jb._jsonPropertyMap["id"] = jb.ID 137 | jb._structPropertyMap["Text"] = jb.Text 138 | jb._jsonPropertyMap["text"] = jb.Text 139 | jb._structPropertyMap["Done"] = jb.Done 140 | jb._jsonPropertyMap["done"] = jb.Done 141 | jb._structPropertyMap["CreatedAt"] = jb.CreatedAt 142 | jb._jsonPropertyMap["createdAt"] = jb.CreatedAt 143 | return jb 144 | } 145 | 146 | // Properties returns all properties on TodoJSONBuilder. 147 | func (b *TodoJSONBuilder) Properties() []*TodoPropertyInfo { 148 | return []*TodoPropertyInfo{ 149 | b.ID, 150 | b.Text, 151 | b.Done, 152 | b.CreatedAt, 153 | } 154 | } 155 | 156 | // AddAll adds all property to TodoJSONBuilder. 157 | func (b *TodoJSONBuilder) AddAll() *TodoJSONBuilder { 158 | b._properties["ID"] = b.ID 159 | b._properties["Text"] = b.Text 160 | b._properties["Done"] = b.Done 161 | b._properties["CreatedAt"] = b.CreatedAt 162 | return b 163 | } 164 | 165 | // Add specified property to TodoJSONBuilder. 166 | func (b *TodoJSONBuilder) Add(infos ...*TodoPropertyInfo) *TodoJSONBuilder { 167 | for _, info := range infos { 168 | b._properties[info.fieldName] = info 169 | } 170 | return b 171 | } 172 | 173 | // AddByJSONNames add properties to TodoJSONBuilder by JSON property name. if name is not in the builder, it will ignore. 174 | func (b *TodoJSONBuilder) AddByJSONNames(names ...string) *TodoJSONBuilder { 175 | for _, name := range names { 176 | info := b._jsonPropertyMap[name] 177 | if info == nil { 178 | continue 179 | } 180 | b._properties[info.fieldName] = info 181 | } 182 | return b 183 | } 184 | 185 | // AddByNames add properties to TodoJSONBuilder by struct property name. if name is not in the builder, it will ignore. 186 | func (b *TodoJSONBuilder) AddByNames(names ...string) *TodoJSONBuilder { 187 | for _, name := range names { 188 | info := b._structPropertyMap[name] 189 | if info == nil { 190 | continue 191 | } 192 | b._properties[info.fieldName] = info 193 | } 194 | return b 195 | } 196 | 197 | // Remove specified property to TodoJSONBuilder. 198 | func (b *TodoJSONBuilder) Remove(infos ...*TodoPropertyInfo) *TodoJSONBuilder { 199 | for _, info := range infos { 200 | delete(b._properties, info.fieldName) 201 | } 202 | return b 203 | } 204 | 205 | // RemoveByJSONNames remove properties to TodoJSONBuilder by JSON property name. if name is not in the builder, it will ignore. 206 | func (b *TodoJSONBuilder) RemoveByJSONNames(names ...string) *TodoJSONBuilder { 207 | 208 | for _, name := range names { 209 | info := b._jsonPropertyMap[name] 210 | if info == nil { 211 | continue 212 | } 213 | delete(b._properties, info.fieldName) 214 | } 215 | return b 216 | } 217 | 218 | // RemoveByNames remove properties to TodoJSONBuilder by struct property name. if name is not in the builder, it will ignore. 219 | func (b *TodoJSONBuilder) RemoveByNames(names ...string) *TodoJSONBuilder { 220 | for _, name := range names { 221 | info := b._structPropertyMap[name] 222 | if info == nil { 223 | continue 224 | } 225 | delete(b._properties, info.fieldName) 226 | } 227 | return b 228 | } 229 | 230 | // Convert specified non-JSON object to JSON object. 231 | func (b *TodoJSONBuilder) Convert(orig *Todo) (*TodoJSON, error) { 232 | if orig == nil { 233 | return nil, nil 234 | } 235 | ret := &TodoJSON{} 236 | 237 | for _, info := range b._properties { 238 | if err := info.Encoder(orig, ret); err != nil { 239 | return nil, err 240 | } 241 | } 242 | 243 | return ret, nil 244 | } 245 | 246 | // ConvertList specified non-JSON slice to JSONList. 247 | func (b *TodoJSONBuilder) ConvertList(orig []*Todo) (TodoJSONList, error) { 248 | if orig == nil { 249 | return nil, nil 250 | } 251 | 252 | list := make(TodoJSONList, len(orig)) 253 | for idx, or := range orig { 254 | json, err := b.Convert(or) 255 | if err != nil { 256 | return nil, err 257 | } 258 | list[idx] = json 259 | } 260 | 261 | return list, nil 262 | } 263 | 264 | // Convert specified JSON object to non-JSON object. 265 | func (orig *TodoJSON) Convert() (*Todo, error) { 266 | ret := &Todo{} 267 | 268 | b := NewTodoJSONBuilder().AddAll() 269 | for _, info := range b._properties { 270 | if err := info.Decoder(orig, ret); err != nil { 271 | return nil, err 272 | } 273 | } 274 | 275 | return ret, nil 276 | } 277 | 278 | // Convert specified JSONList to non-JSON slice. 279 | func (jsonList TodoJSONList) Convert() ([]*Todo, error) { 280 | orig := ([]*TodoJSON)(jsonList) 281 | 282 | list := make([]*Todo, len(orig)) 283 | for idx, or := range orig { 284 | obj, err := or.Convert() 285 | if err != nil { 286 | return nil, err 287 | } 288 | list[idx] = obj 289 | } 290 | 291 | return list, nil 292 | } 293 | 294 | // Marshal non-JSON object to JSON string. 295 | func (b *TodoJSONBuilder) Marshal(orig *Todo) ([]byte, error) { 296 | ret, err := b.Convert(orig) 297 | if err != nil { 298 | return nil, err 299 | } 300 | return json.Marshal(ret) 301 | } 302 | -------------------------------------------------------------------------------- /sample/swagger/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ucon-example", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "", 6 | "main": "index.js", 7 | "scripts": { 8 | "start": "go run *.go", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "author": "", 12 | "license": "MIT", 13 | "dependencies": { 14 | "swagger-ui-dist": "^3.4.4" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /sample/swagger/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | TODO 4 | 5 | 6 |

TODO

7 |
8 | 9 |
10 | ...Loading 11 |
12 |
13 | swagger-ui try with /api/swagger.json . 14 |
15 |
16 | 17 | 167 | 168 | -------------------------------------------------------------------------------- /sample/swagger/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -eu 2 | echo "open http://localhost:8080/" 3 | go run *.go 4 | -------------------------------------------------------------------------------- /sample/swagger/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | swagger-ui-dist@^3.4.4: 6 | version "3.4.4" 7 | resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-3.4.4.tgz#7dd0d7f7884e4387b9d10d4f4f7b0f93436a11d2" 8 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -eux 2 | 3 | cd `dirname $0` 4 | 5 | go mod download 6 | 7 | # build tools 8 | rm -rf build-cmd/ 9 | mkdir build-cmd 10 | 11 | export GOBIN=`pwd -P`/build-cmd 12 | go install golang.org/x/tools/cmd/goimports 13 | go install golang.org/x/lint/golint 14 | go install github.com/favclip/jwg/cmd/jwg 15 | go install github.com/favclip/qbg/cmd/qbg 16 | -------------------------------------------------------------------------------- /swagger/jsonschema_draft4.go: -------------------------------------------------------------------------------- 1 | package swagger 2 | 3 | // Schema is https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#schemaObject 4 | type Schema struct { 5 | Ref string `json:"$ref,omitempty"` 6 | Format string `json:"format,omitempty"` 7 | Title string `json:"title,omitempty"` 8 | Description string `json:"description,omitempty"` 9 | Default interface{} `json:"default,omitempty"` 10 | Maximum *int `json:"maximum,omitempty"` 11 | ExclusiveMaximum *bool `json:"exclusiveMaximum,omitempty"` 12 | Minimum *int `json:"minimum,omitempty"` 13 | ExclusiveMinimum *bool `json:"exclusiveMinimum,omitempty"` 14 | MaxLength *int `json:"maxLength,omitempty"` 15 | MinLength *int `json:"minLength,omitempty"` 16 | Pattern string `json:"pattern,omitempty"` 17 | MaxItems *int `json:"maxItems,omitempty"` 18 | MinItems *int `json:"minItems,omitempty"` 19 | UniqueItems *bool `json:"uniqueItems,omitempty"` 20 | MaxProperties *int `json:"maxProperties,omitempty"` 21 | MinProperties *int `json:"minProperties,omitempty"` 22 | Required []string `json:"required,omitempty"` 23 | Enum []interface{} `json:"enum,omitempty"` 24 | Type string `json:"type,omitempty"` 25 | Items *Schema `json:"items,omitempty"` 26 | AllOf []*Schema `json:"allOf,omitempty"` 27 | Properties map[string]*Schema `json:"properties,omitempty"` 28 | AdditionalProperties map[string]*Schema `json:"additionalProperties,omitempty"` 29 | Discriminator string `json:"discriminator,omitempty"` 30 | ReadOnly *bool `json:"readOnly,omitempty"` 31 | // Xml XML 32 | ExternalDocs *ExternalDocumentation `json:"externalDocs,omitempty"` 33 | Example interface{} `json:"example,omitempty"` 34 | } 35 | 36 | // ShallowCopy returns a clone of *Schema. 37 | func (schema *Schema) ShallowCopy() *Schema { 38 | return &Schema{ 39 | Ref: schema.Ref, 40 | Format: schema.Format, 41 | Title: schema.Title, 42 | Description: schema.Description, 43 | Default: schema.Default, 44 | Maximum: schema.Maximum, 45 | ExclusiveMaximum: schema.ExclusiveMaximum, 46 | Minimum: schema.Minimum, 47 | ExclusiveMinimum: schema.ExclusiveMinimum, 48 | MaxLength: schema.MaxLength, 49 | MinLength: schema.MinLength, 50 | Pattern: schema.Pattern, 51 | MaxItems: schema.MaxItems, 52 | MinItems: schema.MinItems, 53 | UniqueItems: schema.UniqueItems, 54 | MaxProperties: schema.MaxProperties, 55 | MinProperties: schema.MinProperties, 56 | Required: schema.Required, 57 | Enum: schema.Enum, 58 | Type: schema.Type, 59 | Items: schema.Items, 60 | AllOf: schema.AllOf, 61 | Properties: schema.Properties, 62 | AdditionalProperties: schema.AdditionalProperties, 63 | Discriminator: schema.Discriminator, 64 | ReadOnly: schema.ReadOnly, 65 | ExternalDocs: schema.ExternalDocs, 66 | Example: schema.Example, 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /swagger/middleware.go: -------------------------------------------------------------------------------- 1 | package swagger 2 | 3 | import ( 4 | "fmt" 5 | 6 | "net/http" 7 | 8 | "github.com/favclip/ucon" 9 | ) 10 | 11 | var _ ucon.HTTPErrorResponse = &securityError{} 12 | var _ error = &securityError{} 13 | 14 | type securityError struct { 15 | Code int `json:"code"` 16 | Type string `json:"type"` 17 | Message string `json:"message"` 18 | } 19 | 20 | func (ve *securityError) StatusCode() int { 21 | return ve.Code 22 | } 23 | 24 | func (ve *securityError) ErrorMessage() interface{} { 25 | return ve 26 | } 27 | 28 | func (ve *securityError) Error() string { 29 | return fmt.Sprintf("status code %d: %v", ve.StatusCode(), ve.Message) 30 | } 31 | 32 | func newSecurityError(code int, message string) *securityError { 33 | return &securityError{ 34 | Code: code, 35 | Type: "https://github.com/favclip/ucon#swagger-security", 36 | Message: message, 37 | } 38 | } 39 | 40 | var ( 41 | // ErrSecurityDefinitionsIsRequired is returned when security definition is missing in object or path items. 42 | ErrSecurityDefinitionsIsRequired = newSecurityError(http.StatusInternalServerError, "swagger: SecurityDefinitions is required") 43 | // ErrSecuritySettingsAreWrong is returned when required scope is missing in object or path items. 44 | ErrSecuritySettingsAreWrong = newSecurityError(http.StatusInternalServerError, "swagger: security settings are wrong") 45 | // ErrNotImplemented is returned when specified type is not implemented. 46 | ErrNotImplemented = newSecurityError(http.StatusInternalServerError, "swagger: not implemented") 47 | // ErrAccessDenied is returned when access user doesn't have a access grant. 48 | ErrAccessDenied = newSecurityError(http.StatusUnauthorized, "swagger: access denied") 49 | ) 50 | 51 | // CheckSecurityRequirements about request. 52 | func CheckSecurityRequirements(obj *Object, getScopes func(b *ucon.Bubble) ([]string, error)) ucon.MiddlewareFunc { 53 | 54 | return func(b *ucon.Bubble) error { 55 | op, ok := b.RequestHandler.Value(swaggerOperationKey{}).(*Operation) 56 | if !ok { 57 | return b.Next() 58 | } 59 | 60 | var secReqs []SecurityRequirement 61 | if op.Security != nil { 62 | // If len(op.Security) == 0, It overwrite top-level definition. 63 | // check by != nil. 64 | secReqs = op.Security 65 | 66 | } else { 67 | secReqs = obj.Security 68 | } 69 | 70 | // check security. It is ok if any one of the security passes. 71 | passed := false 72 | for _, req := range secReqs { 73 | sec_type: 74 | for name, oauth2ReqScopes := range req { 75 | if obj.SecurityDefinitions == nil { 76 | return ErrSecurityDefinitionsIsRequired 77 | } 78 | 79 | scheme, ok := obj.SecurityDefinitions[name] 80 | if !ok { 81 | return ErrSecurityDefinitionsIsRequired 82 | } 83 | 84 | switch scheme.Type { 85 | case "oauth2": 86 | scopes, err := getScopes(b) 87 | if err != nil { 88 | return err 89 | } 90 | 91 | // all scopes are required. 92 | outer: 93 | for _, reqScope := range oauth2ReqScopes { 94 | for _, scope := range scopes { 95 | if scope == reqScope { 96 | continue outer 97 | } 98 | } 99 | 100 | continue sec_type 101 | } 102 | 103 | passed = true 104 | 105 | case "basic": 106 | fallthrough 107 | case "apiKey": 108 | fallthrough 109 | default: 110 | if len(oauth2ReqScopes) != 0 { 111 | return ErrSecuritySettingsAreWrong 112 | } 113 | 114 | return ErrNotImplemented 115 | } 116 | } 117 | } 118 | if passed { 119 | return b.Next() 120 | } 121 | 122 | return ErrAccessDenied 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /swagger/middleware_customreq_test.go: -------------------------------------------------------------------------------- 1 | package swagger_test 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/favclip/ucon" 8 | "github.com/favclip/ucon/swagger" 9 | ) 10 | 11 | type validatorFunc func(v interface{}) error 12 | 13 | func (f validatorFunc) Validate(v interface{}) error { 14 | return f(v) 15 | } 16 | 17 | type TargetRequestValidate struct { 18 | Text string `swagger:"enum=ok|ng"` 19 | } 20 | 21 | type IgnoreRequestValidate struct { 22 | Text string `swagger:"enum=ok|ng"` 23 | } 24 | 25 | func TestCustomizeReqValidator(t *testing.T) { 26 | // skip validating about *IgnoreRequestValidate 27 | var validator validatorFunc = func(v interface{}) error { 28 | switch v.(type) { 29 | case *IgnoreRequestValidate: 30 | return nil 31 | } 32 | 33 | return swagger.DefaultValidator.Validate(v) 34 | } 35 | middleware := ucon.RequestValidator(validator) 36 | 37 | b, _ := ucon.MakeMiddlewareTestBed(t, middleware, func(req1 *TargetRequestValidate, req2 *IgnoreRequestValidate) {}, nil) 38 | b.Arguments[0] = reflect.ValueOf(&TargetRequestValidate{Text: "ok"}) 39 | b.Arguments[1] = reflect.ValueOf(&IgnoreRequestValidate{Text: "bad"}) 40 | err := b.Next() 41 | if err != nil { 42 | t.Fatalf("unexpected: %v", err) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /swagger/middleware_test.go: -------------------------------------------------------------------------------- 1 | package swagger 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/favclip/ucon" 7 | ) 8 | 9 | func TestSwaggerCheckSecurityRequirements_OAuth2OK(t *testing.T) { 10 | obj := &Object{ 11 | SecurityDefinitions: map[string]*SecurityScheme{ 12 | "foobar": { 13 | Name: "access control by oauth2", 14 | Type: "oauth2", 15 | Flow: "accessCode", 16 | AuthorizationURL: "http://localhost:8080/oauth/authorize", 17 | TokenURL: "http://localhost:8080/oauth/token", 18 | Scopes: map[string]string{ 19 | "write": "modify todos in your account", 20 | "read": "read your todos", 21 | }, 22 | }, 23 | }, 24 | } 25 | op := &Operation{ 26 | Security: []SecurityRequirement{map[string][]string{"foobar": []string{"write", "read"}}}, 27 | } 28 | 29 | getScopes := func(b *ucon.Bubble) ([]string, error) { 30 | return []string{"write", "read"}, nil 31 | } 32 | 33 | b, _ := ucon.MakeMiddlewareTestBed(t, CheckSecurityRequirements(obj, getScopes), func() { 34 | }, &ucon.BubbleTestOption{ 35 | MiddlewareContext: ucon.WithValue(nil, swaggerOperationKey{}, op), 36 | }) 37 | err := b.Next() 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | } 42 | 43 | func TestSwaggerCheckSecurityRequirements_OAuth2NG(t *testing.T) { 44 | obj := &Object{ 45 | SecurityDefinitions: map[string]*SecurityScheme{ 46 | "foobar": { 47 | Name: "access control by oauth2", 48 | Type: "oauth2", 49 | Flow: "accessCode", 50 | AuthorizationURL: "http://localhost:8080/oauth/authorize", 51 | TokenURL: "http://localhost:8080/oauth/token", 52 | Scopes: map[string]string{ 53 | "write": "modify todos in your account", 54 | "read": "read your todos", 55 | }, 56 | }, 57 | }, 58 | } 59 | op := &Operation{ 60 | Security: []SecurityRequirement{map[string][]string{"foobar": []string{"write", "read"}}}, 61 | } 62 | 63 | getScopes := func(b *ucon.Bubble) ([]string, error) { 64 | return []string{"read"}, nil 65 | } 66 | 67 | b, _ := ucon.MakeMiddlewareTestBed(t, CheckSecurityRequirements(obj, getScopes), func() { 68 | }, &ucon.BubbleTestOption{ 69 | MiddlewareContext: ucon.WithValue(nil, swaggerOperationKey{}, op), 70 | }) 71 | err := b.Next() 72 | if err != ErrAccessDenied { 73 | t.Fatal(err) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /swagger/schema_test.go: -------------------------------------------------------------------------------- 1 | package swagger 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/k0kubun/pp" 8 | ) 9 | 10 | func TestSchema(t *testing.T) { 11 | jsonString := ` 12 | { 13 | "swagger": "2.0", 14 | "info": { 15 | "version": "1.0.0", 16 | "title": "Swagger Petstore", 17 | "contact": { 18 | "name": "Swagger API Team", 19 | "url": "http://swagger.io" 20 | }, 21 | "license": { 22 | "name": "Creative Commons 4.0 International", 23 | "url": "http://creativecommons.org/licenses/by/4.0/" 24 | } 25 | }, 26 | "host": "petstore.swagger.io", 27 | "basePath": "/api", 28 | "schemes": [ 29 | "http" 30 | ], 31 | "paths": { 32 | "/pets": { 33 | "get": { 34 | "tags": [ "Pet Operations" ], 35 | "summary": "finds pets in the system", 36 | "responses": { 37 | "200": { 38 | "description": "pet response", 39 | "schema": { 40 | "type": "array", 41 | "items": { 42 | "$ref": "#/definitions/Pet" 43 | } 44 | }, 45 | "headers": { 46 | "x-expires": { 47 | "type": "string" 48 | } 49 | } 50 | }, 51 | "default": { 52 | "description": "unexpected error", 53 | "schema": { 54 | "$ref": "#/definitions/Error" 55 | } 56 | } 57 | } 58 | } 59 | } 60 | }, 61 | "definitions": { 62 | "Pet": { 63 | "type": "object", 64 | "required": [ 65 | "id", 66 | "name" 67 | ], 68 | "properties": { 69 | "id": { 70 | "type": "integer", 71 | "format": "int64" 72 | }, 73 | "name": { 74 | "type": "string" 75 | }, 76 | "tag": { 77 | "type": "string" 78 | } 79 | } 80 | }, 81 | "Error": { 82 | "type": "object", 83 | "required": [ 84 | "code", 85 | "message" 86 | ], 87 | "properties": { 88 | "code": { 89 | "type": "integer", 90 | "format": "int32" 91 | }, 92 | "message": { 93 | "type": "string" 94 | } 95 | } 96 | } 97 | } 98 | } 99 | ` 100 | 101 | obj := &Object{} 102 | err := json.Unmarshal([]byte(jsonString), obj) 103 | if err != nil { 104 | t.Fatal(err) 105 | } 106 | 107 | pp.Print(obj) 108 | } 109 | -------------------------------------------------------------------------------- /swagger/swagger_model_test.go: -------------------------------------------------------------------------------- 1 | package swagger 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestSwaggerCheckSecurityDefinitions_OK(t *testing.T) { 8 | obj := &Object{ 9 | SecurityDefinitions: SecurityDefinitions{ 10 | "foo": { 11 | Type: "oauth2", 12 | Scopes: Scopes{ 13 | "a": "a desc", 14 | "b": "b desc", 15 | }, 16 | }, 17 | }, 18 | Security: []SecurityRequirement{ 19 | { 20 | "foo": {"a", "b"}, 21 | }, 22 | }, 23 | 24 | Paths: Paths{ 25 | "/api/1": { 26 | Get: &Operation{ 27 | Security: []SecurityRequirement{ 28 | { 29 | "foo": {"a", "b"}, 30 | }, 31 | }, 32 | }, 33 | Put: &Operation{ 34 | Security: []SecurityRequirement{ 35 | { 36 | "foo": {"a", "b"}, 37 | }, 38 | }, 39 | }, 40 | Post: &Operation{ 41 | Security: []SecurityRequirement{ 42 | { 43 | "foo": {"a", "b"}, 44 | }, 45 | }, 46 | }, 47 | Delete: &Operation{ 48 | Security: []SecurityRequirement{ 49 | { 50 | "foo": {"a", "b"}, 51 | }, 52 | }, 53 | }, 54 | Options: &Operation{ 55 | Security: []SecurityRequirement{ 56 | { 57 | "foo": {"a", "b"}, 58 | }, 59 | }, 60 | }, 61 | Head: &Operation{ 62 | Security: []SecurityRequirement{ 63 | { 64 | "foo": {"a", "b"}, 65 | }, 66 | }, 67 | }, 68 | Patch: &Operation{ 69 | Security: []SecurityRequirement{ 70 | { 71 | "foo": {"a", "b"}, 72 | }, 73 | }, 74 | }, 75 | }, 76 | }, 77 | } 78 | 79 | err := checkSecurityDefinitions(obj) 80 | if err != nil { 81 | t.Fatal(err) 82 | } 83 | } 84 | 85 | func TestSwaggerCheckSecurityDefinitions_NGMissingScopesInObject(t *testing.T) { 86 | obj := &Object{ 87 | SecurityDefinitions: SecurityDefinitions{ 88 | "foo": { 89 | Type: "oauth2", 90 | Scopes: Scopes{ 91 | "a": "a desc", 92 | }, 93 | }, 94 | }, 95 | Security: []SecurityRequirement{ 96 | { 97 | "foo": {"a", "b"}, 98 | }, 99 | }, 100 | } 101 | 102 | err := checkSecurityDefinitions(obj) 103 | if err != ErrSecuritySettingsAreWrong { 104 | t.Fatal(err) 105 | } 106 | } 107 | 108 | func TestSwaggerCheckSecurityDefinitions_NGMissingScopesInPathItem(t *testing.T) { 109 | obj := &Object{ 110 | SecurityDefinitions: SecurityDefinitions{ 111 | "foo": { 112 | Type: "oauth2", 113 | Scopes: Scopes{ 114 | "a": "a desc", 115 | }, 116 | }, 117 | }, 118 | 119 | Paths: Paths{ 120 | "/api/1": { 121 | Get: &Operation{ 122 | Security: []SecurityRequirement{ 123 | { 124 | "foo": {"a", "b"}, 125 | }, 126 | }, 127 | }, 128 | }, 129 | }, 130 | } 131 | 132 | err := checkSecurityDefinitions(obj) 133 | if err != ErrSecuritySettingsAreWrong { 134 | t.Fatal(err) 135 | } 136 | } 137 | 138 | func TestSwaggerCheckSecurityDefinitions_NGMissingSecurityDefinitions(t *testing.T) { 139 | obj := &Object{ 140 | Security: []SecurityRequirement{ 141 | { 142 | "foo": {"a", "b"}, 143 | }, 144 | }, 145 | } 146 | 147 | err := checkSecurityDefinitions(obj) 148 | if err != ErrSecurityDefinitionsIsRequired { 149 | t.Fatal(err) 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /swagger/validator.go: -------------------------------------------------------------------------------- 1 | package swagger 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | 7 | "github.com/favclip/golidator" 8 | "github.com/favclip/ucon" 9 | ) 10 | 11 | // DefaultValidator used in RequestValidator. 12 | var DefaultValidator ucon.Validator 13 | 14 | var _ ucon.HTTPErrorResponse = &validateError{} 15 | var _ error = &validateError{} 16 | 17 | type validateError struct { 18 | Code int `json:"code"` 19 | Origin error `json:"-"` 20 | } 21 | 22 | type validateMessage struct { 23 | Type string `json:"type"` 24 | Message string `json:"message"` 25 | } 26 | 27 | func (ve *validateError) StatusCode() int { 28 | return ve.Code 29 | } 30 | 31 | func (ve *validateError) ErrorMessage() interface{} { 32 | return &validateMessage{ 33 | Type: "https://github.com/favclip/ucon#swagger-validate", 34 | Message: ve.Origin.Error(), 35 | } 36 | } 37 | 38 | func (ve *validateError) Error() string { 39 | if ve.Origin != nil { 40 | return ve.Origin.Error() 41 | } 42 | return fmt.Sprintf("status code %d: %v", ve.StatusCode(), ve.ErrorMessage()) 43 | } 44 | 45 | // RequestValidator checks request object validity by swagger tag. 46 | func RequestValidator() ucon.MiddlewareFunc { 47 | return ucon.RequestValidator(DefaultValidator) 48 | } 49 | 50 | func init() { 51 | v := &golidator.Validator{} 52 | v.SetTag("swagger") 53 | 54 | v.SetValidationFunc("req", golidator.ReqValidator) 55 | v.SetValidationFunc("d", golidator.DefaultValidator) 56 | v.SetValidationFunc("enum", golidator.EnumValidator) 57 | 58 | v.SetValidationFunc("min", golidator.MinValidator) 59 | v.SetValidationFunc("max", golidator.MaxValidator) 60 | v.SetValidationFunc("minLen", golidator.MinLenValidator) 61 | v.SetValidationFunc("maxLen", golidator.MaxLenValidator) 62 | v.SetValidationFunc("pattern", golidator.RegExpValidator) 63 | 64 | // ignore in=path, in=query pattern 65 | v.SetValidationFunc("in", func(param string, v reflect.Value) (golidator.ValidationResult, error) { 66 | return golidator.ValidationOK, nil 67 | }) 68 | 69 | DefaultValidator = v 70 | } 71 | -------------------------------------------------------------------------------- /swagger/validator_test.go: -------------------------------------------------------------------------------- 1 | package swagger 2 | 3 | import ( 4 | "net/http" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/favclip/ucon" 9 | ) 10 | 11 | type TargetRequestValidate struct { 12 | Text string `swagger:"enum=ok|ng"` 13 | } 14 | 15 | func TestRequestValidator_valid(t *testing.T) { 16 | b, _ := ucon.MakeMiddlewareTestBed(t, RequestValidator(), func(req *TargetRequestValidate) { 17 | }, nil) 18 | b.Arguments[0] = reflect.ValueOf(&TargetRequestValidate{Text: "ok"}) 19 | 20 | err := b.Next() 21 | if err != nil { 22 | t.Fatalf("unexpected: %v", err) 23 | } 24 | } 25 | 26 | func TestRequestValidator_invalid(t *testing.T) { 27 | b, _ := ucon.MakeMiddlewareTestBed(t, RequestValidator(), func(req *TargetRequestValidate) { 28 | }, nil) 29 | b.Arguments[0] = reflect.ValueOf(&TargetRequestValidate{Text: "invalid"}) 30 | 31 | err := b.Next() 32 | if err == nil { 33 | t.Fatalf("unexpected: %v", err) 34 | } 35 | if v := err.(ucon.HTTPErrorResponse).StatusCode(); v != http.StatusBadRequest { 36 | t.Errorf("unexpected: %v", v) 37 | } 38 | } 39 | 40 | type TargetRequestPatternValidate struct { 41 | Text string `swagger:"pattern=\\d+"` 42 | } 43 | 44 | func TestRequestValidator_patternValid(t *testing.T) { 45 | b, _ := ucon.MakeMiddlewareTestBed(t, RequestValidator(), func(req *TargetRequestPatternValidate) { 46 | }, nil) 47 | b.Arguments[0] = reflect.ValueOf(&TargetRequestPatternValidate{Text: "123456789"}) 48 | 49 | err := b.Next() 50 | if err != nil { 51 | t.Fatalf("unexpected: %v", err) 52 | } 53 | } 54 | 55 | func TestRequestValidator_patternInvalid(t *testing.T) { 56 | b, _ := ucon.MakeMiddlewareTestBed(t, RequestValidator(), func(req *TargetRequestPatternValidate) { 57 | }, nil) 58 | b.Arguments[0] = reflect.ValueOf(&TargetRequestPatternValidate{Text: "invalid"}) 59 | 60 | err := b.Next() 61 | if err == nil { 62 | t.Fatalf("unexpected: %v", err) 63 | } 64 | if v := err.(ucon.HTTPErrorResponse).StatusCode(); v != http.StatusBadRequest { 65 | t.Errorf("unexpected: %v", v) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eux 2 | 3 | cd `dirname $0` 4 | 5 | export PATH=$(pwd)/build-cmd:$PATH 6 | 7 | goimports -w . 8 | go generate ./... 9 | go vet . 10 | golint . 11 | golint swagger 12 | go test ./... $@ 13 | -------------------------------------------------------------------------------- /test_utils.go: -------------------------------------------------------------------------------- 1 | package ucon 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "net/http" 7 | "net/http/httptest" 8 | "net/url" 9 | "testing" 10 | ) 11 | 12 | // BubbleTestOption is an option for setting a mock request. 13 | type BubbleTestOption struct { 14 | Method string 15 | URL string 16 | ContentType string 17 | Body io.Reader 18 | MiddlewareContext Context 19 | } 20 | 21 | // MakeMiddlewareTestBed returns a Bubble and ServeMux for handling the request made from the option. 22 | func MakeMiddlewareTestBed(t *testing.T, middleware MiddlewareFunc, handler interface{}, opts *BubbleTestOption) (*Bubble, *ServeMux) { 23 | if opts == nil { 24 | opts = &BubbleTestOption{ 25 | Method: "GET", 26 | URL: "/api/tmp", 27 | } 28 | } 29 | if opts.MiddlewareContext == nil { 30 | opts.MiddlewareContext = background 31 | } 32 | mux := NewServeMux() 33 | mux.Middleware(middleware) 34 | 35 | r, err := http.NewRequest(opts.Method, opts.URL, opts.Body) 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | 40 | if opts.Body != nil { 41 | if opts.ContentType != "" { 42 | r.Header.Add("Content-Type", opts.ContentType) 43 | } else { 44 | r.Header.Add("Content-Type", "application/json") 45 | } 46 | } 47 | 48 | w := httptest.NewRecorder() 49 | 50 | u, err := url.Parse(opts.URL) 51 | if err != nil { 52 | t.Fatal(err) 53 | } 54 | 55 | rd := &RouteDefinition{ 56 | Method: opts.Method, 57 | PathTemplate: ParsePathTemplate(u.Path), 58 | HandlerContainer: &handlerContainerImpl{ 59 | handler: handler, 60 | Context: opts.MiddlewareContext, 61 | }, 62 | } 63 | 64 | b, err := mux.newBubble(context.Background(), w, r, rd) 65 | if err != nil { 66 | t.Fatal(err) 67 | } 68 | 69 | return b, mux 70 | } 71 | 72 | // MakeHandlerTestBed returns a response by the request made from arguments. 73 | // To test some handlers, those must be registered by Handle or HandleFunc before calling this. 74 | func MakeHandlerTestBed(t *testing.T, method string, path string, body io.Reader) *http.Response { 75 | ts := httptest.NewServer(DefaultMux) 76 | defer ts.Close() 77 | 78 | reqURL, err := url.Parse(ts.URL) 79 | if err != nil { 80 | t.Fatal(err) 81 | } 82 | reqURL, err = reqURL.Parse(path) 83 | if err != nil { 84 | t.Fatal(err) 85 | } 86 | 87 | req, err := http.NewRequest(method, reqURL.String(), body) 88 | if err != nil { 89 | t.Fatal(err) 90 | } 91 | if body != nil { 92 | req.Header.Add("Content-Type", "application/json") 93 | } 94 | resp, err := http.DefaultClient.Do(req) 95 | if err != nil { 96 | t.Fatal(err) 97 | } 98 | 99 | return resp 100 | } 101 | -------------------------------------------------------------------------------- /tools.go: -------------------------------------------------------------------------------- 1 | // +build tools 2 | 3 | package ucon 4 | 5 | // from https://github.com/golang/go/issues/25922#issuecomment-412992431 6 | 7 | import ( 8 | _ "github.com/favclip/jwg" 9 | _ "github.com/favclip/qbg" 10 | _ "golang.org/x/lint/golint" 11 | _ "golang.org/x/tools/cmd/goimports" 12 | ) 13 | -------------------------------------------------------------------------------- /url_rawpath.go: -------------------------------------------------------------------------------- 1 | // +build go1.5 2 | 3 | package ucon 4 | 5 | import ( 6 | "net/http" 7 | ) 8 | 9 | func encodedPathFromRequest(r *http.Request) string { 10 | return r.URL.EscapedPath() 11 | } 12 | -------------------------------------------------------------------------------- /url_rawpath_go14.go: -------------------------------------------------------------------------------- 1 | // +build !go1.5 2 | 3 | package ucon 4 | 5 | import ( 6 | "net/http" 7 | ) 8 | 9 | func encodedPathFromRequest(r *http.Request) string { 10 | // url.EscapedPath() が欲しいがappengine環境下ではgo1.4で元データが存在しないのでごまかす /page/foo%2Fbar みたいな構造がうまく処理できない 解決は不可能という認識… 11 | // r.RequestURI から自力で頑張ればイケる…?? 12 | return r.URL.Path 13 | } 14 | -------------------------------------------------------------------------------- /utils_test.go: -------------------------------------------------------------------------------- 1 | package ucon 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | type valueStringTime time.Time 10 | 11 | func (t valueStringTime) ParseString(value string) (interface{}, error) { 12 | v, err := time.Parse("2006-01-02", value) 13 | if err != nil { 14 | return valueStringTime(time.Time{}), err 15 | } 16 | return valueStringTime(v), nil 17 | } 18 | 19 | type ValueStringMapperSample struct { 20 | AString string 21 | BString string `json:"bStr"` 22 | 23 | DInt8 int8 24 | EInt64 int64 25 | FUint8 uint8 26 | GUint64 uint64 27 | HFloat32 float32 28 | IFloat64 float64 29 | JBool bool 30 | KTime valueStringTime 31 | 32 | LPtrBool *bool 33 | MPtrPtrBool **bool 34 | 35 | ValueStringMapperSampleInner 36 | } 37 | 38 | type ValueStringMapperSampleInner struct { 39 | CString string 40 | } 41 | 42 | type ValueStringSliceMapperSample struct { 43 | AStrings []string 44 | BStrings []string `json:"bStrs"` 45 | 46 | DInt8s []int8 47 | EInt64s []int64 48 | FUint8s []uint8 49 | GUint64s []uint64 50 | HFloat32s []float32 51 | IFloat64s []float64 52 | JBools []bool 53 | KTimes []valueStringTime 54 | 55 | // unsupported 56 | // LPtrBools []*bool 57 | // MPtrPtrBools []**bool 58 | 59 | ValueStringSliceMapperSampleInner 60 | 61 | YString string 62 | ZString string 63 | } 64 | 65 | type ValueStringSliceMapperSampleInner struct { 66 | CStrings []string 67 | } 68 | 69 | func TestValueStringMapper(t *testing.T) { 70 | obj := &ValueStringMapperSample{} 71 | target := reflect.ValueOf(obj) 72 | valueStringMapper(target, "AString", "This is A") 73 | valueStringMapper(target, "bStr", "This is B") 74 | valueStringMapper(target, "CString", "This is C") 75 | valueStringMapper(target, "DInt8", "1") 76 | valueStringMapper(target, "EInt64", "2") 77 | valueStringMapper(target, "FUint8", "3") 78 | valueStringMapper(target, "GUint64", "4") 79 | valueStringMapper(target, "HFloat32", "1.25") 80 | valueStringMapper(target, "IFloat64", "2.75") 81 | valueStringMapper(target, "JBool", "true") 82 | valueStringMapper(target, "KTime", "2016-01-07") 83 | valueStringMapper(target, "LPtrBool", "true") 84 | valueStringMapper(target, "MPtrPtrBool", "true") 85 | 86 | if obj.AString != "This is A" { 87 | t.Errorf("unexpected A: %v", obj.AString) 88 | } 89 | if obj.BString != "This is B" { 90 | t.Errorf("unexpected B: %v", obj.BString) 91 | } 92 | if obj.CString != "This is C" { 93 | t.Errorf("unexpected C: %v", obj.CString) 94 | } 95 | if obj.DInt8 != 1 { 96 | t.Errorf("unexpected D: %v", obj.DInt8) 97 | } 98 | if obj.EInt64 != 2 { 99 | t.Errorf("unexpected E: %v", obj.EInt64) 100 | } 101 | if obj.FUint8 != 3 { 102 | t.Errorf("unexpected F: %v", obj.FUint8) 103 | } 104 | if obj.GUint64 != 4 { 105 | t.Errorf("unexpected G: %v", obj.GUint64) 106 | } 107 | if obj.HFloat32 != 1.25 { 108 | t.Errorf("unexpected H: %v", obj.HFloat32) 109 | } 110 | if obj.IFloat64 != 2.75 { 111 | t.Errorf("unexpected I: %v", obj.IFloat64) 112 | } 113 | if obj.JBool != true { 114 | t.Errorf("unexpected J: %v", obj.JBool) 115 | } 116 | if y, m, d := time.Time(obj.KTime).Date(); y != 2016 { 117 | t.Errorf("unexpected KTime.y: %v", y) 118 | } else if m != 1 { 119 | t.Errorf("unexpected KTime.m: %v", m) 120 | } else if d != 7 { 121 | t.Errorf("unexpected KTime.d: %v", d) 122 | } 123 | if obj.LPtrBool == nil || *obj.LPtrBool != true { 124 | t.Errorf("unexpected L: %v", obj.LPtrBool) 125 | } 126 | if obj.MPtrPtrBool == nil || *obj.MPtrPtrBool == nil || **obj.MPtrPtrBool != true { 127 | t.Errorf("unexpected M: %v", obj.LPtrBool) 128 | } 129 | } 130 | 131 | func TestValueStringSliceMapper(t *testing.T) { 132 | obj := &ValueStringSliceMapperSample{} 133 | target := reflect.ValueOf(obj) 134 | valueStringSliceMapper(target, "AStrings", []string{"This is A1", "This is A2"}) 135 | valueStringSliceMapper(target, "bStrs", []string{"This is B1", "This is B2"}) 136 | valueStringSliceMapper(target, "CStrings", []string{"This is C1", "This is C2"}) 137 | valueStringSliceMapper(target, "DInt8s", []string{"1", "11"}) 138 | valueStringSliceMapper(target, "EInt64s", []string{"2", "22"}) 139 | valueStringSliceMapper(target, "FUint8s", []string{"3", "33"}) 140 | valueStringSliceMapper(target, "GUint64s", []string{"4", "44"}) 141 | valueStringSliceMapper(target, "HFloat32s", []string{"1.25", "11.25"}) 142 | valueStringSliceMapper(target, "IFloat64s", []string{"2.75", "22.75"}) 143 | valueStringSliceMapper(target, "JBools", []string{"true", "false"}) 144 | valueStringSliceMapper(target, "KTimes", []string{"2016-01-07", "2016-04-05"}) 145 | // valueStringSliceMapper(target, "LPtrBools", []string{"true", "false"}) 146 | // valueStringSliceMapper(target, "MPtrPtrBools", []string{"true", "false"}) 147 | valueStringSliceMapper(target, "YString", []string{"This is Y"}) 148 | valueStringSliceMapper(target, "ZString", []string{}) 149 | 150 | if len(obj.AStrings) != 2 { 151 | t.Fatalf("unexpected A len: %v", len(obj.AStrings)) 152 | } 153 | if obj.AStrings[0] != "This is A1" { 154 | t.Errorf("unexpected A[0]: %v", obj.AStrings[0]) 155 | } 156 | if obj.AStrings[1] != "This is A2" { 157 | t.Errorf("unexpected A[1]: %v", obj.AStrings[1]) 158 | } 159 | 160 | if len(obj.BStrings) != 2 { 161 | t.Fatalf("unexpected B len: %v", len(obj.BStrings)) 162 | } 163 | if obj.BStrings[0] != "This is B1" { 164 | t.Errorf("unexpected B[0]: %v", obj.BStrings[0]) 165 | } 166 | if obj.BStrings[1] != "This is B2" { 167 | t.Errorf("unexpected B[1]: %v", obj.BStrings[1]) 168 | } 169 | 170 | if len(obj.CStrings) != 2 { 171 | t.Fatalf("unexpected C len: %v", len(obj.CStrings)) 172 | } 173 | if obj.CStrings[0] != "This is C1" { 174 | t.Errorf("unexpected C[0]: %v", obj.CStrings[0]) 175 | } 176 | if obj.CStrings[1] != "This is C2" { 177 | t.Errorf("unexpected C[1]: %v", obj.CStrings[1]) 178 | } 179 | 180 | if len(obj.DInt8s) != 2 { 181 | t.Fatalf("unexpected D len: %v", len(obj.DInt8s)) 182 | } 183 | if obj.DInt8s[0] != 1 { 184 | t.Errorf("unexpected D[0]: %v", obj.DInt8s[0]) 185 | } 186 | if obj.DInt8s[1] != 11 { 187 | t.Errorf("unexpected D[1]: %v", obj.DInt8s[1]) 188 | } 189 | 190 | if len(obj.EInt64s) != 2 { 191 | t.Fatalf("unexpected E len: %v", len(obj.EInt64s)) 192 | } 193 | if obj.EInt64s[0] != 2 { 194 | t.Errorf("unexpected E[0]: %v", obj.EInt64s[0]) 195 | } 196 | if obj.EInt64s[1] != 22 { 197 | t.Errorf("unexpected E[1]: %v", obj.EInt64s[1]) 198 | } 199 | 200 | if len(obj.FUint8s) != 2 { 201 | t.Fatalf("unexpected F len: %v", len(obj.FUint8s)) 202 | } 203 | if obj.FUint8s[0] != 3 { 204 | t.Errorf("unexpected F[0]: %v", obj.FUint8s[0]) 205 | } 206 | if obj.FUint8s[1] != 33 { 207 | t.Errorf("unexpected F[1]: %v", obj.FUint8s[1]) 208 | } 209 | 210 | if len(obj.GUint64s) != 2 { 211 | t.Fatalf("unexpected G len: %v", len(obj.GUint64s)) 212 | } 213 | if obj.GUint64s[0] != 4 { 214 | t.Errorf("unexpected G[0]: %v", obj.GUint64s[0]) 215 | } 216 | if obj.GUint64s[1] != 44 { 217 | t.Errorf("unexpected G[1]: %v", obj.GUint64s[1]) 218 | } 219 | 220 | if len(obj.HFloat32s) != 2 { 221 | t.Fatalf("unexpected H len: %v", len(obj.HFloat32s)) 222 | } 223 | if obj.HFloat32s[0] != 1.25 { 224 | t.Errorf("unexpected H[0]: %v", obj.HFloat32s[0]) 225 | } 226 | if obj.HFloat32s[1] != 11.25 { 227 | t.Errorf("unexpected H[1]: %v", obj.HFloat32s[1]) 228 | } 229 | 230 | if len(obj.IFloat64s) != 2 { 231 | t.Fatalf("unexpected I len: %v", len(obj.IFloat64s)) 232 | } 233 | if obj.IFloat64s[0] != 2.75 { 234 | t.Errorf("unexpected I[0]: %v", obj.IFloat64s[0]) 235 | } 236 | if obj.IFloat64s[1] != 22.75 { 237 | t.Errorf("unexpected I[1]: %v", obj.IFloat64s[1]) 238 | } 239 | 240 | if len(obj.JBools) != 2 { 241 | t.Fatalf("unexpected J len: %v", len(obj.JBools)) 242 | } 243 | if obj.JBools[0] != true { 244 | t.Errorf("unexpected J[0]: %v", obj.JBools[0]) 245 | } 246 | if obj.JBools[1] != false { 247 | t.Errorf("unexpected J[1]: %v", obj.JBools[1]) 248 | } 249 | 250 | if len(obj.KTimes) != 2 { 251 | t.Fatalf("unexpected K len: %v", len(obj.KTimes)) 252 | } 253 | if y, m, d := time.Time(obj.KTimes[0]).Date(); y != 2016 { 254 | t.Errorf("unexpected KTime[0].y: %v", y) 255 | } else if m != 1 { 256 | t.Errorf("unexpected KTime[0].m: %v", m) 257 | } else if d != 7 { 258 | t.Errorf("unexpected KTime[0].d: %v", d) 259 | } 260 | if y, m, d := time.Time(obj.KTimes[1]).Date(); y != 2016 { 261 | t.Errorf("unexpected KTime[1].y: %v", y) 262 | } else if m != 4 { 263 | t.Errorf("unexpected KTime[1].m: %v", m) 264 | } else if d != 5 { 265 | t.Errorf("unexpected KTime[1].d: %v", d) 266 | } 267 | 268 | if obj.YString != "This is Y" { 269 | t.Errorf("unexpected Y: %v", obj.YString) 270 | } 271 | if obj.ZString != "" { 272 | t.Errorf("unexpected Z: %v", obj.ZString) 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /v3/core.go: -------------------------------------------------------------------------------- 1 | package ucon 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "strings" 8 | ) 9 | 10 | // DefaultMux is the default ServeMux in ucon. 11 | var DefaultMux = NewServeMux() 12 | 13 | // NewServeMux allocates and returns a new ServeMux. 14 | func NewServeMux() *ServeMux { 15 | mux := &ServeMux{ 16 | router: &Router{}, 17 | } 18 | mux.router.mux = mux 19 | return mux 20 | } 21 | 22 | // ServeMux is an HTTP request multiplexer. 23 | type ServeMux struct { 24 | Debug bool 25 | router *Router 26 | middlewares []MiddlewareFunc 27 | plugins []*pluginContainer 28 | } 29 | 30 | // MiddlewareFunc is an adapter to hook middleware processing. 31 | // Middleware works with 1 request. 32 | type MiddlewareFunc func(b *Bubble) error 33 | 34 | // Middleware can append Middleware to ServeMux. 35 | func (m *ServeMux) Middleware(f MiddlewareFunc) { 36 | m.middlewares = append(m.middlewares, f) 37 | } 38 | 39 | // Plugin can append Plugin to ServeMux. 40 | func (m *ServeMux) Plugin(plugin interface{}) { 41 | p, ok := plugin.(*pluginContainer) 42 | if !ok { 43 | p = &pluginContainer{base: plugin} 44 | } 45 | p.check() 46 | m.plugins = append(m.plugins, p) 47 | } 48 | 49 | // Prepare the ServeMux. 50 | // Plugin is not show affect to anything. 51 | // This method is enabled plugins. 52 | func (m *ServeMux) Prepare() { 53 | for _, plugin := range m.plugins { 54 | used := false 55 | if sc := plugin.HandlersScanner(); sc != nil { 56 | err := sc.HandlersScannerProcess(m, m.router.handlers) 57 | if err != nil { 58 | panic(err) 59 | } 60 | used = true 61 | } 62 | if !used { 63 | panic(fmt.Sprintf("unused plugin: %#v", plugin)) 64 | } 65 | } 66 | } 67 | 68 | // ServeHTTP dispatches request to the handler. 69 | func (m *ServeMux) ServeHTTP(w http.ResponseWriter, r *http.Request) { 70 | // NOTE ucon内部のルーティングは一元的にこの関数から行う 71 | // Handlerを細分化しhttp.ServeMuxに登録すると、OPTIONSのhandleがうまくできなくなる 72 | // このため、Handlerはucon全体で1つとし、OPTIONSも通常のMethodと同じようにHandlerを設定し利用する 73 | // OPTIONSを適切にhandleするため、全てのHandlerに特殊なHookを入れるよりマシである 74 | 75 | m.router.ServeHTTP(w, r) 76 | } 77 | 78 | // ListenAndServe start accepts the client request. 79 | func (m *ServeMux) ListenAndServe(addr string) error { 80 | m.Prepare() 81 | 82 | server := &http.Server{Addr: addr, Handler: m} 83 | return server.ListenAndServe() 84 | } 85 | 86 | // Handle register the HandlerContainer for the given method & path to the ServeMux. 87 | func (m *ServeMux) Handle(method string, path string, hc HandlerContainer) { 88 | CheckFunction(hc.Handler()) 89 | 90 | pathTmpl := ParsePathTemplate(path) 91 | methods := strings.Split(strings.ToUpper(method), ",") 92 | for _, method := range methods { 93 | rd := &RouteDefinition{ 94 | Method: method, 95 | PathTemplate: pathTmpl, 96 | HandlerContainer: hc, 97 | } 98 | m.router.addRoute(rd) 99 | } 100 | } 101 | 102 | // HandleFunc register the handler function for the given method & path to the ServeMux. 103 | func (m *ServeMux) HandleFunc(method string, path string, h interface{}) { 104 | m.Handle(method, path, &handlerContainerImpl{ 105 | handler: h, 106 | Context: background, 107 | }) 108 | } 109 | 110 | func (m *ServeMux) newBubble(c context.Context, w http.ResponseWriter, r *http.Request, rd *RouteDefinition) (*Bubble, error) { 111 | b := &Bubble{ 112 | R: r, 113 | W: w, 114 | Context: c, 115 | RequestHandler: rd.HandlerContainer, 116 | } 117 | err := b.init(m) 118 | if err != nil { 119 | return nil, err 120 | } 121 | 122 | return b, nil 123 | } 124 | 125 | // HandlerContainer is handler function container. 126 | // and It has a ucon Context that make it possible communicate to Plugins. 127 | type HandlerContainer interface { 128 | Handler() interface{} 129 | Context 130 | } 131 | 132 | type handlerContainerImpl struct { 133 | handler interface{} 134 | Context 135 | } 136 | 137 | func (hc *handlerContainerImpl) Handler() interface{} { 138 | return hc.handler 139 | } 140 | 141 | // Orthodox middlewares enable to DefaultServeMux. 142 | func Orthodox() { 143 | DefaultMux.Middleware(ResponseMapper()) 144 | DefaultMux.Middleware(HTTPRWDI()) 145 | DefaultMux.Middleware(ContextDI()) 146 | DefaultMux.Middleware(RequestObjectMapper()) 147 | } 148 | 149 | // Middleware can append Middleware to ServeMux. 150 | func Middleware(f MiddlewareFunc) { 151 | DefaultMux.Middleware(f) 152 | } 153 | 154 | // Plugin can append Plugin to ServeMux. 155 | func Plugin(plugin interface{}) { 156 | DefaultMux.Plugin(plugin) 157 | } 158 | 159 | // ListenAndServe start accepts the client request. 160 | func ListenAndServe(addr string) { 161 | DefaultMux.ListenAndServe(addr) 162 | } 163 | 164 | // Handle register the HandlerContainer for the given method & path to the ServeMux. 165 | func Handle(method string, path string, hc HandlerContainer) { 166 | DefaultMux.Handle(method, path, hc) 167 | } 168 | 169 | // HandleFunc register the handler function for the given method & path to the ServeMux. 170 | func HandleFunc(method string, path string, h interface{}) { 171 | DefaultMux.HandleFunc(method, path, h) 172 | } 173 | -------------------------------------------------------------------------------- /v3/core_test.go: -------------------------------------------------------------------------------- 1 | package ucon 2 | 3 | import "testing" 4 | 5 | func TestMiddleware(t *testing.T) { 6 | DefaultMux = NewServeMux() 7 | 8 | if v := len(DefaultMux.middlewares); v != 0 { 9 | t.Fatalf("unexpected: %v", v) 10 | } 11 | 12 | Middleware(func(b *Bubble) error { 13 | return nil 14 | }) 15 | 16 | if v := len(DefaultMux.middlewares); v != 1 { 17 | t.Fatalf("unexpected: %v", v) 18 | } 19 | } 20 | 21 | type TargetOfHandlersScannerPlugin struct { 22 | } 23 | 24 | func (obj *TargetOfHandlersScannerPlugin) HandlersScannerProcess(m *ServeMux, rds []*RouteDefinition) error { 25 | m.HandleFunc("GET", "/api/test/{id}", func() {}) 26 | 27 | return nil 28 | } 29 | 30 | func TestPluginWithPluginContainer(t *testing.T) { 31 | DefaultMux = NewServeMux() 32 | 33 | if v := len(DefaultMux.plugins); v != 0 { 34 | t.Fatalf("unexpected: %v", v) 35 | } 36 | 37 | Plugin(&pluginContainer{ 38 | base: &TargetOfHandlersScannerPlugin{}, 39 | }) 40 | 41 | if v := len(DefaultMux.plugins); v != 1 { 42 | t.Fatalf("unexpected: %v", v) 43 | } 44 | } 45 | 46 | func TestPluginWithoutPluginContainer(t *testing.T) { 47 | DefaultMux = NewServeMux() 48 | 49 | if v := len(DefaultMux.plugins); v != 0 { 50 | t.Fatalf("unexpected: %v", v) 51 | } 52 | 53 | Plugin(&TargetOfHandlersScannerPlugin{}) 54 | 55 | if v := len(DefaultMux.plugins); v != 1 { 56 | t.Fatalf("unexpected: %v", v) 57 | } 58 | } 59 | 60 | func TestPrepare(t *testing.T) { 61 | DefaultMux = NewServeMux() 62 | 63 | Plugin(&TargetOfHandlersScannerPlugin{}) 64 | 65 | if v := len(DefaultMux.router.handlers); v != 0 { 66 | t.Fatalf("unexpected: %v", v) 67 | } 68 | 69 | DefaultMux.Prepare() 70 | 71 | if v := len(DefaultMux.router.handlers); v != 1 { 72 | t.Fatalf("unexpected: %v", v) 73 | } 74 | } 75 | 76 | func TestHandle(t *testing.T) { 77 | DefaultMux = NewServeMux() 78 | 79 | if v := len(DefaultMux.router.handlers); v != 0 { 80 | t.Fatalf("unexpected: %v", v) 81 | } 82 | 83 | Handle("GET", "/api/test", &handlerContainerImpl{ 84 | handler: func() {}, 85 | Context: background, 86 | }) 87 | 88 | HandleFunc("GET", "/api/test/{id}", func() {}) 89 | HandleFunc("PUT", "/api/test/{id}", func() {}) 90 | 91 | if v := len(DefaultMux.router.handlers); v != 3 { 92 | t.Fatalf("unexpected: %v", v) 93 | } 94 | } 95 | 96 | func TestUconContextWithValue(t *testing.T) { 97 | var ctx Context = background 98 | if v := ctx.Value("a"); v != nil { 99 | t.Fatalf("unexpected: %v", v) 100 | } 101 | 102 | ctx = WithValue(ctx, "a", "b") 103 | 104 | if v := ctx.Value("a"); v.(string) != "b" { 105 | t.Fatalf("unexpected: %v", v) 106 | } 107 | 108 | ctx = WithValue(ctx, 1, 2) 109 | 110 | if v := ctx.Value("a"); v.(string) != "b" { 111 | t.Fatalf("unexpected: %v", v) 112 | } 113 | if v := ctx.Value(1); v.(int) != 2 { 114 | t.Fatalf("unexpected: %v", v) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /v3/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/favclip/ucon/v3 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/favclip/genbase v1.0.0 // indirect 7 | github.com/favclip/golidator v2.1.1+incompatible 8 | github.com/favclip/jwg v1.1.0 9 | github.com/favclip/qbg v1.1.1 10 | github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 // indirect 11 | github.com/k0kubun/pp v3.0.1+incompatible 12 | github.com/mattn/go-colorable v0.1.6 // indirect 13 | github.com/pmezard/go-difflib v1.0.0 // indirect 14 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b 15 | golang.org/x/tools v0.0.0-20200408132156-9ee5ef7a2c0d 16 | google.golang.org/appengine v1.6.5 // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /v3/go.sum: -------------------------------------------------------------------------------- 1 | github.com/favclip/genbase v1.0.0 h1:nvoQl0lpkQi3yrSHIrlo0+qfuxv7ZFXpgXii/8CfphA= 2 | github.com/favclip/genbase v1.0.0/go.mod h1:woIlAaQeKArqr+4fq0587kpfuLsjhJV3DA7xGtEdK7Q= 3 | github.com/favclip/golidator v2.1.1+incompatible h1:ktb02g0J1vXnhB6LaTmffeROb5Wxacr6nJkw4cKs9/Q= 4 | github.com/favclip/golidator v2.1.1+incompatible/go.mod h1:ruXV1d2tSVPa7CCm99hW+DYn58ac/sM5eeL73+Y7k1w= 5 | github.com/favclip/jwg v1.1.0 h1:JCveviH6hJz0YiHqisF+5pNC4rIV52lUdLjEVwoiFUY= 6 | github.com/favclip/jwg v1.1.0/go.mod h1:pnVGpkY4wkjLNei7EW96N+nrtl5vX/n2mELPHQBH9Mk= 7 | github.com/favclip/qbg v1.1.1 h1:+XMzBTY7L2lvhrurHB+QYqzW0RZtRsNoSitpGnmbgYE= 8 | github.com/favclip/qbg v1.1.1/go.mod h1:MVeXoe5BQr3gFch7dOojqXoMdI+vOs81mGmfBy0vFPo= 9 | github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= 10 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 11 | github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 h1:uC1QfSlInpQF+M0ao65imhwqKnz3Q2z/d8PWZRMQvDM= 12 | github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= 13 | github.com/k0kubun/pp v3.0.1+incompatible h1:3tqvf7QgUnZ5tXO6pNAZlrvHgl6DvifjDrd9g2S9Z40= 14 | github.com/k0kubun/pp v3.0.1+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg= 15 | github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE= 16 | github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 17 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 18 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 19 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 20 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 21 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 22 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 23 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 24 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k= 25 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 26 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 27 | golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ= 28 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 29 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 30 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 31 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 32 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b h1:0mm1VjtFUOIlE1SbDlwjYaDxZVDP2S5ou6y0gSgXHu8= 33 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 34 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 35 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 36 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 37 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 38 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 39 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8= 40 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 41 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 42 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 43 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 44 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 45 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 46 | golang.org/x/tools v0.0.0-20200408132156-9ee5ef7a2c0d h1:2DXIdtvIYvvWOcAOsX81FwOUBoQoMZhosWn7KjXEl94= 47 | golang.org/x/tools v0.0.0-20200408132156-9ee5ef7a2c0d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 48 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 49 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 50 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 51 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 52 | google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= 53 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 54 | -------------------------------------------------------------------------------- /v3/plugin.go: -------------------------------------------------------------------------------- 1 | package ucon 2 | 3 | import "fmt" 4 | 5 | type pluginContainer struct { 6 | base interface{} 7 | } 8 | 9 | // HandlersScannerPlugin is an interface to make a plugin for scanning request handlers. 10 | type HandlersScannerPlugin interface { 11 | HandlersScannerProcess(m *ServeMux, rds []*RouteDefinition) error 12 | } 13 | 14 | func (p *pluginContainer) check() { 15 | if p.HandlersScanner() != nil { 16 | return 17 | } 18 | 19 | panic(fmt.Sprintf("unused plugin: %#v", p.base)) 20 | } 21 | 22 | // HandlersScanner returns itself if it implements HandlersScannerPlugin. 23 | func (p *pluginContainer) HandlersScanner() HandlersScannerPlugin { 24 | if v, ok := p.base.(HandlersScannerPlugin); ok { 25 | return v 26 | } 27 | 28 | return nil 29 | } 30 | 31 | type emptyCtx int 32 | 33 | var background = new(emptyCtx) 34 | 35 | func (*emptyCtx) Value(key interface{}) interface{} { 36 | return nil 37 | } 38 | 39 | // Context is a key-value store. 40 | type Context interface { 41 | Value(key interface{}) interface{} 42 | } 43 | 44 | // WithValue returns a new context containing the value. 45 | // Values contained by parent context are inherited. 46 | func WithValue(parent Context, key interface{}, val interface{}) Context { 47 | return &valueCtx{parent, key, val} 48 | } 49 | 50 | type valueCtx struct { 51 | Context 52 | key interface{} 53 | val interface{} 54 | } 55 | 56 | func (c *valueCtx) Value(key interface{}) interface{} { 57 | if c.key == key { 58 | return c.val 59 | } 60 | return c.Context.Value(key) 61 | } 62 | -------------------------------------------------------------------------------- /v3/request.go: -------------------------------------------------------------------------------- 1 | package ucon 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "reflect" 9 | ) 10 | 11 | // ErrInvalidRequestHandler is the error that Bubble.RequestHandler is not a function. 12 | var ErrInvalidRequestHandler = errors.New("invalid request handler. not function") 13 | 14 | // ErrInvalidArgumentLength is the error that length of Bubble.Arguments does not match to RequestHandler arguments. 15 | var ErrInvalidArgumentLength = errors.New("invalid arguments") 16 | 17 | // ErrInvalidArgumentValue is the error that value in Bubble.Arguments is invalid. 18 | var ErrInvalidArgumentValue = errors.New("invalid argument value") 19 | 20 | // Bubble is a context of data processing that will be passed to a request handler at last. 21 | // The name `Bubble` means that the processing flow is a event-bubbling. 22 | // Processors, called `middleware`, are executed in order with same context, and at last the RequestHandler will be called. 23 | type Bubble struct { 24 | R *http.Request 25 | W http.ResponseWriter 26 | Context context.Context 27 | RequestHandler HandlerContainer 28 | 29 | Debug bool 30 | 31 | Handled bool 32 | ArgumentTypes []reflect.Type 33 | Arguments []reflect.Value 34 | Returns []reflect.Value 35 | 36 | queueIndex int 37 | mux *ServeMux 38 | } 39 | 40 | func (b *Bubble) checkHandlerType() error { 41 | if _, ok := b.RequestHandler.(HandlerContainer); ok { 42 | return nil 43 | } 44 | hv := reflect.ValueOf(b.RequestHandler) 45 | if hv.Type().Kind() == reflect.Func { 46 | return nil 47 | } 48 | 49 | return ErrInvalidRequestHandler 50 | } 51 | 52 | func (b *Bubble) handler() interface{} { 53 | if hv, ok := b.RequestHandler.(HandlerContainer); ok { 54 | return hv.Handler() 55 | } 56 | hv := reflect.ValueOf(b.RequestHandler) 57 | if hv.Type().Kind() == reflect.Func { 58 | return b.RequestHandler 59 | } 60 | 61 | return nil 62 | } 63 | 64 | func (b *Bubble) init(m *ServeMux) error { 65 | err := b.checkHandlerType() 66 | if err != nil { 67 | return err 68 | } 69 | 70 | hv := reflect.ValueOf(b.handler()) 71 | numIn := hv.Type().NumIn() 72 | b.ArgumentTypes = make([]reflect.Type, numIn) 73 | for i := 0; i < numIn; i++ { 74 | b.ArgumentTypes[i] = hv.Type().In(i) 75 | } 76 | b.Arguments = make([]reflect.Value, numIn) 77 | 78 | b.mux = m 79 | b.Debug = m.Debug 80 | 81 | return nil 82 | } 83 | 84 | // Next passes the bubble to next middleware. 85 | // If the bubble reaches at last, RequestHandler will be called. 86 | func (b *Bubble) Next() error { 87 | if b.queueIndex < len(b.mux.middlewares) { 88 | qi := b.queueIndex 89 | b.queueIndex++ 90 | m := b.mux.middlewares[qi] 91 | err := m(b) 92 | return err 93 | } 94 | 95 | return b.do() 96 | } 97 | 98 | func (b *Bubble) do() error { 99 | hv := reflect.ValueOf(b.handler()) 100 | 101 | if len(b.Arguments) != len(b.ArgumentTypes) || len(b.Arguments) != hv.Type().NumIn() { 102 | return ErrInvalidArgumentLength 103 | } 104 | for idx, arg := range b.Arguments { 105 | if !arg.IsValid() { 106 | fmt.Printf("ArgumentInvalid %d\n", idx) 107 | return ErrInvalidArgumentValue 108 | } 109 | if !arg.Type().AssignableTo(hv.Type().In(idx)) { 110 | fmt.Printf("TypeMismatch %d, %+v, %+v\n", idx, b.Arguments[idx], hv.Type().In(idx)) 111 | return ErrInvalidArgumentValue 112 | } 113 | } 114 | 115 | b.Returns = hv.Call(b.Arguments) 116 | 117 | b.Handled = true 118 | 119 | return nil 120 | } 121 | -------------------------------------------------------------------------------- /v3/request_test.go: -------------------------------------------------------------------------------- 1 | package ucon 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func noopMiddleware(b *Bubble) error { 11 | return b.Next() 12 | } 13 | 14 | func TestBubbleInit(t *testing.T) { 15 | b, _ := MakeMiddlewareTestBed(t, noopMiddleware, func(ctx context.Context) error { 16 | return nil 17 | }, nil) 18 | 19 | if v := len(b.ArgumentTypes); v != 1 { 20 | t.Errorf("unexpected: %v", v) 21 | } 22 | if v := b.ArgumentTypes[0]; v != contextType { 23 | t.Errorf("unexpected: %v", v) 24 | } 25 | if v := len(b.Arguments); v != 1 { 26 | t.Errorf("unexpected: %v", v) 27 | } 28 | if v := len(b.Returns); v != 0 { 29 | t.Errorf("unexpected: %v", v) 30 | } 31 | } 32 | 33 | func TestBubbleDo(t *testing.T) { 34 | b, _ := MakeMiddlewareTestBed(t, noopMiddleware, func(ctx context.Context) error { 35 | return nil 36 | }, nil) 37 | 38 | b.Arguments = nil 39 | if v := b.Next(); v != ErrInvalidArgumentLength { 40 | t.Errorf("unexpected: %v", v) 41 | } 42 | 43 | b.Arguments = make([]reflect.Value, 1) 44 | b.Arguments[0] = reflect.Value{} 45 | if v := b.Next(); v != ErrInvalidArgumentValue { 46 | t.Errorf("unexpected: %v", v) 47 | } 48 | 49 | b.Arguments[0] = reflect.ValueOf(time.Time{}) 50 | if v := b.Next(); v != ErrInvalidArgumentValue { 51 | t.Errorf("unexpected: %v", v) 52 | } 53 | 54 | if v := b.Handled; v { 55 | t.Errorf("unexpected: %v", v) 56 | } 57 | 58 | b.Arguments[0] = reflect.ValueOf(context.Background()) 59 | if v := b.Next(); v != nil { 60 | t.Errorf("unexpected: %v", v) 61 | } 62 | 63 | if v := len(b.Returns); v != 1 { 64 | t.Errorf("unexpected: %v", v) 65 | } 66 | if v := b.Returns[0]; !v.IsNil() { 67 | t.Errorf("unexpected: %v", v) 68 | } 69 | if v := b.Handled; !v { 70 | t.Errorf("unexpected: %v", v) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /v3/routing.go: -------------------------------------------------------------------------------- 1 | // +build go1.7 2 | 3 | package ucon 4 | 5 | import ( 6 | "context" 7 | "net/http" 8 | ) 9 | 10 | func getDefaultContext(r *http.Request) context.Context { 11 | return r.Context() 12 | } 13 | -------------------------------------------------------------------------------- /v3/routing_classic.go: -------------------------------------------------------------------------------- 1 | // +build !go1.7 2 | 3 | package ucon 4 | 5 | import ( 6 | "context" 7 | "net/http" 8 | ) 9 | 10 | func getDefaultContext(r *http.Request) context.Context { 11 | return context.Background() 12 | } 13 | -------------------------------------------------------------------------------- /v3/routing_common.go: -------------------------------------------------------------------------------- 1 | package ucon 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "net/url" 7 | "regexp" 8 | "strings" 9 | ) 10 | 11 | type methodMatchRate int 12 | 13 | const ( 14 | noMethodMatch methodMatchRate = iota 15 | starMethodMatch 16 | overloadMethodMatch 17 | exactMethodMatch 18 | ) 19 | 20 | const ( 21 | noPathMatch int = iota 22 | starPathMatch 23 | exactPathMatch 24 | ) 25 | 26 | // Router is a handler to pass requests to the best-matched route definition. 27 | // For the router decides the best route definition, there are 3 rules. 28 | // 29 | // 1. Methods must match. 30 | // a. If the method of request is `HEAD`, exceptionally `GET` definition is also allowed. 31 | // b. If the method of definition is `*`, the definition matches on all method. 32 | // 2. Paths must match as longer as possible. 33 | // a. The path of definition must match to the request path completely. 34 | // b. Select the longest match. 35 | // * Against Request[/api/foo/hi/comments/1], Definition[/api/foo/{bar}/] is stronger than Definition[/api/foo/]. 36 | // 3. If there are multiple options after 1 and 2 rules, select the earliest one which have been added to router. 37 | // 38 | type Router struct { 39 | mux *ServeMux 40 | handlers []*RouteDefinition 41 | } 42 | 43 | func (ro *Router) addRoute(rd *RouteDefinition) { 44 | ro.handlers = append(ro.handlers, rd) 45 | } 46 | 47 | // ServeHTTP routes a request to the handler and creates new bubble. 48 | func (ro *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) { 49 | rd := ro.pickupBestRouteDefinition(r) 50 | 51 | if rd == nil { 52 | http.NotFound(w, r) 53 | return 54 | } 55 | 56 | ctx := getDefaultContext(r) 57 | 58 | match, params := rd.PathTemplate.Match(encodedPathFromRequest(r)) 59 | if !match { 60 | http.Error(w, "[ucon] invalid handler picked", http.StatusInternalServerError) 61 | return 62 | } 63 | ctx = context.WithValue(ctx, PathParameterKey, params) 64 | 65 | b, err := ro.mux.newBubble(ctx, w, r, rd) 66 | if err != nil { 67 | http.Error(w, err.Error(), http.StatusInternalServerError) 68 | return 69 | } 70 | err = b.Next() 71 | if err != nil { 72 | http.Error(w, err.Error(), http.StatusInternalServerError) 73 | return 74 | } 75 | } 76 | 77 | func (ro *Router) pickupBestRouteDefinition(r *http.Request) *RouteDefinition { 78 | // NOTE 動作概要 79 | // 設定されているHandler群から適切なものを選択しそれにroutingする 80 | // "適切なもの"の選び方は以下の通り 81 | // 1. Methodが一致する 82 | // a. 指定MethodがHEADの場合、GETも探索する(明示的にHEADのものが優先される) 83 | // b. 指定Methodが*の場合、すべてのMethodに対して一致するものとする 84 | // 2. RequestPathが一致する 85 | // a. Handler側のパスは全長一致しなければならない 86 | // b. より長いパス長のものを優先する /api/foo/bar なら3節 という数え方 87 | // /api/foo/ と /api/foo/{bar}/ というHandlerがあったら、 /api/foo/hi/comments/1 は /api/foo/{bar}/ に割り当てられる 88 | // 3. 1,2での評価が最も高いものが複数ある場合は、より早くServeMuxに追加されたHandlerを選ぶ 89 | // 90 | // ルーティングのサンプル 91 | // Handler: OPTIONS / , POST /api/todo & Request: OPTIONS /api/todo -> OPTIONS / が選択される 92 | // Handler: * /api/todo/ , POST /api/todo/{id} & Request: GET /api/todo/1 -> * /api/todo/ が選択される 93 | 94 | var bestRoute *RouteDefinition 95 | var bestMethodMatchRate methodMatchRate 96 | var bestPathMatchRate int 97 | 98 | methodMatchRate := func(rd *RouteDefinition) methodMatchRate { 99 | if rd.Method == "*" { 100 | return starMethodMatch 101 | } 102 | 103 | if rd.Method == "GET" && r.Method == "HEAD" { 104 | return overloadMethodMatch 105 | } 106 | 107 | if rd.Method == r.Method { 108 | return exactMethodMatch 109 | } 110 | 111 | return noMethodMatch 112 | } 113 | 114 | pathMatchRate := func(rd *RouteDefinition) int { 115 | match, _ := rd.PathTemplate.Match(r.URL.Path) 116 | if !match { 117 | 118 | return noPathMatch 119 | } 120 | 121 | tempPathTokens := rd.PathTemplate.splittedPathTemplate 122 | reqPathTokens := strings.Split(r.URL.Path, "/") 123 | 124 | if len(reqPathTokens) < len(tempPathTokens) { 125 | // tempPath must not be longer than reqPath 126 | return noPathMatch 127 | } 128 | 129 | var rate int 130 | for i, token := range tempPathTokens { 131 | if i == 0 { 132 | // first token is always "" 133 | continue 134 | } 135 | if rd.PathTemplate.isVariables[i] { 136 | // variable token matches to everything. 137 | rate += exactPathMatch 138 | continue 139 | } 140 | if token == "" { 141 | // "/a/" can match to "/a/c", but it's weaker than exact match. 142 | rate += starPathMatch 143 | continue 144 | } 145 | if token == reqPathTokens[i] { 146 | rate += exactPathMatch 147 | } 148 | } 149 | 150 | return rate 151 | } 152 | 153 | for _, rd := range ro.handlers { 154 | mRate := methodMatchRate(rd) 155 | if mRate == noMethodMatch { 156 | continue 157 | } else if mRate < bestMethodMatchRate { 158 | continue 159 | } 160 | 161 | pRate := pathMatchRate(rd) 162 | if pRate == noPathMatch { 163 | continue 164 | } else if pRate < bestPathMatchRate { 165 | continue 166 | } 167 | 168 | if bestMethodMatchRate == mRate && bestPathMatchRate == pRate { 169 | continue 170 | } 171 | 172 | bestMethodMatchRate = mRate 173 | bestPathMatchRate = pRate 174 | bestRoute = rd 175 | } 176 | 177 | return bestRoute 178 | } 179 | 180 | // RouteDefinition is a definition of route handling. 181 | // If a request matches on both the method and the path, the handler runs. 182 | type RouteDefinition struct { 183 | Method string 184 | PathTemplate *PathTemplate 185 | HandlerContainer HandlerContainer 186 | } 187 | 188 | // PathTemplate is a path with parameters template. 189 | type PathTemplate struct { 190 | PathTemplate string 191 | httpHandlePath string 192 | isVariables []bool 193 | splittedPathTemplate []string 194 | PathParameters []string 195 | } 196 | 197 | // Match checks whether PathTemplate matches the request path. 198 | // If the path contains parameter templates, those key-value map is also returned. 199 | func (pt *PathTemplate) Match(requestPath string) (bool, map[string]string) { 200 | if pt.PathTemplate == pt.httpHandlePath { 201 | // TODO ざっくりした実装なので後でリファクタリングすること 202 | if strings.HasPrefix(requestPath, pt.httpHandlePath) { 203 | return true, nil 204 | } 205 | } 206 | 207 | requestPathSplitted := strings.Split(requestPath, "/") 208 | if requiredLen, requestLen := len(pt.splittedPathTemplate), len(requestPathSplitted); requiredLen < requestLen { 209 | // I want to match /js/index.js to / :) 210 | requestPathSplitted = requestPathSplitted[0:requiredLen] 211 | } else if requiredLen != requestLen { 212 | return false, nil 213 | } 214 | 215 | params := make(map[string]string) 216 | for idx, s := range pt.splittedPathTemplate { 217 | reqPart := requestPathSplitted[idx] 218 | if pt.isVariables[idx] { 219 | if reqPart == "" { 220 | return false, nil 221 | } 222 | v, err := url.QueryUnescape(reqPart) 223 | if err != nil { 224 | v = reqPart 225 | } 226 | params[s[1:len(s)-1]] = v 227 | } else if s != reqPart { 228 | return false, nil 229 | } else { 230 | // match 231 | } 232 | } 233 | 234 | return true, params 235 | } 236 | 237 | // ParsePathTemplate parses path string to PathTemplate. 238 | func ParsePathTemplate(pathTmpl string) *PathTemplate { 239 | tmpl := &PathTemplate{} 240 | tmpl.PathTemplate = pathTmpl 241 | vIndex := strings.Index(pathTmpl, "{") 242 | if vIndex == -1 { 243 | tmpl.httpHandlePath = pathTmpl 244 | } else { 245 | tmpl.httpHandlePath = pathTmpl[:vIndex] 246 | } 247 | 248 | tmpl.splittedPathTemplate = strings.Split(pathTmpl, "/") 249 | tmpl.isVariables = make([]bool, len(tmpl.splittedPathTemplate)) 250 | 251 | re := regexp.MustCompile("^\\{(.+)\\}$") 252 | for idx, param := range tmpl.splittedPathTemplate { 253 | if re.MatchString(param) { 254 | key := re.FindStringSubmatch(param)[1] 255 | tmpl.PathParameters = append(tmpl.PathParameters, key) 256 | tmpl.isVariables[idx] = true 257 | continue 258 | } 259 | } 260 | 261 | return tmpl 262 | } 263 | -------------------------------------------------------------------------------- /v3/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -eux 2 | 3 | cd `dirname $0` 4 | 5 | go mod download 6 | 7 | # build tools 8 | rm -rf build-cmd/ 9 | mkdir build-cmd 10 | 11 | export GOBIN=`pwd -P`/build-cmd 12 | go install golang.org/x/tools/cmd/goimports 13 | go install golang.org/x/lint/golint 14 | go install github.com/favclip/jwg/cmd/jwg 15 | go install github.com/favclip/qbg/cmd/qbg 16 | -------------------------------------------------------------------------------- /v3/swagger/jsonschema_draft4.go: -------------------------------------------------------------------------------- 1 | package swagger 2 | 3 | // Schema is https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#schemaObject 4 | type Schema struct { 5 | Ref string `json:"$ref,omitempty"` 6 | Format string `json:"format,omitempty"` 7 | Title string `json:"title,omitempty"` 8 | Description string `json:"description,omitempty"` 9 | Default interface{} `json:"default,omitempty"` 10 | Maximum *int `json:"maximum,omitempty"` 11 | ExclusiveMaximum *bool `json:"exclusiveMaximum,omitempty"` 12 | Minimum *int `json:"minimum,omitempty"` 13 | ExclusiveMinimum *bool `json:"exclusiveMinimum,omitempty"` 14 | MaxLength *int `json:"maxLength,omitempty"` 15 | MinLength *int `json:"minLength,omitempty"` 16 | Pattern string `json:"pattern,omitempty"` 17 | MaxItems *int `json:"maxItems,omitempty"` 18 | MinItems *int `json:"minItems,omitempty"` 19 | UniqueItems *bool `json:"uniqueItems,omitempty"` 20 | MaxProperties *int `json:"maxProperties,omitempty"` 21 | MinProperties *int `json:"minProperties,omitempty"` 22 | Required []string `json:"required,omitempty"` 23 | Enum []interface{} `json:"enum,omitempty"` 24 | Type string `json:"type,omitempty"` 25 | Items *Schema `json:"items,omitempty"` 26 | AllOf []*Schema `json:"allOf,omitempty"` 27 | Properties map[string]*Schema `json:"properties,omitempty"` 28 | AdditionalProperties *Schema `json:"additionalProperties,omitempty"` 29 | Discriminator string `json:"discriminator,omitempty"` 30 | ReadOnly *bool `json:"readOnly,omitempty"` 31 | // Xml XML 32 | ExternalDocs *ExternalDocumentation `json:"externalDocs,omitempty"` 33 | Example interface{} `json:"example,omitempty"` 34 | } 35 | 36 | // ShallowCopy returns a clone of *Schema. 37 | func (schema *Schema) ShallowCopy() *Schema { 38 | return &Schema{ 39 | Ref: schema.Ref, 40 | Format: schema.Format, 41 | Title: schema.Title, 42 | Description: schema.Description, 43 | Default: schema.Default, 44 | Maximum: schema.Maximum, 45 | ExclusiveMaximum: schema.ExclusiveMaximum, 46 | Minimum: schema.Minimum, 47 | ExclusiveMinimum: schema.ExclusiveMinimum, 48 | MaxLength: schema.MaxLength, 49 | MinLength: schema.MinLength, 50 | Pattern: schema.Pattern, 51 | MaxItems: schema.MaxItems, 52 | MinItems: schema.MinItems, 53 | UniqueItems: schema.UniqueItems, 54 | MaxProperties: schema.MaxProperties, 55 | MinProperties: schema.MinProperties, 56 | Required: schema.Required, 57 | Enum: schema.Enum, 58 | Type: schema.Type, 59 | Items: schema.Items, 60 | AllOf: schema.AllOf, 61 | Properties: schema.Properties, 62 | AdditionalProperties: schema.AdditionalProperties, 63 | Discriminator: schema.Discriminator, 64 | ReadOnly: schema.ReadOnly, 65 | ExternalDocs: schema.ExternalDocs, 66 | Example: schema.Example, 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /v3/swagger/middleware.go: -------------------------------------------------------------------------------- 1 | package swagger 2 | 3 | import ( 4 | "fmt" 5 | 6 | "net/http" 7 | 8 | "github.com/favclip/ucon/v3" 9 | ) 10 | 11 | var _ ucon.HTTPErrorResponse = &securityError{} 12 | var _ error = &securityError{} 13 | 14 | type securityError struct { 15 | Code int `json:"code"` 16 | Type string `json:"type"` 17 | Message string `json:"message"` 18 | } 19 | 20 | func (ve *securityError) StatusCode() int { 21 | return ve.Code 22 | } 23 | 24 | func (ve *securityError) ErrorMessage() interface{} { 25 | return ve 26 | } 27 | 28 | func (ve *securityError) Error() string { 29 | return fmt.Sprintf("status code %d: %v", ve.StatusCode(), ve.Message) 30 | } 31 | 32 | func newSecurityError(code int, message string) *securityError { 33 | return &securityError{ 34 | Code: code, 35 | Type: "https://github.com/favclip/ucon#swagger-security", 36 | Message: message, 37 | } 38 | } 39 | 40 | var ( 41 | // ErrSecurityDefinitionsIsRequired is returned when security definition is missing in object or path items. 42 | ErrSecurityDefinitionsIsRequired = newSecurityError(http.StatusInternalServerError, "swagger: SecurityDefinitions is required") 43 | // ErrSecuritySettingsAreWrong is returned when required scope is missing in object or path items. 44 | ErrSecuritySettingsAreWrong = newSecurityError(http.StatusInternalServerError, "swagger: security settings are wrong") 45 | // ErrNotImplemented is returned when specified type is not implemented. 46 | ErrNotImplemented = newSecurityError(http.StatusInternalServerError, "swagger: not implemented") 47 | // ErrAccessDenied is returned when access user doesn't have a access grant. 48 | ErrAccessDenied = newSecurityError(http.StatusUnauthorized, "swagger: access denied") 49 | ) 50 | 51 | // CheckSecurityRequirements about request. 52 | func CheckSecurityRequirements(obj *Object, getScopes func(b *ucon.Bubble) ([]string, error)) ucon.MiddlewareFunc { 53 | 54 | return func(b *ucon.Bubble) error { 55 | op, ok := b.RequestHandler.Value(swaggerOperationKey{}).(*Operation) 56 | if !ok { 57 | return b.Next() 58 | } 59 | 60 | var secReqs []SecurityRequirement 61 | if op.Security != nil { 62 | // If len(op.Security) == 0, It overwrite top-level definition. 63 | // check by != nil. 64 | secReqs = op.Security 65 | 66 | } else { 67 | secReqs = obj.Security 68 | } 69 | 70 | // check security. It is ok if any one of the security passes. 71 | passed := false 72 | for _, req := range secReqs { 73 | sec_type: 74 | for name, oauth2ReqScopes := range req { 75 | if obj.SecurityDefinitions == nil { 76 | return ErrSecurityDefinitionsIsRequired 77 | } 78 | 79 | scheme, ok := obj.SecurityDefinitions[name] 80 | if !ok { 81 | return ErrSecurityDefinitionsIsRequired 82 | } 83 | 84 | switch scheme.Type { 85 | case "oauth2": 86 | scopes, err := getScopes(b) 87 | if err != nil { 88 | return err 89 | } 90 | 91 | // all scopes are required. 92 | outer: 93 | for _, reqScope := range oauth2ReqScopes { 94 | for _, scope := range scopes { 95 | if scope == reqScope { 96 | continue outer 97 | } 98 | } 99 | 100 | continue sec_type 101 | } 102 | 103 | passed = true 104 | 105 | case "basic": 106 | fallthrough 107 | case "apiKey": 108 | fallthrough 109 | default: 110 | if len(oauth2ReqScopes) != 0 { 111 | return ErrSecuritySettingsAreWrong 112 | } 113 | 114 | return ErrNotImplemented 115 | } 116 | } 117 | } 118 | if passed { 119 | return b.Next() 120 | } 121 | 122 | return ErrAccessDenied 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /v3/swagger/middleware_customreq_test.go: -------------------------------------------------------------------------------- 1 | package swagger_test 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/favclip/ucon/v3" 8 | "github.com/favclip/ucon/v3/swagger" 9 | ) 10 | 11 | type validatorFunc func(v interface{}) error 12 | 13 | func (f validatorFunc) Validate(v interface{}) error { 14 | return f(v) 15 | } 16 | 17 | type TargetRequestValidate struct { 18 | Text string `swagger:"enum=ok|ng"` 19 | } 20 | 21 | type IgnoreRequestValidate struct { 22 | Text string `swagger:"enum=ok|ng"` 23 | } 24 | 25 | func TestCustomizeReqValidator(t *testing.T) { 26 | // skip validating about *IgnoreRequestValidate 27 | var validator validatorFunc = func(v interface{}) error { 28 | switch v.(type) { 29 | case *IgnoreRequestValidate: 30 | return nil 31 | } 32 | 33 | return swagger.DefaultValidator.Validate(v) 34 | } 35 | middleware := ucon.RequestValidator(validator) 36 | 37 | b, _ := ucon.MakeMiddlewareTestBed(t, middleware, func(req1 *TargetRequestValidate, req2 *IgnoreRequestValidate) {}, nil) 38 | b.Arguments[0] = reflect.ValueOf(&TargetRequestValidate{Text: "ok"}) 39 | b.Arguments[1] = reflect.ValueOf(&IgnoreRequestValidate{Text: "bad"}) 40 | err := b.Next() 41 | if err != nil { 42 | t.Fatalf("unexpected: %v", err) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /v3/swagger/middleware_test.go: -------------------------------------------------------------------------------- 1 | package swagger 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/favclip/ucon/v3" 7 | ) 8 | 9 | func TestSwaggerCheckSecurityRequirements_OAuth2OK(t *testing.T) { 10 | obj := &Object{ 11 | SecurityDefinitions: map[string]*SecurityScheme{ 12 | "foobar": { 13 | Name: "access control by oauth2", 14 | Type: "oauth2", 15 | Flow: "accessCode", 16 | AuthorizationURL: "http://localhost:8080/oauth/authorize", 17 | TokenURL: "http://localhost:8080/oauth/token", 18 | Scopes: map[string]string{ 19 | "write": "modify todos in your account", 20 | "read": "read your todos", 21 | }, 22 | }, 23 | }, 24 | } 25 | op := &Operation{ 26 | Security: []SecurityRequirement{map[string][]string{"foobar": []string{"write", "read"}}}, 27 | } 28 | 29 | getScopes := func(b *ucon.Bubble) ([]string, error) { 30 | return []string{"write", "read"}, nil 31 | } 32 | 33 | b, _ := ucon.MakeMiddlewareTestBed(t, CheckSecurityRequirements(obj, getScopes), func() { 34 | }, &ucon.BubbleTestOption{ 35 | MiddlewareContext: ucon.WithValue(nil, swaggerOperationKey{}, op), 36 | }) 37 | err := b.Next() 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | } 42 | 43 | func TestSwaggerCheckSecurityRequirements_OAuth2NG(t *testing.T) { 44 | obj := &Object{ 45 | SecurityDefinitions: map[string]*SecurityScheme{ 46 | "foobar": { 47 | Name: "access control by oauth2", 48 | Type: "oauth2", 49 | Flow: "accessCode", 50 | AuthorizationURL: "http://localhost:8080/oauth/authorize", 51 | TokenURL: "http://localhost:8080/oauth/token", 52 | Scopes: map[string]string{ 53 | "write": "modify todos in your account", 54 | "read": "read your todos", 55 | }, 56 | }, 57 | }, 58 | } 59 | op := &Operation{ 60 | Security: []SecurityRequirement{map[string][]string{"foobar": []string{"write", "read"}}}, 61 | } 62 | 63 | getScopes := func(b *ucon.Bubble) ([]string, error) { 64 | return []string{"read"}, nil 65 | } 66 | 67 | b, _ := ucon.MakeMiddlewareTestBed(t, CheckSecurityRequirements(obj, getScopes), func() { 68 | }, &ucon.BubbleTestOption{ 69 | MiddlewareContext: ucon.WithValue(nil, swaggerOperationKey{}, op), 70 | }) 71 | err := b.Next() 72 | if err != ErrAccessDenied { 73 | t.Fatal(err) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /v3/swagger/schema_test.go: -------------------------------------------------------------------------------- 1 | package swagger 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/k0kubun/pp" 8 | ) 9 | 10 | func TestSchema(t *testing.T) { 11 | jsonString := ` 12 | { 13 | "swagger": "2.0", 14 | "info": { 15 | "version": "1.0.0", 16 | "title": "Swagger Petstore", 17 | "contact": { 18 | "name": "Swagger API Team", 19 | "url": "http://swagger.io" 20 | }, 21 | "license": { 22 | "name": "Creative Commons 4.0 International", 23 | "url": "http://creativecommons.org/licenses/by/4.0/" 24 | } 25 | }, 26 | "host": "petstore.swagger.io", 27 | "basePath": "/api", 28 | "schemes": [ 29 | "http" 30 | ], 31 | "paths": { 32 | "/pets": { 33 | "get": { 34 | "tags": [ "Pet Operations" ], 35 | "summary": "finds pets in the system", 36 | "responses": { 37 | "200": { 38 | "description": "pet response", 39 | "schema": { 40 | "type": "array", 41 | "items": { 42 | "$ref": "#/definitions/Pet" 43 | } 44 | }, 45 | "headers": { 46 | "x-expires": { 47 | "type": "string" 48 | } 49 | } 50 | }, 51 | "default": { 52 | "description": "unexpected error", 53 | "schema": { 54 | "$ref": "#/definitions/Error" 55 | } 56 | } 57 | } 58 | } 59 | } 60 | }, 61 | "definitions": { 62 | "Pet": { 63 | "type": "object", 64 | "required": [ 65 | "id", 66 | "name" 67 | ], 68 | "properties": { 69 | "id": { 70 | "type": "integer", 71 | "format": "int64" 72 | }, 73 | "name": { 74 | "type": "string" 75 | }, 76 | "tag": { 77 | "type": "string" 78 | } 79 | } 80 | }, 81 | "Error": { 82 | "type": "object", 83 | "required": [ 84 | "code", 85 | "message" 86 | ], 87 | "properties": { 88 | "code": { 89 | "type": "integer", 90 | "format": "int32" 91 | }, 92 | "message": { 93 | "type": "string" 94 | } 95 | } 96 | } 97 | } 98 | } 99 | ` 100 | 101 | obj := &Object{} 102 | err := json.Unmarshal([]byte(jsonString), obj) 103 | if err != nil { 104 | t.Fatal(err) 105 | } 106 | 107 | pp.Print(obj) 108 | } 109 | -------------------------------------------------------------------------------- /v3/swagger/swagger_model_test.go: -------------------------------------------------------------------------------- 1 | package swagger 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestSwaggerCheckSecurityDefinitions_OK(t *testing.T) { 8 | obj := &Object{ 9 | SecurityDefinitions: SecurityDefinitions{ 10 | "foo": { 11 | Type: "oauth2", 12 | Scopes: Scopes{ 13 | "a": "a desc", 14 | "b": "b desc", 15 | }, 16 | }, 17 | }, 18 | Security: []SecurityRequirement{ 19 | { 20 | "foo": {"a", "b"}, 21 | }, 22 | }, 23 | 24 | Paths: Paths{ 25 | "/api/1": { 26 | Get: &Operation{ 27 | Security: []SecurityRequirement{ 28 | { 29 | "foo": {"a", "b"}, 30 | }, 31 | }, 32 | }, 33 | Put: &Operation{ 34 | Security: []SecurityRequirement{ 35 | { 36 | "foo": {"a", "b"}, 37 | }, 38 | }, 39 | }, 40 | Post: &Operation{ 41 | Security: []SecurityRequirement{ 42 | { 43 | "foo": {"a", "b"}, 44 | }, 45 | }, 46 | }, 47 | Delete: &Operation{ 48 | Security: []SecurityRequirement{ 49 | { 50 | "foo": {"a", "b"}, 51 | }, 52 | }, 53 | }, 54 | Options: &Operation{ 55 | Security: []SecurityRequirement{ 56 | { 57 | "foo": {"a", "b"}, 58 | }, 59 | }, 60 | }, 61 | Head: &Operation{ 62 | Security: []SecurityRequirement{ 63 | { 64 | "foo": {"a", "b"}, 65 | }, 66 | }, 67 | }, 68 | Patch: &Operation{ 69 | Security: []SecurityRequirement{ 70 | { 71 | "foo": {"a", "b"}, 72 | }, 73 | }, 74 | }, 75 | }, 76 | }, 77 | } 78 | 79 | err := checkSecurityDefinitions(obj) 80 | if err != nil { 81 | t.Fatal(err) 82 | } 83 | } 84 | 85 | func TestSwaggerCheckSecurityDefinitions_NGMissingScopesInObject(t *testing.T) { 86 | obj := &Object{ 87 | SecurityDefinitions: SecurityDefinitions{ 88 | "foo": { 89 | Type: "oauth2", 90 | Scopes: Scopes{ 91 | "a": "a desc", 92 | }, 93 | }, 94 | }, 95 | Security: []SecurityRequirement{ 96 | { 97 | "foo": {"a", "b"}, 98 | }, 99 | }, 100 | } 101 | 102 | err := checkSecurityDefinitions(obj) 103 | if err != ErrSecuritySettingsAreWrong { 104 | t.Fatal(err) 105 | } 106 | } 107 | 108 | func TestSwaggerCheckSecurityDefinitions_NGMissingScopesInPathItem(t *testing.T) { 109 | obj := &Object{ 110 | SecurityDefinitions: SecurityDefinitions{ 111 | "foo": { 112 | Type: "oauth2", 113 | Scopes: Scopes{ 114 | "a": "a desc", 115 | }, 116 | }, 117 | }, 118 | 119 | Paths: Paths{ 120 | "/api/1": { 121 | Get: &Operation{ 122 | Security: []SecurityRequirement{ 123 | { 124 | "foo": {"a", "b"}, 125 | }, 126 | }, 127 | }, 128 | }, 129 | }, 130 | } 131 | 132 | err := checkSecurityDefinitions(obj) 133 | if err != ErrSecuritySettingsAreWrong { 134 | t.Fatal(err) 135 | } 136 | } 137 | 138 | func TestSwaggerCheckSecurityDefinitions_NGMissingSecurityDefinitions(t *testing.T) { 139 | obj := &Object{ 140 | Security: []SecurityRequirement{ 141 | { 142 | "foo": {"a", "b"}, 143 | }, 144 | }, 145 | } 146 | 147 | err := checkSecurityDefinitions(obj) 148 | if err != ErrSecurityDefinitionsIsRequired { 149 | t.Fatal(err) 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /v3/swagger/validator.go: -------------------------------------------------------------------------------- 1 | package swagger 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | 7 | "github.com/favclip/golidator" 8 | "github.com/favclip/ucon/v3" 9 | ) 10 | 11 | // DefaultValidator used in RequestValidator. 12 | var DefaultValidator ucon.Validator 13 | 14 | var _ ucon.HTTPErrorResponse = &validateError{} 15 | var _ error = &validateError{} 16 | 17 | type validateError struct { 18 | Code int `json:"code"` 19 | Origin error `json:"-"` 20 | } 21 | 22 | type validateMessage struct { 23 | Type string `json:"type"` 24 | Message string `json:"message"` 25 | } 26 | 27 | func (ve *validateError) StatusCode() int { 28 | return ve.Code 29 | } 30 | 31 | func (ve *validateError) ErrorMessage() interface{} { 32 | return &validateMessage{ 33 | Type: "https://github.com/favclip/ucon#swagger-validate", 34 | Message: ve.Origin.Error(), 35 | } 36 | } 37 | 38 | func (ve *validateError) Error() string { 39 | if ve.Origin != nil { 40 | return ve.Origin.Error() 41 | } 42 | return fmt.Sprintf("status code %d: %v", ve.StatusCode(), ve.ErrorMessage()) 43 | } 44 | 45 | // RequestValidator checks request object validity by swagger tag. 46 | func RequestValidator() ucon.MiddlewareFunc { 47 | return ucon.RequestValidator(DefaultValidator) 48 | } 49 | 50 | func init() { 51 | v := &golidator.Validator{} 52 | v.SetTag("swagger") 53 | 54 | v.SetValidationFunc("req", golidator.ReqValidator) 55 | v.SetValidationFunc("d", golidator.DefaultValidator) 56 | v.SetValidationFunc("enum", golidator.EnumValidator) 57 | 58 | v.SetValidationFunc("min", golidator.MinValidator) 59 | v.SetValidationFunc("max", golidator.MaxValidator) 60 | v.SetValidationFunc("minLen", golidator.MinLenValidator) 61 | v.SetValidationFunc("maxLen", golidator.MaxLenValidator) 62 | v.SetValidationFunc("pattern", golidator.RegExpValidator) 63 | 64 | // ignore in=path, in=query pattern 65 | v.SetValidationFunc("in", func(param string, v reflect.Value) (golidator.ValidationResult, error) { 66 | return golidator.ValidationOK, nil 67 | }) 68 | 69 | DefaultValidator = v 70 | } 71 | -------------------------------------------------------------------------------- /v3/swagger/validator_test.go: -------------------------------------------------------------------------------- 1 | package swagger 2 | 3 | import ( 4 | "net/http" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/favclip/ucon/v3" 9 | ) 10 | 11 | type TargetRequestValidate struct { 12 | Text string `swagger:"enum=ok|ng"` 13 | } 14 | 15 | func TestRequestValidator_valid(t *testing.T) { 16 | b, _ := ucon.MakeMiddlewareTestBed(t, RequestValidator(), func(req *TargetRequestValidate) { 17 | }, nil) 18 | b.Arguments[0] = reflect.ValueOf(&TargetRequestValidate{Text: "ok"}) 19 | 20 | err := b.Next() 21 | if err != nil { 22 | t.Fatalf("unexpected: %v", err) 23 | } 24 | } 25 | 26 | func TestRequestValidator_invalid(t *testing.T) { 27 | b, _ := ucon.MakeMiddlewareTestBed(t, RequestValidator(), func(req *TargetRequestValidate) { 28 | }, nil) 29 | b.Arguments[0] = reflect.ValueOf(&TargetRequestValidate{Text: "invalid"}) 30 | 31 | err := b.Next() 32 | if err == nil { 33 | t.Fatalf("unexpected: %v", err) 34 | } 35 | if v := err.(ucon.HTTPErrorResponse).StatusCode(); v != http.StatusBadRequest { 36 | t.Errorf("unexpected: %v", v) 37 | } 38 | } 39 | 40 | type TargetRequestPatternValidate struct { 41 | Text string `swagger:"pattern=\\d+"` 42 | } 43 | 44 | func TestRequestValidator_patternValid(t *testing.T) { 45 | b, _ := ucon.MakeMiddlewareTestBed(t, RequestValidator(), func(req *TargetRequestPatternValidate) { 46 | }, nil) 47 | b.Arguments[0] = reflect.ValueOf(&TargetRequestPatternValidate{Text: "123456789"}) 48 | 49 | err := b.Next() 50 | if err != nil { 51 | t.Fatalf("unexpected: %v", err) 52 | } 53 | } 54 | 55 | func TestRequestValidator_patternInvalid(t *testing.T) { 56 | b, _ := ucon.MakeMiddlewareTestBed(t, RequestValidator(), func(req *TargetRequestPatternValidate) { 57 | }, nil) 58 | b.Arguments[0] = reflect.ValueOf(&TargetRequestPatternValidate{Text: "invalid"}) 59 | 60 | err := b.Next() 61 | if err == nil { 62 | t.Fatalf("unexpected: %v", err) 63 | } 64 | if v := err.(ucon.HTTPErrorResponse).StatusCode(); v != http.StatusBadRequest { 65 | t.Errorf("unexpected: %v", v) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /v3/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eux 2 | 3 | cd `dirname $0` 4 | 5 | export PATH=$(pwd)/build-cmd:$PATH 6 | 7 | goimports -w . 8 | go generate ./... 9 | go vet . 10 | golint . 11 | golint swagger 12 | go test ./... $@ 13 | -------------------------------------------------------------------------------- /v3/test_utils.go: -------------------------------------------------------------------------------- 1 | package ucon 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "net/http" 7 | "net/http/httptest" 8 | "net/url" 9 | "testing" 10 | ) 11 | 12 | // BubbleTestOption is an option for setting a mock request. 13 | type BubbleTestOption struct { 14 | Method string 15 | URL string 16 | ContentType string 17 | Body io.Reader 18 | MiddlewareContext Context 19 | } 20 | 21 | // MakeMiddlewareTestBed returns a Bubble and ServeMux for handling the request made from the option. 22 | func MakeMiddlewareTestBed(t *testing.T, middleware MiddlewareFunc, handler interface{}, opts *BubbleTestOption) (*Bubble, *ServeMux) { 23 | if opts == nil { 24 | opts = &BubbleTestOption{ 25 | Method: "GET", 26 | URL: "/api/tmp", 27 | } 28 | } 29 | if opts.MiddlewareContext == nil { 30 | opts.MiddlewareContext = background 31 | } 32 | mux := NewServeMux() 33 | mux.Middleware(middleware) 34 | 35 | r, err := http.NewRequest(opts.Method, opts.URL, opts.Body) 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | 40 | if opts.Body != nil { 41 | if opts.ContentType != "" { 42 | r.Header.Add("Content-Type", opts.ContentType) 43 | } else { 44 | r.Header.Add("Content-Type", "application/json") 45 | } 46 | } 47 | 48 | w := httptest.NewRecorder() 49 | 50 | u, err := url.Parse(opts.URL) 51 | if err != nil { 52 | t.Fatal(err) 53 | } 54 | 55 | rd := &RouteDefinition{ 56 | Method: opts.Method, 57 | PathTemplate: ParsePathTemplate(u.Path), 58 | HandlerContainer: &handlerContainerImpl{ 59 | handler: handler, 60 | Context: opts.MiddlewareContext, 61 | }, 62 | } 63 | 64 | b, err := mux.newBubble(context.Background(), w, r, rd) 65 | if err != nil { 66 | t.Fatal(err) 67 | } 68 | 69 | return b, mux 70 | } 71 | 72 | // MakeHandlerTestBed returns a response by the request made from arguments. 73 | // To test some handlers, those must be registered by Handle or HandleFunc before calling this. 74 | func MakeHandlerTestBed(t *testing.T, method string, path string, body io.Reader) *http.Response { 75 | ts := httptest.NewServer(DefaultMux) 76 | defer ts.Close() 77 | 78 | reqURL, err := url.Parse(ts.URL) 79 | if err != nil { 80 | t.Fatal(err) 81 | } 82 | reqURL, err = reqURL.Parse(path) 83 | if err != nil { 84 | t.Fatal(err) 85 | } 86 | 87 | req, err := http.NewRequest(method, reqURL.String(), body) 88 | if err != nil { 89 | t.Fatal(err) 90 | } 91 | if body != nil { 92 | req.Header.Add("Content-Type", "application/json") 93 | } 94 | resp, err := http.DefaultClient.Do(req) 95 | if err != nil { 96 | t.Fatal(err) 97 | } 98 | 99 | return resp 100 | } 101 | -------------------------------------------------------------------------------- /v3/tools.go: -------------------------------------------------------------------------------- 1 | // +build tools 2 | 3 | package ucon 4 | 5 | // from https://github.com/golang/go/issues/25922#issuecomment-412992431 6 | 7 | import ( 8 | _ "github.com/favclip/jwg" 9 | _ "github.com/favclip/qbg" 10 | _ "golang.org/x/lint/golint" 11 | _ "golang.org/x/tools/cmd/goimports" 12 | ) 13 | -------------------------------------------------------------------------------- /v3/url_rawpath.go: -------------------------------------------------------------------------------- 1 | // +build go1.5 2 | 3 | package ucon 4 | 5 | import ( 6 | "net/http" 7 | ) 8 | 9 | func encodedPathFromRequest(r *http.Request) string { 10 | return r.URL.EscapedPath() 11 | } 12 | -------------------------------------------------------------------------------- /v3/url_rawpath_go14.go: -------------------------------------------------------------------------------- 1 | // +build !go1.5 2 | 3 | package ucon 4 | 5 | import ( 6 | "net/http" 7 | ) 8 | 9 | func encodedPathFromRequest(r *http.Request) string { 10 | // url.EscapedPath() が欲しいがappengine環境下ではgo1.4で元データが存在しないのでごまかす /page/foo%2Fbar みたいな構造がうまく処理できない 解決は不可能という認識… 11 | // r.RequestURI から自力で頑張ればイケる…?? 12 | return r.URL.Path 13 | } 14 | -------------------------------------------------------------------------------- /v3/utils_test.go: -------------------------------------------------------------------------------- 1 | package ucon 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | type valueStringTime time.Time 10 | 11 | func (t valueStringTime) ParseString(value string) (interface{}, error) { 12 | v, err := time.Parse("2006-01-02", value) 13 | if err != nil { 14 | return valueStringTime(time.Time{}), err 15 | } 16 | return valueStringTime(v), nil 17 | } 18 | 19 | type ValueStringMapperSample struct { 20 | AString string 21 | BString string `json:"bStr"` 22 | 23 | DInt8 int8 24 | EInt64 int64 25 | FUint8 uint8 26 | GUint64 uint64 27 | HFloat32 float32 28 | IFloat64 float64 29 | JBool bool 30 | KTime valueStringTime 31 | 32 | LPtrBool *bool 33 | MPtrPtrBool **bool 34 | 35 | ValueStringMapperSampleInner 36 | } 37 | 38 | type ValueStringMapperSampleInner struct { 39 | CString string 40 | } 41 | 42 | type ValueStringSliceMapperSample struct { 43 | AStrings []string 44 | BStrings []string `json:"bStrs"` 45 | 46 | DInt8s []int8 47 | EInt64s []int64 48 | FUint8s []uint8 49 | GUint64s []uint64 50 | HFloat32s []float32 51 | IFloat64s []float64 52 | JBools []bool 53 | KTimes []valueStringTime 54 | 55 | // unsupported 56 | // LPtrBools []*bool 57 | // MPtrPtrBools []**bool 58 | 59 | ValueStringSliceMapperSampleInner 60 | 61 | YString string 62 | ZString string 63 | } 64 | 65 | type ValueStringSliceMapperSampleInner struct { 66 | CStrings []string 67 | } 68 | 69 | func TestValueStringMapper(t *testing.T) { 70 | obj := &ValueStringMapperSample{} 71 | target := reflect.ValueOf(obj) 72 | valueStringMapper(target, "AString", "This is A") 73 | valueStringMapper(target, "bStr", "This is B") 74 | valueStringMapper(target, "CString", "This is C") 75 | valueStringMapper(target, "DInt8", "1") 76 | valueStringMapper(target, "EInt64", "2") 77 | valueStringMapper(target, "FUint8", "3") 78 | valueStringMapper(target, "GUint64", "4") 79 | valueStringMapper(target, "HFloat32", "1.25") 80 | valueStringMapper(target, "IFloat64", "2.75") 81 | valueStringMapper(target, "JBool", "true") 82 | valueStringMapper(target, "KTime", "2016-01-07") 83 | valueStringMapper(target, "LPtrBool", "true") 84 | valueStringMapper(target, "MPtrPtrBool", "true") 85 | 86 | if obj.AString != "This is A" { 87 | t.Errorf("unexpected A: %v", obj.AString) 88 | } 89 | if obj.BString != "This is B" { 90 | t.Errorf("unexpected B: %v", obj.BString) 91 | } 92 | if obj.CString != "This is C" { 93 | t.Errorf("unexpected C: %v", obj.CString) 94 | } 95 | if obj.DInt8 != 1 { 96 | t.Errorf("unexpected D: %v", obj.DInt8) 97 | } 98 | if obj.EInt64 != 2 { 99 | t.Errorf("unexpected E: %v", obj.EInt64) 100 | } 101 | if obj.FUint8 != 3 { 102 | t.Errorf("unexpected F: %v", obj.FUint8) 103 | } 104 | if obj.GUint64 != 4 { 105 | t.Errorf("unexpected G: %v", obj.GUint64) 106 | } 107 | if obj.HFloat32 != 1.25 { 108 | t.Errorf("unexpected H: %v", obj.HFloat32) 109 | } 110 | if obj.IFloat64 != 2.75 { 111 | t.Errorf("unexpected I: %v", obj.IFloat64) 112 | } 113 | if obj.JBool != true { 114 | t.Errorf("unexpected J: %v", obj.JBool) 115 | } 116 | if y, m, d := time.Time(obj.KTime).Date(); y != 2016 { 117 | t.Errorf("unexpected KTime.y: %v", y) 118 | } else if m != 1 { 119 | t.Errorf("unexpected KTime.m: %v", m) 120 | } else if d != 7 { 121 | t.Errorf("unexpected KTime.d: %v", d) 122 | } 123 | if obj.LPtrBool == nil || *obj.LPtrBool != true { 124 | t.Errorf("unexpected L: %v", obj.LPtrBool) 125 | } 126 | if obj.MPtrPtrBool == nil || *obj.MPtrPtrBool == nil || **obj.MPtrPtrBool != true { 127 | t.Errorf("unexpected M: %v", obj.LPtrBool) 128 | } 129 | } 130 | 131 | func TestValueStringSliceMapper(t *testing.T) { 132 | obj := &ValueStringSliceMapperSample{} 133 | target := reflect.ValueOf(obj) 134 | valueStringSliceMapper(target, "AStrings", []string{"This is A1", "This is A2"}) 135 | valueStringSliceMapper(target, "bStrs", []string{"This is B1", "This is B2"}) 136 | valueStringSliceMapper(target, "CStrings", []string{"This is C1", "This is C2"}) 137 | valueStringSliceMapper(target, "DInt8s", []string{"1", "11"}) 138 | valueStringSliceMapper(target, "EInt64s", []string{"2", "22"}) 139 | valueStringSliceMapper(target, "FUint8s", []string{"3", "33"}) 140 | valueStringSliceMapper(target, "GUint64s", []string{"4", "44"}) 141 | valueStringSliceMapper(target, "HFloat32s", []string{"1.25", "11.25"}) 142 | valueStringSliceMapper(target, "IFloat64s", []string{"2.75", "22.75"}) 143 | valueStringSliceMapper(target, "JBools", []string{"true", "false"}) 144 | valueStringSliceMapper(target, "KTimes", []string{"2016-01-07", "2016-04-05"}) 145 | // valueStringSliceMapper(target, "LPtrBools", []string{"true", "false"}) 146 | // valueStringSliceMapper(target, "MPtrPtrBools", []string{"true", "false"}) 147 | valueStringSliceMapper(target, "YString", []string{"This is Y"}) 148 | valueStringSliceMapper(target, "ZString", []string{}) 149 | 150 | if len(obj.AStrings) != 2 { 151 | t.Fatalf("unexpected A len: %v", len(obj.AStrings)) 152 | } 153 | if obj.AStrings[0] != "This is A1" { 154 | t.Errorf("unexpected A[0]: %v", obj.AStrings[0]) 155 | } 156 | if obj.AStrings[1] != "This is A2" { 157 | t.Errorf("unexpected A[1]: %v", obj.AStrings[1]) 158 | } 159 | 160 | if len(obj.BStrings) != 2 { 161 | t.Fatalf("unexpected B len: %v", len(obj.BStrings)) 162 | } 163 | if obj.BStrings[0] != "This is B1" { 164 | t.Errorf("unexpected B[0]: %v", obj.BStrings[0]) 165 | } 166 | if obj.BStrings[1] != "This is B2" { 167 | t.Errorf("unexpected B[1]: %v", obj.BStrings[1]) 168 | } 169 | 170 | if len(obj.CStrings) != 2 { 171 | t.Fatalf("unexpected C len: %v", len(obj.CStrings)) 172 | } 173 | if obj.CStrings[0] != "This is C1" { 174 | t.Errorf("unexpected C[0]: %v", obj.CStrings[0]) 175 | } 176 | if obj.CStrings[1] != "This is C2" { 177 | t.Errorf("unexpected C[1]: %v", obj.CStrings[1]) 178 | } 179 | 180 | if len(obj.DInt8s) != 2 { 181 | t.Fatalf("unexpected D len: %v", len(obj.DInt8s)) 182 | } 183 | if obj.DInt8s[0] != 1 { 184 | t.Errorf("unexpected D[0]: %v", obj.DInt8s[0]) 185 | } 186 | if obj.DInt8s[1] != 11 { 187 | t.Errorf("unexpected D[1]: %v", obj.DInt8s[1]) 188 | } 189 | 190 | if len(obj.EInt64s) != 2 { 191 | t.Fatalf("unexpected E len: %v", len(obj.EInt64s)) 192 | } 193 | if obj.EInt64s[0] != 2 { 194 | t.Errorf("unexpected E[0]: %v", obj.EInt64s[0]) 195 | } 196 | if obj.EInt64s[1] != 22 { 197 | t.Errorf("unexpected E[1]: %v", obj.EInt64s[1]) 198 | } 199 | 200 | if len(obj.FUint8s) != 2 { 201 | t.Fatalf("unexpected F len: %v", len(obj.FUint8s)) 202 | } 203 | if obj.FUint8s[0] != 3 { 204 | t.Errorf("unexpected F[0]: %v", obj.FUint8s[0]) 205 | } 206 | if obj.FUint8s[1] != 33 { 207 | t.Errorf("unexpected F[1]: %v", obj.FUint8s[1]) 208 | } 209 | 210 | if len(obj.GUint64s) != 2 { 211 | t.Fatalf("unexpected G len: %v", len(obj.GUint64s)) 212 | } 213 | if obj.GUint64s[0] != 4 { 214 | t.Errorf("unexpected G[0]: %v", obj.GUint64s[0]) 215 | } 216 | if obj.GUint64s[1] != 44 { 217 | t.Errorf("unexpected G[1]: %v", obj.GUint64s[1]) 218 | } 219 | 220 | if len(obj.HFloat32s) != 2 { 221 | t.Fatalf("unexpected H len: %v", len(obj.HFloat32s)) 222 | } 223 | if obj.HFloat32s[0] != 1.25 { 224 | t.Errorf("unexpected H[0]: %v", obj.HFloat32s[0]) 225 | } 226 | if obj.HFloat32s[1] != 11.25 { 227 | t.Errorf("unexpected H[1]: %v", obj.HFloat32s[1]) 228 | } 229 | 230 | if len(obj.IFloat64s) != 2 { 231 | t.Fatalf("unexpected I len: %v", len(obj.IFloat64s)) 232 | } 233 | if obj.IFloat64s[0] != 2.75 { 234 | t.Errorf("unexpected I[0]: %v", obj.IFloat64s[0]) 235 | } 236 | if obj.IFloat64s[1] != 22.75 { 237 | t.Errorf("unexpected I[1]: %v", obj.IFloat64s[1]) 238 | } 239 | 240 | if len(obj.JBools) != 2 { 241 | t.Fatalf("unexpected J len: %v", len(obj.JBools)) 242 | } 243 | if obj.JBools[0] != true { 244 | t.Errorf("unexpected J[0]: %v", obj.JBools[0]) 245 | } 246 | if obj.JBools[1] != false { 247 | t.Errorf("unexpected J[1]: %v", obj.JBools[1]) 248 | } 249 | 250 | if len(obj.KTimes) != 2 { 251 | t.Fatalf("unexpected K len: %v", len(obj.KTimes)) 252 | } 253 | if y, m, d := time.Time(obj.KTimes[0]).Date(); y != 2016 { 254 | t.Errorf("unexpected KTime[0].y: %v", y) 255 | } else if m != 1 { 256 | t.Errorf("unexpected KTime[0].m: %v", m) 257 | } else if d != 7 { 258 | t.Errorf("unexpected KTime[0].d: %v", d) 259 | } 260 | if y, m, d := time.Time(obj.KTimes[1]).Date(); y != 2016 { 261 | t.Errorf("unexpected KTime[1].y: %v", y) 262 | } else if m != 4 { 263 | t.Errorf("unexpected KTime[1].m: %v", m) 264 | } else if d != 5 { 265 | t.Errorf("unexpected KTime[1].d: %v", d) 266 | } 267 | 268 | if obj.YString != "This is Y" { 269 | t.Errorf("unexpected Y: %v", obj.YString) 270 | } 271 | if obj.ZString != "" { 272 | t.Errorf("unexpected Z: %v", obj.ZString) 273 | } 274 | } 275 | --------------------------------------------------------------------------------