diff --git a/_example/basic/main.go b/_example/basic/main.go index a2b8b77..1d0a1c7 100644 --- a/_example/basic/main.go +++ b/_example/basic/main.go @@ -11,14 +11,15 @@ import ( "fmt" "os" + "go.osspkg.com/logx" + "go.osspkg.com/xc" + "go.osspkg.com/goppy/v3" "go.osspkg.com/goppy/v3/console" "go.osspkg.com/goppy/v3/dic/broker" "go.osspkg.com/goppy/v3/metrics" "go.osspkg.com/goppy/v3/plugins" "go.osspkg.com/goppy/v3/web" - "go.osspkg.com/logx" - "go.osspkg.com/xc" ) type IStatus interface { diff --git a/_example/go-gen/001000_users_table.sql b/_example/orm-gen/001000_users_table.sql similarity index 100% rename from _example/go-gen/001000_users_table.sql rename to _example/orm-gen/001000_users_table.sql diff --git a/_example/go-gen/001001_meta_table.sql b/_example/orm-gen/001001_meta_table.sql similarity index 100% rename from _example/go-gen/001001_meta_table.sql rename to _example/orm-gen/001001_meta_table.sql diff --git a/_example/go-gen/empty.go b/_example/orm-gen/empty.go similarity index 93% rename from _example/go-gen/empty.go rename to _example/orm-gen/empty.go index 1fa4759..ae1670b 100644 --- a/_example/go-gen/empty.go +++ b/_example/orm-gen/empty.go @@ -3,7 +3,7 @@ * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ -package go_gen +package orm_gen type Empty struct { A string diff --git a/_example/go-gen/model.go b/_example/orm-gen/model.go similarity index 98% rename from _example/go-gen/model.go rename to _example/orm-gen/model.go index f5d88a3..622585f 100644 --- a/_example/go-gen/model.go +++ b/_example/orm-gen/model.go @@ -3,7 +3,7 @@ * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ -package go_gen +package orm_gen import ( "time" diff --git a/_example/go-gen/repo_init.go b/_example/orm-gen/repo_init.go similarity index 98% rename from _example/go-gen/repo_init.go rename to _example/orm-gen/repo_init.go index 9c674de..2990f79 100755 --- a/_example/go-gen/repo_init.go +++ b/_example/orm-gen/repo_init.go @@ -4,7 +4,7 @@ */ // Code generated by goppy-cli for goppy.orm. DO NOT EDIT. -package go_gen +package orm_gen import ( "fmt" diff --git a/_example/go-gen/repo_meta.go b/_example/orm-gen/repo_meta.go similarity index 99% rename from _example/go-gen/repo_meta.go rename to _example/orm-gen/repo_meta.go index 4e2a91b..799d312 100755 --- a/_example/go-gen/repo_meta.go +++ b/_example/orm-gen/repo_meta.go @@ -4,7 +4,7 @@ */ // Code generated by goppy-cli for goppy.orm. DO NOT EDIT. -package go_gen +package orm_gen import ( "context" diff --git a/_example/go-gen/repo_user.go b/_example/orm-gen/repo_user.go similarity index 99% rename from _example/go-gen/repo_user.go rename to _example/orm-gen/repo_user.go index 00a94f9..a78cf32 100755 --- a/_example/go-gen/repo_user.go +++ b/_example/orm-gen/repo_user.go @@ -4,7 +4,7 @@ */ // Code generated by goppy-cli for goppy.orm. DO NOT EDIT. -package go_gen +package orm_gen import ( "context" diff --git a/_example/web-server-gen/Makefile b/_example/web-server-gen/Makefile new file mode 100644 index 0000000..166867c --- /dev/null +++ b/_example/web-server-gen/Makefile @@ -0,0 +1,6 @@ +SHELL=/bin/bash + +run: + go generate -run easyjson ./transport + go run main.go --config=config.yaml + diff --git a/_example/web-server-gen/config.yaml b/_example/web-server-gen/config.yaml new file mode 100755 index 0000000..9418c8c --- /dev/null +++ b/_example/web-server-gen/config.yaml @@ -0,0 +1,17 @@ +env: dev + +log: + file_path: /dev/stdout + format: json + level: 4 + +http: + - tag: main + addr: 0.0.0.0:10000 + +metrics: + addr: 0.0.0.0:12000 + gauge: + - users_request + +custom: diff --git a/_example/web-server-gen/main.go b/_example/web-server-gen/main.go new file mode 100644 index 0000000..7338d8d --- /dev/null +++ b/_example/web-server-gen/main.go @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2022-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package main + +import ( + "context" + "fmt" + + "go.osspkg.com/goppy/v3" + "go.osspkg.com/goppy/v3/_example/web-server-gen/transport" + "go.osspkg.com/goppy/v3/_example/web-server-gen/types" + "go.osspkg.com/goppy/v3/web" +) + +func main() { + // Specify the path to the config via the argument: `--config`. + // Specify the path to the pidfile via the argument: `--pid`. + app := goppy.New("app_name", "v1.0.0", "app description") + app.Plugins( + web.WithServer(), + ) + app.Plugins( + NewController, + func(routes web.ServerPool, c *Controller) *transport.JSONRPCHandler { + return transport.NewJSONRPCHandler(routes, c, c, c) + }, + ) + app.Run() +} + +type Controller struct{} + +func NewController() *Controller { + return &Controller{} +} + +func (c Controller) Name(ctx context.Context, userID int64) (name string, err error) { + //TODO implement me + panic("implement me") +} + +func (c Controller) ByID(ctx context.Context, ID int64) (text bool, err error) { + //TODO implement me + panic("implement me") +} + +func (c Controller) List(ctx context.Context, userID int64) (text []types.Text, err error) { + //TODO implement me + panic("implement me") +} + +func (c Controller) Root(ctx context.Context, userID int64, userName string) (status bool, err error) { + switch userID { + case 0: + return false, fmt.Errorf("userID 0") + default: + return true, nil + } +} + +func (c Controller) Auth(ctx context.Context, userID int64, userName string) (status bool, err error) { + switch userID { + case 0: + return false, fmt.Errorf("userID 0") + default: + return true, nil + } +} diff --git a/_example/web-server-gen/test.http b/_example/web-server-gen/test.http new file mode 100644 index 0000000..cfd39aa --- /dev/null +++ b/_example/web-server-gen/test.http @@ -0,0 +1,23 @@ +### +POST http://127.0.0.1:10000/rpc +Content-Type: application/json +Cookie: x-user-id=1 +x-user-id: 1 + +[ + { + "id": "1", + "method":"api.root", + "params": { + "userName": "xxxx" + } + }, + { + "id": "2", + "method":"api.auth", + "params": { + "userName": "xxxx" + } + } +] + diff --git a/_example/web-server-gen/transport/jsonrpc_handler.go b/_example/web-server-gen/transport/jsonrpc_handler.go new file mode 100644 index 0000000..42f9da7 --- /dev/null +++ b/_example/web-server-gen/transport/jsonrpc_handler.go @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2022-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +// Code generated by goppy-cli wsg. DO NOT EDIT. +package transport + +import ( + nethttp "net/http" + + web "go.osspkg.com/goppy/v3/web" + + time "time" + + context "context" + + stdjson "encoding/json" + + fmt "fmt" +) + +func (v *JSONRPCHandler) callApiRoot(ctx context.Context, webCtx web.Ctx, param stdjson.RawMessage) (any, error) { + var req jsonrpcApiRootModelRequest + err := stdjson.Unmarshal(param, &req) + if err != nil { + err = fmt.Errorf("invalid request: %w", err) + return nil, err + } + var res jsonrpcApiRootModelResponse + var inUserID int64 + //Module: cookie + inUserID, _ = web.StrTo[int64](webCtx.Cookie().Get("x-user-id")) + if err != nil { + err = fmt.Errorf("invalid request: %w", err) + return nil, err + } + + res.Status, err = v.handleApi.Root(ctx, inUserID, req.UserName) + if err != nil { + return nil, err + } + return res, nil +} +func (v *JSONRPCHandler) callApiAuth(ctx context.Context, webCtx web.Ctx, param stdjson.RawMessage) (any, error) { + var req jsonrpcApiAuthModelRequest + err := stdjson.Unmarshal(param, &req) + if err != nil { + err = fmt.Errorf("invalid request: %w", err) + return nil, err + } + var res jsonrpcApiAuthModelResponse + var inUserID int64 + //Module: header + inUserID, _ = web.StrTo[int64](webCtx.Header().Get("x-user-id")) + if err != nil { + err = fmt.Errorf("invalid request: %w", err) + return nil, err + } + + var outStatus bool + defer func() { + //Module: header + if err != nil { + return + } + webCtx.Header().Set("x-user-id", fmt.Sprintf("%v", outStatus)) + + }() + defer func() { + //Module: cookie + if err != nil { + return + } + webCtx.Cookie().Set(&nethttp.Cookie{ + Name: "uid", + Value: fmt.Sprintf("%v", outStatus), + Path: "/", + Expires: time.Now().Add(86400 * time.Second), + Secure: true, + HttpOnly: true, + SameSite: nethttp.SameSiteStrictMode, + }) + + }() + defer func() { + //Module: cookie + if err != nil { + return + } + webCtx.Cookie().Set(&nethttp.Cookie{ + Name: "uid", + Value: fmt.Sprintf("%v", outStatus), + Path: "/", + Expires: time.Now().Add(86400 * time.Second), + Secure: true, + HttpOnly: true, + SameSite: nethttp.SameSiteStrictMode, + }) + + }() + outStatus, err = v.handleApi.Auth(ctx, inUserID, req.UserName) + if err != nil { + return nil, err + } + return res, nil +} +func (v *JSONRPCHandler) callUserName(ctx context.Context, webCtx web.Ctx, param stdjson.RawMessage) (any, error) { + var req jsonrpcUserNameModelRequest + err := stdjson.Unmarshal(param, &req) + if err != nil { + err = fmt.Errorf("invalid request: %w", err) + return nil, err + } + var res jsonrpcUserNameModelResponse + res.Name, err = v.handleUser.Name(ctx, req.UserID) + if err != nil { + return nil, err + } + return res, nil +} +func (v *JSONRPCHandler) callPostByID(ctx context.Context, webCtx web.Ctx, param stdjson.RawMessage) (any, error) { + var req jsonrpcPostByIDModelRequest + err := stdjson.Unmarshal(param, &req) + if err != nil { + err = fmt.Errorf("invalid request: %w", err) + return nil, err + } + var res jsonrpcPostByIDModelResponse + res.Text, err = v.handlePost.ByID(ctx, req.ID) + if err != nil { + return nil, err + } + return res, nil +} +func (v *JSONRPCHandler) callPostList(ctx context.Context, webCtx web.Ctx, param stdjson.RawMessage) (any, error) { + var req jsonrpcPostListModelRequest + err := stdjson.Unmarshal(param, &req) + if err != nil { + err = fmt.Errorf("invalid request: %w", err) + return nil, err + } + var res jsonrpcPostListModelResponse + res.Text, err = v.handlePost.List(ctx, req.UserID) + if err != nil { + return nil, err + } + return res, nil +} diff --git a/_example/web-server-gen/transport/jsonrpc_model.go b/_example/web-server-gen/transport/jsonrpc_model.go new file mode 100644 index 0000000..4d9a8f3 --- /dev/null +++ b/_example/web-server-gen/transport/jsonrpc_model.go @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2022-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +// Code generated by goppy-cli wsg. DO NOT EDIT. +package transport + +//go:generate easyjson +import types "go.osspkg.com/goppy/v3/_example/web-server-gen/types" + +//easyjson:json +type jsonrpcApiRootModelRequest struct { + UserName string `json:"userName"` +} + +//easyjson:json +type jsonrpcApiRootModelResponse struct { + Status bool `json:"status"` +} + +//easyjson:json +type jsonrpcApiAuthModelRequest struct { + UserName string `json:"userName"` +} + +//easyjson:json +type jsonrpcApiAuthModelResponse struct{} + +//easyjson:json +type jsonrpcUserNameModelRequest struct { + UserID int64 `json:"userID"` +} + +//easyjson:json +type jsonrpcUserNameModelResponse struct { + Name string `json:"name"` +} + +//easyjson:json +type jsonrpcPostByIDModelRequest struct { + ID int64 `json:"ID"` +} + +//easyjson:json +type jsonrpcPostByIDModelResponse struct { + Text bool `json:"text"` +} + +//easyjson:json +type jsonrpcPostListModelRequest struct { + UserID int64 `json:"userID"` +} + +//easyjson:json +type jsonrpcPostListModelResponse struct { + Text []types.Text `json:"text,omitempty"` +} diff --git a/_example/web-server-gen/transport/jsonrpc_model_base.go b/_example/web-server-gen/transport/jsonrpc_model_base.go new file mode 100644 index 0000000..528d8fa --- /dev/null +++ b/_example/web-server-gen/transport/jsonrpc_model_base.go @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2022-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +// Code generated by goppy-cli wsg. DO NOT EDIT. +package transport + +//go:generate easyjson +import stdjson "encoding/json" + +//easyjson:json +type baseRequest struct { + Id string `json:"id"` + Method string `json:"method"` + Params stdjson.RawMessage `json:"params"` +} + +//easyjson:json +type bulkRequest []baseRequest + +//easyjson:json +type baseResponse struct { + Id string `json:"id"` + Result any `json:"result,omitempty"` + Error *errResponse `json:"error,omitempty"` +} + +//easyjson:json +type bulkResponse []baseResponse + +//easyjson:json +type errResponse struct { + Message string `json:"message"` + Code int64 `json:"code"` + Ctx map[string]string `json:"ctx,omitempty"` +} +type TError interface { + GetCode() int64 + GetMessage() string + GetContext() map[string]string +} + +func toJSONRPCError(e error) *errResponse { + if e == nil { + return nil + } + err := &errResponse{} + te, ok := e.(TError) + if ok { + err.Code = te.GetCode() + err.Message = te.GetMessage() + err.Ctx = te.GetContext() + } else { + err.Message = e.Error() + } + return err +} diff --git a/_example/web-server-gen/transport/jsonrpc_model_base_easyjson.go b/_example/web-server-gen/transport/jsonrpc_model_base_easyjson.go new file mode 100644 index 0000000..7e1804e --- /dev/null +++ b/_example/web-server-gen/transport/jsonrpc_model_base_easyjson.go @@ -0,0 +1,471 @@ +// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT. + +package transport + +import ( + json "encoding/json" + + easyjson "github.com/mailru/easyjson" + jlexer "github.com/mailru/easyjson/jlexer" + jwriter "github.com/mailru/easyjson/jwriter" +) + +// suppress unused package warning +var ( + _ *json.RawMessage + _ *jlexer.Lexer + _ *jwriter.Writer + _ easyjson.Marshaler +) + +func easyjson6850ac6fDecodeGoOsspkgComGoppyV3ExampleWebServerGenTransport(in *jlexer.Lexer, out *errResponse) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + switch key { + case "message": + if in.IsNull() { + in.Skip() + } else { + out.Message = string(in.String()) + } + case "code": + if in.IsNull() { + in.Skip() + } else { + out.Code = int64(in.Int64()) + } + case "ctx": + if in.IsNull() { + in.Skip() + } else { + in.Delim('{') + if !in.IsDelim('}') { + out.Ctx = make(map[string]string) + } else { + out.Ctx = nil + } + for !in.IsDelim('}') { + key := string(in.String()) + in.WantColon() + var v1 string + if in.IsNull() { + in.Skip() + } else { + v1 = string(in.String()) + } + (out.Ctx)[key] = v1 + in.WantComma() + } + in.Delim('}') + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson6850ac6fEncodeGoOsspkgComGoppyV3ExampleWebServerGenTransport(out *jwriter.Writer, in errResponse) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"message\":" + out.RawString(prefix[1:]) + out.String(string(in.Message)) + } + { + const prefix string = ",\"code\":" + out.RawString(prefix) + out.Int64(int64(in.Code)) + } + if len(in.Ctx) != 0 { + const prefix string = ",\"ctx\":" + out.RawString(prefix) + { + out.RawByte('{') + v2First := true + for v2Name, v2Value := range in.Ctx { + if v2First { + v2First = false + } else { + out.RawByte(',') + } + out.String(string(v2Name)) + out.RawByte(':') + out.String(string(v2Value)) + } + out.RawByte('}') + } + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v errResponse) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson6850ac6fEncodeGoOsspkgComGoppyV3ExampleWebServerGenTransport(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v errResponse) MarshalEasyJSON(w *jwriter.Writer) { + easyjson6850ac6fEncodeGoOsspkgComGoppyV3ExampleWebServerGenTransport(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *errResponse) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson6850ac6fDecodeGoOsspkgComGoppyV3ExampleWebServerGenTransport(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *errResponse) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson6850ac6fDecodeGoOsspkgComGoppyV3ExampleWebServerGenTransport(l, v) +} +func easyjson6850ac6fDecodeGoOsspkgComGoppyV3ExampleWebServerGenTransport1(in *jlexer.Lexer, out *bulkResponse) { + isTopLevel := in.IsStart() + if in.IsNull() { + in.Skip() + *out = nil + } else { + in.Delim('[') + if *out == nil { + if !in.IsDelim(']') { + *out = make(bulkResponse, 0, 1) + } else { + *out = bulkResponse{} + } + } else { + *out = (*out)[:0] + } + for !in.IsDelim(']') { + var v3 baseResponse + if in.IsNull() { + in.Skip() + } else { + (v3).UnmarshalEasyJSON(in) + } + *out = append(*out, v3) + in.WantComma() + } + in.Delim(']') + } + if isTopLevel { + in.Consumed() + } +} +func easyjson6850ac6fEncodeGoOsspkgComGoppyV3ExampleWebServerGenTransport1(out *jwriter.Writer, in bulkResponse) { + if in == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { + out.RawString("null") + } else { + out.RawByte('[') + for v4, v5 := range in { + if v4 > 0 { + out.RawByte(',') + } + (v5).MarshalEasyJSON(out) + } + out.RawByte(']') + } +} + +// MarshalJSON supports json.Marshaler interface +func (v bulkResponse) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson6850ac6fEncodeGoOsspkgComGoppyV3ExampleWebServerGenTransport1(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v bulkResponse) MarshalEasyJSON(w *jwriter.Writer) { + easyjson6850ac6fEncodeGoOsspkgComGoppyV3ExampleWebServerGenTransport1(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *bulkResponse) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson6850ac6fDecodeGoOsspkgComGoppyV3ExampleWebServerGenTransport1(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *bulkResponse) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson6850ac6fDecodeGoOsspkgComGoppyV3ExampleWebServerGenTransport1(l, v) +} +func easyjson6850ac6fDecodeGoOsspkgComGoppyV3ExampleWebServerGenTransport2(in *jlexer.Lexer, out *bulkRequest) { + isTopLevel := in.IsStart() + if in.IsNull() { + in.Skip() + *out = nil + } else { + in.Delim('[') + if *out == nil { + if !in.IsDelim(']') { + *out = make(bulkRequest, 0, 1) + } else { + *out = bulkRequest{} + } + } else { + *out = (*out)[:0] + } + for !in.IsDelim(']') { + var v6 baseRequest + if in.IsNull() { + in.Skip() + } else { + (v6).UnmarshalEasyJSON(in) + } + *out = append(*out, v6) + in.WantComma() + } + in.Delim(']') + } + if isTopLevel { + in.Consumed() + } +} +func easyjson6850ac6fEncodeGoOsspkgComGoppyV3ExampleWebServerGenTransport2(out *jwriter.Writer, in bulkRequest) { + if in == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { + out.RawString("null") + } else { + out.RawByte('[') + for v7, v8 := range in { + if v7 > 0 { + out.RawByte(',') + } + (v8).MarshalEasyJSON(out) + } + out.RawByte(']') + } +} + +// MarshalJSON supports json.Marshaler interface +func (v bulkRequest) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson6850ac6fEncodeGoOsspkgComGoppyV3ExampleWebServerGenTransport2(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v bulkRequest) MarshalEasyJSON(w *jwriter.Writer) { + easyjson6850ac6fEncodeGoOsspkgComGoppyV3ExampleWebServerGenTransport2(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *bulkRequest) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson6850ac6fDecodeGoOsspkgComGoppyV3ExampleWebServerGenTransport2(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *bulkRequest) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson6850ac6fDecodeGoOsspkgComGoppyV3ExampleWebServerGenTransport2(l, v) +} +func easyjson6850ac6fDecodeGoOsspkgComGoppyV3ExampleWebServerGenTransport3(in *jlexer.Lexer, out *baseResponse) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + switch key { + case "id": + if in.IsNull() { + in.Skip() + } else { + out.Id = string(in.String()) + } + case "result": + if m, ok := out.Result.(easyjson.Unmarshaler); ok { + m.UnmarshalEasyJSON(in) + } else if m, ok := out.Result.(json.Unmarshaler); ok { + _ = m.UnmarshalJSON(in.Raw()) + } else { + out.Result = in.Interface() + } + case "error": + if in.IsNull() { + in.Skip() + out.Error = nil + } else { + if out.Error == nil { + out.Error = new(errResponse) + } + if in.IsNull() { + in.Skip() + } else { + (*out.Error).UnmarshalEasyJSON(in) + } + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson6850ac6fEncodeGoOsspkgComGoppyV3ExampleWebServerGenTransport3(out *jwriter.Writer, in baseResponse) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"id\":" + out.RawString(prefix[1:]) + out.String(string(in.Id)) + } + if in.Result != nil { + const prefix string = ",\"result\":" + out.RawString(prefix) + if m, ok := in.Result.(easyjson.Marshaler); ok { + m.MarshalEasyJSON(out) + } else if m, ok := in.Result.(json.Marshaler); ok { + out.Raw(m.MarshalJSON()) + } else { + out.Raw(json.Marshal(in.Result)) + } + } + if in.Error != nil { + const prefix string = ",\"error\":" + out.RawString(prefix) + (*in.Error).MarshalEasyJSON(out) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v baseResponse) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson6850ac6fEncodeGoOsspkgComGoppyV3ExampleWebServerGenTransport3(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v baseResponse) MarshalEasyJSON(w *jwriter.Writer) { + easyjson6850ac6fEncodeGoOsspkgComGoppyV3ExampleWebServerGenTransport3(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *baseResponse) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson6850ac6fDecodeGoOsspkgComGoppyV3ExampleWebServerGenTransport3(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *baseResponse) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson6850ac6fDecodeGoOsspkgComGoppyV3ExampleWebServerGenTransport3(l, v) +} +func easyjson6850ac6fDecodeGoOsspkgComGoppyV3ExampleWebServerGenTransport4(in *jlexer.Lexer, out *baseRequest) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + switch key { + case "id": + if in.IsNull() { + in.Skip() + } else { + out.Id = string(in.String()) + } + case "method": + if in.IsNull() { + in.Skip() + } else { + out.Method = string(in.String()) + } + case "params": + if in.IsNull() { + in.Skip() + } else { + if data := in.Raw(); in.Ok() { + in.AddError((out.Params).UnmarshalJSON(data)) + } + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson6850ac6fEncodeGoOsspkgComGoppyV3ExampleWebServerGenTransport4(out *jwriter.Writer, in baseRequest) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"id\":" + out.RawString(prefix[1:]) + out.String(string(in.Id)) + } + { + const prefix string = ",\"method\":" + out.RawString(prefix) + out.String(string(in.Method)) + } + { + const prefix string = ",\"params\":" + out.RawString(prefix) + out.Raw((in.Params).MarshalJSON()) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v baseRequest) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson6850ac6fEncodeGoOsspkgComGoppyV3ExampleWebServerGenTransport4(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v baseRequest) MarshalEasyJSON(w *jwriter.Writer) { + easyjson6850ac6fEncodeGoOsspkgComGoppyV3ExampleWebServerGenTransport4(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *baseRequest) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson6850ac6fDecodeGoOsspkgComGoppyV3ExampleWebServerGenTransport4(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *baseRequest) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson6850ac6fDecodeGoOsspkgComGoppyV3ExampleWebServerGenTransport4(l, v) +} diff --git a/_example/web-server-gen/transport/jsonrpc_model_easyjson.go b/_example/web-server-gen/transport/jsonrpc_model_easyjson.go new file mode 100644 index 0000000..ab2005a --- /dev/null +++ b/_example/web-server-gen/transport/jsonrpc_model_easyjson.go @@ -0,0 +1,740 @@ +// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT. + +package transport + +import ( + json "encoding/json" + + easyjson "github.com/mailru/easyjson" + jlexer "github.com/mailru/easyjson/jlexer" + jwriter "github.com/mailru/easyjson/jwriter" + + types "go.osspkg.com/goppy/v3/_example/web-server-gen/types" +) + +// suppress unused package warning +var ( + _ *json.RawMessage + _ *jlexer.Lexer + _ *jwriter.Writer + _ easyjson.Marshaler +) + +func easyjsonE35df14fDecodeGoOsspkgComGoppyV3ExampleWebServerGenTransport(in *jlexer.Lexer, out *jsonrpcUserNameModelResponse) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + switch key { + case "name": + if in.IsNull() { + in.Skip() + } else { + out.Name = string(in.String()) + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjsonE35df14fEncodeGoOsspkgComGoppyV3ExampleWebServerGenTransport(out *jwriter.Writer, in jsonrpcUserNameModelResponse) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"name\":" + out.RawString(prefix[1:]) + out.String(string(in.Name)) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v jsonrpcUserNameModelResponse) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjsonE35df14fEncodeGoOsspkgComGoppyV3ExampleWebServerGenTransport(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v jsonrpcUserNameModelResponse) MarshalEasyJSON(w *jwriter.Writer) { + easyjsonE35df14fEncodeGoOsspkgComGoppyV3ExampleWebServerGenTransport(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *jsonrpcUserNameModelResponse) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjsonE35df14fDecodeGoOsspkgComGoppyV3ExampleWebServerGenTransport(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *jsonrpcUserNameModelResponse) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjsonE35df14fDecodeGoOsspkgComGoppyV3ExampleWebServerGenTransport(l, v) +} +func easyjsonE35df14fDecodeGoOsspkgComGoppyV3ExampleWebServerGenTransport1(in *jlexer.Lexer, out *jsonrpcUserNameModelRequest) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + switch key { + case "userID": + if in.IsNull() { + in.Skip() + } else { + out.UserID = int64(in.Int64()) + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjsonE35df14fEncodeGoOsspkgComGoppyV3ExampleWebServerGenTransport1(out *jwriter.Writer, in jsonrpcUserNameModelRequest) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"userID\":" + out.RawString(prefix[1:]) + out.Int64(int64(in.UserID)) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v jsonrpcUserNameModelRequest) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjsonE35df14fEncodeGoOsspkgComGoppyV3ExampleWebServerGenTransport1(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v jsonrpcUserNameModelRequest) MarshalEasyJSON(w *jwriter.Writer) { + easyjsonE35df14fEncodeGoOsspkgComGoppyV3ExampleWebServerGenTransport1(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *jsonrpcUserNameModelRequest) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjsonE35df14fDecodeGoOsspkgComGoppyV3ExampleWebServerGenTransport1(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *jsonrpcUserNameModelRequest) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjsonE35df14fDecodeGoOsspkgComGoppyV3ExampleWebServerGenTransport1(l, v) +} +func easyjsonE35df14fDecodeGoOsspkgComGoppyV3ExampleWebServerGenTransport2(in *jlexer.Lexer, out *jsonrpcPostListModelResponse) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + switch key { + case "text": + if in.IsNull() { + in.Skip() + out.Text = nil + } else { + in.Delim('[') + if out.Text == nil { + if !in.IsDelim(']') { + out.Text = make([]types.Text, 0, 2) + } else { + out.Text = []types.Text{} + } + } else { + out.Text = (out.Text)[:0] + } + for !in.IsDelim(']') { + var v1 types.Text + easyjsonE35df14fDecodeGoOsspkgComGoppyV3ExampleWebServerGenTypes(in, &v1) + out.Text = append(out.Text, v1) + in.WantComma() + } + in.Delim(']') + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjsonE35df14fEncodeGoOsspkgComGoppyV3ExampleWebServerGenTransport2(out *jwriter.Writer, in jsonrpcPostListModelResponse) { + out.RawByte('{') + first := true + _ = first + if len(in.Text) != 0 { + const prefix string = ",\"text\":" + first = false + out.RawString(prefix[1:]) + { + out.RawByte('[') + for v2, v3 := range in.Text { + if v2 > 0 { + out.RawByte(',') + } + easyjsonE35df14fEncodeGoOsspkgComGoppyV3ExampleWebServerGenTypes(out, v3) + } + out.RawByte(']') + } + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v jsonrpcPostListModelResponse) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjsonE35df14fEncodeGoOsspkgComGoppyV3ExampleWebServerGenTransport2(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v jsonrpcPostListModelResponse) MarshalEasyJSON(w *jwriter.Writer) { + easyjsonE35df14fEncodeGoOsspkgComGoppyV3ExampleWebServerGenTransport2(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *jsonrpcPostListModelResponse) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjsonE35df14fDecodeGoOsspkgComGoppyV3ExampleWebServerGenTransport2(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *jsonrpcPostListModelResponse) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjsonE35df14fDecodeGoOsspkgComGoppyV3ExampleWebServerGenTransport2(l, v) +} +func easyjsonE35df14fDecodeGoOsspkgComGoppyV3ExampleWebServerGenTypes(in *jlexer.Lexer, out *types.Text) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + switch key { + case "ID": + if in.IsNull() { + in.Skip() + } else { + out.ID = int64(in.Int64()) + } + case "Text": + if in.IsNull() { + in.Skip() + } else { + out.Text = string(in.String()) + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjsonE35df14fEncodeGoOsspkgComGoppyV3ExampleWebServerGenTypes(out *jwriter.Writer, in types.Text) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"ID\":" + out.RawString(prefix[1:]) + out.Int64(int64(in.ID)) + } + { + const prefix string = ",\"Text\":" + out.RawString(prefix) + out.String(string(in.Text)) + } + out.RawByte('}') +} +func easyjsonE35df14fDecodeGoOsspkgComGoppyV3ExampleWebServerGenTransport3(in *jlexer.Lexer, out *jsonrpcPostListModelRequest) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + switch key { + case "userID": + if in.IsNull() { + in.Skip() + } else { + out.UserID = int64(in.Int64()) + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjsonE35df14fEncodeGoOsspkgComGoppyV3ExampleWebServerGenTransport3(out *jwriter.Writer, in jsonrpcPostListModelRequest) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"userID\":" + out.RawString(prefix[1:]) + out.Int64(int64(in.UserID)) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v jsonrpcPostListModelRequest) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjsonE35df14fEncodeGoOsspkgComGoppyV3ExampleWebServerGenTransport3(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v jsonrpcPostListModelRequest) MarshalEasyJSON(w *jwriter.Writer) { + easyjsonE35df14fEncodeGoOsspkgComGoppyV3ExampleWebServerGenTransport3(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *jsonrpcPostListModelRequest) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjsonE35df14fDecodeGoOsspkgComGoppyV3ExampleWebServerGenTransport3(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *jsonrpcPostListModelRequest) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjsonE35df14fDecodeGoOsspkgComGoppyV3ExampleWebServerGenTransport3(l, v) +} +func easyjsonE35df14fDecodeGoOsspkgComGoppyV3ExampleWebServerGenTransport4(in *jlexer.Lexer, out *jsonrpcPostByIDModelResponse) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + switch key { + case "text": + if in.IsNull() { + in.Skip() + } else { + out.Text = bool(in.Bool()) + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjsonE35df14fEncodeGoOsspkgComGoppyV3ExampleWebServerGenTransport4(out *jwriter.Writer, in jsonrpcPostByIDModelResponse) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"text\":" + out.RawString(prefix[1:]) + out.Bool(bool(in.Text)) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v jsonrpcPostByIDModelResponse) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjsonE35df14fEncodeGoOsspkgComGoppyV3ExampleWebServerGenTransport4(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v jsonrpcPostByIDModelResponse) MarshalEasyJSON(w *jwriter.Writer) { + easyjsonE35df14fEncodeGoOsspkgComGoppyV3ExampleWebServerGenTransport4(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *jsonrpcPostByIDModelResponse) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjsonE35df14fDecodeGoOsspkgComGoppyV3ExampleWebServerGenTransport4(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *jsonrpcPostByIDModelResponse) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjsonE35df14fDecodeGoOsspkgComGoppyV3ExampleWebServerGenTransport4(l, v) +} +func easyjsonE35df14fDecodeGoOsspkgComGoppyV3ExampleWebServerGenTransport5(in *jlexer.Lexer, out *jsonrpcPostByIDModelRequest) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + switch key { + case "ID": + if in.IsNull() { + in.Skip() + } else { + out.ID = int64(in.Int64()) + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjsonE35df14fEncodeGoOsspkgComGoppyV3ExampleWebServerGenTransport5(out *jwriter.Writer, in jsonrpcPostByIDModelRequest) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"ID\":" + out.RawString(prefix[1:]) + out.Int64(int64(in.ID)) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v jsonrpcPostByIDModelRequest) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjsonE35df14fEncodeGoOsspkgComGoppyV3ExampleWebServerGenTransport5(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v jsonrpcPostByIDModelRequest) MarshalEasyJSON(w *jwriter.Writer) { + easyjsonE35df14fEncodeGoOsspkgComGoppyV3ExampleWebServerGenTransport5(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *jsonrpcPostByIDModelRequest) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjsonE35df14fDecodeGoOsspkgComGoppyV3ExampleWebServerGenTransport5(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *jsonrpcPostByIDModelRequest) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjsonE35df14fDecodeGoOsspkgComGoppyV3ExampleWebServerGenTransport5(l, v) +} +func easyjsonE35df14fDecodeGoOsspkgComGoppyV3ExampleWebServerGenTransport6(in *jlexer.Lexer, out *jsonrpcApiRootModelResponse) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + switch key { + case "status": + if in.IsNull() { + in.Skip() + } else { + out.Status = bool(in.Bool()) + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjsonE35df14fEncodeGoOsspkgComGoppyV3ExampleWebServerGenTransport6(out *jwriter.Writer, in jsonrpcApiRootModelResponse) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"status\":" + out.RawString(prefix[1:]) + out.Bool(bool(in.Status)) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v jsonrpcApiRootModelResponse) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjsonE35df14fEncodeGoOsspkgComGoppyV3ExampleWebServerGenTransport6(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v jsonrpcApiRootModelResponse) MarshalEasyJSON(w *jwriter.Writer) { + easyjsonE35df14fEncodeGoOsspkgComGoppyV3ExampleWebServerGenTransport6(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *jsonrpcApiRootModelResponse) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjsonE35df14fDecodeGoOsspkgComGoppyV3ExampleWebServerGenTransport6(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *jsonrpcApiRootModelResponse) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjsonE35df14fDecodeGoOsspkgComGoppyV3ExampleWebServerGenTransport6(l, v) +} +func easyjsonE35df14fDecodeGoOsspkgComGoppyV3ExampleWebServerGenTransport7(in *jlexer.Lexer, out *jsonrpcApiRootModelRequest) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + switch key { + case "userName": + if in.IsNull() { + in.Skip() + } else { + out.UserName = string(in.String()) + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjsonE35df14fEncodeGoOsspkgComGoppyV3ExampleWebServerGenTransport7(out *jwriter.Writer, in jsonrpcApiRootModelRequest) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"userName\":" + out.RawString(prefix[1:]) + out.String(string(in.UserName)) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v jsonrpcApiRootModelRequest) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjsonE35df14fEncodeGoOsspkgComGoppyV3ExampleWebServerGenTransport7(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v jsonrpcApiRootModelRequest) MarshalEasyJSON(w *jwriter.Writer) { + easyjsonE35df14fEncodeGoOsspkgComGoppyV3ExampleWebServerGenTransport7(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *jsonrpcApiRootModelRequest) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjsonE35df14fDecodeGoOsspkgComGoppyV3ExampleWebServerGenTransport7(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *jsonrpcApiRootModelRequest) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjsonE35df14fDecodeGoOsspkgComGoppyV3ExampleWebServerGenTransport7(l, v) +} +func easyjsonE35df14fDecodeGoOsspkgComGoppyV3ExampleWebServerGenTransport8(in *jlexer.Lexer, out *jsonrpcApiAuthModelResponse) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + switch key { + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjsonE35df14fEncodeGoOsspkgComGoppyV3ExampleWebServerGenTransport8(out *jwriter.Writer, in jsonrpcApiAuthModelResponse) { + out.RawByte('{') + first := true + _ = first + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v jsonrpcApiAuthModelResponse) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjsonE35df14fEncodeGoOsspkgComGoppyV3ExampleWebServerGenTransport8(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v jsonrpcApiAuthModelResponse) MarshalEasyJSON(w *jwriter.Writer) { + easyjsonE35df14fEncodeGoOsspkgComGoppyV3ExampleWebServerGenTransport8(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *jsonrpcApiAuthModelResponse) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjsonE35df14fDecodeGoOsspkgComGoppyV3ExampleWebServerGenTransport8(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *jsonrpcApiAuthModelResponse) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjsonE35df14fDecodeGoOsspkgComGoppyV3ExampleWebServerGenTransport8(l, v) +} +func easyjsonE35df14fDecodeGoOsspkgComGoppyV3ExampleWebServerGenTransport9(in *jlexer.Lexer, out *jsonrpcApiAuthModelRequest) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + switch key { + case "userName": + if in.IsNull() { + in.Skip() + } else { + out.UserName = string(in.String()) + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjsonE35df14fEncodeGoOsspkgComGoppyV3ExampleWebServerGenTransport9(out *jwriter.Writer, in jsonrpcApiAuthModelRequest) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"userName\":" + out.RawString(prefix[1:]) + out.String(string(in.UserName)) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v jsonrpcApiAuthModelRequest) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjsonE35df14fEncodeGoOsspkgComGoppyV3ExampleWebServerGenTransport9(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v jsonrpcApiAuthModelRequest) MarshalEasyJSON(w *jwriter.Writer) { + easyjsonE35df14fEncodeGoOsspkgComGoppyV3ExampleWebServerGenTransport9(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *jsonrpcApiAuthModelRequest) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjsonE35df14fDecodeGoOsspkgComGoppyV3ExampleWebServerGenTransport9(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *jsonrpcApiAuthModelRequest) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjsonE35df14fDecodeGoOsspkgComGoppyV3ExampleWebServerGenTransport9(l, v) +} diff --git a/_example/web-server-gen/transport/jsonrpc_transport.go b/_example/web-server-gen/transport/jsonrpc_transport.go new file mode 100644 index 0000000..70c449c --- /dev/null +++ b/_example/web-server-gen/transport/jsonrpc_transport.go @@ -0,0 +1,185 @@ +/* + * Copyright (c) 2022-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +// Code generated by goppy-cli wsg. DO NOT EDIT. +package transport + +import ( + context "context" + errors "errors" + + web "go.osspkg.com/goppy/v3/web" + + time "time" + + strings "strings" + + syncing "go.osspkg.com/syncing" + + pool "go.osspkg.com/ioutils/pool" + + logx "go.osspkg.com/logx" + + types "go.osspkg.com/goppy/v3/_example/web-server-gen/types" +) + +var ( + poolBaseResponse = pool.New[*syncing.Slice[baseResponse]](func() *syncing.Slice[baseResponse] { + return syncing.NewSlice[baseResponse](uint(2)) + }) + + errUnsupportedMethod = errors.New("unsupported method") +) + +type JSONRPCHandler struct { + routes web.ServerPool + handleApi types.Api + handleUser types.User + handlePost types.Post +} + +func NewJSONRPCHandler(routes web.ServerPool, handleApi types.Api, handleUser types.User, handlePost types.Post) *JSONRPCHandler { + return &JSONRPCHandler{ + routes: routes, + handleApi: handleApi, + handleUser: handleUser, + handlePost: handlePost, + } +} + +func (v *JSONRPCHandler) Down() error { + return nil +} + +func (v *JSONRPCHandler) Up(ctx context.Context) error { + v.routes.All(func(tag string, r web.Router) { + switch tag { + case "main", "admin": + r.Post("/rpc", func(webCtx web.Ctx) { + var req bulkRequest + if err := webCtx.BindJSON(&req); err != nil { + webCtx.String(400, err.Error()) + return + } + + res := poolBaseResponse.Get() + defer poolBaseResponse.Put(res) + + wg := syncing.NewGroup(webCtx.Context()) + wg.OnPanic(func(err error) { + logx.Error("panic", "err", err) + }) + + for _, item := range req { + item := item + switch strings.ToLower(item.Method) { + case "api.root": + wg.Background("api.root", func(ctx context.Context) { + out := baseResponse{ + Id: item.Id, + } + var err error + var result any + ctx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + result, err = v.callApiRoot(ctx, webCtx, item.Params) + if err != nil { + out.Error = toJSONRPCError(err) + } else { + out.Result = result + } + res.Append(out) + }) + + case "api.auth": + wg.Background("api.auth", func(ctx context.Context) { + out := baseResponse{ + Id: item.Id, + } + var err error + var result any + ctx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + result, err = v.callApiAuth(ctx, webCtx, item.Params) + if err != nil { + out.Error = toJSONRPCError(err) + } else { + out.Result = result + } + res.Append(out) + }) + + case "user.name": + wg.Background("user.name", func(ctx context.Context) { + out := baseResponse{ + Id: item.Id, + } + var err error + var result any + ctx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + result, err = v.callUserName(ctx, webCtx, item.Params) + if err != nil { + out.Error = toJSONRPCError(err) + } else { + out.Result = result + } + res.Append(out) + }) + + case "post.byid": + wg.Background("post.byid", func(ctx context.Context) { + out := baseResponse{ + Id: item.Id, + } + var err error + var result any + ctx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + result, err = v.callPostByID(ctx, webCtx, item.Params) + if err != nil { + out.Error = toJSONRPCError(err) + } else { + out.Result = result + } + res.Append(out) + }) + + case "post.list": + wg.Background("post.list", func(ctx context.Context) { + out := baseResponse{ + Id: item.Id, + } + var err error + var result any + ctx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + result, err = v.callPostList(ctx, webCtx, item.Params) + if err != nil { + out.Error = toJSONRPCError(err) + } else { + out.Result = result + } + res.Append(out) + }) + + default: + out := baseResponse{ + Id: item.Id, + } + err := errUnsupportedMethod + out.Error = toJSONRPCError(err) + res.Append(out) + + } + } + wg.Wait() + webCtx.JSON(200, bulkResponse(res.Extract())) + }) + + } + }) + return nil +} diff --git a/_example/web-server-gen/types/custom.go b/_example/web-server-gen/types/custom.go new file mode 100644 index 0000000..6db6310 --- /dev/null +++ b/_example/web-server-gen/types/custom.go @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2022-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package types + +import ( + "context" + "fmt" +) + +func StdOut(ctx context.Context, arg []Text) error { + fmt.Println(arg) + return nil +} diff --git a/_example/web-server-gen/types/interfaces1.go b/_example/web-server-gen/types/interfaces1.go new file mode 100644 index 0000000..0e35170 --- /dev/null +++ b/_example/web-server-gen/types/interfaces1.go @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2022-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package types + +import "context" + +type Api interface { + // Root + // @wsg in.userID=cookie:x-user-id + Root( + ctx context.Context, + userID int64, + userName string, + ) (status bool, err error) + + // Auth + // @wsg in.userID=header:x-user-id + // @wsg out.status=header:x-user-id,cookie:uid + // @wsg out.status=cookie:uid + Auth( + ctx context.Context, + userID int64, + userName string, + ) (status bool, err error) +} + +type User interface { + Name( + ctx context.Context, + userID int64, + ) (name string, err error) +} diff --git a/_example/web-server-gen/types/interfaces2.go b/_example/web-server-gen/types/interfaces2.go new file mode 100644 index 0000000..90aafcb --- /dev/null +++ b/_example/web-server-gen/types/interfaces2.go @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2022-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package types + +import "context" + +type Post interface { + ByID( + ctx context.Context, + ID int64, + ) (text bool, err error) + + List( + ctx context.Context, + userID int64, + ) (text []Text, err error) +} + +type Text struct { + ID int64 + Text string +} diff --git a/_example/web-server-gen/types/wsg.go b/_example/web-server-gen/types/wsg.go new file mode 100644 index 0000000..96ca6c8 --- /dev/null +++ b/_example/web-server-gen/types/wsg.go @@ -0,0 +1,8 @@ +/* + * Copyright (c) 2022-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package types + +//go:generate goppy wsg --mod=json-rpc,rest --pool=main,admin --out=./../transport diff --git a/apigen/builder/builder.go b/apigen/builder/builder.go new file mode 100644 index 0000000..e5fd289 --- /dev/null +++ b/apigen/builder/builder.go @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2022-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package builder + +import ( + "fmt" + "os" + "path/filepath" + "slices" + "strings" + + "go.osspkg.com/bb" + "go.osspkg.com/gogen/golang" + "go.osspkg.com/gogen/types" + + at "go.osspkg.com/goppy/v3/apigen/types" + "go.osspkg.com/goppy/v3/console" +) + +type Builder struct { + Out string + Mods []string + Pool []string + IFace map[string]struct{} + Files []at.File +} + +func (b *Builder) filterObjects(objs []at.Face) []at.Face { + if len(b.IFace) == 0 { + return objs + } + + result := make([]at.Face, 0, len(objs)) + for _, obj := range objs { + if _, ok := b.IFace[strings.ToLower(obj.Name)]; ok { + result = append(result, obj) + } + } + + return result +} + +func (b *Builder) getWorkFiles() []at.File { + workFiles := make([]at.File, 0, len(b.Files)) + + for _, file := range b.Files { + file.Faces = b.filterObjects(file.Faces) + if len(file.Faces) > 0 { + workFiles = append(workFiles, file) + } + } + + slices.SortFunc(workFiles, func(a, b at.File) int { + return strings.Compare(a.FilePath, b.FilePath) + }) + + return workFiles +} + +func (b *Builder) WriteFile(fileName string, tok types.Token) error { + buf := bb.New(1024) + if err := golang.Render(buf, tok); err != nil { + return err + } + + fullPath := b.Out + "/" + fileName + dir := filepath.Dir(fullPath) + if err := os.MkdirAll(dir, 0766); err != nil { + return fmt.Errorf("mkdir %q: %w", dir, err) + } + + console.Debugf("Writing file %s", fullPath) + return os.WriteFile(fullPath, buf.Bytes(), 0666) +} + +func (b *Builder) Build() error { + //golang.SetRawMode() + + pkgName := filepath.Base(b.Out) + files := b.getWorkFiles() + + for _, name := range b.Mods { + mod, ok := at.Resolve[at.GlobalModule](name) + if !ok { + continue + } + + err := mod.Build(b, at.GlobalMeta{PkgName: pkgName, Pool: b.Pool}, files) + if err != nil { + return fmt.Errorf("build module %q: %w", name, err) + } + } + + return nil +} diff --git a/apigen/builder/common.go b/apigen/builder/common.go new file mode 100644 index 0000000..906a25b --- /dev/null +++ b/apigen/builder/common.go @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2022-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package builder + +import ( + modjsonrpc "go.osspkg.com/goppy/v3/apigen/module/mod-json-rpc" + modparamcookie "go.osspkg.com/goppy/v3/apigen/module/mod-param-cookie" + modparamheader "go.osspkg.com/goppy/v3/apigen/module/mod-param-header" + "go.osspkg.com/goppy/v3/apigen/types" +) + +func init() { + types.Register[types.GlobalModule](modjsonrpc.Module{FilePrefix: "jsonrpc"}) + types.Register[types.ParamModule](modparamcookie.Module{}) + types.Register[types.ParamModule](modparamheader.Module{}) +} + +const ( + TagModule = "module" +) diff --git a/apigen/module/mod-json-rpc/build_base_model.go b/apigen/module/mod-json-rpc/build_base_model.go new file mode 100644 index 0000000..e88d5c3 --- /dev/null +++ b/apigen/module/mod-json-rpc/build_base_model.go @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2022-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package mod_json_rpc + +import ( + "fmt" + + . "go.osspkg.com/gogen/golang" //nolint:staticcheck + + at "go.osspkg.com/goppy/v3/apigen/types" + "go.osspkg.com/goppy/v3/apigen/util" +) + +func (v Module) buildBaseRPCModel(w at.Writer, m at.GlobalMeta) error { + baseReq := Comment(jsonGenComment). + Type().ID(modelBaseReq).Struct(). + Block( + ID(util.ToUpperCamelCase(fieldID)).String(). + Raw(fmt.Sprintf("`json:\"%s\"`", fieldID)), + ID(util.ToUpperCamelCase(fieldMethod)).String(). + Raw(fmt.Sprintf("`json:\"%s\"`", fieldMethod)), + ID(util.ToUpperCamelCase(fieldParams)).Pkg("stdjson").ID("RawMessage"). + Raw(fmt.Sprintf("`json:\"%s\"`", fieldParams)), + ) + + bulkBaseReq := Comment(jsonGenComment). + Type().ID(modelBulkBaseReq).Slice().ID(modelBaseReq) + + errRes := Comment(jsonGenComment). + Type().ID(modelBaseErr).Struct(). + Block( + ID(util.ToUpperCamelCase(fieldMessage)).String(). + Raw(fmt.Sprintf("`json:\"%s\"`", fieldMessage)), + ID(util.ToUpperCamelCase(fieldCode)).Int64(). + Raw(fmt.Sprintf("`json:\"%s\"`", fieldCode)), + ID(util.ToUpperCamelCase(fieldCtx)).Map(String(), String()). + Raw(fmt.Sprintf("`json:\"%s,omitempty\"`", fieldCtx)), + ) + + errType := Type().ID(errInterface).Interface(). + Block( + ID("GetCode").Bracket().Int64(), + ID("GetMessage").Bracket().String(), + ID("GetContext").Bracket().Map(String(), String()), + ) + + baseRes := Comment(jsonGenComment). + Type().ID(modelBaseRes).Struct(). + Block( + ID(util.ToUpperCamelCase(fieldID)).String(). + Raw(fmt.Sprintf("`json:\"%s\"`", fieldID)), + ID(util.ToUpperCamelCase(fieldResult)).Any(). + Raw(fmt.Sprintf("`json:\"%s,omitempty\"`", fieldResult)), + ID(util.ToUpperCamelCase(fieldError)).Op("*").ID("errResponse"). + Raw(fmt.Sprintf("`json:\"%s,omitempty\"`", fieldError)), + ) + + bulkBaseRes := Comment(jsonGenComment). + Type().ID(modelBulkBaseRes).Slice().ID(modelBaseRes) + + toErr := Func().ID("toJSONRPCError"). + Bracket(ID("e").Error()). + Bracket(Op("*").ID(modelBaseErr)).Block( + If().ID("e").Op("==").Nil().Block(Return().Nil()), + ID("err").Op(":=").Op("&").ID(modelBaseErr).Block(), + List(ID("te"), ID("ok")).Op(":=").ID("e").Op(".").Bracket(ID(errInterface)), + If().ID("ok").Block( + ID("err").Op(".").ID("Code").Op("="). + ID("te").Op(".").ID("GetCode").Bracket(), + ID("err").Op(".").ID("Message").Op("="). + ID("te").Op(".").ID("GetMessage").Bracket(), + ID("err").Op(".").ID("Ctx").Op("="). + ID("te").Op(".").ID("GetContext").Bracket(), + ).Else().Block( + ID("err").Op(".").ID("Message").Op("="). + ID("e").Op(".").ID("Error").Bracket(), + ), + Return().ID("err"), + ) + + t := Comment("Code generated by goppy-cli wsg. DO NOT EDIT."). + Package(m.PkgName). + Comment("go:generate easyjson"). + Import("stdjson", "encoding/json"). + Join(baseReq, bulkBaseReq, baseRes, bulkBaseRes, errRes, errType, toErr) + + return w.WriteFile(v.FilePrefix+"_model_base.go", t) +} diff --git a/apigen/module/mod-json-rpc/build_transport.go b/apigen/module/mod-json-rpc/build_transport.go new file mode 100644 index 0000000..42de296 --- /dev/null +++ b/apigen/module/mod-json-rpc/build_transport.go @@ -0,0 +1,224 @@ +/* + * Copyright (c) 2022-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package mod_json_rpc + +import ( + "strings" + + . "go.osspkg.com/gogen/golang" //nolint:staticcheck + "go.osspkg.com/gogen/types" + "go.osspkg.com/syncing" + + at "go.osspkg.com/goppy/v3/apigen/types" + "go.osspkg.com/goppy/v3/apigen/util" +) + +func (v Module) buildTransport(w at.Writer, m at.GlobalMeta, files []at.File) error { + + t := Comment("Code generated by goppy-cli wsg. DO NOT EDIT."). + Package(m.PkgName) + + list := syncing.NewMap[string, string](10) + list.Set("context", "context") + list.Set("strings", "strings") + list.Set("errors", "errors") + //list.Set("stdjson", "encoding/json") + list.Set("syncing", "go.osspkg.com/syncing") + list.Set("pool", "go.osspkg.com/ioutils/pool") + list.Set("web", "go.osspkg.com/goppy/v3/web") + list.Set("logx", "go.osspkg.com/logx") + list.Set("time", "time") + + for _, file := range files { + for _, object := range file.Faces { + list.Set(object.Alias, object.Pkg) + for alias, link := range file.Imports.Yield() { + list.Set(alias, link) + } + } + } + + for alias, link := range list.Yield() { + t.Import(alias, link) + } + + fields := []types.Token{ + ID("routes").Pkg("web").ID("ServerPool"), + } + fieldsInit := []types.Token{ + ID("routes").Op(":").ID("routes").Op(","), + } + for _, file := range files { + for _, object := range file.Faces { + fields = append(fields, + ID("handle"+object.Name).Pkg(object.Alias).ID(object.Name), + ) + fieldsInit = append(fieldsInit, + ID("handle"+object.Name).Op(":").ID("handle"+object.Name).Op(","), + ) + } + } + + t = t.Join( + Raw(` +var ( + poolBaseResponse = pool.New[*syncing.Slice[baseResponse]](func() *syncing.Slice[baseResponse] { + return syncing.NewSlice[baseResponse](uint(2)) + }) + + errUnsupportedMethod = errors.New("unsupported method") +) +`), + Type().ID(transportName).Struct().Block(fields...), + Line(), + //-- New + Func().ID("New"+transportName).Bracket(fields...).Op("*").ID(transportName).Block( + Return().Op("&").ID(transportName).Block(fieldsInit...), + ), + Line(), + // -- Down + Func().Bracket(ID("v").Op("*").ID(transportName)). + ID("Down").Bracket().Error().Block(Return().Nil()), + // -- Up + Line(), + Func().Bracket(ID("v").Op("*").ID(transportName)). + ID("Up").Bracket(ID("ctx").Pkg("context").ID("Context")).Error(). + Block(v.buildUp(m, files)), + ) + + if err := w.WriteFile(v.FilePrefix+"_transport.go", t); err != nil { + return err + } + + return nil +} + +func (v Module) buildUp(m at.GlobalMeta, files []at.File) types.Token { + tokBase := ID("r").Op(".").ID("Post").Call( + Text("/rpc"), + Func().Bracket(ID("webCtx").Pkg("web").ID("Ctx")).Block( + // --- + Var().ID("req").ID(modelBulkBaseReq), + If(). + ID("err").Op(":="). + ID("webCtx").Op(".").ID("BindJSON").Bracket(Op("&").ID("req")).Op(";"). + ID("err").Op("!=").Nil().Block( + ID("webCtx").Op(".").ID("String").Bracket( + Raw("400"), ID("err").Op(".").ID("Error").Call(), + ), + Return(), + ), + // --- + Line(), + ID("res").Op(":=").ID("poolBaseResponse").Op(".").ID("Get").Call(), + Defer().ID("poolBaseResponse").Op(".").ID("Put").Call(ID("res")), + Line(), + ID("wg").Op(":=").Pkg("syncing").ID("NewGroup").Bracket( + ID("webCtx").Op(".").ID("Context").Call(), + ), + ID("wg").Op(".").ID("OnPanic").Bracket( + Func().Bracket(ID("err").Error()).Block( + Pkg("logx").ID("Error").Call( + Text("panic"), Text("err"), ID("err"), + ), + ), + ), + // --- + Line(), + For().List(Raw("_"), ID("item")).Op(":=").Range().ID("req").Block( + ID("item").Op(":=").ID("item"), + v.buildMethods(files), + ), + ID("wg").Op(".").ID("Wait").Call(), + ID("webCtx").Op(".").ID("JSON").Bracket( + Raw("200"), ID(modelBulkBaseRes).Bracket( + ID("res").Op(".").ID("Extract").Bracket(), + ), + ), + ), + ) + + var poolTags []types.Token //nolint:prealloc + for _, s := range m.Pool { + poolTags = append(poolTags, Text(s)) + } + + return ID("v").Op(".").ID("routes").Op(".").ID("All"). + Bracket( + Func().Bracket(ID("tag").String(), ID("r").Pkg("web").ID("Router")).Block( + Switch().ID("tag").Block( + Case().List(poolTags...).Op(":").Line().Join(tokBase), + ), + ), + ). + Line().Return().Nil() +} + +func (v Module) buildMethods(files []at.File) *Tokens { + var list []types.Token + + for _, file := range files { + for _, object := range file.Faces { + for _, method := range object.Methods { + methodName := strings.ToLower(object.Name + "." + method.Name) + list = append(list, + Case().Text(methodName).Op(":").Join( + ID("wg").Op(".").ID("Background").Call( + Text(methodName), + Func().Bracket(ID("ctx").Pkg("context").ID("Context")).Block( + ID("out").Op(":=").ID(modelBaseRes).Block( + ID(util.ToUpperCamelCase(fieldID)).Op(":"). + ID("item").Op(".").ID(util.ToUpperCamelCase(fieldID)).Op(","), + ), + + Var().ID("err").Error(), + Var().ID("result").Any(), + List(ID("ctx"), ID("cancel")).Op(":=").Pkg("context").ID("WithTimeout").Call(ID( + "ctx"), Raw("5").Op("*").Pkg("time").ID("Second")), + Defer().ID("cancel").Call(), + + List(ID("result"), ID("err")).Op("="). + ID("v").Op(".").ID("call"+object.Name+method.Name). + Call( + ID("ctx"), + ID("webCtx"), + ID("item").Op(".").ID(util.ToUpperCamelCase(fieldParams)), + ), + + If().ID("err").Op("!=").Nil().Block( + ID("out").Op(".").ID(util.ToUpperCamelCase(fieldError)).Op("="). + ID("toJSONRPCError").Bracket(ID("err")), + ).Else().Block( + ID("out").Op(".").ID(util.ToUpperCamelCase(fieldResult)).Op("="). + ID("result"), + ), + ID("res").Op(".").ID("Append").Bracket(ID("out")), + ), + ), + ), + ) + } + } + } + + list = append(list, + Default().Op(":").Join( + ID("out").Op(":=").ID(modelBaseRes).Block( + ID(util.ToUpperCamelCase(fieldID)).Op(":"). + ID("item").Op(".").ID(util.ToUpperCamelCase(fieldID)).Op(","), + ), + ID("err").Op(":=").ID("errUnsupportedMethod"), + ID("out").Op(".").ID(util.ToUpperCamelCase(fieldError)).Op("="). + ID("toJSONRPCError").Bracket(ID("err")), + + ID("res").Op(".").ID("Append").Bracket(ID("out")), + ), + ) + + return Switch().Pkg("strings").ID("ToLower").Bracket( + ID("item").Op(".").ID(util.ToUpperCamelCase(fieldMethod)), + ).Block(list...) +} diff --git a/apigen/module/mod-json-rpc/build_transport_handlers.go b/apigen/module/mod-json-rpc/build_transport_handlers.go new file mode 100644 index 0000000..8d01b14 --- /dev/null +++ b/apigen/module/mod-json-rpc/build_transport_handlers.go @@ -0,0 +1,218 @@ +/* + * Copyright (c) 2022-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package mod_json_rpc + +import ( + "fmt" + + "go.osspkg.com/do" + . "go.osspkg.com/gogen/golang" //nolint:staticcheck + "go.osspkg.com/gogen/types" + "go.osspkg.com/syncing" + + at "go.osspkg.com/goppy/v3/apigen/types" + "go.osspkg.com/goppy/v3/apigen/util" +) + +func (v Module) buildTransportHandlers(w at.Writer, m at.GlobalMeta, files []at.File) error { + t := Comment("Code generated by goppy-cli wsg. DO NOT EDIT."). + Package(m.PkgName) + + list := syncing.NewMap[string, string](10) + list.Set("context", "context") + list.Set("stdjson", "encoding/json") + list.Set("web", "go.osspkg.com/goppy/v3/web") + + var handlers []types.Token + for _, file := range files { + handlers = append(handlers, v.buildTransportHandler(list, file)...) + } + + for alias, link := range list.Yield() { + t.Import(alias, link) + } + + if err := w.WriteFile(v.FilePrefix+"_handler.go", t.Join(handlers...)); err != nil { + return err + } + + return nil +} + +func (v Module) buildTransportHandler(imp at.ImportSetter, file at.File) []types.Token { + var models []types.Token + + for _, object := range file.Faces { + for _, method := range object.Methods { + handle := Func().Bracket(ID("v").Op("*").ID(transportName)). + ID("call"+object.Name+method.Name).Bracket( + ID("ctx").Pkg("context").ID("Context"), + ID("webCtx").Pkg("web").ID("Ctx"), + ID("param").Pkg("stdjson").ID("RawMessage"), + ).Bracket( + Any(), + Error(), + ) + + var handleSrc []types.Token + + handleSrc = append(handleSrc, + Var().ID("req").ID(fmt.Sprintf(modelNameReq, object.Name+method.Name)), + ID("err").Op(":=").Pkg("stdjson").ID("Unmarshal").Bracket( + ID("param"), Op("&").ID("req"), + ), + If().ID("err").Op("!=").Nil().Block( + ID("err").Op("=").Pkg("fmt").ID("Errorf").Bracket( + Text("invalid request: %w"), + ID("err"), + ), + Return().List( + Nil(), + ID("err"), + )), + Var().ID("res").ID(fmt.Sprintf(modelNameRes, object.Name+method.Name)), + ) + + // -------------------------------------- + + inParamArgs := make(map[string]string) + for _, p := range method.InParams { + vals, ok := method.Tags["in."+p.Name] + if !ok { + continue + } + paramName := "in" + util.ToUpperCamelCase(p.Name) + inParamArgs[p.Name] = paramName + handleSrc = append(handleSrc, + Var().ID(paramName). + Raw(do.IfElse(p.Slice, "[]", "")). + Raw(do.IfElse(p.Ptr, "*", "")). + Pkg(p.Pkg).ID(p.Type), + ) + for _, item := range vals { + modName, modVal, modArgs := at.TagSplit(item) + mod, ok := at.Resolve[at.ParamModule](modName) + if !ok { + continue + } + j := &at.Join{Tok: Comment("Module: " + mod.Name())} + err := mod.Build(j, at.ParamMeta{ + Type: at.ParamIn, + CodeName: paramName, + Import: imp, + Value: modVal, + Args: modArgs, + }, p) + util.PanicIfError(err, + "failed to build module %s: method: %s.%s, param: %s", + modName, object.Name, method.Name, p.Name, + ) + handleSrc = append(handleSrc, j.Tok) + } + } + + var ( + handleIn []types.Token + ) + for _, p := range method.InParams { + + switch { + case p.Pkg == "context" && p.Type == "Context": + handleIn = append(handleIn, ID("ctx")) + default: + if name, ok := inParamArgs[p.Name]; ok { + if link, ok := file.Imports.Get(p.Pkg); ok { + imp.Set(p.Pkg, link) + } + + handleIn = append(handleIn, ID(name)) + } else { + handleIn = append(handleIn, ID("req").Op(".").ID(util.ToUpperCamelCase(p.Name))) + } + } + } + + // -------------------------------------- + + outParamArgs := make(map[string]string) + for _, p := range method.OutParams { + vals, ok := method.Tags["out."+p.Name] + if !ok { + continue + } + paramName := "out" + util.ToUpperCamelCase(p.Name) + outParamArgs[p.Name] = paramName + handleSrc = append(handleSrc, + Var().ID(paramName). + Raw(do.IfElse(p.Slice, "[]", "")). + Raw(do.IfElse(p.Ptr, "*", "")). + Pkg(p.Pkg).ID(p.Type), + ) + for _, item := range vals { + modName, modVal, modArgs := at.TagSplit(item) + mod, ok := at.Resolve[at.ParamModule](modName) + if !ok { + continue + } + j := &at.Join{Tok: Comment("Module: " + mod.Name())} + err := mod.Build(j, at.ParamMeta{ + Type: at.ParamOut, + CodeName: paramName, + Import: imp, + Value: modVal, + Args: modArgs, + }, p) + util.PanicIfError(err, + "failed to build module %s: method: %s.%s, param: %s", + modName, object.Name, method.Name, p.Name, + ) + handleSrc = append(handleSrc, + Defer().Func().Bracket().Block(j.Tok).Bracket(), + ) + } + } + + var ( + handleOut []types.Token + ) + for _, p := range method.OutParams { + + switch { //nolint:staticcheck + case p.Type == "error": + handleOut = append(handleOut, ID("err")) + default: + if name, ok := outParamArgs[p.Name]; ok { + if link, ok := file.Imports.Get(p.Pkg); ok { + imp.Set(p.Pkg, link) + } + + handleOut = append(handleOut, ID(name)) + } else { + handleOut = append(handleOut, ID("res").Op(".").ID(util.ToUpperCamelCase(p.Name))) + } + } + } + + // -------------------------------------- + + handleSrc = append(handleSrc, + List(handleOut...).Op("=").ID("v").Op(".").ID("handle"+object.Name).Op(".").ID(method.Name).Call(handleIn...), + If().ID("err").Op("!=").Nil().Block( + //ID("err").Op("=").Pkg("fmt").ID("Errorf").Bracket( + // Text("encode request: %w"), + // ID("err"), + //), + Return().List(Nil(), ID("err")), + ), + Return().List(ID("res"), Nil()), + ) + + models = append(models, handle.Block(handleSrc...)) + } + } + + return models +} diff --git a/apigen/module/mod-json-rpc/build_transport_models.go b/apigen/module/mod-json-rpc/build_transport_models.go new file mode 100644 index 0000000..3b18c74 --- /dev/null +++ b/apigen/module/mod-json-rpc/build_transport_models.go @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2022-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package mod_json_rpc + +import ( + "fmt" + + "go.osspkg.com/do" + . "go.osspkg.com/gogen/golang" //nolint:staticcheck + "go.osspkg.com/gogen/types" + "go.osspkg.com/syncing" + + at "go.osspkg.com/goppy/v3/apigen/types" + "go.osspkg.com/goppy/v3/apigen/util" +) + +func (v Module) buildTransportModels(w at.Writer, m at.GlobalMeta, files []at.File) error { + t := Comment("Code generated by goppy-cli wsg. DO NOT EDIT."). + Package(m.PkgName). + Comment("go:generate easyjson") + + list := syncing.NewMap[string, string](10) + + var models []types.Token + for _, file := range files { + models = append(models, v.buildTransportModel(list, file)...) + } + + for alias, link := range list.Yield() { + t.Import(alias, link) + } + + if err := w.WriteFile(v.FilePrefix+"_model.go", t.Join(models...)); err != nil { + return err + } + + return nil +} + +func (v Module) buildTransportModel(imp at.ImportSetter, file at.File) []types.Token { + var models []types.Token + + for _, object := range file.Faces { + for _, method := range object.Methods { + + args := map[string][]at.Param{ + modelNameReq: method.InParams, + modelNameRes: method.OutParams, + } + + for tmpl, params := range args { + + var ( + argsOut []types.Token + ) + + for _, p := range params { + if ignoreModelParam(tmpl, p.Type, p.Pkg) { + continue + } + + if vals, ok := method.Tags[do.IfElse(tmpl == modelNameReq, "in.", "out.")+p.Name]; ok && noBodyParam(vals) { + continue + } + + if link, ok := file.Imports.Get(p.Pkg); ok { + imp.Set(p.Pkg, link) + } + + argsOut = append(argsOut, + ID(util.ToUpperCamelCase(p.Name)). + Raw(do.IfElse(p.Slice, "[]", "")). + Raw(do.IfElse(p.Ptr, "*", "")). + Pkg(p.Pkg).ID(p.Type). + Raw(fmt.Sprintf("`json:\"%s%s\"`", p.Name, + do.IfElse(p.Omitempty, ",omitempty", ""))), + ) + } + + models = append(models, Line().Comment(jsonGenComment). + Type().ID(fmt.Sprintf(tmpl, object.Name+method.Name)).Struct().Block(argsOut...), + ) + } + } + } + + return models +} diff --git a/apigen/module/mod-json-rpc/common.go b/apigen/module/mod-json-rpc/common.go new file mode 100644 index 0000000..cea0d77 --- /dev/null +++ b/apigen/module/mod-json-rpc/common.go @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2022-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package mod_json_rpc + +import "strings" + +const ( + transportName = "JSONRPCHandler" + + modelNameReq = "jsonrpc%sModelRequest" + modelNameRes = "jsonrpc%sModelResponse" + + modelBaseReq = "baseRequest" + modelBulkBaseReq = "bulkRequest" + modelBaseRes = "baseResponse" + modelBulkBaseRes = "bulkResponse" + modelBaseErr = "errResponse" + errInterface = "TError" +) + +const ( + fieldMethod = "method" + fieldParams = "params" + fieldID = "id" + fieldResult = "result" + fieldError = "error" + fieldMessage = "message" + fieldCode = "code" + fieldCtx = "ctx" +) + +const ( + jsonGenComment = "easyjson:json" +) + +func ignoreModelParam(tmpl, pt, pp string) bool { + if tmpl == modelNameRes { + switch { //nolint:staticcheck + case pt == "error": + return true + default: + } + } + + if tmpl == modelNameReq { + switch { + case pp == "context" && pt == "Context": + return true + default: + } + } + + return false +} + +func noBodyParam(vals []string) bool { + for _, val := range vals { + i := strings.Index(val, ":") + if i == -1 { + continue + } + switch strings.ToLower(val[0:i]) { + case "cookie", "header": + return true + default: + } + } + return false +} diff --git a/apigen/module/mod-json-rpc/module.go b/apigen/module/mod-json-rpc/module.go new file mode 100644 index 0000000..77ced70 --- /dev/null +++ b/apigen/module/mod-json-rpc/module.go @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2022-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package mod_json_rpc + +import ( + "go.osspkg.com/errors" + + at "go.osspkg.com/goppy/v3/apigen/types" +) + +type Module struct { + FilePrefix string +} + +func (Module) Name() string { + return "json-rpc" +} + +func (v Module) Build(w at.Writer, m at.GlobalMeta, files []at.File) error { + return errors.Queue( + func() error { return v.buildBaseRPCModel(w, m) }, + func() error { return v.buildTransportModels(w, m, files) }, + func() error { return v.buildTransport(w, m, files) }, + func() error { return v.buildTransportHandlers(w, m, files) }, + ) +} diff --git a/apigen/module/mod-param-cookie/module.go b/apigen/module/mod-param-cookie/module.go new file mode 100644 index 0000000..e490ab1 --- /dev/null +++ b/apigen/module/mod-param-cookie/module.go @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2022-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package mod_param_cookie + +import ( + "fmt" + + . "go.osspkg.com/gogen/golang" //nolint:staticcheck + + at "go.osspkg.com/goppy/v3/apigen/types" +) + +type Module struct{} + +func (Module) Name() string { + return "cookie" +} + +func (v Module) Build(w at.Joiner, m at.ParamMeta, value at.Param) error { + switch m.Type { + case at.ParamIn: + return v.generateIn(w, m, value) + case at.ParamOut: + return v.generateOut(w, m, value) + default: + return fmt.Errorf("unknown type") + } +} + +func (v Module) generateIn(w at.Joiner, m at.ParamMeta, p at.Param) error { + m.Import.Set("web", "go.osspkg.com/goppy/v3/web") + + w.Join( + List(ID(m.CodeName), Raw("_")).Op("="). + ID("web").Op(".").ID("StrTo").Raw("[").Pkg(p.Pkg).ID(p.Type).Raw("]"). + Call( + ID("webCtx").Op(".").ID("Cookie").Call().Op(".").ID("Get").Call(Text(m.Value)), + ), + If().ID("err").Op("!=").Nil().Block( + ID("err").Op("=").Pkg("fmt").ID("Errorf").Bracket( + Text("invalid request: %w"), + ID("err"), + ), + Return().List( + Nil(), + ID("err"), + )), + ) + + return nil +} + +func (v Module) generateOut(w at.Joiner, m at.ParamMeta, _ at.Param) error { + m.Import.Set("nethttp", "net/http") + m.Import.Set("fmt", "fmt") + m.Import.Set("time", "time") + + w.Join( + If().ID("err").Op("!=").Nil().Block(Return()), + ID("webCtx").Op(".").ID("Cookie").Call().Op(".").ID("Set").Bracket( + Op("&").Pkg("nethttp").ID("Cookie").Block( + ID("Name").Op(":").Text(m.Value).Op(","), + ID("Value").Op(":").Pkg("fmt").ID("Sprintf").Bracket( + List(Text("%v"), Raw(m.CodeName)), + ).Op(","), + ID("Path").Op(":").Text(m.Args.Get("path", "/")).Op(","), + ID("Expires").Op(":").Pkg("time").ID("Now").Call().Op(".").ID("Add").Call( + Raw(m.Args.Get("time", "86400")).Op("*").Pkg("time").ID("Second"), + ).Op(","), + ID("Secure").Op(":").Raw(m.Args.Get("secure", "true")).Op(","), + ID("HttpOnly").Op(":").Raw(m.Args.Get("httpOnly", "true")).Op(","), + ID("SameSite").Op(":").Pkg("nethttp").ID(m.Args.Get("sameSite", "SameSiteStrictMode")).Op(","), + ), + ), + ) + return nil +} diff --git a/apigen/module/mod-param-header/module.go b/apigen/module/mod-param-header/module.go new file mode 100644 index 0000000..12d7fdb --- /dev/null +++ b/apigen/module/mod-param-header/module.go @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2022-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package mod_param_header + +import ( + "fmt" + + . "go.osspkg.com/gogen/golang" //nolint:staticcheck + + at "go.osspkg.com/goppy/v3/apigen/types" +) + +type Module struct{} + +func (Module) Name() string { + return "header" +} + +func (v Module) Build(w at.Joiner, m at.ParamMeta, value at.Param) error { + switch m.Type { + case at.ParamIn: + return v.generateIn(w, m, value) + case at.ParamOut: + return v.generateOut(w, m, value) + default: + return fmt.Errorf("unknown type") + } +} + +func (v Module) generateIn(w at.Joiner, m at.ParamMeta, p at.Param) error { + m.Import.Set("web", "go.osspkg.com/goppy/v3/web") + + w.Join( + List(ID(m.CodeName), Raw("_")).Op("="). + ID("web").Op(".").ID("StrTo").Raw("[").Pkg(p.Pkg).ID(p.Type).Raw("]"). + Call( + ID("webCtx").Op(".").ID("Header").Call().Op(".").ID("Get").Call(Text(m.Value)), + ), + If().ID("err").Op("!=").Nil().Block( + ID("err").Op("=").Pkg("fmt").ID("Errorf").Bracket( + Text("invalid request: %w"), + ID("err"), + ), + Return().List( + Nil(), + ID("err"), + )), + ) + + return nil +} + +func (v Module) generateOut(w at.Joiner, m at.ParamMeta, _ at.Param) error { + m.Import.Set("fmt", "fmt") + + w.Join( + If().ID("err").Op("!=").Nil().Block(Return()), + ID("webCtx").Op(".").ID("Header").Call().Op(".").ID("Set").Bracket( + Text(m.Value), + Pkg("fmt").ID("Sprintf").Bracket( + List(Text("%v"), Raw(m.CodeName)), + ), + ), + ) + return nil +} diff --git a/apigen/parser/errors.go b/apigen/parser/errors.go new file mode 100644 index 0000000..d246a6e --- /dev/null +++ b/apigen/parser/errors.go @@ -0,0 +1,10 @@ +/* + * Copyright (c) 2022-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package parser + +import "go.osspkg.com/errors" + +var ErrIsGenerated = errors.New("file is generated") diff --git a/apigen/parser/visitor.go b/apigen/parser/visitor.go new file mode 100644 index 0000000..d958708 --- /dev/null +++ b/apigen/parser/visitor.go @@ -0,0 +1,376 @@ +/* + * Copyright (c) 2022-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package parser + +import ( + "fmt" + "go/ast" + "go/parser" + "go/token" + "io" + "path/filepath" + "strings" + + "go.osspkg.com/bb" + "go.osspkg.com/do" + "go.osspkg.com/errors" + "go.osspkg.com/ioutils/fs" + "go.osspkg.com/syncing" + + "go.osspkg.com/goppy/v3/apigen/types" + "go.osspkg.com/goppy/v3/apigen/util" + "go.osspkg.com/goppy/v3/internal/global" +) + +type ( + visitor struct { + TAG string + + FilePath string + PkgName string + PkgPath string + GoMod string + Imports *syncing.Map[string, string] + Objects []types.Face + } + + Parser interface { + ToFile() types.File + DumpStdout() + } +) + +func New(tag, filePath string) (Parser, error) { + gomod, root, err := global.DetectGoMod(filePath) + if err != nil { + return nil, err + } + + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, filePath, nil, parser.ParseComments) + if err != nil { + return nil, err + } + + if ast.IsGenerated(f) { + return nil, ErrIsGenerated + } + + vis := &visitor{ + TAG: tag, + Imports: syncing.NewMap[string, string](10), + FilePath: strings.TrimPrefix(filePath, root), + GoMod: gomod, + } + + ast.Walk(vis, f) + + return vis, nil +} + +func (v *visitor) ToFile() types.File { + return types.File{ + FilePath: v.FilePath, + PkgName: v.PkgName, + PkgPath: v.PkgPath, + GoMod: v.GoMod, + Imports: v.Imports, + Faces: v.Objects, + } +} + +func (v *visitor) DumpStdout() { + fmt.Println("=============================================================") + fmt.Println("FilePath:", strings.TrimPrefix(v.FilePath, fs.CurrentDir())) + fmt.Println("PkgName:", v.PkgName, "PkgPath:", v.PkgPath) + fmt.Println("Import:") + for alias, path := range v.Imports.Yield() { + fmt.Println(" ", alias, path) + } + for _, object := range v.Objects { + fmt.Println("Interface:", object.Name) + fmt.Println(" tags:") + for key, value := range object.Tags { + fmt.Println(" ", key, ":", value) + } + + for _, method := range object.Methods { + fmt.Println(" method:", method.Name) + + fmt.Println(" tags:") + for key, value := range method.Tags { + fmt.Println(" ", key, ":", value) + } + + fmt.Println(" in:") + for _, value := range method.InParams { + fmt.Println(" ", + "name:", value.Name, ", type:", value.Type, + ", pkg:", value.Pkg, ", omit:", value.Omitempty, + ", ptr:", value.Ptr, ", slice:", value.Slice) + } + + fmt.Println(" out:") + for _, value := range method.OutParams { + fmt.Println(" ", + "name:", value.Name, ", type:", value.Type, + ", pkg:", value.Pkg, ", omit:", value.Omitempty, + ", ptr:", value.Ptr, ", slice:", value.Slice) + } + } + } + fmt.Println("=============================================================") +} + +func (v *visitor) Visit(node ast.Node) ast.Visitor { + switch nodeType := node.(type) { + case *ast.File: + return v.astFile(nodeType) + + case *ast.ImportSpec: + return v.astImportSpec(nodeType) + + case *ast.GenDecl: + for _, spec := range nodeType.Specs { + switch specType := spec.(type) { + case *ast.TypeSpec: + if nodeType.Doc != nil { + specType.Doc = nodeType.Doc + } + v.astTypeSpec(specType) + } + } + return v + + default: + return nil + } +} + +func (v *visitor) astFile(node *ast.File) ast.Visitor { + v.PkgName = node.Name.String() + v.PkgPath = v.GoMod + filepath.Dir(v.FilePath) + + v.Imports.Set(v.PkgName, v.PkgPath) + + return v +} + +func (v *visitor) astImportSpec(node *ast.ImportSpec) ast.Visitor { + path := strings.Trim(node.Path.Value, `"`) + name := util.SplitLast(path, "/") + + if node.Name != nil { + name = node.Name.String() + } + + v.Imports.Set(name, path) + + return v +} + +func (v *visitor) parseDoc(comment *ast.CommentGroup, tags *types.Tags) { + if comment == nil { + return + } + for _, doc := range comment.List { + i := strings.Index(doc.Text, v.TAG) + if i < 0 { + continue + } + v.parseComment(doc.Text[i+len(v.TAG):], tags) + } +} + +func (v *visitor) parseComment(comment string, tags *types.Tags) { + comment = strings.TrimPrefix(comment, "//") + comment = strings.TrimSpace(comment) + + buf := bb.FromBytes([]byte(comment)) + _, err := buf.Seek(0, io.SeekStart) + util.PanicIfError(err, "parse comment") + + for { + key, err := buf.ReadString('=') + if errors.Is(err, io.EOF) { + break + } + util.PanicIfError(err, "parse comment") + key = strings.Trim(strings.TrimSpace(key), "=") + + r, _, err := buf.ReadRune() + if errors.Is(err, io.EOF) { + break + } + util.PanicIfError(err, "parse comment") + + var value string + switch r { + case '"': + value, err = buf.ReadString('"') + value = strings.Trim(strings.TrimSpace(value), "\"") + case '\'': + value, err = buf.ReadString('\'') + value = strings.Trim(strings.TrimSpace(value), "'") + case '`': + value, err = buf.ReadString('`') + value = strings.Trim(strings.TrimSpace(value), "`") + default: + util.PanicIfError(buf.UnreadRune(), "parse comment") + value, err = buf.ReadString(' ') + } + + if errors.Is(err, io.EOF) { + break + } + util.PanicIfError(err, "parse comment") + + (*tags)[key] = append((*tags)[key], do.Convert[string, string]( + strings.Split(value, ","), + func(value string, index int) string { + return strings.TrimSpace(value) + }, + )...) + } +} + +func (v *visitor) parseMethods(fields *ast.FieldList) (result []types.Method) { + for _, field := range fields.List { + if !field.Names[0].IsExported() { + continue + } + + funcType, ok := field.Type.(*ast.FuncType) + if !ok { + continue + } + + method := types.Method{ + Name: field.Names[0].String(), + Tags: make(types.Tags, 10), + InParams: nil, + OutParams: nil, + } + + v.parseDoc(field.Doc, &method.Tags) + //v.parseDoc(field.Comment, &method.Tags) + + if funcType.Params != nil { + for _, param := range funcType.Params.List { + rp := getParam(param) + if !isBaseType(rp.Type) && len(rp.Pkg) == 0 { + rp.Pkg = v.PkgName + } + + method.InParams = append(method.InParams, rp) + } + } + + if funcType.Results != nil { + for _, param := range funcType.Results.List { + rp := getParam(param) + if !isBaseType(rp.Type) && len(rp.Pkg) == 0 { + rp.Pkg = v.PkgName + } + + method.OutParams = append(method.OutParams, rp) + } + } + + result = append(result, method) + + } + + return +} + +func (v *visitor) astTypeSpec(node *ast.TypeSpec) { + faceNode, ok := node.Type.(*ast.InterfaceType) + if !ok { + return + } + + obj := types.Face{ + Pkg: v.PkgPath, + Alias: v.PkgName, + Name: node.Name.String(), + Tags: make(types.Tags, 10), + } + + v.parseDoc(node.Doc, &obj.Tags) + //v.parseDoc(node.Comment, &obj.Tags) + + obj.Methods = append(obj.Methods, v.parseMethods(faceNode.Methods)...) + + v.Objects = append(v.Objects, obj) + + return +} + +func getParam(param *ast.Field) (p types.Param) { + p.Name = param.Names[0].String() + p.Type = getTypeName(param.Type) + p.Slice = strings.HasPrefix(p.Type, "[]") + p.Ptr = strings.HasPrefix(p.Type, "*") + p.Omitempty = p.Ptr || p.Slice + + list := strings.Split(p.Type, ".") + switch len(list) { + case 1: + p.Type = strings.Trim(list[0], "*[].") + case 2: + p.Type = list[1] + p.Pkg = strings.Trim(list[0], "*[].") + default: + util.PanicIfError(fmt.Errorf("invalid type: %s", p.Type), "get param") + } + + return +} + +/* +TODO: change to +import "go/printer" +import "strings" +import "bytes" + +func exprToString(fset *token.FileSet, expr ast.Expr) string { + var buf bytes.Buffer + printer.Fprint(&buf, fset, expr) + return buf.String() +} +*/ + +func getTypeName(expr ast.Expr) string { + switch t := expr.(type) { + case *ast.Ident: + return t.Name + case *ast.SelectorExpr: + return fmt.Sprintf("%s.%s", getTypeName(t.X), t.Sel.Name) + case *ast.StarExpr: + return "*" + getTypeName(t.X) + case *ast.ArrayType: + return "[]" + getTypeName(t.Elt) + default: + return "" + } +} + +func isBaseType(arg string) bool { + switch arg { + case "bool", "string", "byte", "struct", "struct{}", + "any", "interface", "interface{}", + "complex64", "complex128", + "error", "uintptr", + "float32", "float64", + "int", "int8", "int16", "int32", "int64", + "uint", "uint8", "uint16", "uint32", "uint64", + "map": + return true + default: + return false + } +} diff --git a/apigen/types/common.go b/apigen/types/common.go new file mode 100644 index 0000000..737cf49 --- /dev/null +++ b/apigen/types/common.go @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2022-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package types + +import ( + "strings" + + "go.osspkg.com/gogen/golang" + "go.osspkg.com/gogen/types" +) + +func TagSplit(arg string) (string, string, Args) { + val := strings.SplitN(arg, ":", 3) + args := make(Args) + + switch len(val) { + case 0: + return "", "", args + case 1: + return strings.TrimSpace(val[0]), "", args + default: + if len(val) > 2 { + for _, item := range strings.Split(val[2], "|") { + vals := strings.Split(item, "=") + if len(vals) == 2 { + args[strings.TrimSpace(vals[0])] = strings.TrimSpace(vals[1]) + } + } + } + return strings.TrimSpace(val[0]), strings.TrimSpace(val[1]), args + } +} + +type Join struct { + Tok *golang.Tokens +} + +func (j *Join) Join(toks ...types.Token) { + j.Tok = j.Tok.Join(toks...) +} diff --git a/apigen/types/object.go b/apigen/types/object.go new file mode 100644 index 0000000..b3ca68a --- /dev/null +++ b/apigen/types/object.go @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2022-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package types + +import ( + "go.osspkg.com/syncing" +) + +type File struct { + FilePath string + PkgName string + PkgPath string + GoMod string + Imports *syncing.Map[string, string] + Faces []Face +} + +type Face struct { + Alias string + Pkg string + Name string + Methods []Method + Tags Tags +} + +type Method struct { + Name string + Tags Tags + InParams []Param + OutParams []Param +} + +type Param struct { + Name string + Type string + Pkg string + Ptr bool + Slice bool + Omitempty bool +} + +type KV struct { + Key string + Value string +} + +type Tags map[string][]string + +type Args map[string]string + +func (a Args) Get(key, def string) string { + v, ok := a[key] + if !ok { + return def + } + return v +} diff --git a/apigen/types/storage.go b/apigen/types/storage.go new file mode 100644 index 0000000..6b82afa --- /dev/null +++ b/apigen/types/storage.go @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2022-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package types + +import ( + "fmt" + + "go.osspkg.com/syncing" +) + +const ( + globalMod = "g" + faceMod = "f" + methodMod = "m" + paramMod = "p" +) + +var ( + _storage = syncing.NewMap[string, any](10) +) + +func Register[T any](module T) { + addr := "" + + switch vv := any(module).(type) { + case GlobalModule: + addr = globalMod + "/" + vv.Name() + case FaceModule: + addr = faceMod + "/" + vv.Name() + case MethodModule: + addr = methodMod + "/" + vv.Name() + case ParamModule: + addr = paramMod + "/" + vv.Name() + default: + panic("unknown type") + } + + _storage.Set(addr, module) +} + +func Resolve[T any](name string) (T, bool) { + addr := "" + nt := new(T) + + switch any(nt).(type) { + case *GlobalModule: + addr = globalMod + "/" + name + case *FaceModule: + addr = faceMod + "/" + name + case *MethodModule: + addr = methodMod + "/" + name + case *ParamModule: + addr = paramMod + "/" + name + default: + panic(fmt.Sprintf("unknown type: %T", *nt)) + } + + raw, ok := _storage.Get(addr) + if !ok { + var zeroValue T + return zeroValue, false + } + + module, ok := raw.(T) + return module, ok +} diff --git a/apigen/types/type.go b/apigen/types/type.go new file mode 100644 index 0000000..a114e56 --- /dev/null +++ b/apigen/types/type.go @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2022-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package types + +import "go.osspkg.com/gogen/types" + +type GlobalModule interface { + Name() string + Build(w Writer, m GlobalMeta, value []File) error +} + +type FaceModule interface { + Name() string + Build(w Writer, m FaceMeta, value Face) error +} + +type MethodModule interface { + Name() string + Build(w Joiner, m MethodMeta, value Method) error +} + +type ParamModule interface { + Name() string + Build(w Joiner, m ParamMeta, value Param) error +} + +type Writer interface { + WriteFile(fileName string, tok types.Token) error +} + +type Joiner interface { + Join(toks ...types.Token) +} + +type ImportSetter interface { + Set(string, string) +} + +type GlobalMeta struct { + PkgName string + Pool []string +} + +type FaceMeta struct { + PkgName string + Import ImportSetter +} + +type MethodMeta struct { + PkgName string + Import ImportSetter +} + +type ParamType uint8 + +const ( + ParamIn ParamType = 0 + ParamOut ParamType = 1 +) + +type ParamMeta struct { + Type ParamType + CodeName string + Import ImportSetter + Value string + Args Args +} diff --git a/apigen/util/util.go b/apigen/util/util.go new file mode 100644 index 0000000..d3959aa --- /dev/null +++ b/apigen/util/util.go @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2022-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package util + +import ( + "fmt" + "hash/crc32" + "strings" + "unicode" + + "go.osspkg.com/errors" +) + +func ToKebabCase(s string) string { + if s == "" { + return "" + } + + var builder strings.Builder + builder.Grow(len(s) + 2) + + for i, r := range s { + if unicode.IsUpper(r) { + if i > 0 { + builder.WriteByte('-') + } + builder.WriteRune(unicode.ToLower(r)) + } else { + builder.WriteRune(r) + } + } + + return builder.String() +} + +func ToUpperCamelCase(s string) string { + if s == "" { + return "" + } + + var builder strings.Builder + builder.Grow(len(s) + 2) + + for i, r := range s { + if i == 0 && !unicode.IsUpper(r) { + builder.WriteRune(unicode.ToUpper(r)) + } else { + builder.WriteRune(r) + } + } + + return builder.String() +} + +var crc32q = crc32.MakeTable(0xD5828281) + +func CRC32(s string) string { + bl := len(s) + + if bl <= 0 { + return "" + } + + return fmt.Sprintf("%08x", crc32.Checksum([]byte(s), crc32q)) +} + +func SplitLast(s, sep string) string { + result := strings.Split(s, sep) + return result[len(result)-1] +} + +func PanicIfError(err error, msg string, args ...interface{}) { + if err == nil { + return + } + err = errors.Wrapf(err, msg, args...) + panic(err.Error()) +} diff --git a/cmd/goppy/main.go b/cmd/goppy/main.go index 7fb404e..79de584 100644 --- a/cmd/goppy/main.go +++ b/cmd/goppy/main.go @@ -6,11 +6,9 @@ package main import ( - "go.osspkg.com/console" - - "go.osspkg.com/goppy/v3/internal/gen/ormb" - + "go.osspkg.com/goppy/v3/console" "go.osspkg.com/goppy/v3/internal/commands" + "go.osspkg.com/goppy/v3/internal/gen/ormb" "go.osspkg.com/goppy/v3/internal/global" ) @@ -28,10 +26,8 @@ func main() { commands.CmdSetupLib(), commands.CmdSetupApp(), commands.CmdGoSite(), + commands.CmdWSG(), ormb.Command(), - - //gen.Command(), - // commands.CmdGenerate(), ) app.Exec() diff --git a/console/io.go b/console/io.go index f091f73..9b107f0 100644 --- a/console/io.go +++ b/console/io.go @@ -325,6 +325,12 @@ func FatalIfErr(err error, msg string, args ...interface{}) { } } +func FatalIfTrue(condition bool, msg string, args ...interface{}) { + if condition { + Fatalf(fmt.Sprintf(msg, args...)) + } +} + func WarnIfErr(err error, msg string, args ...interface{}) { if err != nil { Warnf(errors.Wrapf(err, msg, args...).Error()) diff --git a/go.mod b/go.mod index 6834782..2b6550b 100644 --- a/go.mod +++ b/go.mod @@ -7,20 +7,21 @@ require ( github.com/go-sql-driver/mysql v1.9.3 github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.3 - github.com/lib/pq v1.10.9 - github.com/mailru/easyjson v0.9.1 - github.com/mattn/go-sqlite3 v1.14.33 - github.com/miekg/dns v1.1.70 + github.com/lib/pq v1.11.2 + github.com/mailru/easyjson v0.9.2 + github.com/mattn/go-sqlite3 v1.14.34 + github.com/miekg/dns v1.1.72 github.com/oschwald/geoip2-golang v1.13.0 github.com/prometheus/client_golang v1.23.2 github.com/prometheus/client_model v0.6.2 go.osspkg.com/algorithms v1.6.1 + go.osspkg.com/bb v1.0.0 go.osspkg.com/casecheck v0.3.0 go.osspkg.com/config v0.2.0 - go.osspkg.com/console v0.3.3 go.osspkg.com/do v0.2.1 go.osspkg.com/errors v0.4.0 go.osspkg.com/events v0.3.0 + go.osspkg.com/gogen v0.1.1 go.osspkg.com/ioutils v0.7.4 go.osspkg.com/logx v0.6.1 go.osspkg.com/network v0.6.0 @@ -31,10 +32,10 @@ require ( go.osspkg.com/validate v0.1.0 go.osspkg.com/xc v0.4.0 go.uber.org/automaxprocs v1.6.0 - golang.org/x/crypto v0.47.0 - golang.org/x/exp v0.0.0-20260112195511-716be5621a96 - golang.org/x/mod v0.32.0 - golang.org/x/oauth2 v0.34.0 + golang.org/x/crypto v0.49.0 + golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 + golang.org/x/mod v0.34.0 + golang.org/x/oauth2 v0.36.0 google.golang.org/protobuf v1.36.11 ) @@ -81,10 +82,10 @@ require ( go.etcd.io/bbolt v1.4.3 // indirect go.uber.org/mock v0.5.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect - golang.org/x/net v0.49.0 // indirect - golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.40.0 // indirect - golang.org/x/text v0.33.0 // indirect - golang.org/x/tools v0.41.0 // indirect + golang.org/x/net v0.52.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.42.0 // indirect + golang.org/x/text v0.35.0 // indirect + golang.org/x/tools v0.43.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 329bf8a..ee73342 100644 --- a/go.sum +++ b/go.sum @@ -81,14 +81,14 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= -github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= -github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= -github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0= -github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= -github.com/miekg/dns v1.1.70 h1:DZ4u2AV35VJxdD9Fo9fIWm119BsQL5cZU1cQ9s0LkqA= -github.com/miekg/dns v1.1.70/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= +github.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs= +github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= +github.com/mailru/easyjson v0.9.2 h1:dX8U45hQsZpxd80nLvDGihsQ/OxlvTkVUXH2r/8cb2M= +github.com/mailru/easyjson v0.9.2/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk= +github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= +github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -130,18 +130,20 @@ go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo= go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= go.osspkg.com/algorithms v1.6.1 h1:Boduh7kuqdw9r/mOkAvN4cyZX1yqUVp7gZtLqBeIHkY= go.osspkg.com/algorithms v1.6.1/go.mod h1:QojMfdersOcebMu/VfERA8Jsn+s/9TGELgaRFiyGuq0= +go.osspkg.com/bb v1.0.0 h1:6rGK0Gioyo7EbgLaTTIRxhF+me2zoMlOef1mGJoO/L0= +go.osspkg.com/bb v1.0.0/go.mod h1:bh7v2dIXJC0ge8BZLsh10EFRbJSLjXCmxq4IwSogc58= go.osspkg.com/casecheck v0.3.0 h1:x15blEszElbrHrEH5H02JIIhGIg/lGZzIt1kQlD3pwM= go.osspkg.com/casecheck v0.3.0/go.mod h1:TRFXDMFJEOtnlp3ET2Hix3osbxwPWhvaiT/HfD3+gBA= go.osspkg.com/config v0.2.0 h1:nPf14TX+HnVgOtlX1vobeTl//bQ3T/fAhXASaDRZ5rI= go.osspkg.com/config v0.2.0/go.mod h1:lYvJ3OfeWIQLr9ToRcDU+7fVCJLirDtKFpdxtAR8K1A= -go.osspkg.com/console v0.3.3 h1:UB/pPoPsgWbyNFix8pEMQHbsXdMv/UK/dgsbRknCH2A= -go.osspkg.com/console v0.3.3/go.mod h1:IknBCliH6mX/ogHa6wbycnGDFYixCGH3WuNc5W5tQe8= go.osspkg.com/do v0.2.1 h1:1JYkt54VHVGVUFxUzTTUvmHcGSezjijvGAaD70Z8/Q8= go.osspkg.com/do v0.2.1/go.mod h1:ZViMiWrU6AfobhImtZvqe5n8ZiC/eT6KMF+5JHbZyAg= go.osspkg.com/errors v0.4.0 h1:E17+WyUzTXEHCTxGm8lOMPOOojzHG1lsOuQtTVGoATQ= go.osspkg.com/errors v0.4.0/go.mod h1:s75ZovPemYtrCtRPVsbQNq9MgMbmLMK1NEypr+uwjXI= go.osspkg.com/events v0.3.0 h1:W2IngTsKs0BKYIglqhrETwtpo6uNSZXWRIt0/l7c6dY= go.osspkg.com/events v0.3.0/go.mod h1:Cjpx+qNM1y2MIAygFyZWYagTuRiYirmKppZQdaZumd4= +go.osspkg.com/gogen v0.1.1 h1:yOixEWv5jpidpq7ZG1yFsoVptVx4YVkfVav5OVBiMZ8= +go.osspkg.com/gogen v0.1.1/go.mod h1:BkoU7OC3LV/kmtjvCnQB1rF9hcK0rF3hZKoaZlP+Uv4= go.osspkg.com/ioutils v0.7.4 h1:Z8Y4jYYmLGWcvHZMLjbai+s48GmHxjMuepsxZcjF5X4= go.osspkg.com/ioutils v0.7.4/go.mod h1:pPIsTL1w1+ESrGTeHDCd6cKsujeWvschxGGP5FqrAqc= go.osspkg.com/logx v0.6.1 h1:JqHk1iFBOKnwO0dZtC7n4sO/0MkUD9c7eulmYEDCnao= @@ -168,27 +170,27 @@ go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= -golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= -golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= -golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU= -golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU= -golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= -golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= -golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= -golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= -golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= -golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA= +golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ= +golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= +golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= -golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= -golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= +golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= +golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/applog/logger.go b/internal/applog/logger.go index ee93fc7..7425fba 100644 --- a/internal/applog/logger.go +++ b/internal/applog/logger.go @@ -12,8 +12,9 @@ import ( "os" "strings" - "go.osspkg.com/console" "go.osspkg.com/logx" + + "go.osspkg.com/goppy/v3/console" ) const formatSyslog = "syslog" diff --git a/internal/commands/build.go b/internal/commands/build.go index 4b5a4ae..afe8788 100644 --- a/internal/commands/build.go +++ b/internal/commands/build.go @@ -8,10 +8,11 @@ package commands import ( "strings" - "go.osspkg.com/console" "go.osspkg.com/do" "go.osspkg.com/ioutils/fs" + "go.osspkg.com/goppy/v3/console" + "go.osspkg.com/goppy/v3/internal/global" ) diff --git a/internal/commands/generate.go b/internal/commands/generate.go index 2bf13e5..5bdfa2b 100644 --- a/internal/commands/generate.go +++ b/internal/commands/generate.go @@ -14,9 +14,9 @@ import ( "strings" "text/template" - "go.osspkg.com/console" "go.osspkg.com/ioutils/fs" + "go.osspkg.com/goppy/v3/console" "go.osspkg.com/goppy/v3/internal/global" ) @@ -28,7 +28,7 @@ func CmdGenerate() console.CommandGetter { data := make(map[string]any, 100) data["go_version"] = strings.TrimLeft(global.GoVersion(), "go") - data["app_module"] = console.Input("Input project name", nil, "app") + data["app_module"] = console.Select("Input project name", nil, "app") data["app_name"] = func() string { vv := strings.Split(data["app_module"].(string), "/") //nolint: errcheck return vv[len(vv)-1] diff --git a/internal/commands/gosite.go b/internal/commands/gosite.go index 95748ad..ada06db 100644 --- a/internal/commands/gosite.go +++ b/internal/commands/gosite.go @@ -12,10 +12,11 @@ import ( "sort" "strings" - "go.osspkg.com/console" "go.osspkg.com/ioutils/codec" "go.osspkg.com/ioutils/fs" + "go.osspkg.com/goppy/v3/console" + "go.osspkg.com/goppy/v3/internal/global" ) diff --git a/internal/commands/license.go b/internal/commands/license.go index 11c4719..f4e54f2 100644 --- a/internal/commands/license.go +++ b/internal/commands/license.go @@ -12,11 +12,12 @@ import ( "strings" "time" - "go.osspkg.com/console" "go.osspkg.com/do" "go.osspkg.com/ioutils/codec" "go.osspkg.com/ioutils/fs" + "go.osspkg.com/goppy/v3/console" + "go.osspkg.com/goppy/v3/internal/global" ) @@ -67,7 +68,7 @@ func CmdLicense() console.CommandGetter { goFiles, err := fs.SearchFilesByExt(currDir, ".go") console.FatalIfErr(err, "Get go files") for _, file := range goFiles { - if strings.HasSuffix(file, "_easyjson.go") || strings.Contains(file, "/vendor/") { + if global.NeedSkipFile(file) { continue } if _, ok := ignoreFiles[strings.TrimLeft(strings.TrimPrefix(file, currDir), "/")]; ok { diff --git a/internal/commands/lint.go b/internal/commands/lint.go index 8f978e9..89080bd 100644 --- a/internal/commands/lint.go +++ b/internal/commands/lint.go @@ -9,9 +9,10 @@ import ( "fmt" "path/filepath" - "go.osspkg.com/console" "go.osspkg.com/ioutils/fs" + "go.osspkg.com/goppy/v3/console" + "go.osspkg.com/goppy/v3/internal/global" ) diff --git a/internal/commands/setup.go b/internal/commands/setup.go index f47f74b..babf586 100644 --- a/internal/commands/setup.go +++ b/internal/commands/setup.go @@ -12,9 +12,10 @@ import ( "path/filepath" "strings" - "go.osspkg.com/console" "go.osspkg.com/ioutils/fs" + "go.osspkg.com/goppy/v3/console" + "go.osspkg.com/goppy/v3/internal/global" ) diff --git a/internal/commands/tag.go b/internal/commands/tag.go index 629c536..974005a 100644 --- a/internal/commands/tag.go +++ b/internal/commands/tag.go @@ -12,11 +12,12 @@ import ( "strings" "go.osspkg.com/algorithms/graph/kahn" - "go.osspkg.com/console" "go.osspkg.com/ioutils/fs" "go.osspkg.com/validate" "golang.org/x/mod/modfile" + "go.osspkg.com/goppy/v3/console" + "go.osspkg.com/goppy/v3/internal/global" ) diff --git a/internal/commands/tests.go b/internal/commands/tests.go index deae0a3..6b8dde1 100644 --- a/internal/commands/tests.go +++ b/internal/commands/tests.go @@ -8,7 +8,7 @@ package commands import ( "os" - "go.osspkg.com/console" + "go.osspkg.com/goppy/v3/console" "go.osspkg.com/goppy/v3/internal/global" ) diff --git a/internal/commands/wsg.go b/internal/commands/wsg.go new file mode 100644 index 0000000..d9fb7c8 --- /dev/null +++ b/internal/commands/wsg.go @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2022-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package commands + +import ( + "os" + "strings" + + "go.osspkg.com/do" + "go.osspkg.com/errors" + "go.osspkg.com/ioutils/fs" + + "go.osspkg.com/goppy/v3/apigen/builder" + "go.osspkg.com/goppy/v3/apigen/parser" + "go.osspkg.com/goppy/v3/console" + "go.osspkg.com/goppy/v3/internal/global" +) + +func CmdWSG() console.CommandGetter { + return console.NewCommand(func(setter console.CommandSetter) { + setter.Setup("wsg", "generate web server api") + setter.Flag(func(flagsSetter console.FlagsSetter) { + flagsSetter.String("out", "output file specified") + flagsSetter.StringVar("iface", "", "interface names (optional)") + flagsSetter.StringVar("mod", "json-rpc", "generation modules (optional)") + flagsSetter.StringVar("pool", "main", "web server pool list (optional)") + }) + setter.ExecFunc(func(out, _iface, _mod, _pool string) { + console.ShowDebug(true) + console.Infof("--- GENERATE ---") + + curDir := fs.CurrentDir() + + console.FatalIfTrue(len(out) == 0, "no output file specified") + console.FatalIfTrue(out == curDir, "output dir equals current dir") + + console.FatalIfErr(os.RemoveAll(out), "failed to remove old output directory") + console.FatalIfErr(os.MkdirAll(out, 0755), "failed to create directory") + + files, err := fs.SearchFilesByExt(curDir, ".go") + console.FatalIfErr(err, "search files in %s", curDir) + + files = do.Filter[string](files, func(value string, index int) bool { + return !strings.HasPrefix(value, out) + }) + + mods := do.Treat[string](strings.Split(_mod, ","), func(value string, index int) string { + return strings.ToLower(strings.TrimSpace(value)) + }) + + pool := do.Treat[string](strings.Split(_pool, ","), func(value string, index int) string { + return strings.ToLower(strings.TrimSpace(value)) + }) + + face := do.Entries[string, string, struct{}]( + do.Filter[string](strings.Split(_iface, ","), + func(value string, index int) bool { + return len(value) > 0 + }, + ), + func(s string) (string, struct{}) { + return strings.ToLower(strings.TrimSpace(s)), struct{}{} + }, + ) + + build := &builder.Builder{ + Out: out, + IFace: face, + Mods: mods, + Pool: pool, + } + + for _, filePath := range files { + if global.NeedSkipFile(filePath) { + continue + } + + console.Debugf("> PARSE FILE: %s", filePath) + + vv, e := parser.New("@wsg", filePath) + if e != nil { + if errors.Is(e, parser.ErrIsGenerated) { + continue + } + console.FatalIfErr(e, "parse go file: %s", filePath) + } + + vv.DumpStdout() + build.Files = append(build.Files, vv.ToFile()) + } + + console.FatalIfErr(build.Build(), "") + }) + }) +} diff --git a/internal/gen/ormb/command.go b/internal/gen/ormb/command.go index 1e4240d..9f53afd 100644 --- a/internal/gen/ormb/command.go +++ b/internal/gen/ormb/command.go @@ -10,10 +10,11 @@ import ( "go/parser" "go/token" - "go.osspkg.com/console" "go.osspkg.com/ioutils/fs" "go.osspkg.com/syncing" + "go.osspkg.com/goppy/v3/console" + "go.osspkg.com/goppy/v3/internal/gen/ormb/common" "go.osspkg.com/goppy/v3/internal/gen/ormb/dialects" "go.osspkg.com/goppy/v3/internal/gen/ormb/visitor" diff --git a/internal/gen/ormb/common/utils.go b/internal/gen/ormb/common/utils.go index 0495ee8..50e2d8e 100644 --- a/internal/gen/ormb/common/utils.go +++ b/internal/gen/ormb/common/utils.go @@ -10,7 +10,7 @@ import ( "io" "strings" - "go.osspkg.com/console" + "go.osspkg.com/goppy/v3/console" ) func SplitLast(s, sep string) string { diff --git a/internal/gen/ormb/generate_sql.go b/internal/gen/ormb/generate_sql.go index e8a869c..23eabb7 100644 --- a/internal/gen/ormb/generate_sql.go +++ b/internal/gen/ormb/generate_sql.go @@ -10,9 +10,10 @@ import ( "os" "strings" - "go.osspkg.com/console" "go.osspkg.com/ioutils/data" + "go.osspkg.com/goppy/v3/console" + "go.osspkg.com/goppy/v3/internal/gen/ormb/common" "go.osspkg.com/goppy/v3/internal/gen/ormb/dialects" "go.osspkg.com/goppy/v3/internal/gen/ormb/visitor" diff --git a/internal/gen/ormb/visitor/visitor.go b/internal/gen/ormb/visitor/visitor.go index f8ab5ce..441d341 100644 --- a/internal/gen/ormb/visitor/visitor.go +++ b/internal/gen/ormb/visitor/visitor.go @@ -9,9 +9,10 @@ import ( "go/ast" "strings" - "go.osspkg.com/console" "go.osspkg.com/syncing" + "go.osspkg.com/goppy/v3/console" + "go.osspkg.com/goppy/v3/internal/gen/ormb/common" "go.osspkg.com/goppy/v3/internal/gen/ormb/table" ) diff --git a/internal/global/exec.go b/internal/global/exec.go index 61bf9ff..8d17c31 100644 --- a/internal/global/exec.go +++ b/internal/global/exec.go @@ -11,10 +11,11 @@ import ( "io" "os" - "go.osspkg.com/console" "go.osspkg.com/events" "go.osspkg.com/ioutils/fs" "go.osspkg.com/ioutils/shell" + + "go.osspkg.com/goppy/v3/console" ) type logger struct { diff --git a/internal/global/global.go b/internal/global/global.go index 3e07398..9d72e95 100644 --- a/internal/global/global.go +++ b/internal/global/global.go @@ -11,9 +11,10 @@ import ( "os" "regexp" - "go.osspkg.com/console" "go.osspkg.com/ioutils/fs" "go.osspkg.com/ioutils/shell" + + "go.osspkg.com/goppy/v3/console" ) const ( diff --git a/internal/global/mods.go b/internal/global/mods.go index c035d40..e086eb3 100644 --- a/internal/global/mods.go +++ b/internal/global/mods.go @@ -6,13 +6,16 @@ package global import ( + "fmt" "os" + "path/filepath" "regexp" "strings" "go.osspkg.com/errors" "go.osspkg.com/ioutils/fs" "go.osspkg.com/validate" + "golang.org/x/mod/modfile" ) var rexModule = regexp.MustCompile(`(?mU)module (.*)\n`) @@ -73,3 +76,40 @@ func ReadModule(filepath string) (*Module, error) { File: filepath, }, nil } + +func DetectGoMod(curPath string) (mod string, root string, err error) { + root = filepath.Dir(curPath) + for { + + mods, e := fs.SearchFilesByExt(root, ".mod") + if e != nil { + return "", "", e + } + + if len(mods) != 0 { + for _, s := range mods { + b, e := os.ReadFile(s) + if e != nil { + return "", "", e + } + + f, e := modfile.Parse("go.mod", b, nil) + if e != nil { + return "", "", e + } + mod = f.Module.Mod.Path + return + } + } + + root, err = filepath.Abs(root + "/..") + if err != nil { + return + } + + if root == "/" { + err = fmt.Errorf("failed to detect go.mod") + return + } + } +} diff --git a/internal/global/skip_files.go b/internal/global/skip_files.go new file mode 100644 index 0000000..0b829bf --- /dev/null +++ b/internal/global/skip_files.go @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2022-2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package global + +import "strings" + +func NeedSkipFile(filepath string) bool { + return strings.HasSuffix(filepath, "_gen.go") || + strings.HasSuffix(filepath, "_test.go") || + strings.HasSuffix(filepath, "_easyjson.go") || + strings.HasSuffix(filepath, "_mock.go") || + strings.Contains(filepath, "/vendor/") +} diff --git a/web/ctx.go b/web/ctx.go index 70517c0..0787901 100644 --- a/web/ctx.go +++ b/web/ctx.go @@ -9,6 +9,7 @@ package web import ( "context" + "encoding" "encoding/json" "fmt" "io" @@ -16,6 +17,8 @@ import ( "net/url" "slices" "strconv" + "strings" + "time" "go.osspkg.com/ioutils" "go.osspkg.com/ioutils/data" @@ -41,12 +44,12 @@ type ( BindRaw() (*data.Buffer, error) BindBytes(in *[]byte) error - BindJSON(in json.Unmarshaler) error + BindJSON(in any) error BindXML(in any) error Bytes(code int, b []byte) String(code int, b string, args ...any) - JSON(code int, in json.Marshaler) + JSON(code int, in any) Stream(code int, in []byte, filename string) StreamFile(code int, in io.Reader, filename string) @@ -223,7 +226,7 @@ func (v *_ctx) BindRaw() (*data.Buffer, error) { return buf, nil } -func (v *_ctx) BindJSON(in json.Unmarshaler) error { +func (v *_ctx) BindJSON(in any) error { return encoders.JSONDecode(v.r, in) } @@ -280,7 +283,7 @@ func (v *_ctx) String(code int, b string, args ...any) { } // JSON recording the response in json format -func (v *_ctx) JSON(code int, in json.Marshaler) { +func (v *_ctx) JSON(code int, in any) { encoders.JSONEncode(v.w, code, in) } @@ -343,3 +346,113 @@ func (v *_ctx) URL() *url.URL { func (v *_ctx) Redirect(uri string) { http.Redirect(v.w, v.r, uri, http.StatusMovedPermanently) } + +/**********************************************************************************************************************/ + +type UnStringer interface { + UnString(string) +} + +func StrTo[T any](s string) (T, error) { + var v T + if len(s) == 0 { + return v, nil + } + + var err error + + switch p := any(&v).(type) { + case *string: + *p = s + case *int: + var val int64 + val, err = strconv.ParseInt(s, 10, strconv.IntSize) + *p = int(val) + case *int8: + var val int64 + val, err = strconv.ParseInt(s, 10, 8) + *p = int8(val) + case *int16: + var val int64 + val, err = strconv.ParseInt(s, 10, 16) + *p = int16(val) + case *int32: + var val int64 + val, err = strconv.ParseInt(s, 10, 32) + *p = int32(val) + case *int64: + *p, err = strconv.ParseInt(s, 10, 64) + case *uint: + var val uint64 + val, err = strconv.ParseUint(s, 10, strconv.IntSize) + *p = uint(val) + case *uint8: + var val uint64 + val, err = strconv.ParseUint(s, 10, 8) + *p = uint8(val) + case *uint16: + var val uint64 + val, err = strconv.ParseUint(s, 10, 16) + *p = uint16(val) + case *uint32: + var val uint64 + val, err = strconv.ParseUint(s, 10, 32) + *p = uint32(val) + case *uint64: + *p, err = strconv.ParseUint(s, 10, 64) + case *float32: + var val float64 + val, err = strconv.ParseFloat(s, 32) + *p = float32(val) + case *float64: + *p, err = strconv.ParseFloat(s, 64) + case *bool: + *p, err = strconv.ParseBool(s) + case *time.Duration: + *p, err = time.ParseDuration(s) + default: + if us, ok := any(&v).(UnStringer); ok { + us.UnString(s) + } else if bu, ok := any(&v).(encoding.BinaryUnmarshaler); ok { + err = bu.UnmarshalBinary([]byte(s)) + } else if tu, ok := any(&v).(encoding.TextUnmarshaler); ok { + err = tu.UnmarshalText([]byte(s)) + } else if ju, ok := any(&v).(json.Unmarshaler); ok { + err = ju.UnmarshalJSON([]byte(s)) + } else { + err = fmt.Errorf("parser: unsupported type %T", v) + } + } + + return v, err +} + +func StrToSlice[T any](s, sep string) ([]T, error) { + if s == "" { + return nil, nil + } + + count := strings.Count(s, sep) + 1 + result := make([]T, 0, count) + + for { + idx := strings.Index(s, sep) + if idx < 0 { + val, err := StrTo[T](strings.TrimSpace(s)) + if err != nil { + return nil, err + } + result = append(result, val) + break + } + + val, err := StrTo[T](strings.TrimSpace(s[:idx])) + if err != nil { + return nil, err + } + result = append(result, val) + s = s[idx+len(sep):] + } + + return result, nil +} diff --git a/web/encoders/json.go b/web/encoders/json.go index 5942191..e9c375f 100644 --- a/web/encoders/json.go +++ b/web/encoders/json.go @@ -27,7 +27,7 @@ func JSONEncode(w http.ResponseWriter, code int, obj any) { } } -func JSONDecode(r *http.Request, obj json.Unmarshaler) error { +func JSONDecode(r *http.Request, obj any) error { b, err := ioutils.ReadAll(r.Body) if err != nil { return err