From 1dde737b64169c6dd2536686ffb414beca24f927 Mon Sep 17 00:00:00 2001 From: asim Date: Tue, 14 Oct 2025 11:13:35 +0100 Subject: [PATCH] move micro cli and protoc-gen-micro to cmd/ --- client/rpc_client.go | 4 +- cmd/README.md | 212 ++ cmd/cmd.go | 14 +- cmd/micro/cli/README.md | 223 ++ cmd/micro/cli/cli.go | 369 +++ cmd/micro/cli/new/new.go | 257 ++ cmd/micro/cli/new/template/handler.go | 66 + cmd/micro/cli/new/template/ignore.go | 7 + cmd/micro/cli/new/template/main.go | 27 + cmd/micro/cli/new/template/makefile.go | 32 + cmd/micro/cli/new/template/module.go | 14 + cmd/micro/cli/new/template/proto.go | 35 + cmd/micro/cli/new/template/readme.go | 30 + cmd/micro/cli/util/dynamic.go | 416 +++ cmd/micro/cli/util/dynamic_test.go | 379 +++ cmd/micro/cli/util/util.go | 72 + cmd/micro/main.go | 26 + cmd/micro/run/run.go | 227 ++ cmd/micro/server/server.go | 1072 +++++++ cmd/micro/server/util_jwt.go | 91 + cmd/micro/web/main.js | 34 + cmd/micro/web/styles.css | 236 ++ cmd/micro/web/templates/api.html | 34 + cmd/micro/web/templates/auth_login.html | 15 + cmd/micro/web/templates/auth_tokens.html | 63 + cmd/micro/web/templates/auth_users.html | 40 + cmd/micro/web/templates/base.html | 52 + cmd/micro/web/templates/form.html | 23 + cmd/micro/web/templates/home.html | 21 + cmd/micro/web/templates/log.html | 5 + cmd/micro/web/templates/logs.html | 8 + cmd/micro/web/templates/service.html | 26 + cmd/micro/web/templates/status.html | 29 + cmd/protoc-gen-micro/README.md | 144 + .../examples/greeter/greeter.pb.go | 223 ++ .../examples/greeter/greeter.pb.micro.go | 183 ++ .../examples/greeter/greeter.proto | 17 + cmd/protoc-gen-micro/generator/Makefile | 40 + cmd/protoc-gen-micro/generator/generator.go | 2748 +++++++++++++++++ cmd/protoc-gen-micro/generator/name_test.go | 135 + cmd/protoc-gen-micro/main.go | 105 + cmd/protoc-gen-micro/plugin/micro/micro.go | 531 ++++ go.mod | 16 +- go.sum | 24 +- server/rpc_server.go | 74 +- transport/http2_buf_pool.go | 10 +- transport/http_socket.go | 74 +- 47 files changed, 8363 insertions(+), 120 deletions(-) create mode 100644 cmd/README.md create mode 100644 cmd/micro/cli/README.md create mode 100644 cmd/micro/cli/cli.go create mode 100644 cmd/micro/cli/new/new.go create mode 100644 cmd/micro/cli/new/template/handler.go create mode 100644 cmd/micro/cli/new/template/ignore.go create mode 100644 cmd/micro/cli/new/template/main.go create mode 100644 cmd/micro/cli/new/template/makefile.go create mode 100644 cmd/micro/cli/new/template/module.go create mode 100644 cmd/micro/cli/new/template/proto.go create mode 100644 cmd/micro/cli/new/template/readme.go create mode 100644 cmd/micro/cli/util/dynamic.go create mode 100644 cmd/micro/cli/util/dynamic_test.go create mode 100644 cmd/micro/cli/util/util.go create mode 100644 cmd/micro/main.go create mode 100644 cmd/micro/run/run.go create mode 100644 cmd/micro/server/server.go create mode 100644 cmd/micro/server/util_jwt.go create mode 100644 cmd/micro/web/main.js create mode 100644 cmd/micro/web/styles.css create mode 100644 cmd/micro/web/templates/api.html create mode 100644 cmd/micro/web/templates/auth_login.html create mode 100644 cmd/micro/web/templates/auth_tokens.html create mode 100644 cmd/micro/web/templates/auth_users.html create mode 100644 cmd/micro/web/templates/base.html create mode 100644 cmd/micro/web/templates/form.html create mode 100644 cmd/micro/web/templates/home.html create mode 100644 cmd/micro/web/templates/log.html create mode 100644 cmd/micro/web/templates/logs.html create mode 100644 cmd/micro/web/templates/service.html create mode 100644 cmd/micro/web/templates/status.html create mode 100644 cmd/protoc-gen-micro/README.md create mode 100644 cmd/protoc-gen-micro/examples/greeter/greeter.pb.go create mode 100644 cmd/protoc-gen-micro/examples/greeter/greeter.pb.micro.go create mode 100644 cmd/protoc-gen-micro/examples/greeter/greeter.proto create mode 100644 cmd/protoc-gen-micro/generator/Makefile create mode 100644 cmd/protoc-gen-micro/generator/generator.go create mode 100644 cmd/protoc-gen-micro/generator/name_test.go create mode 100644 cmd/protoc-gen-micro/main.go create mode 100644 cmd/protoc-gen-micro/plugin/micro/micro.go diff --git a/client/rpc_client.go b/client/rpc_client.go index 98e80647..480eb5a4 100644 --- a/client/rpc_client.go +++ b/client/rpc_client.go @@ -30,11 +30,11 @@ const ( ) type rpcClient struct { - seq uint64 + seq uint64 opts Options once atomic.Value pool pool.Pool - mu sync.RWMutex + mu sync.RWMutex } func newRPCClient(opt ...Option) Client { diff --git a/cmd/README.md b/cmd/README.md new file mode 100644 index 00000000..0c2d9cec --- /dev/null +++ b/cmd/README.md @@ -0,0 +1,212 @@ +# Micro + +Go Micro Command Line + +## Install the CLI + +Install `micro` via `go install` + +``` +go install go-micro.dev/v5/cmd/micro@latest +``` + +Or via install script + + +## Create a service + +Create your service (all setup is now automatic!): + +``` +micro new helloworld +``` + +This will: +- Create a new service in the `helloworld` directory +- Automatically run `go mod tidy` and `make proto` for you +- Show the updated project tree including generated files +- Warn you if `protoc` is not installed, with install instructions + +## Run the service + +Run the service + +``` +micro run +``` + +List services to see it's running and registered itself + +``` +micro services +``` + +## Describe the service + +Describe the service to see available endpoints + +``` +micro describe helloworld +``` + +Output + +``` +{ + "name": "helloworld", + "version": "latest", + "metadata": null, + "endpoints": [ + { + "request": { + "name": "Request", + "type": "Request", + "values": [ + { + "name": "name", + "type": "string", + "values": null + } + ] + }, + "response": { + "name": "Response", + "type": "Response", + "values": [ + { + "name": "msg", + "type": "string", + "values": null + } + ] + }, + "metadata": {}, + "name": "Helloworld.Call" + }, + { + "request": { + "name": "Context", + "type": "Context", + "values": null + }, + "response": { + "name": "Stream", + "type": "Stream", + "values": null + }, + "metadata": { + "stream": "true" + }, + "name": "Helloworld.Stream" + } + ], + "nodes": [ + { + "metadata": { + "broker": "http", + "protocol": "mucp", + "registry": "mdns", + "server": "mucp", + "transport": "http" + }, + "id": "helloworld-31e55be7-ac83-4810-89c8-a6192fb3ae83", + "address": "127.0.0.1:39963" + } + ] +} +``` + +## Call the service + +Call via RPC endpoint + +``` +micro call helloworld Helloworld.Call '{"name": "Asim"}' +``` + +## Create a client + +Create a client to call the service + +```go +package main + +import ( + "context" + "fmt" + + "go-micro.dev/v5" +) + +type Request struct { + Name string +} + +type Response struct { + Message string +} + +func main() { + client := micro.New("helloworld").Client() + + req := client.NewRequest("helloworld", "Helloworld.Call", &Request{Name: "John"}) + + var rsp Response + + err := client.Call(context.TODO(), req, &rsp) + if err != nil { + fmt.Println(err) + return + } + + fmt.Println(rsp.Message) +} +``` + +## Protobuf + +Use protobuf for code generation with [protoc-gen-micro](https://github.com/micro/go-micro/tree/master/cmd/protoc-gen-micro) + +## Server + +The micro server is an api and web dashboard that provide a fixed entrypoint for seeing and querying services. + +Run it like so + +``` +micro server +``` + +Then browse to [localhost:8080](http://localhost:8080) + +### API Endpoints + +The API provides a fixed HTTP entrypoint for calling services + +``` +curl http://localhost:8080/api/helloworld/Helloworld/Call -d '{"name": "John"}' +``` +See /api for more details and documentation for each service + +### Web Dashboard + +The web dashboard provides a modern, secure UI for managing and exploring your Micro services. Major features include: + +- **Dynamic Service & Endpoint Forms**: Browse all registered services and endpoints. For each endpoint, a dynamic form is generated for easy testing and exploration. +- **API Documentation**: The `/api` page lists all available services and endpoints, with request/response schemas and a sidebar for quick navigation. A documentation banner explains authentication requirements. +- **JWT Authentication**: All login and token management uses a custom JWT utility. Passwords are securely stored with bcrypt. All `/api/x` endpoints and authenticated pages require an `Authorization: Bearer ` header (or `micro_token` cookie as fallback). +- **Token Management**: The `/auth/tokens` page allows you to generate, view (obfuscated), and copy JWT tokens. Tokens are stored and can be revoked. When a user is deleted, all their tokens are revoked immediately. +- **User Management**: The `/auth/users` page allows you to create, list, and delete users. Passwords are never shown or stored in plaintext. +- **Token Revocation**: JWT tokens are stored and checked for revocation on every request. Revoked or deleted tokens are immediately invalidated. +- **Security**: All protected endpoints use consistent authentication logic. Unauthorized or revoked tokens receive a 401 error. All sensitive actions require authentication. +- **Logs & Status**: View service logs and status (PID, uptime, etc) directly from the dashboard. + +To get started, run: + +``` +micro server +``` + +Then browse to [localhost:8080](http://localhost:8080) and log in with the default admin account (`admin`/`micro`). + +> **Note:** See the `/api` page for details on API authentication and how to generate tokens for use with the HTTP API diff --git a/cmd/cmd.go b/cmd/cmd.go index dace8684..34842f40 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -4,12 +4,16 @@ package cmd import ( "fmt" "math/rand" - "sort" "os" + "sort" "strings" "time" "github.com/urfave/cli/v2" + "go-micro.dev/v5/auth" + "go-micro.dev/v5/broker" + nbroker "go-micro.dev/v5/broker/nats" + rabbit "go-micro.dev/v5/broker/rabbitmq" "go-micro.dev/v5/cache" "go-micro.dev/v5/cache/redis" "go-micro.dev/v5/client" @@ -19,15 +23,11 @@ import ( "go-micro.dev/v5/debug/profile/pprof" "go-micro.dev/v5/debug/trace" "go-micro.dev/v5/events" - "go-micro.dev/v5/logger" - mprofile "go-micro.dev/v5/profile" - "go-micro.dev/v5/auth" - "go-micro.dev/v5/broker" - nbroker "go-micro.dev/v5/broker/nats" - rabbit "go-micro.dev/v5/broker/rabbitmq" "go-micro.dev/v5/genai" "go-micro.dev/v5/genai/gemini" "go-micro.dev/v5/genai/openai" + "go-micro.dev/v5/logger" + mprofile "go-micro.dev/v5/profile" "go-micro.dev/v5/registry" "go-micro.dev/v5/registry/consul" "go-micro.dev/v5/registry/etcd" diff --git a/cmd/micro/cli/README.md b/cmd/micro/cli/README.md new file mode 100644 index 00000000..f2f4065d --- /dev/null +++ b/cmd/micro/cli/README.md @@ -0,0 +1,223 @@ +# Micro [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) + +A Go microservices toolkit + +## Overview + +Micro is a toolkit for Go microservices development. It provides the foundation for building services in the cloud. +The core of Micro is the [Go Micro](https://github.com/micro/go-micro) framework, which developers import and use in their code to +write services. Surrounding this we introduce a number of tools to make it easy to serve and consume services. + +## Install the CLI + +Install `micro` via `go install` + +``` +go install go-micro.dev/v5@latest +``` + +Or via install script + +``` +wget -q https://raw.githubusercontent.com/micro/micro/master/scripts/install.sh -O - | /bin/bash +``` + +For releases see the [latest](https://go-micro.dev/releases/latest) tag + +## Create a service + +Create your service (all setup is now automatic!): + +``` +micro new helloworld +``` + +This will: +- Create a new service in the `helloworld` directory +- Automatically run `go mod tidy` and `make proto` for you +- Show the updated project tree including generated files +- Warn you if `protoc` is not installed, with install instructions + +## Run the service + +Run the service + +``` +micro run +``` + +List services to see it's running and registered itself + +``` +micro services +``` + +## Describe the service + +Describe the service to see available endpoints + +``` +micro describe helloworld +``` + +Output + +``` +{ + "name": "helloworld", + "version": "latest", + "metadata": null, + "endpoints": [ + { + "request": { + "name": "Request", + "type": "Request", + "values": [ + { + "name": "name", + "type": "string", + "values": null + } + ] + }, + "response": { + "name": "Response", + "type": "Response", + "values": [ + { + "name": "msg", + "type": "string", + "values": null + } + ] + }, + "metadata": {}, + "name": "Helloworld.Call" + }, + { + "request": { + "name": "Context", + "type": "Context", + "values": null + }, + "response": { + "name": "Stream", + "type": "Stream", + "values": null + }, + "metadata": { + "stream": "true" + }, + "name": "Helloworld.Stream" + } + ], + "nodes": [ + { + "metadata": { + "broker": "http", + "protocol": "mucp", + "registry": "mdns", + "server": "mucp", + "transport": "http" + }, + "id": "helloworld-31e55be7-ac83-4810-89c8-a6192fb3ae83", + "address": "127.0.0.1:39963" + } + ] +} +``` + +## Call the service + +Call via RPC endpoint + +``` +micro call helloworld Helloworld.Call '{"name": "Asim"}' +``` + +## Create a client + +Create a client to call the service + +```go +package main + +import ( + "context" + "fmt" + + "go-micro.dev/v5" +) + +type Request struct { + Name string +} + +type Response struct { + Message string +} + +func main() { + client := micro.New("helloworld").Client() + + req := client.NewRequest("helloworld", "Helloworld.Call", &Request{Name: "John"}) + + var rsp Response + + err := client.Call(context.TODO(), req, &rsp) + if err != nil { + fmt.Println(err) + return + } + + fmt.Println(rsp.Message) +} +``` + +## Protobuf + +Use protobuf for code generation with [protoc-gen-micro](https://go-micro.dev/tree/master/cmd/protoc-gen-micro) + +## Server + +The micro server is an api and web dashboard that provide a fixed entrypoint for seeing and querying services. + +Run it like so + +``` +micro server +``` + +Then browse to [localhost:8080](http://localhost:8080) + +### API Endpoints + +The API provides a fixed HTTP entrypoint for calling services + +``` +curl http://localhost:8080/api/helloworld/Helloworld/Call -d '{"name": "John"}' +``` +See /api for more details and documentation for each service + +### Web Dashboard + +The web dashboard provides a modern, secure UI for managing and exploring your Micro services. Major features include: + +- **Dynamic Service & Endpoint Forms**: Browse all registered services and endpoints. For each endpoint, a dynamic form is generated for easy testing and exploration. +- **API Documentation**: The `/api` page lists all available services and endpoints, with request/response schemas and a sidebar for quick navigation. A documentation banner explains authentication requirements. +- **JWT Authentication**: All login and token management uses a custom JWT utility. Passwords are securely stored with bcrypt. All `/api/x` endpoints and authenticated pages require an `Authorization: Bearer ` header (or `micro_token` cookie as fallback). +- **Token Management**: The `/auth/tokens` page allows you to generate, view (obfuscated), and copy JWT tokens. Tokens are stored and can be revoked. When a user is deleted, all their tokens are revoked immediately. +- **User Management**: The `/auth/users` page allows you to create, list, and delete users. Passwords are never shown or stored in plaintext. +- **Token Revocation**: JWT tokens are stored and checked for revocation on every request. Revoked or deleted tokens are immediately invalidated. +- **Security**: All protected endpoints use consistent authentication logic. Unauthorized or revoked tokens receive a 401 error. All sensitive actions require authentication. +- **Logs & Status**: View service logs and status (PID, uptime, etc) directly from the dashboard. + +To get started, run: + +``` +micro server +``` + +Then browse to [localhost:8080](http://localhost:8080) and log in with the default admin account (`admin`/`micro`). + +> **Note:** See the `/api` page for details on API authentication and how to generate tokens for use with the HTTP API diff --git a/cmd/micro/cli/cli.go b/cmd/micro/cli/cli.go new file mode 100644 index 00000000..b5365cde --- /dev/null +++ b/cmd/micro/cli/cli.go @@ -0,0 +1,369 @@ +package microcli + +import ( + "bufio" + "context" + "encoding/json" + "fmt" + "os" + "os/exec" + "os/signal" + "path/filepath" + "strings" + "syscall" + + "github.com/urfave/cli/v2" + "go-micro.dev/v5/client" + "go-micro.dev/v5/cmd" + "go-micro.dev/v5/codec/bytes" + "go-micro.dev/v5/genai" + "go-micro.dev/v5/registry" + + "go-micro.dev/v5/cmd/micro/cli/new" + "go-micro.dev/v5/cmd/micro/cli/util" +) + +var ( + // version is set by the release action + // this is the default for local builds + version = "5.0.0-dev" +) + +func genProtoHandler(c *cli.Context) error { + cmd := exec.Command("find", ".", "-name", "*.proto", "-exec", "protoc", "--proto_path=.", "--micro_out=.", "--go_out=.", `{}`, `;`) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} + +func genTextHandler(c *cli.Context) error { + prompt := c.String("prompt") + if len(prompt) == 0 { + return nil + } + + gen := genai.DefaultGenAI + if gen.String() == "noop" { + return nil + } + + res, err := gen.Generate(prompt) + if err != nil { + return err + } + + fmt.Println(res.Text) + return nil +} + +func lastNonEmptyLine(s string) string { + lines := strings.Split(s, "\n") + for i := len(lines) - 1; i >= 0; i-- { + if strings.TrimSpace(lines[i]) != "" { + return lines[i] + } + } + return "" +} + +func lastLogLine(path string) string { + f, err := os.Open(path) + if err != nil { + return "" + } + defer f.Close() + var last string + scan := bufio.NewScanner(f) + for scan.Scan() { + if strings.TrimSpace(scan.Text()) != "" { + last = scan.Text() + } + } + return last +} + +func waitAndCleanup(procs []*exec.Cmd, pidFiles []string) { + ch := make(chan os.Signal, 1) + signal.Notify(ch, os.Interrupt) + go func() { + <-ch + for _, proc := range procs { + if proc.Process != nil { + _ = proc.Process.Kill() + } + } + for _, pf := range pidFiles { + _ = os.Remove(pf) + } + os.Exit(1) + }() + for i, proc := range procs { + _ = proc.Wait() + if proc.Process != nil { + _ = os.Remove(pidFiles[i]) + } + } +} + +func init() { + cmd.Register([]*cli.Command{ + { + Name: "new", + Usage: "Create a new service", + Action: new.Run, + }, + { + Name: "gen", + Usage: "Generate various things", + Subcommands: []*cli.Command{ + { + Name: "text", + Usage: "Generate text via an LLM", + Action: genTextHandler, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "prompt", + Aliases: []string{"p"}, + Usage: "The prompt to generate text from", + }, + }, + }, + { + Name: "proto", + Usage: "Generate proto requires protoc and protoc-gen-micro", + Action: genProtoHandler, + }, + }, + }, + { + Name: "services", + Usage: "List available services", + Action: func(ctx *cli.Context) error { + services, err := registry.ListServices() + if err != nil { + return err + } + for _, service := range services { + fmt.Println(service.Name) + } + return nil + }, + }, + { + Name: "call", + Usage: "Call a service", + Action: func(ctx *cli.Context) error { + args := ctx.Args() + + if args.Len() < 2 { + return fmt.Errorf("Usage: [service] [endpoint] [request]") + } + + service := args.Get(0) + endpoint := args.Get(1) + request := `{}` + + if args.Len() == 3 { + request = args.Get(2) + } + + req := client.NewRequest(service, endpoint, &bytes.Frame{Data: []byte(request)}) + var rsp bytes.Frame + err := client.Call(context.TODO(), req, &rsp) + if err != nil { + return err + } + + fmt.Print(string(rsp.Data)) + return nil + }, + }, + { + Name: "describe", + Usage: "Describe a service", + Action: func(ctx *cli.Context) error { + args := ctx.Args() + + if args.Len() != 1 { + return fmt.Errorf("Usage: [service]") + } + + service := args.Get(0) + services, err := registry.GetService(service) + if err != nil { + return err + } + if len(services) == 0 { + return nil + } + b, _ := json.MarshalIndent(services[0], "", " ") + fmt.Println(string(b)) + return nil + }, + }, + { + Name: "status", + Usage: "Check status of running services", + Action: func(ctx *cli.Context) error { + homeDir, err := os.UserHomeDir() + if err != nil { + return fmt.Errorf("failed to get home dir: %w", err) + } + runDir := filepath.Join(homeDir, "micro", "run") + files, err := os.ReadDir(runDir) + if err != nil { + return fmt.Errorf("failed to read run dir: %w", err) + } + fmt.Printf("%-20s %-8s %-8s %s\n", "SERVICE", "PID", "STATUS", "DIRECTORY") + for _, f := range files { + if f.IsDir() || !strings.HasSuffix(f.Name(), ".pid") { + continue + } + service := f.Name()[:len(f.Name())-4] + pidFilePath := filepath.Join(runDir, f.Name()) + pidFile, err := os.Open(pidFilePath) + if err != nil { + continue + } + var pid int + var dir string + scanner := bufio.NewScanner(pidFile) + if scanner.Scan() { + fmt.Sscanf(scanner.Text(), "%d", &pid) + } + if scanner.Scan() { + dir = scanner.Text() + } + pidFile.Close() + status := "stopped" + if pid > 0 { + proc, err := os.FindProcess(pid) + if err == nil { + if err := proc.Signal(syscall.Signal(0)); err == nil { + status = "running" + } + } + } + fmt.Printf("%-20s %-8d %-8s %-40s %s\n", service, pid, status, "", dir) + } + return nil + }, + }, + { + Name: "stop", + Usage: "Stop a running service", + Action: func(ctx *cli.Context) error { + if ctx.Args().Len() != 1 { + return fmt.Errorf("Usage: micro stop [service]") + } + service := ctx.Args().Get(0) + homeDir, err := os.UserHomeDir() + if err != nil { + return fmt.Errorf("failed to get home dir: %w", err) + } + runDir := filepath.Join(homeDir, "micro", "run") + pidFilePath := filepath.Join(runDir, service+".pid") + pidFile, err := os.Open(pidFilePath) + if err != nil { + return fmt.Errorf("no pid file for service %s", service) + } + var pid int + var dir string + scanner := bufio.NewScanner(pidFile) + if scanner.Scan() { + fmt.Sscanf(scanner.Text(), "%d", &pid) + } + if scanner.Scan() { + dir = scanner.Text() + } + pidFile.Close() + if pid <= 0 { + _ = os.Remove(pidFilePath) + return fmt.Errorf("service %s is not running", service) + } + proc, err := os.FindProcess(pid) + if err != nil { + _ = os.Remove(pidFilePath) + return fmt.Errorf("could not find process for %s", service) + } + if err := proc.Signal(syscall.SIGTERM); err != nil { + _ = os.Remove(pidFilePath) + return fmt.Errorf("failed to stop service %s: %v", service, err) + } + _ = os.Remove(pidFilePath) + fmt.Printf("Stopped service %s (pid %d) in directory %s\n", service, pid, dir) + return nil + }, + }, + { + Name: "logs", + Usage: "Show logs for a service, or list available logs if no service is specified", + Action: func(ctx *cli.Context) error { + homeDir, err := os.UserHomeDir() + if err != nil { + return fmt.Errorf("failed to get home dir: %w", err) + } + logsDir := filepath.Join(homeDir, "micro", "logs") + if ctx.Args().Len() == 0 { + // List available logs + dirEntries, err := os.ReadDir(logsDir) + if err != nil { + return fmt.Errorf("could not list logs directory: %v", err) + } + fmt.Println("Available logs:") + found := false + for _, entry := range dirEntries { + if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".log") { + fmt.Println(" ", strings.TrimSuffix(entry.Name(), ".log")) + found = true + } + } + if !found { + fmt.Println(" (no logs found)") + } + return nil + } + service := ctx.Args().Get(0) + logFilePath := filepath.Join(logsDir, service+".log") + f, err := os.Open(logFilePath) + if err != nil { + return fmt.Errorf("could not open log file for service %s: %v", service, err) + } + defer f.Close() + scan := bufio.NewScanner(f) + for scan.Scan() { + fmt.Println(scan.Text()) + } + return scan.Err() + }, + }, + }...) + + cmd.App().Action = func(c *cli.Context) error { + if c.Args().Len() == 0 { + return nil + } + + v, err := exec.LookPath("micro-" + c.Args().First()) + if err == nil { + ce := exec.Command(v, c.Args().Slice()[1:]...) + ce.Stdout = os.Stdout + ce.Stderr = os.Stderr + return ce.Run() + } + + command := c.Args().Get(0) + args := c.Args().Slice() + + if srv, err := util.LookupService(command); err != nil { + return util.CliError(err) + } else if srv != nil && util.ShouldRenderHelp(args) { + return cli.Exit(util.FormatServiceUsage(srv, c), 0) + } else if srv != nil { + err := util.CallService(srv, args) + return util.CliError(err) + } + + return nil + } +} diff --git a/cmd/micro/cli/new/new.go b/cmd/micro/cli/new/new.go new file mode 100644 index 00000000..51eb657b --- /dev/null +++ b/cmd/micro/cli/new/new.go @@ -0,0 +1,257 @@ +// Package new generates micro service templates +package new + +import ( + "fmt" + "go/build" + "os" + "os/exec" + "path" + "path/filepath" + "runtime" + "strings" + "text/template" + "time" + + "github.com/urfave/cli/v2" + "github.com/xlab/treeprint" + tmpl "go-micro.dev/v5/cmd/micro/cli/new/template" +) + +func protoComments(goDir, alias string) []string { + return []string{ + "\ndownload protoc zip packages (protoc-$VERSION-$PLATFORM.zip) and install:\n", + "visit https://github.com/protocolbuffers/protobuf/releases", + "\ncompile the proto file " + alias + ".proto:\n", + "cd " + alias, + "go mod tidy", + "make proto\n", + } +} + +type config struct { + // foo + Alias string + // github.com/micro/foo + Dir string + // $GOPATH/src/github.com/micro/foo + GoDir string + // $GOPATH + GoPath string + // UseGoPath + UseGoPath bool + // Files + Files []file + // Comments + Comments []string +} + +type file struct { + Path string + Tmpl string +} + +func write(c config, file, tmpl string) error { + fn := template.FuncMap{ + "title": func(s string) string { + return strings.ReplaceAll(strings.Title(s), "-", "") + }, + "dehyphen": func(s string) string { + return strings.ReplaceAll(s, "-", "") + }, + "lower": func(s string) string { + return strings.ToLower(s) + }, + } + + f, err := os.Create(file) + if err != nil { + return err + } + defer f.Close() + + t, err := template.New("f").Funcs(fn).Parse(tmpl) + if err != nil { + return err + } + + return t.Execute(f, c) +} + +func create(c config) error { + // check if dir exists + if _, err := os.Stat(c.Dir); !os.IsNotExist(err) { + return fmt.Errorf("%s already exists", c.Dir) + } + + fmt.Printf("Creating service %s\n\n", c.Alias) + + t := treeprint.New() + + // write the files + for _, file := range c.Files { + f := filepath.Join(c.Dir, file.Path) + dir := filepath.Dir(f) + + if _, err := os.Stat(dir); os.IsNotExist(err) { + if err := os.MkdirAll(dir, 0755); err != nil { + return err + } + } + + addFileToTree(t, file.Path) + if err := write(c, f, file.Tmpl); err != nil { + return err + } + } + + // print tree + fmt.Println(t.String()) + + for _, comment := range c.Comments { + fmt.Println(comment) + } + + // just wait + <-time.After(time.Millisecond * 250) + + return nil +} + +func addFileToTree(root treeprint.Tree, file string) { + split := strings.Split(file, "/") + curr := root + for i := 0; i < len(split)-1; i++ { + n := curr.FindByValue(split[i]) + if n != nil { + curr = n + } else { + curr = curr.AddBranch(split[i]) + } + } + if curr.FindByValue(split[len(split)-1]) == nil { + curr.AddNode(split[len(split)-1]) + } +} + +func Run(ctx *cli.Context) error { + dir := ctx.Args().First() + if len(dir) == 0 { + fmt.Println("specify service name") + return nil + } + + // check if the path is absolute, we don't want this + // we want to a relative path so we can install in GOPATH + if path.IsAbs(dir) { + fmt.Println("require relative path as service will be installed in GOPATH") + return nil + } + + // Check for protoc + if _, err := exec.LookPath("protoc"); err != nil { + fmt.Println("WARNING: protoc is not installed or not in your PATH.") + fmt.Println("Please install protoc from https://github.com/protocolbuffers/protobuf/releases") + fmt.Println("After installing, re-run 'make proto' in your service directory if needed.") + } + + var goPath string + var goDir string + + goPath = build.Default.GOPATH + + // don't know GOPATH, runaway.... + if len(goPath) == 0 { + fmt.Println("unknown GOPATH") + return nil + } + + // attempt to split path if not windows + if runtime.GOOS == "windows" { + goPath = strings.Split(goPath, ";")[0] + } else { + goPath = strings.Split(goPath, ":")[0] + } + goDir = filepath.Join(goPath, "src", path.Clean(dir)) + + c := config{ + Alias: dir, + Comments: nil, // Remove redundant protoComments + Dir: dir, + GoDir: goDir, + GoPath: goPath, + UseGoPath: false, + Files: []file{ + {"main.go", tmpl.MainSRV}, + {"handler/" + dir + ".go", tmpl.HandlerSRV}, + {"proto/" + dir + ".proto", tmpl.ProtoSRV}, + {"Makefile", tmpl.Makefile}, + {"README.md", tmpl.Readme}, + {".gitignore", tmpl.GitIgnore}, + }, + } + + // set gomodule + if os.Getenv("GO111MODULE") != "off" { + c.Files = append(c.Files, file{"go.mod", tmpl.Module}) + } + + // create the files + if err := create(c); err != nil { + return err + } + + // Run go mod tidy and make proto + fmt.Println("\nRunning 'go mod tidy' and 'make proto'...") + if err := runInDir(dir, "go mod tidy"); err != nil { + fmt.Printf("Error running 'go mod tidy': %v\n", err) + } + if err := runInDir(dir, "make proto"); err != nil { + fmt.Printf("Error running 'make proto': %v\n", err) + } + + // Print updated tree including generated files + fmt.Println("\nProject structure after 'make proto':") + printTree(dir) + + fmt.Println("\nService created successfully! Start coding in your new service directory.") + return nil +} + +func runInDir(dir, cmd string) error { + parts := strings.Fields(cmd) + c := exec.Command(parts[0], parts[1:]...) + c.Dir = dir + c.Stdout = os.Stdout + c.Stderr = os.Stderr + return c.Run() +} + +func printTree(dir string) { + t := treeprint.New() + walk := func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + rel, _ := filepath.Rel(dir, path) + if rel == "." { + return nil + } + parts := strings.Split(rel, string(os.PathSeparator)) + curr := t + for i := 0; i < len(parts)-1; i++ { + n := curr.FindByValue(parts[i]) + if n != nil { + curr = n + } else { + curr = curr.AddBranch(parts[i]) + } + } + if !info.IsDir() { + curr.AddNode(parts[len(parts)-1]) + } + return nil + } + filepath.Walk(dir, walk) + fmt.Println(t.String()) +} diff --git a/cmd/micro/cli/new/template/handler.go b/cmd/micro/cli/new/template/handler.go new file mode 100644 index 00000000..16e3160e --- /dev/null +++ b/cmd/micro/cli/new/template/handler.go @@ -0,0 +1,66 @@ +package template + +var ( + HandlerSRV = `package handler + +import ( + "context" + + log "go-micro.dev/v5/logger" + + pb "{{.Dir}}/proto" +) + +type {{title .Alias}} struct{} + +// Return a new handler +func New() *{{title .Alias}} { + return &{{title .Alias}}{} +} + +// Call is a single request handler called via client.Call or the generated client code +func (e *{{title .Alias}}) Call(ctx context.Context, req *pb.Request, rsp *pb.Response) error { + log.Info("Received {{title .Alias}}.Call request") + rsp.Msg = "Hello " + req.Name + return nil +} + +// Stream is a server side stream handler called via client.Stream or the generated client code +func (e *{{title .Alias}}) Stream(ctx context.Context, req *pb.StreamingRequest, stream pb.{{title .Alias}}_StreamStream) error { + log.Infof("Received {{title .Alias}}.Stream request with count: %d", req.Count) + + for i := 0; i < int(req.Count); i++ { + log.Infof("Responding: %d", i) + if err := stream.Send(&pb.StreamingResponse{ + Count: int64(i), + }); err != nil { + return err + } + } + + return nil +} +` + + SubscriberSRV = `package subscriber + +import ( + "context" + log "go-micro.dev/v5/logger" + + pb "{{.Dir}}/proto" +) + +type {{title .Alias}} struct{} + +func (e *{{title .Alias}}) Handle(ctx context.Context, msg *pb.Message) error { + log.Info("Handler Received message: ", msg.Say) + return nil +} + +func Handler(ctx context.Context, msg *pb.Message) error { + log.Info("Function Received message: ", msg.Say) + return nil +} +` +) diff --git a/cmd/micro/cli/new/template/ignore.go b/cmd/micro/cli/new/template/ignore.go new file mode 100644 index 00000000..6e2d569e --- /dev/null +++ b/cmd/micro/cli/new/template/ignore.go @@ -0,0 +1,7 @@ +package template + +var ( + GitIgnore = ` +{{.Alias}} +` +) diff --git a/cmd/micro/cli/new/template/main.go b/cmd/micro/cli/new/template/main.go new file mode 100644 index 00000000..ddcb72c4 --- /dev/null +++ b/cmd/micro/cli/new/template/main.go @@ -0,0 +1,27 @@ +package template + +var ( + MainSRV = `package main + +import ( + "{{.Dir}}/handler" + pb "{{.Dir}}/proto" + + "go-micro.dev/v5" +) + +func main() { + // Create service + service := micro.New("{{lower .Alias}}") + + // Initialize service + service.Init() + + // Register handler + pb.Register{{title .Alias}}Handler(service.Server(), handler.New()) + + // Run service + service.Run() +} +` +) diff --git a/cmd/micro/cli/new/template/makefile.go b/cmd/micro/cli/new/template/makefile.go new file mode 100644 index 00000000..07112b4d --- /dev/null +++ b/cmd/micro/cli/new/template/makefile.go @@ -0,0 +1,32 @@ +package template + +var ( + Makefile = ` +GOPATH:=$(shell go env GOPATH) +.PHONY: init +init: + go install google.golang.org/protobuf/cmd/protoc-gen-go@latest + go install go-micro.dev/v5/cmd/protoc-gen-micro@latest + go install github.com/google/gnostic/cmd/protoc-gen-openapi@latest + +.PHONY: api +api: + protoc --openapi_out=. --proto_path=. proto/{{.Alias}}.proto + +.PHONY: proto +proto: + protoc --proto_path=. --micro_out=. --go_out=:. proto/{{.Alias}}.proto + +.PHONY: build +build: + go build -o {{.Alias}} *.go + +.PHONY: test +test: + go test -v ./... -cover + +.PHONY: docker +docker: + docker build . -t {{.Alias}}:latest +` +) diff --git a/cmd/micro/cli/new/template/module.go b/cmd/micro/cli/new/template/module.go new file mode 100644 index 00000000..58097f9b --- /dev/null +++ b/cmd/micro/cli/new/template/module.go @@ -0,0 +1,14 @@ +package template + +var ( + Module = `module {{.Dir}} + +go 1.18 + +require ( + go-micro.dev/v5 latest + github.com/golang/protobuf latest + google.golang.org/protobuf latest +) +` +) diff --git a/cmd/micro/cli/new/template/proto.go b/cmd/micro/cli/new/template/proto.go new file mode 100644 index 00000000..d7c4b09f --- /dev/null +++ b/cmd/micro/cli/new/template/proto.go @@ -0,0 +1,35 @@ +package template + +var ( + ProtoSRV = `syntax = "proto3"; + +package {{dehyphen .Alias}}; + +option go_package = "./proto;{{dehyphen .Alias}}"; + +service {{title .Alias}} { + rpc Call(Request) returns (Response) {} + rpc Stream(StreamingRequest) returns (stream StreamingResponse) {} +} + +message Message { + string say = 1; +} + +message Request { + string name = 1; +} + +message Response { + string msg = 1; +} + +message StreamingRequest { + int64 count = 1; +} + +message StreamingResponse { + int64 count = 1; +} +` +) diff --git a/cmd/micro/cli/new/template/readme.go b/cmd/micro/cli/new/template/readme.go new file mode 100644 index 00000000..cb5150fc --- /dev/null +++ b/cmd/micro/cli/new/template/readme.go @@ -0,0 +1,30 @@ +package template + +var ( + Readme = `# {{title .Alias}} Service + +This is the {{title .Alias}} service + +Generated with + +` + "```" + + ` +micro new {{.Alias}} +` + "```" + ` + +## Usage + +Generate the proto code + +` + "```" + + ` +make proto +` + "```" + ` + +Run the service + +` + "```" + + ` +micro run . +` + "```" +) diff --git a/cmd/micro/cli/util/dynamic.go b/cmd/micro/cli/util/dynamic.go new file mode 100644 index 00000000..51e8cc05 --- /dev/null +++ b/cmd/micro/cli/util/dynamic.go @@ -0,0 +1,416 @@ +package util + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "math" + "os" + "sort" + "strconv" + "strings" + "unicode" + + "github.com/stretchr/objx" + "github.com/urfave/cli/v2" + "go-micro.dev/v5/client" + "go-micro.dev/v5/registry" +) + +// LookupService queries the service for a service with the given alias. If +// no services are found for a given alias, the registry will return nil and +// the error will also be nil. An error is only returned if there was an issue +// listing from the registry. +func LookupService(name string) (*registry.Service, error) { + // return a lookup in the default domain as a catch all + return serviceWithName(name) +} + +// FormatServiceUsage returns a string containing the service usage. +func FormatServiceUsage(srv *registry.Service, c *cli.Context) string { + alias := c.Args().First() + subcommand := c.Args().Get(1) + + commands := make([]string, len(srv.Endpoints)) + endpoints := make([]*registry.Endpoint, len(srv.Endpoints)) + for i, e := range srv.Endpoints { + // map "Helloworld.Call" to "helloworld.call" + parts := strings.Split(e.Name, ".") + for i, part := range parts { + parts[i] = lowercaseInitial(part) + } + name := strings.Join(parts, ".") + + // remove the prefix if it is the service name, e.g. rather than + // "micro run helloworld helloworld call", it would be + // "micro run helloworld call". + name = strings.TrimPrefix(name, alias+".") + + // instead of "micro run helloworld foo.bar", the command should + // be "micro run helloworld foo bar". + commands[i] = strings.Replace(name, ".", " ", 1) + endpoints[i] = e + } + + result := "" + if len(subcommand) > 0 && subcommand != "--help" { + result += fmt.Sprintf("NAME:\n\tmicro %v %v\n\n", alias, subcommand) + result += fmt.Sprintf("USAGE:\n\tmicro %v %v [flags]\n\n", alias, subcommand) + result += fmt.Sprintf("FLAGS:\n") + + for i, command := range commands { + if command == subcommand { + result += renderFlags(endpoints[i]) + } + } + } else { + // sort the command names alphabetically + sort.Strings(commands) + + result += fmt.Sprintf("NAME:\n\tmicro %v\n\n", alias) + result += fmt.Sprintf("VERSION:\n\t%v\n\n", srv.Version) + result += fmt.Sprintf("USAGE:\n\tmicro %v [command]\n\n", alias) + result += fmt.Sprintf("COMMANDS:\n\t%v\n", strings.Join(commands, "\n\t")) + + } + + return result +} + +func lowercaseInitial(str string) string { + for i, v := range str { + return string(unicode.ToLower(v)) + str[i+1:] + } + return "" +} + +func renderFlags(endpoint *registry.Endpoint) string { + ret := "" + for _, value := range endpoint.Request.Values { + ret += renderValue([]string{}, value) + "\n" + } + return ret +} + +func renderValue(path []string, value *registry.Value) string { + if len(value.Values) > 0 { + renders := []string{} + for _, v := range value.Values { + renders = append(renders, renderValue(append(path, value.Name), v)) + } + return strings.Join(renders, "\n") + } + return fmt.Sprintf("\t--%v %v", strings.Join(append(path, value.Name), "_"), value.Type) +} + +// CallService will call a service using the arguments and flags provided +// in the context. It will print the result or error to stdout. If there +// was an error performing the call, it will be returned. +func CallService(srv *registry.Service, args []string) error { + // parse the flags and args + args, flags, err := splitCmdArgs(args) + if err != nil { + return err + } + + // construct the endpoint + endpoint, err := constructEndpoint(args) + if err != nil { + return err + } + + // ensure the endpoint exists on the service + var ep *registry.Endpoint + for _, e := range srv.Endpoints { + if e.Name == endpoint { + ep = e + break + } + } + if ep == nil { + return fmt.Errorf("Endpoint %v not found for service %v", endpoint, srv.Name) + } + + // parse the flags + body, err := FlagsToRequest(flags, ep.Request) + if err != nil { + return err + } + + // create a context for the call based on the cli context + callCtx := context.TODO() + + // TODO: parse out --header or --metadata + + // construct and execute the request using the json content type + req := client.DefaultClient.NewRequest(srv.Name, endpoint, body, client.WithContentType("application/json")) + var rsp json.RawMessage + + if err := client.DefaultClient.Call(callCtx, req, &rsp); err != nil { + return err + } + + // format the response + var out bytes.Buffer + defer out.Reset() + if err := json.Indent(&out, rsp, "", "\t"); err != nil { + return err + } + out.Write([]byte("\n")) + out.WriteTo(os.Stdout) + + return nil +} + +// splitCmdArgs takes a cli context and parses out the args and flags, for +// example "micro helloworld --name=foo call apple" would result in "call", +// "apple" as args and {"name":"foo"} as the flags. +func splitCmdArgs(arguments []string) ([]string, map[string][]string, error) { + args := []string{} + flags := map[string][]string{} + + prev := "" + for _, a := range arguments { + if !strings.HasPrefix(a, "--") { + if len(prev) == 0 { + args = append(args, a) + continue + } + _, exists := flags[prev] + if !exists { + flags[prev] = []string{} + } + + flags[prev] = append(flags[prev], a) + prev = "" + continue + } + + // comps would be "foo", "bar" for "--foo=bar" + comps := strings.Split(strings.TrimPrefix(a, "--"), "=") + _, exists := flags[comps[0]] + if !exists { + flags[comps[0]] = []string{} + } + switch len(comps) { + case 1: + prev = comps[0] + case 2: + flags[comps[0]] = append(flags[comps[0]], comps[1]) + default: + return nil, nil, fmt.Errorf("Invalid flag: %v. Expected format: --foo=bar", a) + } + } + + return args, flags, nil +} + +// constructEndpoint takes a slice of args and converts it into a valid endpoint +// such as Helloworld.Call or Foo.Bar, it will return an error if an invalid number +// of arguments were provided +func constructEndpoint(args []string) (string, error) { + var epComps []string + switch len(args) { + case 1: + epComps = append(args, "call") + case 2: + epComps = args + case 3: + epComps = args[1:3] + default: + return "", fmt.Errorf("Incorrect number of arguments") + } + + // transform the endpoint components, e.g ["helloworld", "call"] to the + // endpoint name: "Helloworld.Call". + return fmt.Sprintf("%v.%v", strings.Title(epComps[0]), strings.Title(epComps[1])), nil +} + +// ShouldRenderHelp returns true if the help flag was passed +func ShouldRenderHelp(args []string) bool { + args, flags, _ := splitCmdArgs(args) + + // only 1 arg e.g micro helloworld + if len(args) == 1 { + return true + } + + for key := range flags { + if key == "help" { + return true + } + } + + return false +} + +// FlagsToRequest parses a set of flags, e.g {name:"Foo", "options_surname","Bar"} and +// converts it into a request body. If the key is not a valid object in the request, an +// error will be returned. +// +// This function constructs []interface{} slices +// as opposed to typed ([]string etc) slices for easier testing +func FlagsToRequest(flags map[string][]string, req *registry.Value) (map[string]interface{}, error) { + coerceValue := func(valueType string, value []string) (interface{}, error) { + switch valueType { + case "bool": + if len(value) == 0 || len(strings.TrimSpace(value[0])) == 0 { + return true, nil + } + return strconv.ParseBool(value[0]) + case "int32": + i, err := strconv.Atoi(value[0]) + if err != nil { + return nil, err + } + if i < math.MinInt32 || i > math.MaxInt32 { + return nil, fmt.Errorf("value out of range for int32: %d", i) + } + return int32(i), nil + case "int64": + return strconv.ParseInt(value[0], 0, 64) + case "float64": + return strconv.ParseFloat(value[0], 64) + case "[]bool": + // length is one if it's a `,` separated int slice + if len(value) == 1 { + value = strings.Split(value[0], ",") + } + ret := []interface{}{} + for _, v := range value { + i, err := strconv.ParseBool(v) + if err != nil { + return nil, err + } + ret = append(ret, i) + } + return ret, nil + case "[]int32": + // length is one if it's a `,` separated int slice + if len(value) == 1 { + value = strings.Split(value[0], ",") + } + ret := []interface{}{} + for _, v := range value { + i, err := strconv.Atoi(v) + if err != nil { + return nil, err + } + if i < math.MinInt32 || i > math.MaxInt32 { + return nil, fmt.Errorf("value out of range for int32: %d", i) + } + ret = append(ret, int32(i)) + } + return ret, nil + case "[]int64": + // length is one if it's a `,` separated int slice + if len(value) == 1 { + value = strings.Split(value[0], ",") + } + ret := []interface{}{} + for _, v := range value { + i, err := strconv.ParseInt(v, 0, 64) + if err != nil { + return nil, err + } + ret = append(ret, i) + } + return ret, nil + case "[]float64": + // length is one if it's a `,` separated float slice + if len(value) == 1 { + value = strings.Split(value[0], ",") + } + ret := []interface{}{} + for _, v := range value { + i, err := strconv.ParseFloat(v, 64) + if err != nil { + return nil, err + } + ret = append(ret, i) + } + return ret, nil + case "[]string": + // length is one it's a `,` separated string slice + if len(value) == 1 { + value = strings.Split(value[0], ",") + } + ret := []interface{}{} + for _, v := range value { + ret = append(ret, v) + } + return ret, nil + case "string": + return value[0], nil + case "map[string]string": + var val map[string]string + if err := json.Unmarshal([]byte(value[0]), &val); err != nil { + return value[0], nil + } + return val, nil + default: + return value, nil + } + return nil, nil + } + + result := objx.MustFromJSON("{}") + + var flagType func(key string, values []*registry.Value, path ...string) (string, bool) + + flagType = func(key string, values []*registry.Value, path ...string) (string, bool) { + for _, attr := range values { + if strings.Join(append(path, attr.Name), "-") == key { + return attr.Type, true + } + if attr.Values != nil { + typ, found := flagType(key, attr.Values, append(path, attr.Name)...) + if found { + return typ, found + } + } + } + return "", false + } + + for key, value := range flags { + ty, found := flagType(key, req.Values) + if !found { + return nil, fmt.Errorf("Unknown flag: %v", key) + } + parsed, err := coerceValue(ty, value) + if err != nil { + return nil, err + } + // objx.Set does not create the path, + // so we do that here + if strings.Contains(key, "-") { + parts := strings.Split(key, "-") + for i, _ := range parts { + pToCreate := strings.Join(parts[0:i], ".") + if i > 0 && i < len(parts) && !result.Has(pToCreate) { + result.Set(pToCreate, map[string]interface{}{}) + } + } + } + path := strings.Replace(key, "-", ".", -1) + result.Set(path, parsed) + } + + return result, nil +} + +// find a service in a domain matching the name +func serviceWithName(name string) (*registry.Service, error) { + srvs, err := registry.GetService(name) + if err == registry.ErrNotFound { + return nil, nil + } else if err != nil { + return nil, err + } + if len(srvs) == 0 { + return nil, nil + } + return srvs[0], nil +} diff --git a/cmd/micro/cli/util/dynamic_test.go b/cmd/micro/cli/util/dynamic_test.go new file mode 100644 index 00000000..ba59a9b2 --- /dev/null +++ b/cmd/micro/cli/util/dynamic_test.go @@ -0,0 +1,379 @@ +package util + +import ( + "reflect" + "strings" + "testing" + + "github.com/davecgh/go-spew/spew" + goregistry "go-micro.dev/v5/registry" +) + +type parseCase struct { + args []string + values *goregistry.Value + expected map[string]interface{} +} + +func TestDynamicFlagParsing(t *testing.T) { + cases := []parseCase{ + { + args: []string{"--ss=a,b"}, + values: &goregistry.Value{ + Values: []*goregistry.Value{ + { + Name: "ss", + Type: "[]string", + }, + }, + }, + expected: map[string]interface{}{ + "ss": []interface{}{"a", "b"}, + }, + }, + { + args: []string{"--ss", "a,b"}, + values: &goregistry.Value{ + Values: []*goregistry.Value{ + { + Name: "ss", + Type: "[]string", + }, + }, + }, + expected: map[string]interface{}{ + "ss": []interface{}{"a", "b"}, + }, + }, + { + args: []string{"--ss=a", "--ss=b"}, + values: &goregistry.Value{ + Values: []*goregistry.Value{ + { + Name: "ss", + Type: "[]string", + }, + }, + }, + expected: map[string]interface{}{ + "ss": []interface{}{"a", "b"}, + }, + }, + { + args: []string{"--ss", "a", "--ss", "b"}, + values: &goregistry.Value{ + Values: []*goregistry.Value{ + { + Name: "ss", + Type: "[]string", + }, + }, + }, + expected: map[string]interface{}{ + "ss": []interface{}{"a", "b"}, + }, + }, + { + args: []string{"--bs=true,false"}, + values: &goregistry.Value{ + Values: []*goregistry.Value{ + { + Name: "bs", + Type: "[]bool", + }, + }, + }, + expected: map[string]interface{}{ + "bs": []interface{}{true, false}, + }, + }, + { + args: []string{"--bs", "true,false"}, + values: &goregistry.Value{ + Values: []*goregistry.Value{ + { + Name: "bs", + Type: "[]bool", + }, + }, + }, + expected: map[string]interface{}{ + "bs": []interface{}{true, false}, + }, + }, + { + args: []string{"--bs=true", "--bs=false"}, + values: &goregistry.Value{ + Values: []*goregistry.Value{ + { + Name: "bs", + Type: "[]bool", + }, + }, + }, + expected: map[string]interface{}{ + "bs": []interface{}{true, false}, + }, + }, + { + args: []string{"--bs", "true", "--bs", "false"}, + values: &goregistry.Value{ + Values: []*goregistry.Value{ + { + Name: "bs", + Type: "[]bool", + }, + }, + }, + expected: map[string]interface{}{ + "bs": []interface{}{true, false}, + }, + }, + { + args: []string{"--is=10,20"}, + values: &goregistry.Value{ + Values: []*goregistry.Value{ + { + Name: "is", + Type: "[]int32", + }, + }, + }, + expected: map[string]interface{}{ + "is": []interface{}{int32(10), int32(20)}, + }, + }, + { + args: []string{"--is", "10,20"}, + values: &goregistry.Value{ + Values: []*goregistry.Value{ + { + Name: "is", + Type: "[]int32", + }, + }, + }, + expected: map[string]interface{}{ + "is": []interface{}{int32(10), int32(20)}, + }, + }, + { + args: []string{"--is=10", "--is=20"}, + values: &goregistry.Value{ + Values: []*goregistry.Value{ + { + Name: "is", + Type: "[]int32", + }, + }, + }, + expected: map[string]interface{}{ + "is": []interface{}{int32(10), int32(20)}, + }, + }, + { + args: []string{"--is", "10", "--is", "20"}, + values: &goregistry.Value{ + Values: []*goregistry.Value{ + { + Name: "is", + Type: "[]int32", + }, + }, + }, + expected: map[string]interface{}{ + "is": []interface{}{int32(10), int32(20)}, + }, + }, + { + args: []string{"--is=10,20"}, + values: &goregistry.Value{ + Values: []*goregistry.Value{ + { + Name: "is", + Type: "[]int64", + }, + }, + }, + expected: map[string]interface{}{ + "is": []interface{}{int64(10), int64(20)}, + }, + }, + { + args: []string{"--is", "10,20"}, + values: &goregistry.Value{ + Values: []*goregistry.Value{ + { + Name: "is", + Type: "[]int64", + }, + }, + }, + expected: map[string]interface{}{ + "is": []interface{}{int64(10), int64(20)}, + }, + }, + { + args: []string{"--is=10", "--is=20"}, + values: &goregistry.Value{ + Values: []*goregistry.Value{ + { + Name: "is", + Type: "[]int64", + }, + }, + }, + expected: map[string]interface{}{ + "is": []interface{}{int64(10), int64(20)}, + }, + }, + { + args: []string{"--is", "10", "--is", "20"}, + values: &goregistry.Value{ + Values: []*goregistry.Value{ + { + Name: "is", + Type: "[]int64", + }, + }, + }, + expected: map[string]interface{}{ + "is": []interface{}{int64(10), int64(20)}, + }, + }, + { + args: []string{"--fs=10.1,20.2"}, + values: &goregistry.Value{ + Values: []*goregistry.Value{ + { + Name: "fs", + Type: "[]float64", + }, + }, + }, + expected: map[string]interface{}{ + "fs": []interface{}{float64(10.1), float64(20.2)}, + }, + }, + { + args: []string{"--fs", "10.1,20.2"}, + values: &goregistry.Value{ + Values: []*goregistry.Value{ + { + Name: "fs", + Type: "[]float64", + }, + }, + }, + expected: map[string]interface{}{ + "fs": []interface{}{float64(10.1), float64(20.2)}, + }, + }, + { + args: []string{"--fs=10.1", "--fs=20.2"}, + values: &goregistry.Value{ + Values: []*goregistry.Value{ + { + Name: "fs", + Type: "[]float64", + }, + }, + }, + expected: map[string]interface{}{ + "fs": []interface{}{float64(10.1), float64(20.2)}, + }, + }, + { + args: []string{"--fs", "10.1", "--fs", "20.2"}, + values: &goregistry.Value{ + Values: []*goregistry.Value{ + { + Name: "fs", + Type: "[]float64", + }, + }, + }, + expected: map[string]interface{}{ + "fs": []interface{}{float64(10.1), float64(20.2)}, + }, + }, + { + args: []string{"--user_email=someemail"}, + values: &goregistry.Value{ + Values: []*goregistry.Value{ + { + Name: "user_email", + Type: "string", + }, + }, + }, + expected: map[string]interface{}{ + "user_email": "someemail", + }, + }, + { + args: []string{"--user_email=someemail", "--user_name=somename"}, + values: &goregistry.Value{ + Values: []*goregistry.Value{ + { + Name: "user_email", + Type: "string", + }, + { + Name: "user_name", + Type: "string", + }, + }, + }, + expected: map[string]interface{}{ + "user_email": "someemail", + "user_name": "somename", + }, + }, + { + args: []string{"--b"}, + values: &goregistry.Value{ + Values: []*goregistry.Value{ + { + Name: "b", + Type: "bool", + }, + }, + }, + expected: map[string]interface{}{ + "b": true, + }, + }, + { + args: []string{"--user_friend_email=hi"}, + values: &goregistry.Value{ + Values: []*goregistry.Value{ + { + Name: "user_friend_email", + Type: "string", + }, + }, + }, + expected: map[string]interface{}{ + "user_friend_email": "hi", + }, + }, + } + for _, c := range cases { + t.Run(strings.Join(c.args, " "), func(t *testing.T) { + _, flags, err := splitCmdArgs(c.args) + if err != nil { + t.Fatal(err) + } + req, err := FlagsToRequest(flags, c.values) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(c.expected, req) { + spew.Dump("Expected:", c.expected, "got: ", req) + t.Fatalf("Expected %v, got %v", c.expected, req) + } + }) + + } +} diff --git a/cmd/micro/cli/util/util.go b/cmd/micro/cli/util/util.go new file mode 100644 index 00000000..d3636c58 --- /dev/null +++ b/cmd/micro/cli/util/util.go @@ -0,0 +1,72 @@ +// Package cliutil contains methods used across all cli commands +// @todo: get rid of os.Exits and use errors instread +package util + +import ( + "fmt" + "regexp" + "strings" + + "github.com/urfave/cli/v2" + merrors "go-micro.dev/v5/errors" +) + +type Exec func(*cli.Context, []string) ([]byte, error) + +func Print(e Exec) func(*cli.Context) error { + return func(c *cli.Context) error { + rsp, err := e(c, c.Args().Slice()) + if err != nil { + return CliError(err) + } + if len(rsp) > 0 { + fmt.Printf("%s\n", string(rsp)) + } + return nil + } +} + +// CliError returns a user friendly message from error. If we can't determine a good one returns an error with code 128 +func CliError(err error) cli.ExitCoder { + if err == nil { + return nil + } + // if it's already a cli.ExitCoder we use this + cerr, ok := err.(cli.ExitCoder) + if ok { + return cerr + } + + // grpc errors + if mname := regexp.MustCompile(`malformed method name: \\?"(\w+)\\?"`).FindStringSubmatch(err.Error()); len(mname) > 0 { + return cli.Exit(fmt.Sprintf(`Method name "%s" invalid format. Expecting service.endpoint`, mname[1]), 3) + } + if service := regexp.MustCompile(`service ([\w\.]+): route not found`).FindStringSubmatch(err.Error()); len(service) > 0 { + return cli.Exit(fmt.Sprintf(`Service "%s" not found`, service[1]), 4) + } + if service := regexp.MustCompile(`unknown service ([\w\.]+)`).FindStringSubmatch(err.Error()); len(service) > 0 { + if strings.Contains(service[0], ".") { + return cli.Exit(fmt.Sprintf(`Service method "%s" not found`, service[1]), 5) + } + return cli.Exit(fmt.Sprintf(`Service "%s" not found`, service[1]), 5) + } + if address := regexp.MustCompile(`Error while dialing dial tcp.*?([\w]+\.[\w:\.]+): `).FindStringSubmatch(err.Error()); len(address) > 0 { + return cli.Exit(fmt.Sprintf(`Failed to connect to micro server at %s`, address[1]), 4) + } + + merr, ok := err.(*merrors.Error) + if !ok { + return cli.Exit(err, 128) + } + + switch merr.Code { + case 408: + return cli.Exit("Request timed out", 1) + case 401: + // TODO check if not signed in, prompt to sign in + return cli.Exit("Not authorized to perform this request", 2) + } + + // fallback to using the detail from the merr + return cli.Exit(merr.Detail, 127) +} diff --git a/cmd/micro/main.go b/cmd/micro/main.go new file mode 100644 index 00000000..20700173 --- /dev/null +++ b/cmd/micro/main.go @@ -0,0 +1,26 @@ +package main + +import ( + "embed" + "go-micro.dev/v5/cmd" + + _ "go-micro.dev/v5/cmd/micro/cli" + _ "go-micro.dev/v5/cmd/micro/run" + "go-micro.dev/v5/cmd/micro/server" +) + +//go:embed web/styles.css web/main.js web/templates/* +var webFS embed.FS + +var version = "5.0.0-dev" + +func init() { + server.HTML = webFS +} + +func main() { + cmd.Init( + cmd.Name("micro"), + cmd.Version(version), + ) +} diff --git a/cmd/micro/run/run.go b/cmd/micro/run/run.go new file mode 100644 index 00000000..520966a4 --- /dev/null +++ b/cmd/micro/run/run.go @@ -0,0 +1,227 @@ +package run + +import ( + "bufio" + "crypto/md5" + "fmt" + "io" + "os" + "os/exec" + "os/signal" + "path/filepath" + "strconv" + "strings" + "syscall" + "time" + + "github.com/urfave/cli/v2" + "go-micro.dev/v5/cmd" +) + +// Color codes for log output +var colors = []string{ + "\033[31m", // red + "\033[32m", // green + "\033[33m", // yellow + "\033[34m", // blue + "\033[35m", // magenta + "\033[36m", // cyan +} + +func colorFor(idx int) string { + return colors[idx%len(colors)] +} + +func Run(c *cli.Context) error { + dir := c.Args().Get(0) + var tmpDir string + if len(dir) == 0 { + dir = "." + } else if strings.HasPrefix(dir, "github.com/") || strings.HasPrefix(dir, "https://github.com/") { + // Handle git URLs + repo := dir + if strings.HasPrefix(repo, "https://") { + repo = strings.TrimPrefix(repo, "https://") + } + // Clone to a temp directory + tmp, err := os.MkdirTemp("", "micro-run-") + if err != nil { + return fmt.Errorf("failed to create temp dir: %w", err) + } + tmpDir = tmp + cloneURL := repo + if !strings.HasPrefix(cloneURL, "https://") { + cloneURL = "https://" + repo + } + // Run git clone + cmd := exec.Command("git", "clone", cloneURL, tmpDir) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to clone repo %s: %w", cloneURL, err) + } + dir = tmpDir + } + + homeDir, err := os.UserHomeDir() + if err != nil { + return fmt.Errorf("failed to get home dir: %w", err) + } + logsDir := filepath.Join(homeDir, "micro", "logs") + if err := os.MkdirAll(logsDir, 0755); err != nil { + return fmt.Errorf("failed to create logs dir: %w", err) + } + runDir := filepath.Join(homeDir, "micro", "run") + if err := os.MkdirAll(runDir, 0755); err != nil { + return fmt.Errorf("failed to create run dir: %w", err) + } + binDir := filepath.Join(homeDir, "micro", "bin") + if err := os.MkdirAll(binDir, 0755); err != nil { + return fmt.Errorf("failed to create bin dir: %w", err) + } + + // Always run all services (find all main.go) + var mainFiles []string + err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + return nil + } + if info.Name() == "main.go" { + mainFiles = append(mainFiles, path) + } + return nil + }) + if err != nil { + return fmt.Errorf("error walking the path: %w", err) + } + if len(mainFiles) == 0 { + return fmt.Errorf("no main.go files found in %s", dir) + } + var procs []*exec.Cmd + var pidFiles []string + for i, mainFile := range mainFiles { + serviceDir := filepath.Dir(mainFile) + var serviceName string + absServiceDir, _ := filepath.Abs(serviceDir) + // Determine service name: if absServiceDir matches the provided dir (which may be "."), use cwd + if absServiceDir == dir { + cwd, _ := os.Getwd() + serviceName = filepath.Base(cwd) + } else { + serviceName = filepath.Base(serviceDir) + } + serviceNameForPid := serviceName + "-" + fmt.Sprintf("%x", md5.Sum([]byte(absServiceDir)))[:8] + logFilePath := filepath.Join(logsDir, serviceNameForPid+".log") + binPath := filepath.Join(binDir, serviceNameForPid) + pidFilePath := filepath.Join(runDir, serviceNameForPid+".pid") + + // Check if pid file exists and process is running + if pidBytes, err := os.ReadFile(pidFilePath); err == nil { + lines := strings.Split(string(pidBytes), "\n") + if len(lines) > 0 && len(lines[0]) > 0 { + pid := lines[0] + if _, err := os.FindProcess(parsePid(pid)); err == nil { + if processRunning(pid) { + fmt.Fprintf(os.Stderr, "Service %s already running (pid %s)\n", serviceNameForPid, pid) + continue + } + } + } + } + + logFile, err := os.OpenFile(logFilePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to open log file for %s: %v\n", serviceName, err) + continue + } + buildCmd := exec.Command("go", "build", "-o", binPath, ".") + buildCmd.Dir = serviceDir + buildOut, buildErr := buildCmd.CombinedOutput() + if buildErr != nil { + logFile.WriteString(string(buildOut)) + logFile.Close() + fmt.Fprintf(os.Stderr, "failed to build %s: %v\n", serviceName, buildErr) + continue + } + cmd := exec.Command(binPath) + cmd.Dir = serviceDir + pr, pw := io.Pipe() + cmd.Stdout = pw + cmd.Stderr = pw + color := colorFor(i) + go func(name string, color string, pr *io.PipeReader, logFile *os.File) { + defer logFile.Close() + scanner := bufio.NewScanner(pr) + for scanner.Scan() { + line := scanner.Text() + // Write to terminal with color and service name + fmt.Printf("%s[%s]\033[0m %s\n", color, name, line) + // Write to log file with service name prefix + logFile.WriteString("[" + name + "] " + line + "\n") + } + }(serviceName, color, pr, logFile) + if err := cmd.Start(); err != nil { + fmt.Fprintf(os.Stderr, "failed to start service %s: %v\n", serviceName, err) + pw.Close() + continue + } + procs = append(procs, cmd) + pidFiles = append(pidFiles, pidFilePath) + os.WriteFile(pidFilePath, []byte(fmt.Sprintf("%d\n%s\n%s\n%s\n", cmd.Process.Pid, absServiceDir, serviceName, time.Now().Format(time.RFC3339))), 0644) + } + ch := make(chan os.Signal, 1) + signal.Notify(ch, os.Interrupt) + go func() { + <-ch + for _, proc := range procs { + if proc.Process != nil { + _ = proc.Process.Kill() + } + } + for _, pf := range pidFiles { + _ = os.Remove(pf) + } + os.Exit(1) + }() + for _, proc := range procs { + _ = proc.Wait() + } + return nil +} + +// Add helpers for process check +func parsePid(pidStr string) int { + pid, _ := strconv.Atoi(pidStr) + return pid +} +func processRunning(pidStr string) bool { + pid := parsePid(pidStr) + if pid <= 0 { + return false + } + proc, err := os.FindProcess(pid) + if err != nil { + return false + } + // On Unix, sending signal 0 checks if process exists + return proc.Signal(syscall.Signal(0)) == nil +} + +func init() { + cmd.Register(&cli.Command{ + Name: "run", + Usage: "Run all services in a directory", + Action: Run, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "address", + Aliases: []string{"a"}, + Usage: "Address to bind the micro web UI (default :8080)", + Value: ":8080", + }, + }, + }) +} diff --git a/cmd/micro/server/server.go b/cmd/micro/server/server.go new file mode 100644 index 00000000..cc7510b4 --- /dev/null +++ b/cmd/micro/server/server.go @@ -0,0 +1,1072 @@ +package server + +import ( + "bytes" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/base64" + "encoding/json" + "encoding/pem" + "fmt" + "io" + "io/fs" + "log" + "net/http" + "os" + "path/filepath" + "sort" + "strconv" + "strings" + "sync" + "syscall" + "text/template" + "time" + + "github.com/urfave/cli/v2" + "go-micro.dev/v5/cmd" + "go-micro.dev/v5/registry" + "go-micro.dev/v5/store" + "golang.org/x/crypto/bcrypt" +) + +// HTML is the embedded filesystem for templates and static files, set by main.go +var HTML fs.FS + +var ( + apiCache struct { + sync.Mutex + data map[string]any + time time.Time + } +) + +type templates struct { + api *template.Template + service *template.Template + form *template.Template + home *template.Template + logs *template.Template + log *template.Template + status *template.Template + authTokens *template.Template + authLogin *template.Template + authUsers *template.Template +} + +type TemplateUser struct { + ID string +} + +// Define a local Account struct to replace auth.Account +// (matches fields used in the code) +type Account struct { + ID string `json:"id"` + Type string `json:"type"` + Scopes []string `json:"scopes"` + Metadata map[string]string `json:"metadata"` +} + +func parseTemplates() *templates { + return &templates{ + api: template.Must(template.ParseFS(HTML, "html/templates/base.html", "html/templates/api.html")), + service: template.Must(template.ParseFS(HTML, "html/templates/base.html", "html/templates/service.html")), + form: template.Must(template.ParseFS(HTML, "html/templates/base.html", "html/templates/form.html")), + home: template.Must(template.ParseFS(HTML, "html/templates/base.html", "html/templates/home.html")), + logs: template.Must(template.ParseFS(HTML, "html/templates/base.html", "html/templates/logs.html")), + log: template.Must(template.ParseFS(HTML, "html/templates/base.html", "html/templates/log.html")), + status: template.Must(template.ParseFS(HTML, "html/templates/base.html", "html/templates/status.html")), + authTokens: template.Must(template.ParseFS(HTML, "html/templates/base.html", "html/templates/auth_tokens.html")), + authLogin: template.Must(template.ParseFS(HTML, "html/templates/base.html", "html/templates/auth_login.html")), + authUsers: template.Must(template.ParseFS(HTML, "html/templates/base.html", "html/templates/auth_users.html")), + } +} + +// Helper to render templates +func render(w http.ResponseWriter, tmpl *template.Template, data any) error { + return tmpl.Execute(w, data) +} + +// Helper to extract user info from JWT cookie +func getUser(r *http.Request) string { + cookie, err := r.Cookie("micro_token") + if err != nil || cookie.Value == "" { + return "" + } + // Parse JWT claims (just decode, don't verify) + parts := strings.Split(cookie.Value, ".") + if len(parts) != 3 { + return "" + } + payload, err := decodeSegment(parts[1]) + if err != nil { + return "" + } + var claims map[string]any + if err := json.Unmarshal(payload, &claims); err != nil { + return "" + } + if sub, ok := claims["sub"].(string); ok { + return sub + } + if id, ok := claims["id"].(string); ok { + return id + } + return "" +} + +// Helper to decode JWT base64url segment +func decodeSegment(seg string) ([]byte, error) { + // JWT uses base64url, no padding + missing := len(seg) % 4 + if missing != 0 { + seg += strings.Repeat("=", 4-missing) + } + return decodeBase64Url(seg) +} + +func decodeBase64Url(s string) ([]byte, error) { + return base64.URLEncoding.DecodeString(s) +} + +// Helper: store JWT token +func storeJWTToken(storeInst store.Store, token, userID string) { + storeInst.Write(&store.Record{Key: "jwt/" + token, Value: []byte(userID)}) +} + +// Helper: check if JWT token is revoked (not present in store) +func isTokenRevoked(storeInst store.Store, token string) bool { + recs, _ := storeInst.Read("jwt/" + token) + return len(recs) == 0 +} + +// Helper: delete all JWT tokens for a user +func deleteUserTokens(storeInst store.Store, userID string) { + recs, _ := storeInst.Read("jwt/", store.ReadPrefix()) + for _, rec := range recs { + if string(rec.Value) == userID { + storeInst.Delete(rec.Key) + } + } +} + +// Updated authRequired to accept storeInst as argument +func authRequired(storeInst store.Store) func(http.HandlerFunc) http.HandlerFunc { + return func(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var token string + // 1. Check Authorization: Bearer header + authz := r.Header.Get("Authorization") + if strings.HasPrefix(authz, "Bearer ") { + token = strings.TrimPrefix(authz, "Bearer ") + token = strings.TrimSpace(token) + } + // 2. Fallback to micro_token cookie if no header + if token == "" { + cookie, err := r.Cookie("micro_token") + if err == nil && cookie.Value != "" { + token = cookie.Value + } + } + if token == "" { + if strings.HasPrefix(r.URL.Path, "/api/") && r.URL.Path != "/api" && r.URL.Path != "/api/" { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + w.Write([]byte(`{"error":"missing or invalid token"}`)) + return + } + // For API endpoints, return 401. For UI, redirect to login. + if strings.HasPrefix(r.URL.Path, "/api/") { + w.WriteHeader(http.StatusUnauthorized) + w.Write([]byte("Unauthorized: missing token")) + return + } + http.Redirect(w, r, "/auth/login", http.StatusFound) + return + } + claims, err := ParseJWT(token) + if err != nil { + if strings.HasPrefix(r.URL.Path, "/api/") && r.URL.Path != "/api" && r.URL.Path != "/api/" { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + w.Write([]byte(`{"error":"invalid token"}`)) + return + } + if strings.HasPrefix(r.URL.Path, "/api/") { + w.WriteHeader(http.StatusUnauthorized) + w.Write([]byte("Unauthorized: invalid token")) + return + } + http.Redirect(w, r, "/auth/login", http.StatusFound) + return + } + if exp, ok := claims["exp"].(float64); ok { + if int64(exp) < time.Now().Unix() { + if strings.HasPrefix(r.URL.Path, "/api/") && r.URL.Path != "/api" && r.URL.Path != "/api/" { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + w.Write([]byte(`{"error":"token expired"}`)) + return + } + if strings.HasPrefix(r.URL.Path, "/api/") { + w.WriteHeader(http.StatusUnauthorized) + w.Write([]byte("Unauthorized: token expired")) + return + } + http.Redirect(w, r, "/auth/login", http.StatusFound) + return + } + } + // Check for token revocation + if isTokenRevoked(storeInst, token) { + if strings.HasPrefix(r.URL.Path, "/api/") && r.URL.Path != "/api" && r.URL.Path != "/api/" { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + w.Write([]byte(`{"error":"token revoked"}`)) + return + } + if strings.HasPrefix(r.URL.Path, "/api/") { + w.WriteHeader(http.StatusUnauthorized) + w.Write([]byte("Unauthorized: token revoked")) + return + } + http.Redirect(w, r, "/auth/login", http.StatusFound) + return + } + next(w, r) + } + } +} + +func wrapAuth(authRequired func(http.HandlerFunc) http.HandlerFunc) func(http.HandlerFunc) http.HandlerFunc { + return func(h http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + path := r.URL.Path + if strings.HasPrefix(path, "/auth/login") || strings.HasPrefix(path, "/auth/logout") || + path == "/styles.css" || path == "/main.js" { + h(w, r) + return + } + authRequired(h)(w, r) + } + } +} + +func getDashboardData() (serviceCount, runningCount, stoppedCount int, statusDot string) { + homeDir, err := os.UserHomeDir() + if err != nil { + return + } + pidDir := homeDir + "/micro/run" + dirEntries, err := os.ReadDir(pidDir) + if err != nil { + return + } + for _, entry := range dirEntries { + if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".pid") || strings.HasPrefix(entry.Name(), ".") { + continue + } + pidFile := pidDir + "/" + entry.Name() + pidBytes, err := os.ReadFile(pidFile) + if err != nil { + continue + } + lines := strings.Split(string(pidBytes), "\n") + pid := "-" + if len(lines) > 0 && len(lines[0]) > 0 { + pid = lines[0] + } + serviceCount++ + if pid != "-" { + if _, err := os.FindProcess(parsePid(pid)); err == nil { + if processRunning(pid) { + runningCount++ + } else { + stoppedCount++ + } + } else { + stoppedCount++ + } + } else { + stoppedCount++ + } + } + if serviceCount > 0 && runningCount == serviceCount { + statusDot = "green" + } else if serviceCount > 0 && runningCount > 0 { + statusDot = "yellow" + } else { + statusDot = "red" + } + return +} + +func getSidebarEndpoints() ([]map[string]string, error) { + apiCache.Lock() + defer apiCache.Unlock() + if apiCache.data != nil && time.Since(apiCache.time) < 30*time.Second { + if v, ok := apiCache.data["SidebarEndpoints"]; ok { + if endpoints, ok := v.([]map[string]string); ok { + return endpoints, nil + } + } + } + services, err := registry.ListServices() + if err != nil { + return nil, err + } + var sidebarEndpoints []map[string]string + for _, srv := range services { + anchor := strings.ReplaceAll(srv.Name, ".", "-") + sidebarEndpoints = append(sidebarEndpoints, map[string]string{"Name": srv.Name, "Anchor": anchor}) + } + sort.Slice(sidebarEndpoints, func(i, j int) bool { + return sidebarEndpoints[i]["Name"] < sidebarEndpoints[j]["Name"] + }) + return sidebarEndpoints, nil +} + +func registerHandlers(tmpls *templates, storeInst store.Store) { + authMw := authRequired(storeInst) + wrap := wrapAuth(authMw) + + // Serve static files from root (not /html/) with correct Content-Type + http.HandleFunc("/styles.css", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/css; charset=utf-8") + f, err := HTML.Open("html/styles.css") + if err != nil { + w.WriteHeader(404) + return + } + defer f.Close() + io.Copy(w, f) + }) + + http.HandleFunc("/main.js", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/javascript; charset=utf-8") + f, err := HTML.Open("html/main.js") + if err != nil { + w.WriteHeader(404) + return + } + defer f.Close() + io.Copy(w, f) + }) + + // Serve /html/styles.css and /html/main.js for compatibility + http.HandleFunc("/html/styles.css", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/css; charset=utf-8") + f, err := HTML.Open("html/styles.css") + if err != nil { + w.WriteHeader(404) + return + } + defer f.Close() + io.Copy(w, f) + }) + http.HandleFunc("/html/main.js", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/javascript; charset=utf-8") + f, err := HTML.Open("html/main.js") + if err != nil { + w.WriteHeader(404) + return + } + defer f.Close() + io.Copy(w, f) + }) + + http.HandleFunc("/", wrap(func(w http.ResponseWriter, r *http.Request) { + path := r.URL.Path + if strings.HasPrefix(path, "/auth/") { + // Let the dedicated /auth/* handlers process this + return + } + userID := getUser(r) + var user any + if userID != "" { + user = &TemplateUser{ID: userID} + } else { + user = nil + } + if path == "/" { + serviceCount, runningCount, stoppedCount, statusDot := getDashboardData() + // Do NOT include SidebarEndpoints on home page + err := tmpls.home.Execute(w, map[string]any{ + "Title": "Micro Dashboard", + "WebLink": "/", + "ServiceCount": serviceCount, + "RunningCount": runningCount, + "StoppedCount": stoppedCount, + "StatusDot": statusDot, + "User": user, + // No SidebarEndpoints or SidebarEndpointsEnabled here + }) + if err != nil { + log.Printf("[TEMPLATE ERROR] home: %v", err) + } + return + } + if path == "/api" || path == "/api/" { + apiCache.Lock() + useCache := false + if apiCache.data != nil && time.Since(apiCache.time) < 30*time.Second { + useCache = true + } + var apiData map[string]any + var sidebarEndpoints []map[string]string + if useCache { + apiData = apiCache.data + if v, ok := apiData["SidebarEndpoints"]; ok { + sidebarEndpoints, _ = v.([]map[string]string) + } + } else { + services, _ := registry.ListServices() + var apiServices []map[string]any + for _, srv := range services { + srvs, err := registry.GetService(srv.Name) + if err != nil || len(srvs) == 0 { + continue + } + s := srvs[0] + if len(s.Endpoints) == 0 { + continue + } + endpoints := []map[string]any{} + for _, ep := range s.Endpoints { + parts := strings.Split(ep.Name, ".") + if len(parts) != 2 { + continue + } + apiPath := fmt.Sprintf("/api/%s/%s/%s", s.Name, parts[0], parts[1]) + var params, response string + if ep.Request != nil && len(ep.Request.Values) > 0 { + params += "
    " + for _, v := range ep.Request.Values { + params += fmt.Sprintf("
  • %s %s
  • ", v.Name, v.Type) + } + params += "
" + } else { + params = "No parameters" + } + if ep.Response != nil && len(ep.Response.Values) > 0 { + response += "
    " + for _, v := range ep.Response.Values { + response += fmt.Sprintf("
  • %s %s
  • ", v.Name, v.Type) + } + response += "
" + } else { + response = "No response fields" + } + endpoints = append(endpoints, map[string]any{ + "Name": ep.Name, + "Path": apiPath, + "Params": params, + "Response": response, + }) + } + anchor := strings.ReplaceAll(s.Name, ".", "-") + apiServices = append(apiServices, map[string]any{ + "Name": s.Name, + "Anchor": anchor, + "Endpoints": endpoints, + }) + sidebarEndpoints = append(sidebarEndpoints, map[string]string{"Name": s.Name, "Anchor": anchor}) + } + sort.Slice(sidebarEndpoints, func(i, j int) bool { + return sidebarEndpoints[i]["Name"] < sidebarEndpoints[j]["Name"] + }) + apiData = map[string]any{"Title": "API", "WebLink": "/", "Services": apiServices, "SidebarEndpoints": sidebarEndpoints, "SidebarEndpointsEnabled": true, "User": user} + apiCache.data = apiData + apiCache.time = time.Now() + } + apiCache.Unlock() + // Add API auth doc at the top + apiData["ApiAuthDoc"] = `
+API Authentication Required: All API calls to /api/... endpoints (except this page) must include an Authorization: Bearer <token> header.
+You can generate tokens on the Tokens page. +
` + _ = render(w, tmpls.api, apiData) + return + } + if path == "/services" { + // Do NOT include SidebarEndpoints on this page + services, _ := registry.ListServices() + var serviceNames []string + for _, service := range services { + serviceNames = append(serviceNames, service.Name) + } + sort.Strings(serviceNames) + _ = render(w, tmpls.service, map[string]any{"Title": "Services", "WebLink": "/", "Services": serviceNames, "User": user}) + return + } + if path == "/logs" || path == "/logs/" { + // Do NOT include SidebarEndpoints on this page + homeDir, err := os.UserHomeDir() + if err != nil { + w.WriteHeader(500) + w.Write([]byte("Could not get home directory")) + return + } + logsDir := homeDir + "/micro/logs" + dirEntries, err := os.ReadDir(logsDir) + if err != nil { + w.WriteHeader(500) + w.Write([]byte("Could not list logs directory: " + err.Error())) + return + } + serviceNames := []string{} + for _, entry := range dirEntries { + name := entry.Name() + if !entry.IsDir() && strings.HasSuffix(name, ".log") && !strings.HasPrefix(name, ".") { + serviceNames = append(serviceNames, strings.TrimSuffix(name, ".log")) + } + } + _ = render(w, tmpls.logs, map[string]any{"Title": "Logs", "WebLink": "/", "Services": serviceNames, "User": user}) + return + } + if strings.HasPrefix(path, "/logs/") { + // Do NOT include SidebarEndpoints on this page + service := strings.TrimPrefix(path, "/logs/") + if service == "" { + w.WriteHeader(404) + w.Write([]byte("Service not specified")) + return + } + homeDir, err := os.UserHomeDir() + if err != nil { + w.WriteHeader(500) + w.Write([]byte("Could not get home directory")) + return + } + logFilePath := homeDir + "/micro/logs/" + service + ".log" + f, err := os.Open(logFilePath) + if err != nil { + w.WriteHeader(404) + w.Write([]byte("Could not open log file for service: " + service)) + return + } + defer f.Close() + logBytes, err := io.ReadAll(f) + if err != nil { + w.WriteHeader(500) + w.Write([]byte("Could not read log file for service: " + service)) + return + } + logText := string(logBytes) + _ = render(w, tmpls.log, map[string]any{"Title": "Logs for " + service, "WebLink": "/logs", "Service": service, "Log": logText, "User": user}) + return + } + if path == "/status" { + // Do NOT include SidebarEndpoints on this page + homeDir, err := os.UserHomeDir() + if err != nil { + w.WriteHeader(500) + w.Write([]byte("Could not get home directory")) + return + } + pidDir := homeDir + "/micro/run" + dirEntries, err := os.ReadDir(pidDir) + if err != nil { + w.WriteHeader(500) + w.Write([]byte("Could not list pid directory: " + err.Error())) + return + } + statuses := []map[string]string{} + for _, entry := range dirEntries { + if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".pid") || strings.HasPrefix(entry.Name(), ".") { + continue + } + pidFile := pidDir + "/" + entry.Name() + pidBytes, err := os.ReadFile(pidFile) + if err != nil { + statuses = append(statuses, map[string]string{ + "Service": entry.Name(), + "Dir": "-", + "Status": "unknown", + "PID": "-", + "Uptime": "-", + "ID": strings.TrimSuffix(entry.Name(), ".pid"), + }) + continue + } + lines := strings.Split(string(pidBytes), "\n") + pid := "-" + dir := "-" + service := "-" + start := "-" + if len(lines) > 0 && len(lines[0]) > 0 { + pid = lines[0] + } + if len(lines) > 1 && len(lines[1]) > 0 { + dir = lines[1] + } + if len(lines) > 2 && len(lines[2]) > 0 { + service = lines[2] + } + if len(lines) > 3 && len(lines[3]) > 0 { + start = lines[3] + } + status := "stopped" + if pid != "-" { + if _, err := os.FindProcess(parsePid(pid)); err == nil { + if processRunning(pid) { + status = "running" + } + } else { + status = "stopped" + } + } + uptime := "-" + if start != "-" { + if t, err := parseStartTime(start); err == nil { + uptime = time.Since(t).Truncate(time.Second).String() + } + } + statuses = append(statuses, map[string]string{ + "Service": service, + "Dir": dir, + "Status": status, + "PID": pid, + "Uptime": uptime, + "ID": strings.TrimSuffix(entry.Name(), ".pid"), + }) + } + _ = render(w, tmpls.status, map[string]any{"Title": "Service Status", "WebLink": "/", "Statuses": statuses, "User": user}) + return + } + // Match /{service} and /{service}/{endpoint} + parts := strings.Split(strings.Trim(path, "/"), "/") + if len(parts) >= 1 && parts[0] != "api" && parts[0] != "html" && parts[0] != "services" { + service := parts[0] + if len(parts) == 1 { + s, err := registry.GetService(service) + if err != nil || len(s) == 0 { + w.WriteHeader(404) + w.Write([]byte(fmt.Sprintf("Service not found: %s", service))) + return + } + endpoints := []map[string]string{} + for _, ep := range s[0].Endpoints { + endpoints = append(endpoints, map[string]string{ + "Name": ep.Name, + "Path": fmt.Sprintf("/%s/%s", service, ep.Name), + }) + } + b, _ := json.MarshalIndent(s[0], "", " ") + _ = render(w, tmpls.service, map[string]any{ + "Title": "Service: " + service, + "WebLink": "/", + "ServiceName": service, + "Endpoints": endpoints, + "Description": string(b), + "User": user, + }) + return + } + if len(parts) == 2 { + service := parts[0] + endpoint := parts[1] // Use the actual endpoint name from the URL, e.g. Foo.Bar + s, err := registry.GetService(service) + if err != nil || len(s) == 0 { + w.WriteHeader(404) + w.Write([]byte("Service not found: " + service)) + return + } + var ep *registry.Endpoint + for _, eps := range s[0].Endpoints { + if eps.Name == endpoint { + ep = eps + break + } + } + if ep == nil { + w.WriteHeader(404) + w.Write([]byte("Endpoint not found")) + return + } + if r.Method == "GET" { + // Build form fields from endpoint request values + var inputs []map[string]string + if ep.Request != nil && len(ep.Request.Values) > 0 { + for _, input := range ep.Request.Values { + inputs = append(inputs, map[string]string{ + "Label": input.Name, + "Name": input.Name, + "Placeholder": input.Name, + "Value": "", + }) + } + } + _ = render(w, tmpls.form, map[string]any{ + "Title": "Service: " + service, + "WebLink": "/", + "ServiceName": service, + "EndpointName": ep.Name, + "Inputs": inputs, + "Action": service + "/" + endpoint, + "User": user, + }) + return + } + if r.Method == "POST" { + // Parse form values into a map + var reqBody map[string]interface{} + if strings.HasPrefix(r.Header.Get("Content-Type"), "application/json") { + defer r.Body.Close() + json.NewDecoder(r.Body).Decode(&reqBody) + } else { + reqBody = map[string]interface{}{} + r.ParseForm() + for k, v := range r.Form { + if len(v) == 1 { + if len(v[0]) == 0 { + continue + } + reqBody[k] = v[0] + } else { + reqBody[k] = v + } + } + } + // For now, just echo the request body as JSON + w.Header().Set("Content-Type", "application/json") + b, _ := json.MarshalIndent(reqBody, "", " ") + w.Write(b) + return + } + } + } + w.WriteHeader(404) + w.Write([]byte("Not found")) + })) + http.HandleFunc("/auth/logout", func(w http.ResponseWriter, r *http.Request) { + http.SetCookie(w, &http.Cookie{Name: "micro_token", Value: "", Path: "/", Expires: time.Now().Add(-1 * time.Hour), HttpOnly: true}) + http.Redirect(w, r, "/auth/login", http.StatusSeeOther) + }) + http.HandleFunc("/auth/tokens", authMw(func(w http.ResponseWriter, r *http.Request) { + userID := getUser(r) + var user any + if userID != "" { + user = &TemplateUser{ID: userID} + } else { + user = nil + } + if r.Method == "POST" { + id := r.FormValue("id") + typeStr := r.FormValue("type") + scopesStr := r.FormValue("scopes") + accType := "user" + if typeStr == "admin" { + accType = "admin" + } else if typeStr == "service" { + accType = "service" + } + scopes := []string{"*"} + if scopesStr != "" { + scopes = strings.Split(scopesStr, ",") + for i := range scopes { + scopes[i] = strings.TrimSpace(scopes[i]) + } + } + acc := &Account{ + ID: id, + Type: accType, + Scopes: scopes, + Metadata: map[string]string{"created": time.Now().Format(time.RFC3339)}, + } + // Service tokens do not require a password, generate a JWT directly + tok, _ := GenerateJWT(acc.ID, acc.Type, acc.Scopes, 24*time.Hour) + acc.Metadata["token"] = tok + b, _ := json.Marshal(acc) + storeInst.Write(&store.Record{Key: "auth/" + id, Value: b}) + storeJWTToken(storeInst, tok, acc.ID) // Store the JWT token + http.Redirect(w, r, "/auth/tokens", http.StatusSeeOther) + return + } + recs, _ := storeInst.Read("auth/", store.ReadPrefix()) + var tokens []map[string]any + for _, rec := range recs { + var acc Account + if err := json.Unmarshal(rec.Value, &acc); err == nil { + tok := "" + if t, ok := acc.Metadata["token"]; ok { + tok = t + } + var tokenPrefix, tokenSuffix string + if len(tok) > 12 { + tokenPrefix = tok[:4] + tokenSuffix = tok[len(tok)-4:] + } else { + tokenPrefix = tok + tokenSuffix = "" + } + tokens = append(tokens, map[string]any{ + "ID": acc.ID, + "Type": acc.Type, + "Scopes": acc.Scopes, + "Metadata": acc.Metadata, + "Token": tok, + "TokenPrefix": tokenPrefix, + "TokenSuffix": tokenSuffix, + }) + } + } + _ = tmpls.authTokens.Execute(w, map[string]any{"Title": "Auth Tokens", "Tokens": tokens, "User": user, "Sub": userID}) + })) + + http.HandleFunc("/auth/users", authMw(func(w http.ResponseWriter, r *http.Request) { + userID := getUser(r) + var user any + if userID != "" { + user = &TemplateUser{ID: userID} + } else { + user = nil + } + if r.Method == "POST" { + if del := r.FormValue("delete"); del != "" { + // Delete user + storeInst.Delete("auth/" + del) + deleteUserTokens(storeInst, del) // Delete all JWT tokens for this user + http.Redirect(w, r, "/auth/users", http.StatusSeeOther) + return + } + id := r.FormValue("id") + if id == "" { + http.Redirect(w, r, "/auth/users", http.StatusSeeOther) + return + } + pass := r.FormValue("password") + typeStr := r.FormValue("type") + accType := "user" + if typeStr == "admin" { + accType = "admin" + } + hash, _ := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost) + acc := &Account{ + ID: id, + Type: accType, + Scopes: []string{"*"}, + Metadata: map[string]string{"created": time.Now().Format(time.RFC3339), "password_hash": string(hash)}, + } + b, _ := json.Marshal(acc) + storeInst.Write(&store.Record{Key: "auth/" + id, Value: b}) + http.Redirect(w, r, "/auth/users", http.StatusSeeOther) + return + } + recs, _ := storeInst.Read("auth/", store.ReadPrefix()) + var users []Account + for _, rec := range recs { + var acc Account + if err := json.Unmarshal(rec.Value, &acc); err == nil { + if acc.Type == "user" || acc.Type == "admin" { + users = append(users, acc) + } + } + } + _ = tmpls.authUsers.Execute(w, map[string]any{"Title": "User Accounts", "Users": users, "User": user}) + })) + http.HandleFunc("/auth/login", func(w http.ResponseWriter, r *http.Request) { + if r.Method == "GET" { + loginTmpl, err := template.ParseFS(HTML, "html/templates/base.html", "html/templates/auth_login.html") + if err != nil { + w.WriteHeader(500) + w.Write([]byte("Template error: " + err.Error())) + return + } + _ = loginTmpl.Execute(w, map[string]any{"Title": "Login", "Error": "", "User": getUser(r), "HideSidebar": true}) + return + } + if r.Method == "POST" { + id := r.FormValue("id") + pass := r.FormValue("password") + recKey := "auth/" + id + recs, _ := storeInst.Read(recKey) + if len(recs) == 0 { + loginTmpl, _ := template.ParseFS(HTML, "html/templates/base.html", "html/templates/auth_login.html") + _ = loginTmpl.Execute(w, map[string]any{"Title": "Login", "Error": "Invalid credentials", "User": "", "HideSidebar": true}) + return + } + var acc Account + if err := json.Unmarshal(recs[0].Value, &acc); err != nil { + loginTmpl, _ := template.ParseFS(HTML, "html/templates/base.html", "html/templates/auth_login.html") + _ = loginTmpl.Execute(w, map[string]any{"Title": "Login", "Error": "Invalid credentials", "User": "", "HideSidebar": true}) + return + } + hash, ok := acc.Metadata["password_hash"] + if !ok || bcrypt.CompareHashAndPassword([]byte(hash), []byte(pass)) != nil { + loginTmpl, _ := template.ParseFS(HTML, "html/templates/base.html", "html/templates/auth_login.html") + _ = loginTmpl.Execute(w, map[string]any{"Title": "Login", "Error": "Invalid credentials", "User": "", "HideSidebar": true}) + return + } + tok, err := GenerateJWT(acc.ID, acc.Type, acc.Scopes, 24*time.Hour) + if err != nil { + log.Printf("[LOGIN ERROR] Token generation failed: %v\nAccount: %+v", err, acc) + loginTmpl, _ := template.ParseFS(HTML, "html/templates/base.html", "html/templates/auth_login.html") + _ = loginTmpl.Execute(w, map[string]any{"Title": "Login", "Error": "Token error", "User": "", "HideSidebar": true}) + return + } + storeJWTToken(storeInst, tok, acc.ID) // Store the JWT token + http.SetCookie(w, &http.Cookie{ + Name: "micro_token", + Value: tok, + Path: "/", + Expires: time.Now().Add(time.Hour * 24), + HttpOnly: true, + }) + http.Redirect(w, r, "/", http.StatusSeeOther) + return + } + w.WriteHeader(405) + w.Write([]byte("Method not allowed")) + }) +} + +func Run(c *cli.Context) error { + if err := initAuth(); err != nil { + log.Fatalf("Failed to initialize auth: %v", err) + } + homeDir, _ := os.UserHomeDir() + keyDir := filepath.Join(homeDir, "micro", "keys") + privPath := filepath.Join(keyDir, "private.pem") + pubPath := filepath.Join(keyDir, "public.pem") + if err := InitJWTKeys(privPath, pubPath); err != nil { + log.Fatalf("Failed to init JWT keys: %v", err) + } + storeInst := store.DefaultStore + tmpls := parseTemplates() + registerHandlers(tmpls, storeInst) + addr := c.String("address") + if addr == "" { + addr = ":8080" + } + log.Printf("[micro-server] Web/API listening on %s", addr) + if err := http.ListenAndServe(addr, nil); err != nil { + log.Fatalf("Web/API server error: %v", err) + } + return nil +} + +// --- PID FILES --- +// --- PID FILES --- +func parsePid(pidStr string) int { + pid, _ := strconv.Atoi(pidStr) + return pid +} +func processRunning(pid string) bool { + proc, err := os.FindProcess(parsePid(pid)) + if err != nil { + return false + } + // On unix, sending syscall.Signal(0) checks if process exists + return proc.Signal(syscall.Signal(0)) == nil +} + +func generateKeyPair(bits int) (*rsa.PrivateKey, error) { + priv, err := rsa.GenerateKey(rand.Reader, bits) + if err != nil { + return nil, err + } + return priv, nil +} +func exportPrivateKeyAsPEM(priv *rsa.PrivateKey) ([]byte, error) { + privKeyBytes := x509.MarshalPKCS1PrivateKey(priv) + block := &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: privKeyBytes, + } + var buf bytes.Buffer + err := pem.Encode(&buf, block) + if err != nil { + return nil, err + } + return buf.Bytes(), nil +} +func exportPublicKeyAsPEM(pub *rsa.PublicKey) ([]byte, error) { + pubKeyBytes := x509.MarshalPKCS1PublicKey(pub) + block := &pem.Block{ + Type: "RSA PUBLIC KEY", + Bytes: pubKeyBytes, + } + var buf bytes.Buffer + err := pem.Encode(&buf, block) + if err != nil { + return nil, err + } + return buf.Bytes(), nil +} +func importPrivateKeyFromPEM(privKeyPEM []byte) (*rsa.PrivateKey, error) { + block, _ := pem.Decode(privKeyPEM) + if block == nil { + return nil, fmt.Errorf("invalid PEM block") + } + return x509.ParsePKCS1PrivateKey(block.Bytes) +} +func importPublicKeyFromPEM(pubKeyPEM []byte) (*rsa.PublicKey, error) { + block, _ := pem.Decode(pubKeyPEM) + if block == nil { + return nil, fmt.Errorf("invalid PEM block") + } + return x509.ParsePKCS1PublicKey(block.Bytes) +} +func initAuth() error { + // --- AUTH SETUP --- + homeDir, _ := os.UserHomeDir() + keyDir := filepath.Join(homeDir, "micro", "keys") + privPath := filepath.Join(keyDir, "private.pem") + pubPath := filepath.Join(keyDir, "public.pem") + os.MkdirAll(keyDir, 0700) + // Generate keypair if not exist + if _, err := os.Stat(privPath); os.IsNotExist(err) { + priv, _ := rsa.GenerateKey(rand.Reader, 2048) + privBytes := x509.MarshalPKCS1PrivateKey(priv) + privPem := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: privBytes}) + os.WriteFile(privPath, privPem, 0600) + // Use PKIX format for public key + pubBytes, _ := x509.MarshalPKIXPublicKey(&priv.PublicKey) + pubPem := pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: pubBytes}) + os.WriteFile(pubPath, pubPem, 0644) + } + _, _ = os.ReadFile(privPath) + _, _ = os.ReadFile(pubPath) + storeInst := store.DefaultStore + // --- Ensure default admin account exists --- + adminID := "admin" + adminPass := "micro" + adminKey := "auth/" + adminID + if recs, _ := storeInst.Read(adminKey); len(recs) == 0 { + // Hash the admin password with bcrypt + hash, err := bcrypt.GenerateFromPassword([]byte(adminPass), bcrypt.DefaultCost) + if err != nil { + return err + } + acc := &Account{ + ID: adminID, + Type: "admin", + Scopes: []string{"*"}, + Metadata: map[string]string{"created": time.Now().Format(time.RFC3339), "password_hash": string(hash)}, + } + b, _ := json.Marshal(acc) + storeInst.Write(&store.Record{Key: adminKey, Value: b}) + } + return nil +} + +// parseStartTime parses a string as RFC3339 time +func parseStartTime(s string) (time.Time, error) { + return time.Parse(time.RFC3339, s) +} +func init() { + cmd.Register(&cli.Command{ + Name: "server", + Usage: "Run the micro server", + Action: Run, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "address", + Usage: "Address to listen on", + EnvVars: []string{"MICRO_SERVER_ADDRESS"}, + Value: ":8080", + }, + }, + }) +} diff --git a/cmd/micro/server/util_jwt.go b/cmd/micro/server/util_jwt.go new file mode 100644 index 00000000..1012f28a --- /dev/null +++ b/cmd/micro/server/util_jwt.go @@ -0,0 +1,91 @@ +package server + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "errors" + "os" + "time" + + "github.com/golang-jwt/jwt/v5" +) + +var ( + jwtPrivateKey *rsa.PrivateKey + jwtPublicKey *rsa.PublicKey +) + +// Load or generate RSA keys for JWT +func InitJWTKeys(privPath, pubPath string) error { + var err error + if _, err = os.Stat(privPath); os.IsNotExist(err) { + priv, _ := rsa.GenerateKey(rand.Reader, 2048) + privBytes := x509.MarshalPKCS1PrivateKey(priv) + privPem := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: privBytes}) + os.WriteFile(privPath, privPem, 0600) + pubBytes, _ := x509.MarshalPKIXPublicKey(&priv.PublicKey) + pubPem := pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: pubBytes}) + os.WriteFile(pubPath, pubPem, 0644) + } + privPem, err := os.ReadFile(privPath) + if err != nil { + return err + } + block, _ := pem.Decode(privPem) + if block == nil { + return errors.New("invalid private key PEM") + } + jwtPrivateKey, err = x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return err + } + pubPem, err := os.ReadFile(pubPath) + if err != nil { + return err + } + block, _ = pem.Decode(pubPem) + if block == nil { + return errors.New("invalid public key PEM") + } + pub, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + return err + } + var ok bool + jwtPublicKey, ok = pub.(*rsa.PublicKey) + if !ok { + return errors.New("not RSA public key") + } + return nil +} + +// Generate a JWT for a user +func GenerateJWT(userID, userType string, scopes []string, expiry time.Duration) (string, error) { + claims := jwt.MapClaims{ + "sub": userID, + "type": userType, + "scopes": scopes, + "exp": time.Now().Add(expiry).Unix(), + } + token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) + return token.SignedString(jwtPrivateKey) +} + +// Parse and validate a JWT, returns claims if valid +func ParseJWT(tokenStr string) (jwt.MapClaims, error) { + token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok { + return nil, errors.New("unexpected signing method") + } + return jwtPublicKey, nil + }) + if err != nil { + return nil, err + } + if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { + return claims, nil + } + return nil, errors.New("invalid token") +} diff --git a/cmd/micro/web/main.js b/cmd/micro/web/main.js new file mode 100644 index 00000000..75f80df9 --- /dev/null +++ b/cmd/micro/web/main.js @@ -0,0 +1,34 @@ +// Minimal JS for reactive form submissions + +document.addEventListener('DOMContentLoaded', function() { + document.querySelectorAll('form[data-reactive]')?.forEach(function(form) { + form.addEventListener('submit', async function(e) { + e.preventDefault(); + const formData = new FormData(form); + const params = {}; + for (const [key, value] of formData.entries()) { + params[key] = value; + } + const action = form.getAttribute('action'); + const method = form.getAttribute('method') || 'POST'; + try { + const resp = await fetch(action, { + method, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(params) + }); + const data = await resp.json(); + // Find or create a response container + let respDiv = form.querySelector('.js-response'); + if (!respDiv) { + respDiv = document.createElement('div'); + respDiv.className = 'js-response'; + form.appendChild(respDiv); + } + respDiv.innerHTML = '
' + JSON.stringify(data, null, 2) + '
'; + } catch (err) { + alert('Error: ' + err); + } + }); + }); +}); diff --git a/cmd/micro/web/styles.css b/cmd/micro/web/styles.css new file mode 100644 index 00000000..a0d13818 --- /dev/null +++ b/cmd/micro/web/styles.css @@ -0,0 +1,236 @@ +body { + background: #fff; + color: #111; + font-family: 'Inter', 'Segoe UI', 'Arial', 'Helvetica Neue', Arial, sans-serif; + font-size: 15px; + margin: 0; + padding: 0; + line-height: 1.7; +} + +header, nav, footer { + background: #fff; + color: #111; + padding: 1.2em 2em 1.2em 2em; + margin-bottom: 2em; +} +nav { + margin: 20px; + border-radius: 20px; + +} +main { + max-width: 1400px; + margin: 0 auto; + padding: 2em 1em 3em 1em; + background: #fff; + margin-left: 100px; /* leave space for sidebar */ + margin-right: 100px; +} + +h1, h2, h3, h4, h5, h6 { + color: #111; + font-weight: 600; + margin-top: 2em; + margin-bottom: 0.5em; + letter-spacing: -0.01em; +} + +h1 { + font-size: 2.2em; + margin-top: 0; +} + +h2 { + font-size: 1.4em; +} + +hr { + border: none; + border-top: 1px solid #222; + margin: 2em 0; +} + +a { + color: #111; + text-decoration: none; + transition: background 0.2s; +} +a:hover { + font-weight: bold; +} + +ul, ol { + margin: 1em 0 1em 2em; + padding: 0; +} + +li { + margin-bottom: 0.5em; +} + +pre, code { + background: #f7f7f7; + color: #111; + font-family: inherit; + font-size: 0.98em; + border-radius: 5px; + padding: 0.2em 0.4em; +} + +pre { + padding: 1em; + overflow-x: auto; + border-radius: 0; + margin: 1.5em 0; +} + +form { + background: #fff; + border: 1px solid #222; + padding: 1.5em 1.5em 1em 1.5em; + margin: 2em 0; + border-radius: 10px; + box-shadow: none; +} + +input, select, textarea { + background: #fff; + color: #111; + border: 1px solid #222; + border-radius: 7px; + font-size: 1em; + padding: 0.5em 0.7em; + margin-bottom: 1em; + width: 100%; + box-sizing: border-box; + outline: none; + transition: border 0.2s; +} +input:focus, select:focus, textarea:focus { + border: 1.5px solid #111; +} + +button, input[type="submit"], .button { + background: #fff; + color: #111; + border: 1.5px solid #111; + border-radius: 7px; + font-size: 1em; + padding: 0.5em 1.2em; + margin: 0.5em 0.2em 0.5em 0; + cursor: pointer; + font-family: inherit; + transition: background 0.2s, color 0.2s; +} +button:hover, input[type="submit"]:hover, .button:hover { + background: #111; + color: #fff; +} + +.table, table { + width: 100%; + border-collapse: collapse; + background: #fff; + margin: 2em 0; +} +table th, table td { + border: none; + padding: 0.7em 1em; + text-align: left; +} +table th { + background: #f7f7f7; + color: #111; + font-weight: 600; +} +table tr:nth-child(even) { + background: #f7f7f7; +} + +.no-bullets { + list-style: none; + margin: 0; + padding: 0; +} + +.copy-btn { + background: #fff; + color: #111; + border: 1px solid #222; + border-radius: 7px; + font-size: 0.95em; + padding: 0.2em 0.7em; + margin-left: 0.5em; + cursor: pointer; + transition: background 0.2s, color 0.2s; +} +.copy-btn:hover { + background: #111; + color: #fff; +} + +.alert, .error, .success { + background: #fff; + color: #111; + border: 1px solid #222; + padding: 1em 1.5em; + margin: 2em 0; + border-radius: 10px; +} + +::-webkit-scrollbar { + width: 8px; + background: #fff; +} +::-webkit-scrollbar-thumb { + background: #222; +} + +@media (max-width: 800px) { + main { + max-width: 98vw; + padding: 1em 0.2em 2em 0.2em; + margin-left: 0; + } +} + +/* Inline/unstyled form for delete button */ +.form-inline, .form-plain { + display: inline; + background: none; + border: none; + padding: 0; + margin: 0; + box-shadow: none; +} +.form-inline input, .form-inline button, .form-plain input, .form-plain button { + margin: 0; + padding: 0.3em 1em; + border-radius: 7px; + font-size: 1em; +} +.delete-btn, .form-inline .delete-btn, .form-plain .delete-btn { + background: #fff; + color: #c00; + border: 1.5px solid #c00; + border-radius: 7px; + font-size: 1em; + padding: 0.3em 1em; + margin: 0 0.2em; + cursor: pointer; + font-family: inherit; + transition: background 0.2s, color 0.2s; +} +.delete-btn:hover { + background: #c00; + color: #fff; +} + +#title { + text-decoration: none; +} +.log-link:hover { + font-weight: normal; + text-decoration: underline; +} diff --git a/cmd/micro/web/templates/api.html b/cmd/micro/web/templates/api.html new file mode 100644 index 00000000..4716310d --- /dev/null +++ b/cmd/micro/web/templates/api.html @@ -0,0 +1,34 @@ +{{define "content"}} +

API

+

+ API Authentication Required: All API calls to /api/... endpoints (except this page) must include an Authorization: Bearer <token> header.
+ You can generate tokens on the Tokens page. +

+{{range .Services}} +

{{.Name}}

+ {{if .Endpoints}} +
+ {{range .Endpoints}} +
+ +
+ HTTP Path: {{.Path}} +
+
+
+ Request: +
{{.Params}}
+
+
+ Response: +
{{.Response}}
+
+
+
+ {{end}} +
+ {{else}} +

No endpoints

+ {{end}} +{{end}} +{{end}} diff --git a/cmd/micro/web/templates/auth_login.html b/cmd/micro/web/templates/auth_login.html new file mode 100644 index 00000000..472f777e --- /dev/null +++ b/cmd/micro/web/templates/auth_login.html @@ -0,0 +1,15 @@ +{{define "content"}} +

Login

+
+
+ +
+
+ +
+ +
+{{if .Error}} +
{{.Error}}
+{{end}} +{{end}} diff --git a/cmd/micro/web/templates/auth_tokens.html b/cmd/micro/web/templates/auth_tokens.html new file mode 100644 index 00000000..ed7a0bc3 --- /dev/null +++ b/cmd/micro/web/templates/auth_tokens.html @@ -0,0 +1,63 @@ +{{define "content"}} +

Auth Tokens

+ + + + + + {{range .Tokens}} + + + + + + + + + {{end}} + +
IDTypeScopesMetadataTokenDelete
{{.ID}}{{.Type}}{{range .Scopes}}{{.}} {{end}} + {{range $k, $v := .Metadata}} + {{if and (ne $k "password_hash") (ne $k "token")}} + {{$k}}: {{$v}} + {{end}} + {{end}} + + {{if .Token}} + + {{if .TokenSuffix}} + {{.TokenPrefix}}...{{.TokenSuffix}} + {{else}} + {{.Token}} + {{end}} + + + {{end}} + +
+ + +
+
+

Create New Token

+
+ + + + +
+ +{{end}} diff --git a/cmd/micro/web/templates/auth_users.html b/cmd/micro/web/templates/auth_users.html new file mode 100644 index 00000000..f3dcdfa3 --- /dev/null +++ b/cmd/micro/web/templates/auth_users.html @@ -0,0 +1,40 @@ +{{define "content"}} +

User Accounts

+ + + + + + {{range .Users}} + + + + + + + + {{end}} + +
IDTypeScopesMetadataDelete
{{.ID}}{{.Type}}{{range .Scopes}}{{.}} {{end}} + {{range $k, $v := .Metadata}} + {{if ne $k "password_hash"}} + {{$k}}: {{$v}} + {{end}} + {{end}} + +
+ + +
+
+

Create New User

+
+ + + + +
+{{end}} diff --git a/cmd/micro/web/templates/base.html b/cmd/micro/web/templates/base.html new file mode 100644 index 00000000..aee8a43f --- /dev/null +++ b/cmd/micro/web/templates/base.html @@ -0,0 +1,52 @@ + + + + + + {{.Title}} + + + +
+ {{if not .HideSidebar}} + + {{end}} +
+ {{template "content" .}} +
+
+ + diff --git a/cmd/micro/web/templates/form.html b/cmd/micro/web/templates/form.html new file mode 100644 index 00000000..90c6b003 --- /dev/null +++ b/cmd/micro/web/templates/form.html @@ -0,0 +1,23 @@ +{{define "content"}} +

{{.ServiceName}}

+
+

{{.EndpointName}}

+ {{range .Inputs}} + + + {{end}} + +
+
+{{if .Error}} +
Error: {{.Error}}
+{{end}} +{{if .Response}} +
+

Response

+ {{.ResponseTable}} +
{{.ResponseJSON}}
+
+{{end}} + +{{end}} diff --git a/cmd/micro/web/templates/home.html b/cmd/micro/web/templates/home.html new file mode 100644 index 00000000..32de3a99 --- /dev/null +++ b/cmd/micro/web/templates/home.html @@ -0,0 +1,21 @@ +{{define "content"}} +

Dashboard

+
+
+ + {{if eq .StatusDot "green"}} + + {{else if eq .StatusDot "yellow"}} + + {{else}} + + {{end}} + + Status +
+
Services: {{.ServiceCount}}
+
Running: {{.RunningCount}}
+
Stopped: {{.StoppedCount}}
+
+

Welcome to the Micro dashboard. Use the sidebar to navigate services, logs, status, and API.

+{{end}} diff --git a/cmd/micro/web/templates/log.html b/cmd/micro/web/templates/log.html new file mode 100644 index 00000000..1babde05 --- /dev/null +++ b/cmd/micro/web/templates/log.html @@ -0,0 +1,5 @@ +{{define "content"}} +

Logs for {{.Service}}

+
{{.Log}}
+Back to logs +{{end}} diff --git a/cmd/micro/web/templates/logs.html b/cmd/micro/web/templates/logs.html new file mode 100644 index 00000000..5c5c1c56 --- /dev/null +++ b/cmd/micro/web/templates/logs.html @@ -0,0 +1,8 @@ +{{define "content"}} +

Logs

+
    + {{range .Services}} +
  • {{.}}
  • + {{end}} +
+{{end}} diff --git a/cmd/micro/web/templates/service.html b/cmd/micro/web/templates/service.html new file mode 100644 index 00000000..113562de --- /dev/null +++ b/cmd/micro/web/templates/service.html @@ -0,0 +1,26 @@ +{{define "content"}} +{{if .ServiceName}} +

{{.ServiceName}}

+

Endpoints

+ {{if .Endpoints}} + {{range .Endpoints}} +
{{.Name}}
+ {{end}} + {{else}} +

No endpoints registered

+ {{end}} +

Description

+
{{.Description}}
+{{else}} +

Services

+ {{if .Services}} +
    + {{range .Services}} +
  • {{.}}
  • + {{end}} +
+ {{else}} +

No services registered

+ {{end}} +{{end}} +{{end}} diff --git a/cmd/micro/web/templates/status.html b/cmd/micro/web/templates/status.html new file mode 100644 index 00000000..6b7085b0 --- /dev/null +++ b/cmd/micro/web/templates/status.html @@ -0,0 +1,29 @@ +{{define "content"}} +

Service Status

+ + + + + + + + + + + + + + {{range .Statuses}} + + + + + + + + + + {{end}} + +
ServiceDirectoryStatusPIDUptimeIDLogs
{{.Service}}{{.Dir}}{{.Status}}{{.PID}}{{.Uptime}}{{.ID}}View logs
+{{end}} diff --git a/cmd/protoc-gen-micro/README.md b/cmd/protoc-gen-micro/README.md new file mode 100644 index 00000000..7e568975 --- /dev/null +++ b/cmd/protoc-gen-micro/README.md @@ -0,0 +1,144 @@ +# protoc-gen-micro + +This is protobuf code generation for go-micro. We use protoc-gen-micro to reduce boilerplate code. + +## Install + +``` +go install go-micro.dev/v5/cmd/protoc-gen-micro@latest +``` + +Also required: + +- [protoc](https://github.com/google/protobuf) +- [protoc-gen-go](https://google.golang.org/protobuf) + +## Usage + +Define your service as `greeter.proto` + +``` +syntax = "proto3"; + +package greeter; +option go_package = "/proto;greeter"; + +service Greeter { + rpc Hello(Request) returns (Response) {} +} + +message Request { + string name = 1; +} + +message Response { + string msg = 1; +} +``` + +Generate the code + +``` +protoc --proto_path=. --micro_out=. --go_out=. greeter.proto +``` + +Your output result should be: + +``` +./ + greeter.proto # original protobuf file + greeter.pb.go # auto-generated by protoc-gen-go + greeter.micro.go # auto-generated by protoc-gen-micro +``` + +The micro generated code includes clients and handlers which reduce boiler plate code + +### Server + +Register the handler with your micro server + +```go +type Greeter struct{} + +func (g *Greeter) Hello(ctx context.Context, req *proto.Request, rsp *proto.Response) error { + rsp.Msg = "Hello " + req.Name + return nil +} + +proto.RegisterGreeterHandler(service.Server(), &Greeter{}) +``` + +### Client + +Create a service client with your micro client + +```go +client := proto.NewGreeterService("greeter", service.Client()) +``` + +### Errors + +If you see an error about `protoc-gen-micro` not being found or executable, it's likely your environment may not be configured correctly. If you've already installed `protoc`, `protoc-gen-go`, and `protoc-gen-micro` ensure you've included `$GOPATH/bin` in your `PATH`. + +Alternative specify the Go plugin paths as arguments to the `protoc` command + +``` +protoc --plugin=protoc-gen-go=$GOPATH/bin/protoc-gen-go --plugin=protoc-gen-micro=$GOPATH/bin/protoc-gen-micro --proto_path=. --micro_out=. --go_out=. greeter.proto +``` + +### Endpoint + +Add a micro API endpoint which routes directly to an RPC method + +Usage: + +1. Clone `github.com/googleapis/googleapis` to use this feature as it requires http annotations. +2. The protoc command must include `-I$GOPATH/src/github.com/googleapis/googleapis` for the annotations import. + +```diff +syntax = "proto3"; + +package greeter; +option go_package = "/proto;greeter"; + +import "google/api/annotations.proto"; + +service Greeter { + rpc Hello(Request) returns (Response) { + option (google.api.http) = { post: "/hello"; body: "*"; }; + } +} + +message Request { + string name = 1; +} + +message Response { + string msg = 1; +} +``` + +The proto generates a `RegisterGreeterHandler` function with a [api.Endpoint](https://godoc.org/go-micro.dev/v3/api#Endpoint). + +```diff +func RegisterGreeterHandler(s server.Server, hdlr GreeterHandler, opts ...server.HandlerOption) error { + type greeter interface { + Hello(ctx context.Context, in *Request, out *Response) error + } + type Greeter struct { + greeter + } + h := &greeterHandler{hdlr} + opts = append(opts, api.WithEndpoint(&api.Endpoint{ + Name: "Greeter.Hello", + Path: []string{"/hello"}, + Method: []string{"POST"}, + Handler: "rpc", + })) + return s.Handle(s.NewHandler(&Greeter{h}, opts...)) +} +``` + +## LICENSE + +protoc-gen-micro is a liberal reuse of protoc-gen-go hence we maintain the original license diff --git a/cmd/protoc-gen-micro/examples/greeter/greeter.pb.go b/cmd/protoc-gen-micro/examples/greeter/greeter.pb.go new file mode 100644 index 00000000..aa86e898 --- /dev/null +++ b/cmd/protoc-gen-micro/examples/greeter/greeter.pb.go @@ -0,0 +1,223 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.32.0 +// protoc v4.25.3 +// source: greeter.proto + +package greeter + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Request struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Msg *string `protobuf:"bytes,2,opt,name=msg,proto3,oneof" json:"msg,omitempty"` +} + +func (x *Request) Reset() { + *x = Request{} + if protoimpl.UnsafeEnabled { + mi := &file_greeter_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Request) ProtoMessage() {} + +func (x *Request) ProtoReflect() protoreflect.Message { + mi := &file_greeter_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Request.ProtoReflect.Descriptor instead. +func (*Request) Descriptor() ([]byte, []int) { + return file_greeter_proto_rawDescGZIP(), []int{0} +} + +func (x *Request) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Request) GetMsg() string { + if x != nil && x.Msg != nil { + return *x.Msg + } + return "" +} + +type Response struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Msg string `protobuf:"bytes,1,opt,name=msg,proto3" json:"msg,omitempty"` +} + +func (x *Response) Reset() { + *x = Response{} + if protoimpl.UnsafeEnabled { + mi := &file_greeter_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Response) ProtoMessage() {} + +func (x *Response) ProtoReflect() protoreflect.Message { + mi := &file_greeter_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Response.ProtoReflect.Descriptor instead. +func (*Response) Descriptor() ([]byte, []int) { + return file_greeter_proto_rawDescGZIP(), []int{1} +} + +func (x *Response) GetMsg() string { + if x != nil { + return x.Msg + } + return "" +} + +var File_greeter_proto protoreflect.FileDescriptor + +var file_greeter_proto_rawDesc = []byte{ + 0x0a, 0x0d, 0x67, 0x72, 0x65, 0x65, 0x74, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, + 0x3c, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x15, + 0x0a, 0x03, 0x6d, 0x73, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x03, 0x6d, + 0x73, 0x67, 0x88, 0x01, 0x01, 0x42, 0x06, 0x0a, 0x04, 0x5f, 0x6d, 0x73, 0x67, 0x22, 0x1c, 0x0a, + 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x73, 0x67, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6d, 0x73, 0x67, 0x32, 0x4e, 0x0a, 0x07, 0x47, + 0x72, 0x65, 0x65, 0x74, 0x65, 0x72, 0x12, 0x1e, 0x0a, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x12, + 0x08, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x09, 0x2e, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x23, 0x0a, 0x06, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, + 0x12, 0x08, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x09, 0x2e, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x28, 0x01, 0x30, 0x01, 0x42, 0x0c, 0x5a, 0x0a, 0x2e, + 0x2e, 0x2f, 0x67, 0x72, 0x65, 0x65, 0x74, 0x65, 0x72, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x33, +} + +var ( + file_greeter_proto_rawDescOnce sync.Once + file_greeter_proto_rawDescData = file_greeter_proto_rawDesc +) + +func file_greeter_proto_rawDescGZIP() []byte { + file_greeter_proto_rawDescOnce.Do(func() { + file_greeter_proto_rawDescData = protoimpl.X.CompressGZIP(file_greeter_proto_rawDescData) + }) + return file_greeter_proto_rawDescData +} + +var file_greeter_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_greeter_proto_goTypes = []interface{}{ + (*Request)(nil), // 0: Request + (*Response)(nil), // 1: Response +} +var file_greeter_proto_depIdxs = []int32{ + 0, // 0: Greeter.Hello:input_type -> Request + 0, // 1: Greeter.Stream:input_type -> Request + 1, // 2: Greeter.Hello:output_type -> Response + 1, // 3: Greeter.Stream:output_type -> Response + 2, // [2:4] is the sub-list for method output_type + 0, // [0:2] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_greeter_proto_init() } +func file_greeter_proto_init() { + if File_greeter_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_greeter_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Request); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_greeter_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Response); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + file_greeter_proto_msgTypes[0].OneofWrappers = []interface{}{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_greeter_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_greeter_proto_goTypes, + DependencyIndexes: file_greeter_proto_depIdxs, + MessageInfos: file_greeter_proto_msgTypes, + }.Build() + File_greeter_proto = out.File + file_greeter_proto_rawDesc = nil + file_greeter_proto_goTypes = nil + file_greeter_proto_depIdxs = nil +} diff --git a/cmd/protoc-gen-micro/examples/greeter/greeter.pb.micro.go b/cmd/protoc-gen-micro/examples/greeter/greeter.pb.micro.go new file mode 100644 index 00000000..b510b95a --- /dev/null +++ b/cmd/protoc-gen-micro/examples/greeter/greeter.pb.micro.go @@ -0,0 +1,183 @@ +// Code generated by protoc-gen-micro. DO NOT EDIT. +// source: greeter.proto + +package greeter + +import ( + fmt "fmt" + proto "google.golang.org/protobuf/proto" + math "math" +) + +import ( + context "context" + client "go-micro.dev/v5/client" + server "go-micro.dev/v5/server" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ client.Option +var _ server.Option + +// Client API for Greeter service + +type GreeterService interface { + Hello(ctx context.Context, in *Request, opts ...client.CallOption) (*Response, error) + Stream(ctx context.Context, opts ...client.CallOption) (Greeter_StreamService, error) +} + +type greeterService struct { + c client.Client + name string +} + +func NewGreeterService(name string, c client.Client) GreeterService { + return &greeterService{ + c: c, + name: name, + } +} + +func (c *greeterService) Hello(ctx context.Context, in *Request, opts ...client.CallOption) (*Response, error) { + req := c.c.NewRequest(c.name, "Greeter.Hello", in) + out := new(Response) + err := c.c.Call(ctx, req, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *greeterService) Stream(ctx context.Context, opts ...client.CallOption) (Greeter_StreamService, error) { + req := c.c.NewRequest(c.name, "Greeter.Stream", &Request{}) + stream, err := c.c.Stream(ctx, req, opts...) + if err != nil { + return nil, err + } + return &greeterServiceStream{stream}, nil +} + +type Greeter_StreamService interface { + Context() context.Context + SendMsg(interface{}) error + RecvMsg(interface{}) error + CloseSend() error + Close() error + Send(*Request) error + Recv() (*Response, error) +} + +type greeterServiceStream struct { + stream client.Stream +} + +func (x *greeterServiceStream) CloseSend() error { + return x.stream.CloseSend() +} + +func (x *greeterServiceStream) Close() error { + return x.stream.Close() +} + +func (x *greeterServiceStream) Context() context.Context { + return x.stream.Context() +} + +func (x *greeterServiceStream) SendMsg(m interface{}) error { + return x.stream.Send(m) +} + +func (x *greeterServiceStream) RecvMsg(m interface{}) error { + return x.stream.Recv(m) +} + +func (x *greeterServiceStream) Send(m *Request) error { + return x.stream.Send(m) +} + +func (x *greeterServiceStream) Recv() (*Response, error) { + m := new(Response) + err := x.stream.Recv(m) + if err != nil { + return nil, err + } + return m, nil +} + +// Server API for Greeter service + +type GreeterHandler interface { + Hello(context.Context, *Request, *Response) error + Stream(context.Context, Greeter_StreamStream) error +} + +func RegisterGreeterHandler(s server.Server, hdlr GreeterHandler, opts ...server.HandlerOption) error { + type greeter interface { + Hello(ctx context.Context, in *Request, out *Response) error + Stream(ctx context.Context, stream server.Stream) error + } + type Greeter struct { + greeter + } + h := &greeterHandler{hdlr} + return s.Handle(s.NewHandler(&Greeter{h}, opts...)) +} + +type greeterHandler struct { + GreeterHandler +} + +func (h *greeterHandler) Hello(ctx context.Context, in *Request, out *Response) error { + return h.GreeterHandler.Hello(ctx, in, out) +} + +func (h *greeterHandler) Stream(ctx context.Context, stream server.Stream) error { + return h.GreeterHandler.Stream(ctx, &greeterStreamStream{stream}) +} + +type Greeter_StreamStream interface { + Context() context.Context + SendMsg(interface{}) error + RecvMsg(interface{}) error + Close() error + Send(*Response) error + Recv() (*Request, error) +} + +type greeterStreamStream struct { + stream server.Stream +} + +func (x *greeterStreamStream) Close() error { + return x.stream.Close() +} + +func (x *greeterStreamStream) Context() context.Context { + return x.stream.Context() +} + +func (x *greeterStreamStream) SendMsg(m interface{}) error { + return x.stream.Send(m) +} + +func (x *greeterStreamStream) RecvMsg(m interface{}) error { + return x.stream.Recv(m) +} + +func (x *greeterStreamStream) Send(m *Response) error { + return x.stream.Send(m) +} + +func (x *greeterStreamStream) Recv() (*Request, error) { + m := new(Request) + if err := x.stream.Recv(m); err != nil { + return nil, err + } + return m, nil +} diff --git a/cmd/protoc-gen-micro/examples/greeter/greeter.proto b/cmd/protoc-gen-micro/examples/greeter/greeter.proto new file mode 100644 index 00000000..b9a2b3a0 --- /dev/null +++ b/cmd/protoc-gen-micro/examples/greeter/greeter.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; + +option go_package = "../greeter"; + +service Greeter { + rpc Hello(Request) returns (Response) {} + rpc Stream(stream Request) returns (stream Response) {} +} + +message Request { + string name = 1; + optional string msg = 2; +} + +message Response { + string msg = 1; +} diff --git a/cmd/protoc-gen-micro/generator/Makefile b/cmd/protoc-gen-micro/generator/Makefile new file mode 100644 index 00000000..a1f1a354 --- /dev/null +++ b/cmd/protoc-gen-micro/generator/Makefile @@ -0,0 +1,40 @@ +# Go support for Protocol Buffers - Google's data interchange format +# +# Copyright 2010 The Go Authors. All rights reserved. +# https://github.com/golang/protobuf +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +include $(GOROOT)/src/Make.inc + +TARG=github.com/golang/protobuf/protoc-gen-go/generator +GOFILES=\ + generator.go\ + +DEPS=../descriptor ../plugin ../../proto + +include $(GOROOT)/src/Make.pkg diff --git a/cmd/protoc-gen-micro/generator/generator.go b/cmd/protoc-gen-micro/generator/generator.go new file mode 100644 index 00000000..ecbf19df --- /dev/null +++ b/cmd/protoc-gen-micro/generator/generator.go @@ -0,0 +1,2748 @@ +// Go support for Protocol Buffers - Google's data interchange format +// +// Copyright 2010 The Go Authors. All rights reserved. +// https://google.golang.org/protobuf +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/* +The code generator for the plugin for the Google protocol buffer compiler. +It generates Go code from the protocol buffer description files read by the +main routine. +*/ +package generator + +import ( + "bufio" + "bytes" + "compress/gzip" + "crypto/sha256" + "encoding/hex" + "fmt" + "go/ast" + "go/build" + "go/parser" + "go/printer" + "go/token" + "log" + "os" + "path" + "sort" + "strconv" + "strings" + "unicode" + "unicode/utf8" + + "google.golang.org/protobuf/proto" + descriptor "google.golang.org/protobuf/types/descriptorpb" + plugin "google.golang.org/protobuf/types/pluginpb" +) + +// SupportedFeatures used to signaling that code generator supports proto3 optional +// https://github.com/protocolbuffers/protobuf/blob/master/docs/implementing_proto3_presence.md#signaling-that-your-code-generator-supports-proto3-optional +var SupportedFeatures = uint64(plugin.CodeGeneratorResponse_FEATURE_PROTO3_OPTIONAL) + +// A Plugin provides functionality to add to the output during Go code generation, +// such as to produce RPC stubs. +type Plugin interface { + // Name identifies the plugin. + Name() string + // Init is called once after data structures are built but before + // code generation begins. + Init(g *Generator) + // Generate produces the code generated by the plugin for this file, + // except for the imports, by calling the generator's methods P, In, and Out. + Generate(file *FileDescriptor) + // GenerateImports produces the import declarations for this file. + // It is called after Generate. + GenerateImports(file *FileDescriptor, imports map[GoImportPath]GoPackageName) +} + +var plugins []Plugin + +// RegisterPlugin installs a (second-order) plugin to be run when the Go output is generated. +// It is typically called during initialization. +func RegisterPlugin(p Plugin) { + plugins = append(plugins, p) +} + +// A GoImportPath is the import path of a Go package. e.g., "google.golang.org/genproto/protobuf". +type GoImportPath string + +func (p GoImportPath) String() string { return strconv.Quote(string(p)) } + +// A GoPackageName is the name of a Go package. e.g., "protobuf". +type GoPackageName string + +// Each type we import as a protocol buffer (other than FileDescriptorProto) needs +// a pointer to the FileDescriptorProto that represents it. These types achieve that +// wrapping by placing each Proto inside a struct with the pointer to its File. The +// structs have the same names as their contents, with "Proto" removed. +// FileDescriptor is used to store the things that it points to. + +// The file and package name method are common to messages and enums. +type common struct { + file *FileDescriptor // File this object comes from. +} + +// GoImportPath is the import path of the Go package containing the type. +func (c *common) GoImportPath() GoImportPath { + return c.file.importPath +} + +func (c *common) File() *FileDescriptor { return c.file } + +func fileIsProto3(file *descriptor.FileDescriptorProto) bool { + return file.GetSyntax() == "proto3" +} + +func (c *common) proto3() bool { return fileIsProto3(c.file.FileDescriptorProto) } + +// Descriptor represents a protocol buffer message. +type Descriptor struct { + common + *descriptor.DescriptorProto + parent *Descriptor // The containing message, if any. + nested []*Descriptor // Inner messages, if any. + enums []*EnumDescriptor // Inner enums, if any. + ext []*ExtensionDescriptor // Extensions, if any. + typename []string // Cached typename vector. + index int // The index into the container, whether the file or another message. + path string // The SourceCodeInfo path as comma-separated integers. + group bool +} + +// TypeName returns the elements of the dotted type name. +// The package name is not part of this name. +func (d *Descriptor) TypeName() []string { + if d.typename != nil { + return d.typename + } + n := 0 + for parent := d; parent != nil; parent = parent.parent { + n++ + } + s := make([]string, n) + for parent := d; parent != nil; parent = parent.parent { + n-- + s[n] = parent.GetName() + } + d.typename = s + return s +} + +// EnumDescriptor describes an enum. If it's at top level, its parent will be nil. +// Otherwise it will be the descriptor of the message in which it is defined. +type EnumDescriptor struct { + common + *descriptor.EnumDescriptorProto + parent *Descriptor // The containing message, if any. + typename []string // Cached typename vector. + index int // The index into the container, whether the file or a message. + path string // The SourceCodeInfo path as comma-separated integers. +} + +// TypeName returns the elements of the dotted type name. +// The package name is not part of this name. +func (e *EnumDescriptor) TypeName() (s []string) { + if e.typename != nil { + return e.typename + } + name := e.GetName() + if e.parent == nil { + s = make([]string, 1) + } else { + pname := e.parent.TypeName() + s = make([]string, len(pname)+1) + copy(s, pname) + } + s[len(s)-1] = name + e.typename = s + return s +} + +// Everything but the last element of the full type name, CamelCased. +// The values of type Foo.Bar are call Foo_value1... not Foo_Bar_value1... . +func (e *EnumDescriptor) prefix() string { + if e.parent == nil { + // If the enum is not part of a message, the prefix is just the type name. + return CamelCase(*e.Name) + "_" + } + typeName := e.TypeName() + return CamelCaseSlice(typeName[0:len(typeName)-1]) + "_" +} + +// The integer value of the named constant in this enumerated type. +func (e *EnumDescriptor) integerValueAsString(name string) string { + for _, c := range e.Value { + if c.GetName() == name { + return fmt.Sprint(c.GetNumber()) + } + } + log.Fatal("cannot find value for enum constant") + return "" +} + +// ExtensionDescriptor describes an extension. If it's at top level, its parent will be nil. +// Otherwise it will be the descriptor of the message in which it is defined. +type ExtensionDescriptor struct { + common + *descriptor.FieldDescriptorProto + parent *Descriptor // The containing message, if any. +} + +// TypeName returns the elements of the dotted type name. +// The package name is not part of this name. +func (e *ExtensionDescriptor) TypeName() (s []string) { + name := e.GetName() + if e.parent == nil { + // top-level extension + s = make([]string, 1) + } else { + pname := e.parent.TypeName() + s = make([]string, len(pname)+1) + copy(s, pname) + } + s[len(s)-1] = name + return s +} + +// DescName returns the variable name used for the generated descriptor. +func (e *ExtensionDescriptor) DescName() string { + // The full type name. + typeName := e.TypeName() + // Each scope of the extension is individually CamelCased, and all are joined with "_" with an "E_" prefix. + for i, s := range typeName { + typeName[i] = CamelCase(s) + } + return "E_" + strings.Join(typeName, "_") +} + +// ImportedDescriptor describes a type that has been publicly imported from another file. +type ImportedDescriptor struct { + common + o Object +} + +func (id *ImportedDescriptor) TypeName() []string { return id.o.TypeName() } + +// FileDescriptor describes an protocol buffer descriptor file (.proto). +// It includes slices of all the messages and enums defined within it. +// Those slices are constructed by WrapTypes. +type FileDescriptor struct { + *descriptor.FileDescriptorProto + desc []*Descriptor // All the messages defined in this file. + enum []*EnumDescriptor // All the enums defined in this file. + ext []*ExtensionDescriptor // All the top-level extensions defined in this file. + imp []*ImportedDescriptor // All types defined in files publicly imported by this file. + + // Comments, stored as a map of path (comma-separated integers) to the comment. + comments map[string]*descriptor.SourceCodeInfo_Location + + // The full list of symbols that are exported, + // as a map from the exported object to its symbols. + // This is used for supporting public imports. + exported map[Object][]symbol + + importPath GoImportPath // Import path of this file's package. + packageName GoPackageName // Name of this file's Go package. + + proto3 bool // whether to generate proto3 code for this file +} + +// VarName is the variable name we'll use in the generated code to refer +// to the compressed bytes of this descriptor. It is not exported, so +// it is only valid inside the generated package. +func (d *FileDescriptor) VarName() string { + h := sha256.Sum256([]byte(d.GetName())) + return fmt.Sprintf("fileDescriptor_%s", hex.EncodeToString(h[:8])) +} + +// goPackageOption interprets the file's go_package option. +// If there is no go_package, it returns ("", "", false). +// If there's a simple name, it returns ("", pkg, true). +// If the option implies an import path, it returns (impPath, pkg, true). +func (d *FileDescriptor) goPackageOption() (impPath GoImportPath, pkg GoPackageName, ok bool) { + opt := d.GetOptions().GetGoPackage() + if opt == "" { + return "", "", false + } + // A semicolon-delimited suffix delimits the import path and package name. + sc := strings.Index(opt, ";") + if sc >= 0 { + return GoImportPath(opt[:sc]), cleanPackageName(opt[sc+1:]), true + } + // The presence of a slash implies there's an import path. + slash := strings.LastIndex(opt, "/") + if slash >= 0 { + return GoImportPath(opt), cleanPackageName(opt[slash+1:]), true + } + return "", cleanPackageName(opt), true +} + +// goFileName returns the output name for the generated Go file. +func (d *FileDescriptor) goFileName(pathType pathType, moduleRoot string) string { + name := *d.Name + if ext := path.Ext(name); ext == ".proto" || ext == ".protodevel" { + name = name[:len(name)-len(ext)] + } + name += ".pb.micro.go" + + if pathType == pathTypeSourceRelative { + return name + } + + // Does the file have a "go_package" option? + // If it does, it may override the filename. + if impPath, _, ok := d.goPackageOption(); ok && impPath != "" { + if pathType == pathModuleRoot && moduleRoot != "" { + root := moduleRoot + if !strings.HasSuffix(root, "/") { + root = root + "/" + } + name = strings.TrimPrefix(name, root) + } else { + // Replace the existing dirname with the declared import path. + _, name = path.Split(name) + name = path.Join(string(impPath), name) + } + + return name + } + + return name +} + +func (d *FileDescriptor) addExport(obj Object, sym symbol) { + d.exported[obj] = append(d.exported[obj], sym) +} + +// symbol is an interface representing an exported Go symbol. +type symbol interface { + // GenerateAlias should generate an appropriate alias + // for the symbol from the named package. + GenerateAlias(g *Generator, filename string, pkg GoPackageName) +} + +type messageSymbol struct { + sym string + hasExtensions, isMessageSet bool + oneofTypes []string +} + +type getterSymbol struct { + name string + typ string + typeName string // canonical name in proto world; empty for proto.Message and similar + genType bool // whether typ contains a generated type (message/group/enum) +} + +func (ms *messageSymbol) GenerateAlias(g *Generator, filename string, pkg GoPackageName) { + g.P("// ", ms.sym, " from public import ", filename) + g.P("type ", ms.sym, " = ", pkg, ".", ms.sym) + for _, name := range ms.oneofTypes { + g.P("type ", name, " = ", pkg, ".", name) + } +} + +type enumSymbol struct { + name string + proto3 bool // Whether this came from a proto3 file. +} + +func (es enumSymbol) GenerateAlias(g *Generator, filename string, pkg GoPackageName) { + s := es.name + g.P("// ", s, " from public import ", filename) + g.P("type ", s, " = ", pkg, ".", s) + g.P("var ", s, "_name = ", pkg, ".", s, "_name") + g.P("var ", s, "_value = ", pkg, ".", s, "_value") +} + +type constOrVarSymbol struct { + sym string + typ string // either "const" or "var" + cast string // if non-empty, a type cast is required (used for enums) +} + +func (cs constOrVarSymbol) GenerateAlias(g *Generator, filename string, pkg GoPackageName) { + v := string(pkg) + "." + cs.sym + if cs.cast != "" { + v = cs.cast + "(" + v + ")" + } + g.P(cs.typ, " ", cs.sym, " = ", v) +} + +// Object is an interface abstracting the abilities shared by enums, messages, extensions and imported objects. +type Object interface { + GoImportPath() GoImportPath + TypeName() []string + File() *FileDescriptor +} + +// Generator is the type whose methods generate the output, stored in the associated response structure. +type Generator struct { + *bytes.Buffer + + Request *plugin.CodeGeneratorRequest // The input. + Response *plugin.CodeGeneratorResponse // The output. + + Param map[string]string // Command-line parameters. + PackageImportPath string // Go import path of the package we're generating code for + ImportPrefix string // String to prefix to imported package file names. + ImportMap map[string]string // Mapping from .proto file name to import path + ModuleRoot string // Mapping from the module prefix + + Pkg map[string]string // The names under which we import support packages + + outputImportPath GoImportPath // Package we're generating code for. + allFiles []*FileDescriptor // All files in the tree + allFilesByName map[string]*FileDescriptor // All files by filename. + genFiles []*FileDescriptor // Those files we will generate output for. + file *FileDescriptor // The file we are compiling now. + packageNames map[GoImportPath]GoPackageName // Imported package names in the current file. + usedPackages map[GoImportPath]bool // Packages used in current file. + usedPackageNames map[GoPackageName]bool // Package names used in the current file. + addedImports map[GoImportPath]bool // Additional imports to emit. + typeNameToObject map[string]Object // Key is a fully-qualified name in input syntax. + init []string // Lines to emit in the init function. + indent string + pathType pathType // How to generate output filenames. + writeOutput bool +} + +type pathType int + +const ( + pathTypeImport pathType = iota + pathTypeSourceRelative + pathModuleRoot +) + +// New creates a new generator and allocates the request and response protobufs. +func New() *Generator { + g := new(Generator) + g.Buffer = new(bytes.Buffer) + g.Request = new(plugin.CodeGeneratorRequest) + g.Response = new(plugin.CodeGeneratorResponse) + return g +} + +// Error reports a problem, including an error, and exits the program. +func (g *Generator) Error(err error, msgs ...string) { + s := strings.Join(msgs, " ") + ":" + err.Error() + log.Print("protoc-gen-micro: error:", s) + os.Exit(1) +} + +// Fail reports a problem and exits the program. +func (g *Generator) Fail(msgs ...string) { + s := strings.Join(msgs, " ") + log.Print("protoc-gen-micro: error:", s) + os.Exit(1) +} + +// CommandLineParameters breaks the comma-separated list of key=value pairs +// in the parameter (a member of the request protobuf) into a key/value map. +// It then sets file name mappings defined by those entries. +func (g *Generator) CommandLineParameters(parameter string) { + g.Param = make(map[string]string) + for _, p := range strings.Split(parameter, ",") { + if i := strings.Index(p, "="); i < 0 { + g.Param[p] = "" + } else { + g.Param[p[0:i]] = p[i+1:] + } + } + + g.ImportMap = make(map[string]string) + pluginList := "none" // Default list of plugin names to enable (empty means all). + for k, v := range g.Param { + switch k { + case "import_prefix": + g.ImportPrefix = v + case "import_path": + g.PackageImportPath = v + case "module": + if g.pathType == pathTypeSourceRelative { + g.Fail(fmt.Sprintf(`Cannot set module=%q after paths=source_relative`, v)) + } + g.pathType = pathModuleRoot + g.ModuleRoot = v + case "paths": + switch v { + case "import": + g.pathType = pathTypeImport + case "source_relative": + if g.pathType == pathModuleRoot { + g.Fail("Cannot set paths=source_relative after setting module=") + } + g.pathType = pathTypeSourceRelative + default: + g.Fail(fmt.Sprintf(`Unknown path type %q: want "import" or "source_relative".`, v)) + } + case "plugins": + pluginList = v + default: + if len(k) > 0 && k[0] == 'M' { + g.ImportMap[k[1:]] = v + } + } + } + if pluginList != "" { + // Amend the set of plugins. + enabled := map[string]bool{ + "micro": true, + } + for _, name := range strings.Split(pluginList, "+") { + enabled[name] = true + } + var nplugins []Plugin + for _, p := range plugins { + if enabled[p.Name()] { + nplugins = append(nplugins, p) + } + } + plugins = nplugins + } +} + +// DefaultPackageName returns the package name printed for the object. +// If its file is in a different package, it returns the package name we're using for this file, plus ".". +// Otherwise it returns the empty string. +func (g *Generator) DefaultPackageName(obj Object) string { + importPath := obj.GoImportPath() + if importPath == g.outputImportPath { + return "" + } + return string(g.GoPackageName(importPath)) + "." +} + +// GoPackageName returns the name used for a package. +func (g *Generator) GoPackageName(importPath GoImportPath) GoPackageName { + if name, ok := g.packageNames[importPath]; ok { + return name + } + name := cleanPackageName(baseName(string(importPath))) + for i, orig := 1, name; g.usedPackageNames[name] || isGoPredeclaredIdentifier[string(name)]; i++ { + name = orig + GoPackageName(strconv.Itoa(i)) + } + g.packageNames[importPath] = name + g.usedPackageNames[name] = true + return name +} + +// AddImport adds a package to the generated file's import section. +// It returns the name used for the package. +func (g *Generator) AddImport(importPath GoImportPath) GoPackageName { + g.addedImports[importPath] = true + return g.GoPackageName(importPath) +} + +var globalPackageNames = map[GoPackageName]bool{ + "fmt": true, + "math": true, + "proto": true, +} + +// Create and remember a guaranteed unique package name. Pkg is the candidate name. +// The FileDescriptor parameter is unused. +func RegisterUniquePackageName(pkg string, f *FileDescriptor) string { + name := cleanPackageName(pkg) + for i, orig := 1, name; globalPackageNames[name]; i++ { + name = orig + GoPackageName(strconv.Itoa(i)) + } + globalPackageNames[name] = true + return string(name) +} + +var isGoKeyword = map[string]bool{ + "break": true, + "case": true, + "chan": true, + "const": true, + "continue": true, + "default": true, + "else": true, + "defer": true, + "fallthrough": true, + "for": true, + "func": true, + "go": true, + "goto": true, + "if": true, + "import": true, + "interface": true, + "map": true, + "package": true, + "range": true, + "return": true, + "select": true, + "struct": true, + "switch": true, + "type": true, + "var": true, +} + +var isGoPredeclaredIdentifier = map[string]bool{ + "append": true, + "bool": true, + "byte": true, + "cap": true, + "close": true, + "complex": true, + "complex128": true, + "complex64": true, + "copy": true, + "delete": true, + "error": true, + "false": true, + "float32": true, + "float64": true, + "imag": true, + "int": true, + "int16": true, + "int32": true, + "int64": true, + "int8": true, + "iota": true, + "len": true, + "make": true, + "new": true, + "nil": true, + "panic": true, + "print": true, + "println": true, + "real": true, + "recover": true, + "rune": true, + "string": true, + "true": true, + "uint": true, + "uint16": true, + "uint32": true, + "uint64": true, + "uint8": true, + "uintptr": true, +} + +func cleanPackageName(name string) GoPackageName { + name = strings.Map(badToUnderscore, name) + // Identifier must not be keyword or predeclared identifier: insert _. + if isGoKeyword[name] { + name = "_" + name + } + // Identifier must not begin with digit: insert _. + if r, _ := utf8.DecodeRuneInString(name); unicode.IsDigit(r) { + name = "_" + name + } + return GoPackageName(name) +} + +// defaultGoPackage returns the package name to use, +// derived from the import path of the package we're building code for. +func (g *Generator) defaultGoPackage() GoPackageName { + p := g.PackageImportPath + if i := strings.LastIndex(p, "/"); i >= 0 { + p = p[i+1:] + } + return cleanPackageName(p) +} + +// SetPackageNames sets the package name for this run. +// The package name must agree across all files being generated. +// It also defines unique package names for all imported files. +func (g *Generator) SetPackageNames() { + g.outputImportPath = g.genFiles[0].importPath + + defaultPackageNames := make(map[GoImportPath]GoPackageName) + for _, f := range g.genFiles { + if _, p, ok := f.goPackageOption(); ok { + defaultPackageNames[f.importPath] = p + } + } + for _, f := range g.genFiles { + if _, p, ok := f.goPackageOption(); ok { + // Source file: option go_package = "quux/bar"; + f.packageName = p + } else if p, ok := defaultPackageNames[f.importPath]; ok { + // A go_package option in another file in the same package. + // + // This is a poor choice in general, since every source file should + // contain a go_package option. Supported mainly for historical + // compatibility. + f.packageName = p + } else if p := g.defaultGoPackage(); p != "" { + // Command-line: import_path=quux/bar. + // + // The import_path flag sets a package name for files which don't + // contain a go_package option. + f.packageName = p + } else if p := f.GetPackage(); p != "" { + // Source file: package quux.bar; + f.packageName = cleanPackageName(p) + } else { + // Source filename. + f.packageName = cleanPackageName(baseName(f.GetName())) + } + } + + // Check that all files have a consistent package name and import path. + for _, f := range g.genFiles[1:] { + if a, b := g.genFiles[0].importPath, f.importPath; a != b { + g.Fail(fmt.Sprintf("inconsistent package import paths: %v, %v", a, b)) + } + if a, b := g.genFiles[0].packageName, f.packageName; a != b { + g.Fail(fmt.Sprintf("inconsistent package names: %v, %v", a, b)) + } + } + + // Names of support packages. These never vary (if there are conflicts, + // we rename the conflicting package), so this could be removed someday. + g.Pkg = map[string]string{ + "fmt": "fmt", + "math": "math", + "proto": "proto", + } +} + +// WrapTypes walks the incoming data, wrapping DescriptorProtos, EnumDescriptorProtos +// and FileDescriptorProtos into file-referenced objects within the Generator. +// It also creates the list of files to generate and so should be called before GenerateAllFiles. +func (g *Generator) WrapTypes() { + g.allFiles = make([]*FileDescriptor, 0, len(g.Request.ProtoFile)) + g.allFilesByName = make(map[string]*FileDescriptor, len(g.allFiles)) + genFileNames := make(map[string]bool) + for _, n := range g.Request.FileToGenerate { + genFileNames[n] = true + } + for _, f := range g.Request.ProtoFile { + fd := &FileDescriptor{ + FileDescriptorProto: f, + exported: make(map[Object][]symbol), + proto3: fileIsProto3(f), + } + // The import path may be set in a number of ways. + if substitution, ok := g.ImportMap[f.GetName()]; ok { + // Command-line: M=foo.proto=quux/bar. + // + // Explicit mapping of source file to import path. + fd.importPath = GoImportPath(substitution) + } else if genFileNames[f.GetName()] && g.PackageImportPath != "" { + // Command-line: import_path=quux/bar. + // + // The import_path flag sets the import path for every file that + // we generate code for. + fd.importPath = GoImportPath(g.PackageImportPath) + } else if p, _, _ := fd.goPackageOption(); p != "" { + // Source file: option go_package = "quux/bar"; + // + // The go_package option sets the import path. Most users should use this. + fd.importPath = p + } else { + // Source filename. + // + // Last resort when nothing else is available. + fd.importPath = GoImportPath(path.Dir(f.GetName())) + } + // We must wrap the descriptors before we wrap the enums + fd.desc = wrapDescriptors(fd) + g.buildNestedDescriptors(fd.desc) + fd.enum = wrapEnumDescriptors(fd, fd.desc) + g.buildNestedEnums(fd.desc, fd.enum) + fd.ext = wrapExtensions(fd) + extractComments(fd) + g.allFiles = append(g.allFiles, fd) + g.allFilesByName[f.GetName()] = fd + } + for _, fd := range g.allFiles { + fd.imp = wrapImported(fd, g) + } + + g.genFiles = make([]*FileDescriptor, 0, len(g.Request.FileToGenerate)) + for _, fileName := range g.Request.FileToGenerate { + fd := g.allFilesByName[fileName] + if fd == nil { + g.Fail("could not find file named", fileName) + } + g.genFiles = append(g.genFiles, fd) + } +} + +// Scan the descriptors in this file. For each one, build the slice of nested descriptors +func (g *Generator) buildNestedDescriptors(descs []*Descriptor) { + for _, desc := range descs { + if len(desc.NestedType) != 0 { + for _, nest := range descs { + if nest.parent == desc { + desc.nested = append(desc.nested, nest) + } + } + if len(desc.nested) != len(desc.NestedType) { + g.Fail("internal error: nesting failure for", desc.GetName()) + } + } + } +} + +func (g *Generator) buildNestedEnums(descs []*Descriptor, enums []*EnumDescriptor) { + for _, desc := range descs { + if len(desc.EnumType) != 0 { + for _, enum := range enums { + if enum.parent == desc { + desc.enums = append(desc.enums, enum) + } + } + if len(desc.enums) != len(desc.EnumType) { + g.Fail("internal error: enum nesting failure for", desc.GetName()) + } + } + } +} + +// Construct the Descriptor +func newDescriptor(desc *descriptor.DescriptorProto, parent *Descriptor, file *FileDescriptor, index int) *Descriptor { + d := &Descriptor{ + common: common{file}, + DescriptorProto: desc, + parent: parent, + index: index, + } + if parent == nil { + d.path = fmt.Sprintf("%d,%d", messagePath, index) + } else { + d.path = fmt.Sprintf("%s,%d,%d", parent.path, messageMessagePath, index) + } + + // The only way to distinguish a group from a message is whether + // the containing message has a TYPE_GROUP field that matches. + if parent != nil { + parts := d.TypeName() + if file.Package != nil { + parts = append([]string{*file.Package}, parts...) + } + exp := "." + strings.Join(parts, ".") + for _, field := range parent.Field { + if field.GetType() == descriptor.FieldDescriptorProto_TYPE_GROUP && field.GetTypeName() == exp { + d.group = true + break + } + } + } + + for _, field := range desc.Extension { + d.ext = append(d.ext, &ExtensionDescriptor{common{file}, field, d}) + } + + return d +} + +// Return a slice of all the Descriptors defined within this file +func wrapDescriptors(file *FileDescriptor) []*Descriptor { + sl := make([]*Descriptor, 0, len(file.MessageType)+10) + for i, desc := range file.MessageType { + sl = wrapThisDescriptor(sl, desc, nil, file, i) + } + return sl +} + +// Wrap this Descriptor, recursively +func wrapThisDescriptor(sl []*Descriptor, desc *descriptor.DescriptorProto, parent *Descriptor, file *FileDescriptor, index int) []*Descriptor { + sl = append(sl, newDescriptor(desc, parent, file, index)) + me := sl[len(sl)-1] + for i, nested := range desc.NestedType { + sl = wrapThisDescriptor(sl, nested, me, file, i) + } + return sl +} + +// Construct the EnumDescriptor +func newEnumDescriptor(desc *descriptor.EnumDescriptorProto, parent *Descriptor, file *FileDescriptor, index int) *EnumDescriptor { + ed := &EnumDescriptor{ + common: common{file}, + EnumDescriptorProto: desc, + parent: parent, + index: index, + } + if parent == nil { + ed.path = fmt.Sprintf("%d,%d", enumPath, index) + } else { + ed.path = fmt.Sprintf("%s,%d,%d", parent.path, messageEnumPath, index) + } + return ed +} + +// Return a slice of all the EnumDescriptors defined within this file +func wrapEnumDescriptors(file *FileDescriptor, descs []*Descriptor) []*EnumDescriptor { + sl := make([]*EnumDescriptor, 0, len(file.EnumType)+10) + // Top-level enums. + for i, enum := range file.EnumType { + sl = append(sl, newEnumDescriptor(enum, nil, file, i)) + } + // Enums within messages. Enums within embedded messages appear in the outer-most message. + for _, nested := range descs { + for i, enum := range nested.EnumType { + sl = append(sl, newEnumDescriptor(enum, nested, file, i)) + } + } + return sl +} + +// Return a slice of all the top-level ExtensionDescriptors defined within this file. +func wrapExtensions(file *FileDescriptor) []*ExtensionDescriptor { + var sl []*ExtensionDescriptor + for _, field := range file.Extension { + sl = append(sl, &ExtensionDescriptor{common{file}, field, nil}) + } + return sl +} + +// Return a slice of all the types that are publicly imported into this file. +func wrapImported(file *FileDescriptor, g *Generator) (sl []*ImportedDescriptor) { + for _, index := range file.PublicDependency { + df := g.fileByName(file.Dependency[index]) + for _, d := range df.desc { + if d.GetOptions().GetMapEntry() { + continue + } + sl = append(sl, &ImportedDescriptor{common{file}, d}) + } + for _, e := range df.enum { + sl = append(sl, &ImportedDescriptor{common{file}, e}) + } + for _, ext := range df.ext { + sl = append(sl, &ImportedDescriptor{common{file}, ext}) + } + } + return +} + +func extractComments(file *FileDescriptor) { + file.comments = make(map[string]*descriptor.SourceCodeInfo_Location) + for _, loc := range file.GetSourceCodeInfo().GetLocation() { + if loc.LeadingComments == nil { + continue + } + var p []string + for _, n := range loc.Path { + p = append(p, strconv.Itoa(int(n))) + } + file.comments[strings.Join(p, ",")] = loc + } +} + +// BuildTypeNameMap builds the map from fully qualified type names to objects. +// The key names for the map come from the input data, which puts a period at the beginning. +// It should be called after SetPackageNames and before GenerateAllFiles. +func (g *Generator) BuildTypeNameMap() { + g.typeNameToObject = make(map[string]Object) + for _, f := range g.allFiles { + // The names in this loop are defined by the proto world, not us, so the + // package name may be empty. If so, the dotted package name of X will + // be ".X"; otherwise it will be ".pkg.X". + dottedPkg := "." + f.GetPackage() + if dottedPkg != "." { + dottedPkg += "." + } + for _, enum := range f.enum { + name := dottedPkg + dottedSlice(enum.TypeName()) + g.typeNameToObject[name] = enum + } + for _, desc := range f.desc { + name := dottedPkg + dottedSlice(desc.TypeName()) + g.typeNameToObject[name] = desc + } + } +} + +// ObjectNamed, given a fully-qualified input type name as it appears in the input data, +// returns the descriptor for the message or enum with that name. +func (g *Generator) ObjectNamed(typeName string) Object { + o, ok := g.typeNameToObject[typeName] + if !ok { + g.Fail("can't find object with type", typeName) + } + return o +} + +// AnnotatedAtoms is a list of atoms (as consumed by P) that records the file name and proto AST path from which they originated. +type AnnotatedAtoms struct { + source string + path string + atoms []interface{} +} + +// Annotate records the file name and proto AST path of a list of atoms +// so that a later call to P can emit a link from each atom to its origin. +func Annotate(file *FileDescriptor, path string, atoms ...interface{}) *AnnotatedAtoms { + return &AnnotatedAtoms{source: *file.Name, path: path, atoms: atoms} +} + +// printAtom prints the (atomic, non-annotation) argument to the generated output. +func (g *Generator) printAtom(v interface{}) { + switch v := v.(type) { + case string: + g.WriteString(v) + case *string: + g.WriteString(*v) + case bool: + fmt.Fprint(g, v) + case *bool: + fmt.Fprint(g, *v) + case int: + fmt.Fprint(g, v) + case *int32: + fmt.Fprint(g, *v) + case *int64: + fmt.Fprint(g, *v) + case float64: + fmt.Fprint(g, v) + case *float64: + fmt.Fprint(g, *v) + case GoPackageName: + g.WriteString(string(v)) + case GoImportPath: + g.WriteString(strconv.Quote(string(v))) + default: + g.Fail(fmt.Sprintf("unknown type in printer: %T", v)) + } +} + +// P prints the arguments to the generated output. It handles strings and int32s, plus +// handling indirections because they may be *string, etc. Any inputs of type AnnotatedAtoms may emit +// annotations in a .meta file in addition to outputting the atoms themselves (if g.annotateCode +// is true). +func (g *Generator) P(str ...interface{}) { + if !g.writeOutput { + return + } + g.WriteString(g.indent) + for _, v := range str { + switch v := v.(type) { + case *AnnotatedAtoms: + for _, v := range v.atoms { + g.printAtom(v) + } + default: + g.printAtom(v) + } + } + g.WriteByte('\n') +} + +// addInitf stores the given statement to be printed inside the file's init function. +// The statement is given as a format specifier and arguments. +func (g *Generator) addInitf(stmt string, a ...interface{}) { + g.init = append(g.init, fmt.Sprintf(stmt, a...)) +} + +// In Indents the output one tab stop. +func (g *Generator) In() { g.indent += "\t" } + +// Out unindents the output one tab stop. +func (g *Generator) Out() { + if len(g.indent) > 0 { + g.indent = g.indent[1:] + } +} + +// GenerateAllFiles generates the output for all the files we're outputting. +func (g *Generator) GenerateAllFiles() { + // Initialize the plugins + for _, p := range plugins { + p.Init(g) + } + // Generate the output. The generator runs for every file, even the files + // that we don't generate output for, so that we can collate the full list + // of exported symbols to support public imports. + genFileMap := make(map[*FileDescriptor]bool, len(g.genFiles)) + for _, file := range g.genFiles { + genFileMap[file] = true + } + for _, file := range g.allFiles { + g.Reset() + g.writeOutput = genFileMap[file] + g.generate(file) + if !g.writeOutput { + continue + } + fname := file.goFileName(g.pathType, g.ModuleRoot) + g.Response.File = append(g.Response.File, &plugin.CodeGeneratorResponse_File{ + Name: proto.String(fname), + Content: proto.String(g.String()), + }) + } + g.Response.SupportedFeatures = proto.Uint64(SupportedFeatures) +} + +// Run all the plugins associated with the file. +func (g *Generator) runPlugins(file *FileDescriptor) { + for _, p := range plugins { + p.Generate(file) + } +} + +// Fill the response protocol buffer with the generated output for all the files we're +// supposed to generate. +func (g *Generator) generate(file *FileDescriptor) { + g.file = file + g.usedPackages = make(map[GoImportPath]bool) + g.packageNames = make(map[GoImportPath]GoPackageName) + g.usedPackageNames = make(map[GoPackageName]bool) + g.addedImports = make(map[GoImportPath]bool) + for name := range globalPackageNames { + g.usedPackageNames[name] = true + } + + for _, td := range g.file.imp { + g.generateImported(td) + } + + g.generateInitFunction() + + // Run the plugins before the imports so we know which imports are necessary. + g.runPlugins(file) + + // Generate header and imports last, though they appear first in the output. + rem := g.Buffer + g.Buffer = new(bytes.Buffer) + g.generateHeader() + g.generateImports() + if !g.writeOutput { + return + } + g.Write(rem.Bytes()) + + // Reformat generated code and patch annotation locations. + fset := token.NewFileSet() + original := g.Bytes() + fileAST, err := parser.ParseFile(fset, "", original, parser.ParseComments) + if err != nil { + // Print out the bad code with line numbers. + // This should never happen in practice, but it can while changing generated code, + // so consider this a debugging aid. + var src bytes.Buffer + s := bufio.NewScanner(bytes.NewReader(original)) + for line := 1; s.Scan(); line++ { + fmt.Fprintf(&src, "%5d\t%s\n", line, s.Bytes()) + } + g.Fail("bad Go source code was generated:", err.Error(), "\n"+src.String()) + } + ast.SortImports(fset, fileAST) + g.Reset() + err = (&printer.Config{Mode: printer.TabIndent | printer.UseSpaces, Tabwidth: 8}).Fprint(g, fset, fileAST) + if err != nil { + g.Fail("generated Go source code could not be reformatted:", err.Error()) + } +} + +// Generate the header, including package definition +func (g *Generator) generateHeader() { + g.P("// Code generated by protoc-gen-micro. DO NOT EDIT.") + if g.file.GetOptions().GetDeprecated() { + g.P("// ", g.file.Name, " is a deprecated file.") + } else { + g.P("// source: ", g.file.Name) + } + g.P() + g.PrintComments(strconv.Itoa(packagePath)) + g.P() + g.P("package ", g.file.packageName) + g.P() +} + +// deprecationComment is the standard comment added to deprecated +// messages, fields, enums, and enum values. +var deprecationComment = "// Deprecated: Do not use." + +// PrintComments prints any comments from the source .proto file. +// The path is a comma-separated list of integers. +// It returns an indication of whether any comments were printed. +// See descriptor.proto for its format. +func (g *Generator) PrintComments(path string) bool { + if !g.writeOutput { + return false + } + if c, ok := g.makeComments(path); ok { + g.P(c) + return true + } + return false +} + +// makeComments generates the comment string for the field, no "\n" at the end +func (g *Generator) makeComments(path string) (string, bool) { + loc, ok := g.file.comments[path] + if !ok { + return "", false + } + w := new(bytes.Buffer) + nl := "" + for _, line := range strings.Split(strings.TrimSuffix(loc.GetLeadingComments(), "\n"), "\n") { + fmt.Fprintf(w, "%s//%s", nl, line) + nl = "\n" + } + return w.String(), true +} + +func (g *Generator) fileByName(filename string) *FileDescriptor { + return g.allFilesByName[filename] +} + +// weak returns whether the ith import of the current file is a weak import. +func (g *Generator) weak(i int32) bool { + for _, j := range g.file.WeakDependency { + if j == i { + return true + } + } + return false +} + +// Generate the imports +func (g *Generator) generateImports() { + imports := make(map[GoImportPath]GoPackageName) + for i, s := range g.file.Dependency { + fd := g.fileByName(s) + importPath := fd.importPath + // Do not import our own package. + if importPath == g.file.importPath { + continue + } + // Do not import weak imports. + if g.weak(int32(i)) { + continue + } + // Do not import a package twice. + if _, ok := imports[importPath]; ok { + continue + } + // We need to import all the dependencies, even if we don't reference them, + // because other code and tools depend on having the full transitive closure + // of protocol buffer types in the binary. + packageName := g.GoPackageName(importPath) + if _, ok := g.usedPackages[importPath]; !ok { + packageName = "_" + } + imports[importPath] = packageName + } + for importPath := range g.addedImports { + imports[importPath] = g.GoPackageName(importPath) + } + // We almost always need a proto import. Rather than computing when we + // do, which is tricky when there's a plugin, just import it and + // reference it later. The same argument applies to the fmt and math packages. + g.P("import (") + g.P(g.Pkg["fmt"] + ` "fmt"`) + g.P(g.Pkg["math"] + ` "math"`) + g.P(g.Pkg["proto"]+" ", GoImportPath(g.ImportPrefix)+"google.golang.org/protobuf/proto") + for importPath, packageName := range imports { + g.P(packageName, " ", GoImportPath(g.ImportPrefix)+importPath) + } + g.P(")") + g.P() + // TODO: may need to worry about uniqueness across plugins + for _, p := range plugins { + p.GenerateImports(g.file, imports) + g.P() + } + g.P("// Reference imports to suppress errors if they are not otherwise used.") + g.P("var _ = ", g.Pkg["proto"], ".Marshal") + g.P("var _ = ", g.Pkg["fmt"], ".Errorf") + g.P("var _ = ", g.Pkg["math"], ".Inf") + g.P() +} + +func (g *Generator) generateImported(id *ImportedDescriptor) { + df := id.o.File() + filename := *df.Name + if df.importPath == g.file.importPath { + // Don't generate type aliases for files in the same Go package as this one. + return + } + if !supportTypeAliases { + g.Fail(fmt.Sprintf("%s: public imports require at least go1.9", filename)) + } + g.usedPackages[df.importPath] = true + + for _, sym := range df.exported[id.o] { + sym.GenerateAlias(g, filename, g.GoPackageName(df.importPath)) + } + + g.P() +} + +// Generate the enum definitions for this EnumDescriptor. +func (g *Generator) generateEnum(enum *EnumDescriptor) { + // The full type name + typeName := enum.TypeName() + // The full type name, CamelCased. + ccTypeName := CamelCaseSlice(typeName) + ccPrefix := enum.prefix() + + deprecatedEnum := "" + if enum.GetOptions().GetDeprecated() { + deprecatedEnum = deprecationComment + } + g.PrintComments(enum.path) + g.P("type ", Annotate(enum.file, enum.path, ccTypeName), " int32", deprecatedEnum) + g.file.addExport(enum, enumSymbol{ccTypeName, enum.proto3()}) + g.P("const (") + for i, e := range enum.Value { + etorPath := fmt.Sprintf("%s,%d,%d", enum.path, enumValuePath, i) + g.PrintComments(etorPath) + + deprecatedValue := "" + if e.GetOptions().GetDeprecated() { + deprecatedValue = deprecationComment + } + + name := ccPrefix + *e.Name + g.P(Annotate(enum.file, etorPath, name), " ", ccTypeName, " = ", e.Number, " ", deprecatedValue) + g.file.addExport(enum, constOrVarSymbol{name, "const", ccTypeName}) + } + g.P(")") + g.P() + g.P("var ", ccTypeName, "_name = map[int32]string{") + generated := make(map[int32]bool) // avoid duplicate values + for _, e := range enum.Value { + duplicate := "" + if _, present := generated[*e.Number]; present { + duplicate = "// Duplicate value: " + } + g.P(duplicate, e.Number, ": ", strconv.Quote(*e.Name), ",") + generated[*e.Number] = true + } + g.P("}") + g.P() + g.P("var ", ccTypeName, "_value = map[string]int32{") + for _, e := range enum.Value { + g.P(strconv.Quote(*e.Name), ": ", e.Number, ",") + } + g.P("}") + g.P() + + if !enum.proto3() { + g.P("func (x ", ccTypeName, ") Enum() *", ccTypeName, " {") + g.P("p := new(", ccTypeName, ")") + g.P("*p = x") + g.P("return p") + g.P("}") + g.P() + } + + g.P("func (x ", ccTypeName, ") String() string {") + g.P("return ", g.Pkg["proto"], ".EnumName(", ccTypeName, "_name, int32(x))") + g.P("}") + g.P() + + if !enum.proto3() { + g.P("func (x *", ccTypeName, ") UnmarshalJSON(data []byte) error {") + g.P("value, err := ", g.Pkg["proto"], ".UnmarshalJSONEnum(", ccTypeName, `_value, data, "`, ccTypeName, `")`) + g.P("if err != nil {") + g.P("return err") + g.P("}") + g.P("*x = ", ccTypeName, "(value)") + g.P("return nil") + g.P("}") + g.P() + } + + var indexes []string + for m := enum.parent; m != nil; m = m.parent { + // XXX: skip groups? + indexes = append([]string{strconv.Itoa(m.index)}, indexes...) + } + indexes = append(indexes, strconv.Itoa(enum.index)) + g.P("func (", ccTypeName, ") EnumDescriptor() ([]byte, []int) {") + g.P("return ", g.file.VarName(), ", []int{", strings.Join(indexes, ", "), "}") + g.P("}") + g.P() + if enum.file.GetPackage() == "google.protobuf" && enum.GetName() == "NullValue" { + g.P("func (", ccTypeName, `) XXX_WellKnownType() string { return "`, enum.GetName(), `" }`) + g.P() + } + + g.generateEnumRegistration(enum) +} + +// The tag is a string like "varint,2,opt,name=fieldname,def=7" that +// identifies details of the field for the protocol buffer marshaling and unmarshaling +// code. The fields are: +// +// wire encoding +// protocol tag number +// opt,req,rep for optional, required, or repeated +// packed whether the encoding is "packed" (optional; repeated primitives only) +// name= the original declared name +// enum= the name of the enum type if it is an enum-typed field. +// proto3 if this field is in a proto3 message +// def= string representation of the default value, if any. +// +// The default value must be in a representation that can be used at run-time +// to generate the default value. Thus bools become 0 and 1, for instance. +func (g *Generator) goTag(message *Descriptor, field *descriptor.FieldDescriptorProto, wiretype string) string { + optrepreq := "" + switch { + case isOptional(field): + optrepreq = "opt" + case isRequired(field): + optrepreq = "req" + case isRepeated(field): + optrepreq = "rep" + } + var defaultValue string + if dv := field.DefaultValue; dv != nil { // set means an explicit default + defaultValue = *dv + // Some types need tweaking. + switch *field.Type { + case descriptor.FieldDescriptorProto_TYPE_BOOL: + if defaultValue == "true" { + defaultValue = "1" + } else { + defaultValue = "0" + } + case descriptor.FieldDescriptorProto_TYPE_STRING, + descriptor.FieldDescriptorProto_TYPE_BYTES: + // Nothing to do. Quoting is done for the whole tag. + case descriptor.FieldDescriptorProto_TYPE_ENUM: + // For enums we need to provide the integer constant. + obj := g.ObjectNamed(field.GetTypeName()) + if id, ok := obj.(*ImportedDescriptor); ok { + // It is an enum that was publicly imported. + // We need the underlying type. + obj = id.o + } + enum, ok := obj.(*EnumDescriptor) + if !ok { + log.Printf("obj is a %T", obj) + if id, ok := obj.(*ImportedDescriptor); ok { + log.Printf("id.o is a %T", id.o) + } + g.Fail("unknown enum type", CamelCaseSlice(obj.TypeName())) + } + defaultValue = enum.integerValueAsString(defaultValue) + case descriptor.FieldDescriptorProto_TYPE_FLOAT: + if def := defaultValue; def != "inf" && def != "-inf" && def != "nan" { + if f, err := strconv.ParseFloat(defaultValue, 32); err == nil { + defaultValue = fmt.Sprint(float32(f)) + } + } + case descriptor.FieldDescriptorProto_TYPE_DOUBLE: + if def := defaultValue; def != "inf" && def != "-inf" && def != "nan" { + if f, err := strconv.ParseFloat(defaultValue, 64); err == nil { + defaultValue = fmt.Sprint(f) + } + } + } + defaultValue = ",def=" + defaultValue + } + enum := "" + if *field.Type == descriptor.FieldDescriptorProto_TYPE_ENUM { + // We avoid using obj.GoPackageName(), because we want to use the + // original (proto-world) package name. + obj := g.ObjectNamed(field.GetTypeName()) + if id, ok := obj.(*ImportedDescriptor); ok { + obj = id.o + } + enum = ",enum=" + if pkg := obj.File().GetPackage(); pkg != "" { + enum += pkg + "." + } + enum += CamelCaseSlice(obj.TypeName()) + } + packed := "" + if (field.Options != nil && field.Options.GetPacked()) || + // Per https://developers.google.com/protocol-buffers/docs/proto3#simple: + // "In proto3, repeated fields of scalar numeric types use packed encoding by default." + (message.proto3() && (field.Options == nil || field.Options.Packed == nil) && + isRepeated(field) && isScalar(field)) { + packed = ",packed" + } + fieldName := field.GetName() + name := fieldName + if *field.Type == descriptor.FieldDescriptorProto_TYPE_GROUP { + // We must use the type name for groups instead of + // the field name to preserve capitalization. + // type_name in FieldDescriptorProto is fully-qualified, + // but we only want the local part. + name = *field.TypeName + if i := strings.LastIndex(name, "."); i >= 0 { + name = name[i+1:] + } + } + if json := field.GetJsonName(); field.Extendee == nil && json != "" && json != name { + // TODO: escaping might be needed, in which case + // perhaps this should be in its own "json" tag. + name += ",json=" + json + } + name = ",name=" + name + if message.proto3() { + name += ",proto3" + } + oneof := "" + if field.OneofIndex != nil { + oneof = ",oneof" + } + return strconv.Quote(fmt.Sprintf("%s,%d,%s%s%s%s%s%s", + wiretype, + field.GetNumber(), + optrepreq, + packed, + name, + enum, + oneof, + defaultValue)) +} + +func needsStar(typ descriptor.FieldDescriptorProto_Type) bool { + switch typ { + case descriptor.FieldDescriptorProto_TYPE_GROUP: + return false + case descriptor.FieldDescriptorProto_TYPE_MESSAGE: + return false + case descriptor.FieldDescriptorProto_TYPE_BYTES: + return false + } + return true +} + +// TypeName is the printed name appropriate for an item. If the object is in the current file, +// TypeName drops the package name and underscores the rest. +// Otherwise the object is from another package; and the result is the underscored +// package name followed by the item name. +// The result always has an initial capital. +func (g *Generator) TypeName(obj Object) string { + return g.DefaultPackageName(obj) + CamelCaseSlice(obj.TypeName()) +} + +// GoType returns a string representing the type name, and the wire type +func (g *Generator) GoType(message *Descriptor, field *descriptor.FieldDescriptorProto) (typ string, wire string) { + // TODO: Options. + switch *field.Type { + case descriptor.FieldDescriptorProto_TYPE_DOUBLE: + typ, wire = "float64", "fixed64" + case descriptor.FieldDescriptorProto_TYPE_FLOAT: + typ, wire = "float32", "fixed32" + case descriptor.FieldDescriptorProto_TYPE_INT64: + typ, wire = "int64", "varint" + case descriptor.FieldDescriptorProto_TYPE_UINT64: + typ, wire = "uint64", "varint" + case descriptor.FieldDescriptorProto_TYPE_INT32: + typ, wire = "int32", "varint" + case descriptor.FieldDescriptorProto_TYPE_UINT32: + typ, wire = "uint32", "varint" + case descriptor.FieldDescriptorProto_TYPE_FIXED64: + typ, wire = "uint64", "fixed64" + case descriptor.FieldDescriptorProto_TYPE_FIXED32: + typ, wire = "uint32", "fixed32" + case descriptor.FieldDescriptorProto_TYPE_BOOL: + typ, wire = "bool", "varint" + case descriptor.FieldDescriptorProto_TYPE_STRING: + typ, wire = "string", "bytes" + case descriptor.FieldDescriptorProto_TYPE_GROUP: + desc := g.ObjectNamed(field.GetTypeName()) + typ, wire = "*"+g.TypeName(desc), "group" + case descriptor.FieldDescriptorProto_TYPE_MESSAGE: + desc := g.ObjectNamed(field.GetTypeName()) + typ, wire = "*"+g.TypeName(desc), "bytes" + case descriptor.FieldDescriptorProto_TYPE_BYTES: + typ, wire = "[]byte", "bytes" + case descriptor.FieldDescriptorProto_TYPE_ENUM: + desc := g.ObjectNamed(field.GetTypeName()) + typ, wire = g.TypeName(desc), "varint" + case descriptor.FieldDescriptorProto_TYPE_SFIXED32: + typ, wire = "int32", "fixed32" + case descriptor.FieldDescriptorProto_TYPE_SFIXED64: + typ, wire = "int64", "fixed64" + case descriptor.FieldDescriptorProto_TYPE_SINT32: + typ, wire = "int32", "zigzag32" + case descriptor.FieldDescriptorProto_TYPE_SINT64: + typ, wire = "int64", "zigzag64" + default: + g.Fail("unknown type for", field.GetName()) + } + if isRepeated(field) { + typ = "[]" + typ + } else if message != nil && message.proto3() { + return + } else if field.OneofIndex != nil && message != nil { + return + } else if needsStar(*field.Type) { + typ = "*" + typ + } + return +} + +func (g *Generator) RecordTypeUse(t string) { + if _, ok := g.typeNameToObject[t]; !ok { + return + } + importPath := g.ObjectNamed(t).GoImportPath() + if importPath == g.outputImportPath { + // Don't record use of objects in our package. + return + } + g.AddImport(importPath) + g.usedPackages[importPath] = true +} + +// Method names that may be generated. Fields with these names get an +// underscore appended. Any change to this set is a potential incompatible +// API change because it changes generated field names. +var methodNames = [...]string{ + "Reset", + "String", + "ProtoMessage", + "Marshal", + "Unmarshal", + "ExtensionRangeArray", + "ExtensionMap", + "Descriptor", +} + +// Names of messages in the `google.protobuf` package for which +// we will generate XXX_WellKnownType methods. +var wellKnownTypes = map[string]bool{ + "Any": true, + "Duration": true, + "Empty": true, + "Struct": true, + "Timestamp": true, + + "Value": true, + "ListValue": true, + "DoubleValue": true, + "FloatValue": true, + "Int64Value": true, + "UInt64Value": true, + "Int32Value": true, + "UInt32Value": true, + "BoolValue": true, + "StringValue": true, + "BytesValue": true, +} + +// getterDefault finds the default value for the field to return from a getter, +// regardless of if it's a built in default or explicit from the source. Returns e.g. "nil", `""`, "Default_MessageType_FieldName" +func (g *Generator) getterDefault(field *descriptor.FieldDescriptorProto, goMessageType string) string { + if isRepeated(field) { + return "nil" + } + if def := field.GetDefaultValue(); def != "" { + defaultConstant := g.defaultConstantName(goMessageType, field.GetName()) + if *field.Type != descriptor.FieldDescriptorProto_TYPE_BYTES { + return defaultConstant + } + return "append([]byte(nil), " + defaultConstant + "...)" + } + switch *field.Type { + case descriptor.FieldDescriptorProto_TYPE_BOOL: + return "false" + case descriptor.FieldDescriptorProto_TYPE_STRING: + return `""` + case descriptor.FieldDescriptorProto_TYPE_GROUP, descriptor.FieldDescriptorProto_TYPE_MESSAGE, descriptor.FieldDescriptorProto_TYPE_BYTES: + return "nil" + case descriptor.FieldDescriptorProto_TYPE_ENUM: + obj := g.ObjectNamed(field.GetTypeName()) + var enum *EnumDescriptor + if id, ok := obj.(*ImportedDescriptor); ok { + // The enum type has been publicly imported. + enum, _ = id.o.(*EnumDescriptor) + } else { + enum, _ = obj.(*EnumDescriptor) + } + if enum == nil { + log.Printf("don't know how to generate getter for %s", field.GetName()) + return "nil" + } + if len(enum.Value) == 0 { + return "0 // empty enum" + } + first := enum.Value[0].GetName() + return g.DefaultPackageName(obj) + enum.prefix() + first + default: + return "0" + } +} + +// defaultConstantName builds the name of the default constant from the message +// type name and the untouched field name, e.g. "Default_MessageType_FieldName" +func (g *Generator) defaultConstantName(goMessageType, protoFieldName string) string { + return "Default_" + goMessageType + "_" + CamelCase(protoFieldName) +} + +// The different types of fields in a message and how to actually print them +// Most of the logic for generateMessage is in the methods of these types. +// +// Note that the content of the field is irrelevant, a simpleField can contain +// anything from a scalar to a group (which is just a message). +// +// Extension fields (and message sets) are however handled separately. +// +// simpleField - a field that is neiter weak nor oneof, possibly repeated +// oneofField - field containing list of subfields: +// - oneofSubField - a field within the oneof + +// msgCtx contains the context for the generator functions. +type msgCtx struct { + goName string // Go struct name of the message, e.g. MessageName + message *Descriptor // The descriptor for the message +} + +// fieldCommon contains data common to all types of fields. +type fieldCommon struct { + goName string // Go name of field, e.g. "FieldName" or "Descriptor_" + protoName string // Name of field in proto language, e.g. "field_name" or "descriptor" + getterName string // Name of the getter, e.g. "GetFieldName" or "GetDescriptor_" + goType string // The Go type as a string, e.g. "*int32" or "*OtherMessage" + tags string // The tag string/annotation for the type, e.g. `protobuf:"varint,8,opt,name=region_id,json=regionId"` + fullPath string // The full path of the field as used by Annotate etc, e.g. "4,0,2,0" +} + +// getProtoName gets the proto name of a field, e.g. "field_name" or "descriptor". +func (f *fieldCommon) getProtoName() string { + return f.protoName +} + +// getGoType returns the go type of the field as a string, e.g. "*int32". +func (f *fieldCommon) getGoType() string { + return f.goType +} + +// simpleField is not weak, not a oneof, not an extension. Can be required, optional or repeated. +type simpleField struct { + fieldCommon + protoTypeName string // Proto type name, empty if primitive, e.g. ".google.protobuf.Duration" + protoType descriptor.FieldDescriptorProto_Type // Actual type enum value, e.g. descriptor.FieldDescriptorProto_TYPE_FIXED64 + deprecated string // Deprecation comment, if any, e.g. "// Deprecated: Do not use." + getterDef string // Default for getters, e.g. "nil", `""` or "Default_MessageType_FieldName" + protoDef string // Default value as defined in the proto file, e.g "yoshi" or "5" + comment string // The full comment for the field, e.g. "// Useful information" +} + +// decl prints the declaration of the field in the struct (if any). +func (f *simpleField) decl(g *Generator, mc *msgCtx) { + g.P(f.comment, Annotate(mc.message.file, f.fullPath, f.goName), "\t", f.goType, "\t`", f.tags, "`", f.deprecated) +} + +// getter prints the getter for the field. +func (f *simpleField) getter(g *Generator, mc *msgCtx) { + star := "" + tname := f.goType + if needsStar(f.protoType) && tname[0] == '*' { + tname = tname[1:] + star = "*" + } + if f.deprecated != "" { + g.P(f.deprecated) + } + g.P("func (m *", mc.goName, ") ", Annotate(mc.message.file, f.fullPath, f.getterName), "() "+tname+" {") + if f.getterDef == "nil" { // Simpler getter + g.P("if m != nil {") + g.P("return m." + f.goName) + g.P("}") + g.P("return nil") + g.P("}") + g.P() + return + } + if mc.message.proto3() { + g.P("if m != nil {") + } else { + g.P("if m != nil && m." + f.goName + " != nil {") + } + g.P("return " + star + "m." + f.goName) + g.P("}") + g.P("return ", f.getterDef) + g.P("}") + g.P() +} + +// setter prints the setter method of the field. +func (f *simpleField) setter(g *Generator, mc *msgCtx) { + // No setter for regular fields yet +} + +// getProtoDef returns the default value explicitly stated in the proto file, e.g "yoshi" or "5". +func (f *simpleField) getProtoDef() string { + return f.protoDef +} + +// getProtoTypeName returns the protobuf type name for the field as returned by field.GetTypeName(), e.g. ".google.protobuf.Duration". +func (f *simpleField) getProtoTypeName() string { + return f.protoTypeName +} + +// getProtoType returns the *field.Type value, e.g. descriptor.FieldDescriptorProto_TYPE_FIXED64. +func (f *simpleField) getProtoType() descriptor.FieldDescriptorProto_Type { + return f.protoType +} + +// oneofSubFields are kept slize held by each oneofField. They do not appear in the top level slize of fields for the message. +type oneofSubField struct { + fieldCommon + protoTypeName string // Proto type name, empty if primitive, e.g. ".google.protobuf.Duration" + protoType descriptor.FieldDescriptorProto_Type // Actual type enum value, e.g. descriptor.FieldDescriptorProto_TYPE_FIXED64 + oneofTypeName string // Type name of the enclosing struct, e.g. "MessageName_FieldName" + fieldNumber int // Actual field number, as defined in proto, e.g. 12 + getterDef string // Default for getters, e.g. "nil", `""` or "Default_MessageType_FieldName" + protoDef string // Default value as defined in the proto file, e.g "yoshi" or "5" + deprecated string // Deprecation comment, if any. +} + +// typedNil prints a nil casted to the pointer to this field. +// - for XXX_OneofWrappers +func (f *oneofSubField) typedNil(g *Generator) { + g.P("(*", f.oneofTypeName, ")(nil),") +} + +// getProtoDef returns the default value explicitly stated in the proto file, e.g "yoshi" or "5". +func (f *oneofSubField) getProtoDef() string { + return f.protoDef +} + +// getProtoTypeName returns the protobuf type name for the field as returned by field.GetTypeName(), e.g. ".google.protobuf.Duration". +func (f *oneofSubField) getProtoTypeName() string { + return f.protoTypeName +} + +// getProtoType returns the *field.Type value, e.g. descriptor.FieldDescriptorProto_TYPE_FIXED64. +func (f *oneofSubField) getProtoType() descriptor.FieldDescriptorProto_Type { + return f.protoType +} + +// oneofField represents the oneof on top level. +// The alternative fields within the oneof are represented by oneofSubField. +type oneofField struct { + fieldCommon + subFields []*oneofSubField // All the possible oneof fields + comment string // The full comment for the field, e.g. "// Types that are valid to be assigned to MyOneof:\n\\" +} + +// decl prints the declaration of the field in the struct (if any). +func (f *oneofField) decl(g *Generator, mc *msgCtx) { + comment := f.comment + for _, sf := range f.subFields { + comment += "//\t*" + sf.oneofTypeName + "\n" + } + g.P(comment, Annotate(mc.message.file, f.fullPath, f.goName), " ", f.goType, " `", f.tags, "`") +} + +// getter for a oneof field will print additional discriminators and interfaces for the oneof, +// also it prints all the getters for the sub fields. +func (f *oneofField) getter(g *Generator, mc *msgCtx) { + // The discriminator type + g.P("type ", f.goType, " interface {") + g.P(f.goType, "()") + g.P("}") + g.P() + // The subField types, fulfilling the discriminator type contract + for _, sf := range f.subFields { + g.P("type ", Annotate(mc.message.file, sf.fullPath, sf.oneofTypeName), " struct {") + g.P(Annotate(mc.message.file, sf.fullPath, sf.goName), " ", sf.goType, " `", sf.tags, "`") + g.P("}") + g.P() + } + for _, sf := range f.subFields { + g.P("func (*", sf.oneofTypeName, ") ", f.goType, "() {}") + g.P() + } + // Getter for the oneof field + g.P("func (m *", mc.goName, ") ", Annotate(mc.message.file, f.fullPath, f.getterName), "() ", f.goType, " {") + g.P("if m != nil { return m.", f.goName, " }") + g.P("return nil") + g.P("}") + g.P() + // Getters for each oneof + for _, sf := range f.subFields { + if sf.deprecated != "" { + g.P(sf.deprecated) + } + g.P("func (m *", mc.goName, ") ", Annotate(mc.message.file, sf.fullPath, sf.getterName), "() "+sf.goType+" {") + g.P("if x, ok := m.", f.getterName, "().(*", sf.oneofTypeName, "); ok {") + g.P("return x.", sf.goName) + g.P("}") + g.P("return ", sf.getterDef) + g.P("}") + g.P() + } +} + +// setter prints the setter method of the field. +func (f *oneofField) setter(g *Generator, mc *msgCtx) { + // No setters for oneof yet +} + +// topLevelField interface implemented by all types of fields on the top level (not oneofSubField). +type topLevelField interface { + decl(g *Generator, mc *msgCtx) // print declaration within the struct + getter(g *Generator, mc *msgCtx) // print getter + setter(g *Generator, mc *msgCtx) // print setter if applicable +} + +// defField interface implemented by all types of fields that can have defaults (not oneofField, but instead oneofSubField). +type defField interface { + getProtoDef() string // default value explicitly stated in the proto file, e.g "yoshi" or "5" + getProtoName() string // proto name of a field, e.g. "field_name" or "descriptor" + getGoType() string // go type of the field as a string, e.g. "*int32" + getProtoTypeName() string // protobuf type name for the field, e.g. ".google.protobuf.Duration" + getProtoType() descriptor.FieldDescriptorProto_Type // *field.Type value, e.g. descriptor.FieldDescriptorProto_TYPE_FIXED64 +} + +// generateDefaultConstants adds constants for default values if needed, which is only if the default value is. +// explicit in the proto. +func (g *Generator) generateDefaultConstants(mc *msgCtx, topLevelFields []topLevelField) { + // Collect fields that can have defaults + dFields := []defField{} + for _, pf := range topLevelFields { + if f, ok := pf.(*oneofField); ok { + for _, osf := range f.subFields { + dFields = append(dFields, osf) + } + continue + } + dFields = append(dFields, pf.(defField)) + } + for _, df := range dFields { + def := df.getProtoDef() + if def == "" { + continue + } + fieldname := g.defaultConstantName(mc.goName, df.getProtoName()) + typename := df.getGoType() + if typename[0] == '*' { + typename = typename[1:] + } + kind := "const " + switch { + case typename == "bool": + case typename == "string": + def = strconv.Quote(def) + case typename == "[]byte": + def = "[]byte(" + strconv.Quote(unescape(def)) + ")" + kind = "var " + case def == "inf", def == "-inf", def == "nan": + // These names are known to, and defined by, the protocol language. + switch def { + case "inf": + def = "math.Inf(1)" + case "-inf": + def = "math.Inf(-1)" + case "nan": + def = "math.NaN()" + } + if df.getProtoType() == descriptor.FieldDescriptorProto_TYPE_FLOAT { + def = "float32(" + def + ")" + } + kind = "var " + case df.getProtoType() == descriptor.FieldDescriptorProto_TYPE_FLOAT: + if f, err := strconv.ParseFloat(def, 32); err == nil { + def = fmt.Sprint(float32(f)) + } + case df.getProtoType() == descriptor.FieldDescriptorProto_TYPE_DOUBLE: + if f, err := strconv.ParseFloat(def, 64); err == nil { + def = fmt.Sprint(f) + } + case df.getProtoType() == descriptor.FieldDescriptorProto_TYPE_ENUM: + // Must be an enum. Need to construct the prefixed name. + obj := g.ObjectNamed(df.getProtoTypeName()) + var enum *EnumDescriptor + if id, ok := obj.(*ImportedDescriptor); ok { + // The enum type has been publicly imported. + enum, _ = id.o.(*EnumDescriptor) + } else { + enum, _ = obj.(*EnumDescriptor) + } + if enum == nil { + log.Printf("don't know how to generate constant for %s", fieldname) + continue + } + def = g.DefaultPackageName(obj) + enum.prefix() + def + } + g.P(kind, fieldname, " ", typename, " = ", def) + g.file.addExport(mc.message, constOrVarSymbol{fieldname, kind, ""}) + } + g.P() +} + +// generateInternalStructFields just adds the XXX_ fields to the message struct. +func (g *Generator) generateInternalStructFields(mc *msgCtx, topLevelFields []topLevelField) { + g.P("XXX_NoUnkeyedLiteral\tstruct{} `json:\"-\"`") // prevent unkeyed struct literals + if len(mc.message.ExtensionRange) > 0 { + messageset := "" + if opts := mc.message.Options; opts != nil && opts.GetMessageSetWireFormat() { + messageset = "protobuf_messageset:\"1\" " + } + g.P(g.Pkg["proto"], ".XXX_InternalExtensions `", messageset, "json:\"-\"`") + } + g.P("XXX_unrecognized\t[]byte `json:\"-\"`") + g.P("XXX_sizecache\tint32 `json:\"-\"`") + +} + +// generateOneofFuncs adds all the utility functions for oneof, including marshalling, unmarshalling and sizer. +func (g *Generator) generateOneofFuncs(mc *msgCtx, topLevelFields []topLevelField) { + ofields := []*oneofField{} + for _, f := range topLevelFields { + if o, ok := f.(*oneofField); ok { + ofields = append(ofields, o) + } + } + if len(ofields) == 0 { + return + } + + // OneofFuncs + g.P("// XXX_OneofWrappers is for the internal use of the proto package.") + g.P("func (*", mc.goName, ") XXX_OneofWrappers() []interface{} {") + g.P("return []interface{}{") + for _, of := range ofields { + for _, sf := range of.subFields { + sf.typedNil(g) + } + } + g.P("}") + g.P("}") + g.P() +} + +// generateMessageStruct adds the actual struct with it's members (but not methods) to the output. +func (g *Generator) generateMessageStruct(mc *msgCtx, topLevelFields []topLevelField) { + comments := g.PrintComments(mc.message.path) + + // Guarantee deprecation comments appear after user-provided comments. + if mc.message.GetOptions().GetDeprecated() { + if comments { + // Convention: Separate deprecation comments from original + // comments with an empty line. + g.P("//") + } + g.P(deprecationComment) + } + + g.P("type ", Annotate(mc.message.file, mc.message.path, mc.goName), " struct {") + for _, pf := range topLevelFields { + pf.decl(g, mc) + } + g.generateInternalStructFields(mc, topLevelFields) + g.P("}") +} + +// generateGetters adds getters for all fields, including oneofs and weak fields when applicable. +func (g *Generator) generateGetters(mc *msgCtx, topLevelFields []topLevelField) { + for _, pf := range topLevelFields { + pf.getter(g, mc) + } +} + +// generateSetters add setters for all fields, including oneofs and weak fields when applicable. +func (g *Generator) generateSetters(mc *msgCtx, topLevelFields []topLevelField) { + for _, pf := range topLevelFields { + pf.setter(g, mc) + } +} + +// generateCommonMethods adds methods to the message that are not on a per field basis. +func (g *Generator) generateCommonMethods(mc *msgCtx) { + // Reset, String and ProtoMessage methods. + g.P("func (m *", mc.goName, ") Reset() { *m = ", mc.goName, "{} }") + g.P("func (m *", mc.goName, ") String() string { return ", g.Pkg["proto"], ".CompactTextString(m) }") + g.P("func (*", mc.goName, ") ProtoMessage() {}") + var indexes []string + for m := mc.message; m != nil; m = m.parent { + indexes = append([]string{strconv.Itoa(m.index)}, indexes...) + } + g.P("func (*", mc.goName, ") Descriptor() ([]byte, []int) {") + g.P("return ", g.file.VarName(), ", []int{", strings.Join(indexes, ", "), "}") + g.P("}") + g.P() + // TODO: Revisit the decision to use a XXX_WellKnownType method + // if we change proto.MessageName to work with multiple equivalents. + if mc.message.file.GetPackage() == "google.protobuf" && wellKnownTypes[mc.message.GetName()] { + g.P("func (*", mc.goName, `) XXX_WellKnownType() string { return "`, mc.message.GetName(), `" }`) + g.P() + } + + // Extension support methods + if len(mc.message.ExtensionRange) > 0 { + g.P() + g.P("var extRange_", mc.goName, " = []", g.Pkg["proto"], ".ExtensionRange{") + for _, r := range mc.message.ExtensionRange { + end := fmt.Sprint(*r.End - 1) // make range inclusive on both ends + g.P("{Start: ", r.Start, ", End: ", end, "},") + } + g.P("}") + g.P("func (*", mc.goName, ") ExtensionRangeArray() []", g.Pkg["proto"], ".ExtensionRange {") + g.P("return extRange_", mc.goName) + g.P("}") + g.P() + } + + // TODO: It does not scale to keep adding another method for every + // operation on protos that we want to switch over to using the + // table-driven approach. Instead, we should only add a single method + // that allows getting access to the *InternalMessageInfo struct and then + // calling Unmarshal, Marshal, Merge, Size, and Discard directly on that. + + // Wrapper for table-driven marshaling and unmarshaling. + g.P("func (m *", mc.goName, ") XXX_Unmarshal(b []byte) error {") + g.P("return xxx_messageInfo_", mc.goName, ".Unmarshal(m, b)") + g.P("}") + + g.P("func (m *", mc.goName, ") XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {") + g.P("return xxx_messageInfo_", mc.goName, ".Marshal(b, m, deterministic)") + g.P("}") + + g.P("func (m *", mc.goName, ") XXX_Merge(src ", g.Pkg["proto"], ".Message) {") + g.P("xxx_messageInfo_", mc.goName, ".Merge(m, src)") + g.P("}") + + g.P("func (m *", mc.goName, ") XXX_Size() int {") // avoid name clash with "Size" field in some message + g.P("return xxx_messageInfo_", mc.goName, ".Size(m)") + g.P("}") + + g.P("func (m *", mc.goName, ") XXX_DiscardUnknown() {") + g.P("xxx_messageInfo_", mc.goName, ".DiscardUnknown(m)") + g.P("}") + + g.P("var xxx_messageInfo_", mc.goName, " ", g.Pkg["proto"], ".InternalMessageInfo") + g.P() +} + +// Generate the type, methods and default constant definitions for this Descriptor. +func (g *Generator) generateMessage(message *Descriptor) { + topLevelFields := []topLevelField{} + oFields := make(map[int32]*oneofField) + // The full type name + typeName := message.TypeName() + // The full type name, CamelCased. + goTypeName := CamelCaseSlice(typeName) + + usedNames := make(map[string]bool) + for _, n := range methodNames { + usedNames[n] = true + } + + // allocNames finds a conflict-free variation of the given strings, + // consistently mutating their suffixes. + // It returns the same number of strings. + allocNames := func(ns ...string) []string { + Loop: + for { + for _, n := range ns { + if usedNames[n] { + for i := range ns { + ns[i] += "_" + } + continue Loop + } + } + for _, n := range ns { + usedNames[n] = true + } + return ns + } + } + + mapFieldTypes := make(map[*descriptor.FieldDescriptorProto]string) // keep track of the map fields to be added later + + // Build a structure more suitable for generating the text in one pass + for i, field := range message.Field { + // Allocate the getter and the field at the same time so name + // collisions create field/method consistent names. + // TODO: This allocation occurs based on the order of the fields + // in the proto file, meaning that a change in the field + // ordering can change generated Method/Field names. + base := CamelCase(*field.Name) + ns := allocNames(base, "Get"+base) + fieldName, fieldGetterName := ns[0], ns[1] + typename, wiretype := g.GoType(message, field) + jsonName := *field.Name + tag := fmt.Sprintf("protobuf:%s json:%q", g.goTag(message, field, wiretype), jsonName+",omitempty") + + oneof := field.OneofIndex != nil + if oneof && oFields[*field.OneofIndex] == nil { + odp := message.OneofDecl[int(*field.OneofIndex)] + base := CamelCase(odp.GetName()) + fname := allocNames(base)[0] + + // This is the first field of a oneof we haven't seen before. + // Generate the union field. + oneofFullPath := fmt.Sprintf("%s,%d,%d", message.path, messageOneofPath, *field.OneofIndex) + c, ok := g.makeComments(oneofFullPath) + if ok { + c += "\n//\n" + } + c += "// Types that are valid to be assigned to " + fname + ":\n" + // Generate the rest of this comment later, + // when we've computed any disambiguation. + + dname := "is" + goTypeName + "_" + fname + tag := `protobuf_oneof:"` + odp.GetName() + `"` + of := oneofField{ + fieldCommon: fieldCommon{ + goName: fname, + getterName: "Get" + fname, + goType: dname, + tags: tag, + protoName: odp.GetName(), + fullPath: oneofFullPath, + }, + comment: c, + } + topLevelFields = append(topLevelFields, &of) + oFields[*field.OneofIndex] = &of + } + + if *field.Type == descriptor.FieldDescriptorProto_TYPE_MESSAGE { + desc := g.ObjectNamed(field.GetTypeName()) + if d, ok := desc.(*Descriptor); ok && d.GetOptions().GetMapEntry() { + // Figure out the Go types and tags for the key and value types. + keyField, valField := d.Field[0], d.Field[1] + keyType, keyWire := g.GoType(d, keyField) + valType, valWire := g.GoType(d, valField) + keyTag, valTag := g.goTag(d, keyField, keyWire), g.goTag(d, valField, valWire) + + // We don't use stars, except for message-typed values. + // Message and enum types are the only two possibly foreign types used in maps, + // so record their use. They are not permitted as map keys. + keyType = strings.TrimPrefix(keyType, "*") + switch *valField.Type { + case descriptor.FieldDescriptorProto_TYPE_ENUM: + valType = strings.TrimPrefix(valType, "*") + g.RecordTypeUse(valField.GetTypeName()) + case descriptor.FieldDescriptorProto_TYPE_MESSAGE: + g.RecordTypeUse(valField.GetTypeName()) + default: + valType = strings.TrimPrefix(valType, "*") + } + + typename = fmt.Sprintf("map[%s]%s", keyType, valType) + mapFieldTypes[field] = typename // record for the getter generation + + tag += fmt.Sprintf(" protobuf_key:%s protobuf_val:%s", keyTag, valTag) + } + } + + fieldDeprecated := "" + if field.GetOptions().GetDeprecated() { + fieldDeprecated = deprecationComment + } + + dvalue := g.getterDefault(field, goTypeName) + if oneof { + tname := goTypeName + "_" + fieldName + // It is possible for this to collide with a message or enum + // nested in this message. Check for collisions. + for { + ok := true + for _, desc := range message.nested { + if CamelCaseSlice(desc.TypeName()) == tname { + ok = false + break + } + } + for _, enum := range message.enums { + if CamelCaseSlice(enum.TypeName()) == tname { + ok = false + break + } + } + if !ok { + tname += "_" + continue + } + break + } + + oneofField := oFields[*field.OneofIndex] + tag := "protobuf:" + g.goTag(message, field, wiretype) + sf := oneofSubField{ + fieldCommon: fieldCommon{ + goName: fieldName, + getterName: fieldGetterName, + goType: typename, + tags: tag, + protoName: field.GetName(), + fullPath: fmt.Sprintf("%s,%d,%d", message.path, messageFieldPath, i), + }, + protoTypeName: field.GetTypeName(), + fieldNumber: int(*field.Number), + protoType: *field.Type, + getterDef: dvalue, + protoDef: field.GetDefaultValue(), + oneofTypeName: tname, + deprecated: fieldDeprecated, + } + oneofField.subFields = append(oneofField.subFields, &sf) + g.RecordTypeUse(field.GetTypeName()) + continue + } + + fieldFullPath := fmt.Sprintf("%s,%d,%d", message.path, messageFieldPath, i) + c, ok := g.makeComments(fieldFullPath) + if ok { + c += "\n" + } + rf := simpleField{ + fieldCommon: fieldCommon{ + goName: fieldName, + getterName: fieldGetterName, + goType: typename, + tags: tag, + protoName: field.GetName(), + fullPath: fieldFullPath, + }, + protoTypeName: field.GetTypeName(), + protoType: *field.Type, + deprecated: fieldDeprecated, + getterDef: dvalue, + protoDef: field.GetDefaultValue(), + comment: c, + } + var pf topLevelField = &rf + + topLevelFields = append(topLevelFields, pf) + g.RecordTypeUse(field.GetTypeName()) + } + + mc := &msgCtx{ + goName: goTypeName, + message: message, + } + + g.generateMessageStruct(mc, topLevelFields) + g.P() + g.generateCommonMethods(mc) + g.P() + g.generateDefaultConstants(mc, topLevelFields) + g.P() + g.generateGetters(mc, topLevelFields) + g.P() + g.generateSetters(mc, topLevelFields) + g.P() + g.generateOneofFuncs(mc, topLevelFields) + g.P() + + var oneofTypes []string + for _, f := range topLevelFields { + if of, ok := f.(*oneofField); ok { + for _, osf := range of.subFields { + oneofTypes = append(oneofTypes, osf.oneofTypeName) + } + } + } + + opts := message.Options + ms := &messageSymbol{ + sym: goTypeName, + hasExtensions: len(message.ExtensionRange) > 0, + isMessageSet: opts != nil && opts.GetMessageSetWireFormat(), + oneofTypes: oneofTypes, + } + g.file.addExport(message, ms) + + for _, ext := range message.ext { + g.generateExtension(ext) + } + + fullName := strings.Join(message.TypeName(), ".") + if g.file.Package != nil { + fullName = *g.file.Package + "." + fullName + } + + g.addInitf("%s.RegisterType((*%s)(nil), %q)", g.Pkg["proto"], goTypeName, fullName) + // Register types for native map types. + for _, k := range mapFieldKeys(mapFieldTypes) { + fullName := strings.TrimPrefix(*k.TypeName, ".") + g.addInitf("%s.RegisterMapType((%s)(nil), %q)", g.Pkg["proto"], mapFieldTypes[k], fullName) + } + +} + +type byTypeName []*descriptor.FieldDescriptorProto + +func (a byTypeName) Len() int { return len(a) } +func (a byTypeName) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a byTypeName) Less(i, j int) bool { return *a[i].TypeName < *a[j].TypeName } + +// mapFieldKeys returns the keys of m in a consistent order. +func mapFieldKeys(m map[*descriptor.FieldDescriptorProto]string) []*descriptor.FieldDescriptorProto { + keys := make([]*descriptor.FieldDescriptorProto, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + sort.Sort(byTypeName(keys)) + return keys +} + +var escapeChars = [256]byte{ + 'a': '\a', 'b': '\b', 'f': '\f', 'n': '\n', 'r': '\r', 't': '\t', 'v': '\v', '\\': '\\', '"': '"', '\'': '\'', '?': '?', +} + +// unescape reverses the "C" escaping that protoc does for default values of bytes fields. +// It is best effort in that it effectively ignores malformed input. Seemingly invalid escape +// sequences are conveyed, unmodified, into the decoded result. +func unescape(s string) string { + // NB: Sadly, we can't use strconv.Unquote because protoc will escape both + // single and double quotes, but strconv.Unquote only allows one or the + // other (based on actual surrounding quotes of its input argument). + + var out []byte + for len(s) > 0 { + // regular character, or too short to be valid escape + if s[0] != '\\' || len(s) < 2 { + out = append(out, s[0]) + s = s[1:] + } else if c := escapeChars[s[1]]; c != 0 { + // escape sequence + out = append(out, c) + s = s[2:] + } else if s[1] == 'x' || s[1] == 'X' { + // hex escape, e.g. "\x80 + if len(s) < 4 { + // too short to be valid + out = append(out, s[:2]...) + s = s[2:] + continue + } + v, err := strconv.ParseUint(s[2:4], 16, 8) + if err != nil { + out = append(out, s[:4]...) + } else { + out = append(out, byte(v)) + } + s = s[4:] + } else if '0' <= s[1] && s[1] <= '7' { + // octal escape, can vary from 1 to 3 octal digits; e.g., "\0" "\40" or "\164" + // so consume up to 2 more bytes or up to end-of-string + n := len(s[1:]) - len(strings.TrimLeft(s[1:], "01234567")) + if n > 3 { + n = 3 + } + v, err := strconv.ParseUint(s[1:1+n], 8, 8) + if err != nil { + out = append(out, s[:1+n]...) + } else { + out = append(out, byte(v)) + } + s = s[1+n:] + } else { + // bad escape, just propagate the slash as-is + out = append(out, s[0]) + s = s[1:] + } + } + + return string(out) +} + +func (g *Generator) generateExtension(ext *ExtensionDescriptor) { + ccTypeName := ext.DescName() + + extObj := g.ObjectNamed(*ext.Extendee) + var extDesc *Descriptor + if id, ok := extObj.(*ImportedDescriptor); ok { + // This is extending a publicly imported message. + // We need the underlying type for goTag. + extDesc = id.o.(*Descriptor) + } else { + extDesc = extObj.(*Descriptor) + } + extendedType := "*" + g.TypeName(extObj) // always use the original + field := ext.FieldDescriptorProto + fieldType, wireType := g.GoType(ext.parent, field) + tag := g.goTag(extDesc, field, wireType) + g.RecordTypeUse(*ext.Extendee) + if n := ext.FieldDescriptorProto.TypeName; n != nil { + // foreign extension type + g.RecordTypeUse(*n) + } + + typeName := ext.TypeName() + + // Special case for proto2 message sets: If this extension is extending + // proto2.bridge.MessageSet, and its final name component is "message_set_extension", + // then drop that last component. + // + // TODO: This should be implemented in the text formatter rather than the generator. + // In addition, the situation for when to apply this special case is implemented + // differently in other languages: + // https://github.com/google/protobuf/blob/aff10976/src/google/protobuf/text_format.cc#L1560 + if extDesc.GetOptions().GetMessageSetWireFormat() && typeName[len(typeName)-1] == "message_set_extension" { + typeName = typeName[:len(typeName)-1] + } + + // For text formatting, the package must be exactly what the .proto file declares, + // ignoring overrides such as the go_package option, and with no dot/underscore mapping. + extName := strings.Join(typeName, ".") + if g.file.Package != nil { + extName = *g.file.Package + "." + extName + } + + g.P("var ", ccTypeName, " = &", g.Pkg["proto"], ".ExtensionDesc{") + g.P("ExtendedType: (", extendedType, ")(nil),") + g.P("ExtensionType: (", fieldType, ")(nil),") + g.P("Field: ", field.Number, ",") + g.P(`Name: "`, extName, `",`) + g.P("Tag: ", tag, ",") + g.P(`Filename: "`, g.file.GetName(), `",`) + + g.P("}") + g.P() + + g.addInitf("%s.RegisterExtension(%s)", g.Pkg["proto"], ext.DescName()) + + g.file.addExport(ext, constOrVarSymbol{ccTypeName, "var", ""}) +} + +func (g *Generator) generateInitFunction() { + if len(g.init) == 0 { + return + } + g.P("func init() {") + for _, l := range g.init { + g.P(l) + } + g.P("}") + g.init = nil +} + +func (g *Generator) generateFileDescriptor(file *FileDescriptor) { + // Make a copy and trim source_code_info data. + // TODO: Trim this more when we know exactly what we need. + pb := proto.Clone(file.FileDescriptorProto).(*descriptor.FileDescriptorProto) + pb.SourceCodeInfo = nil + + b, err := proto.Marshal(pb) + if err != nil { + g.Fail(err.Error()) + } + + var buf bytes.Buffer + w, _ := gzip.NewWriterLevel(&buf, gzip.BestCompression) + w.Write(b) + w.Close() + b = buf.Bytes() + + v := file.VarName() + g.P() + g.P("func init() { ", g.Pkg["proto"], ".RegisterFile(", strconv.Quote(*file.Name), ", ", v, ") }") + g.P("var ", v, " = []byte{") + g.P("// ", len(b), " bytes of a gzipped FileDescriptorProto") + for len(b) > 0 { + n := 16 + if n > len(b) { + n = len(b) + } + + s := "" + for _, c := range b[:n] { + s += fmt.Sprintf("0x%02x,", c) + } + g.P(s) + + b = b[n:] + } + g.P("}") +} + +func (g *Generator) generateEnumRegistration(enum *EnumDescriptor) { + // // We always print the full (proto-world) package name here. + pkg := enum.File().GetPackage() + if pkg != "" { + pkg += "." + } + // The full type name + typeName := enum.TypeName() + // The full type name, CamelCased. + ccTypeName := CamelCaseSlice(typeName) + g.addInitf("%s.RegisterEnum(%q, %[3]s_name, %[3]s_value)", g.Pkg["proto"], pkg+ccTypeName, ccTypeName) +} + +// And now lots of helper functions. + +// Is c an ASCII lower-case letter? +func isASCIILower(c byte) bool { + return 'a' <= c && c <= 'z' +} + +// Is c an ASCII digit? +func isASCIIDigit(c byte) bool { + return '0' <= c && c <= '9' +} + +// CamelCase returns the CamelCased name. +// If there is an interior underscore followed by a lower case letter, +// drop the underscore and convert the letter to upper case. +// There is a remote possibility of this rewrite causing a name collision, +// but it's so remote we're prepared to pretend it's nonexistent - since the +// C++ generator lowercases names, it's extremely unlikely to have two fields +// with different capitalizations. +// In short, _my_field_name_2 becomes XMyFieldName_2. +func CamelCase(s string) string { + if s == "" { + return "" + } + t := make([]byte, 0, 32) + i := 0 + if s[0] == '_' { + // Need a capital letter; drop the '_'. + t = append(t, 'X') + i++ + } + // Invariant: if the next letter is lower case, it must be converted + // to upper case. + // That is, we process a word at a time, where words are marked by _ or + // upper case letter. Digits are treated as words. + for ; i < len(s); i++ { + c := s[i] + if c == '_' && i+1 < len(s) && isASCIILower(s[i+1]) { + continue // Skip the underscore in s. + } + if isASCIIDigit(c) { + t = append(t, c) + continue + } + // Assume we have a letter now - if not, it's a bogus identifier. + // The next word is a sequence of characters that must start upper case. + if isASCIILower(c) { + c ^= ' ' // Make it a capital letter. + } + t = append(t, c) // Guaranteed not lower case. + // Accept lower case sequence that follows. + for i+1 < len(s) && isASCIILower(s[i+1]) { + i++ + t = append(t, s[i]) + } + } + return string(t) +} + +// CamelCaseSlice is like CamelCase, but the argument is a slice of strings to +// be joined with "_". +func CamelCaseSlice(elem []string) string { return CamelCase(strings.Join(elem, "_")) } + +// dottedSlice turns a sliced name into a dotted name. +func dottedSlice(elem []string) string { return strings.Join(elem, ".") } + +// Is this field optional? +func isOptional(field *descriptor.FieldDescriptorProto) bool { + return *field.Proto3Optional +} + +// Is this field required? +func isRequired(field *descriptor.FieldDescriptorProto) bool { + return field.Label != nil && *field.Label == descriptor.FieldDescriptorProto_LABEL_REQUIRED +} + +// Is this field repeated? +func isRepeated(field *descriptor.FieldDescriptorProto) bool { + return field.Label != nil && *field.Label == descriptor.FieldDescriptorProto_LABEL_REPEATED +} + +// Is this field a scalar numeric type? +func isScalar(field *descriptor.FieldDescriptorProto) bool { + if field.Type == nil { + return false + } + switch *field.Type { + case descriptor.FieldDescriptorProto_TYPE_DOUBLE, + descriptor.FieldDescriptorProto_TYPE_FLOAT, + descriptor.FieldDescriptorProto_TYPE_INT64, + descriptor.FieldDescriptorProto_TYPE_UINT64, + descriptor.FieldDescriptorProto_TYPE_INT32, + descriptor.FieldDescriptorProto_TYPE_FIXED64, + descriptor.FieldDescriptorProto_TYPE_FIXED32, + descriptor.FieldDescriptorProto_TYPE_BOOL, + descriptor.FieldDescriptorProto_TYPE_UINT32, + descriptor.FieldDescriptorProto_TYPE_ENUM, + descriptor.FieldDescriptorProto_TYPE_SFIXED32, + descriptor.FieldDescriptorProto_TYPE_SFIXED64, + descriptor.FieldDescriptorProto_TYPE_SINT32, + descriptor.FieldDescriptorProto_TYPE_SINT64: + return true + default: + return false + } +} + +// badToUnderscore is the mapping function used to generate Go names from package names, +// which can be dotted in the input .proto file. It replaces non-identifier characters such as +// dot or dash with underscore. +func badToUnderscore(r rune) rune { + if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' { + return r + } + return '_' +} + +// baseName returns the last path element of the name, with the last dotted suffix removed. +func baseName(name string) string { + // First, find the last element + if i := strings.LastIndex(name, "/"); i >= 0 { + name = name[i+1:] + } + // Now drop the suffix + if i := strings.LastIndex(name, "."); i >= 0 { + name = name[0:i] + } + return name +} + +// The SourceCodeInfo message describes the location of elements of a parsed +// .proto file by way of a "path", which is a sequence of integers that +// describe the route from a FileDescriptorProto to the relevant submessage. +// The path alternates between a field number of a repeated field, and an index +// into that repeated field. The constants below define the field numbers that +// are used. +// +// See descriptor.proto for more information about this. +const ( + // tag numbers in FileDescriptorProto + packagePath = 2 // package + messagePath = 4 // message_type + enumPath = 5 // enum_type + // tag numbers in DescriptorProto + messageFieldPath = 2 // field + messageMessagePath = 3 // nested_type + messageEnumPath = 4 // enum_type + messageOneofPath = 8 // oneof_decl + // tag numbers in EnumDescriptorProto + enumValuePath = 2 // value +) + +var supportTypeAliases bool + +func init() { + for _, tag := range build.Default.ReleaseTags { + if tag == "go1.9" { + supportTypeAliases = true + return + } + } +} diff --git a/cmd/protoc-gen-micro/generator/name_test.go b/cmd/protoc-gen-micro/generator/name_test.go new file mode 100644 index 00000000..f6189a77 --- /dev/null +++ b/cmd/protoc-gen-micro/generator/name_test.go @@ -0,0 +1,135 @@ +// Go support for Protocol Buffers - Google's data interchange format +// +// Copyright 2013 The Go Authors. All rights reserved. +// https://github.com/golang/protobuf +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package generator + +import ( + "testing" + + descriptor "google.golang.org/protobuf/types/descriptorpb" +) + +func TestCamelCase(t *testing.T) { + tests := []struct { + in, want string + }{ + {"one", "One"}, + {"one_two", "OneTwo"}, + {"_my_field_name_2", "XMyFieldName_2"}, + {"Something_Capped", "Something_Capped"}, + {"my_Name", "My_Name"}, + {"OneTwo", "OneTwo"}, + {"_", "X"}, + {"_a_", "XA_"}, + } + for _, tc := range tests { + if got := CamelCase(tc.in); got != tc.want { + t.Errorf("CamelCase(%q) = %q, want %q", tc.in, got, tc.want) + } + } +} + +func TestGoPackageOption(t *testing.T) { + tests := []struct { + in string + impPath GoImportPath + pkg GoPackageName + ok bool + }{ + {"", "", "", false}, + {"foo", "", "foo", true}, + {"github.com/golang/bar", "github.com/golang/bar", "bar", true}, + {"github.com/golang/bar;baz", "github.com/golang/bar", "baz", true}, + {"github.com/golang/string", "github.com/golang/string", "string", true}, + } + for _, tc := range tests { + d := &FileDescriptor{ + FileDescriptorProto: &descriptor.FileDescriptorProto{ + Options: &descriptor.FileOptions{ + GoPackage: &tc.in, + }, + }, + } + impPath, pkg, ok := d.goPackageOption() + if impPath != tc.impPath || pkg != tc.pkg || ok != tc.ok { + t.Errorf("go_package = %q => (%q, %q, %t), want (%q, %q, %t)", tc.in, + impPath, pkg, ok, tc.impPath, tc.pkg, tc.ok) + } + } +} + +func TestPackageNames(t *testing.T) { + g := New() + g.packageNames = make(map[GoImportPath]GoPackageName) + g.usedPackageNames = make(map[GoPackageName]bool) + for _, test := range []struct { + importPath GoImportPath + want GoPackageName + }{ + {"github.com/golang/foo", "foo"}, + {"github.com/golang/second/package/named/foo", "foo1"}, + {"github.com/golang/third/package/named/foo", "foo2"}, + {"github.com/golang/conflicts/with/predeclared/ident/string", "string1"}, + } { + if got := g.GoPackageName(test.importPath); got != test.want { + t.Errorf("GoPackageName(%v) = %v, want %v", test.importPath, got, test.want) + } + } +} + +func TestUnescape(t *testing.T) { + tests := []struct { + in string + out string + }{ + // successful cases, including all kinds of escapes + {"", ""}, + {"foo bar baz frob nitz", "foo bar baz frob nitz"}, + {`\000\001\002\003\004\005\006\007`, string([]byte{0, 1, 2, 3, 4, 5, 6, 7})}, + {`\a\b\f\n\r\t\v\\\?\'\"`, string([]byte{'\a', '\b', '\f', '\n', '\r', '\t', '\v', '\\', '?', '\'', '"'})}, + {`\x10\x20\x30\x40\x50\x60\x70\x80`, string([]byte{16, 32, 48, 64, 80, 96, 112, 128})}, + // variable length octal escapes + {`\0\018\222\377\3\04\005\6\07`, string([]byte{0, 1, '8', 0222, 255, 3, 4, 5, 6, 7})}, + // malformed escape sequences left as is + {"foo \\g bar", "foo \\g bar"}, + {"foo \\xg0 bar", "foo \\xg0 bar"}, + {"\\", "\\"}, + {"\\x", "\\x"}, + {"\\xf", "\\xf"}, + {"\\777", "\\777"}, // overflows byte + } + for _, tc := range tests { + s := unescape(tc.in) + if s != tc.out { + t.Errorf("doUnescape(%q) = %q; should have been %q", tc.in, s, tc.out) + } + } +} diff --git a/cmd/protoc-gen-micro/main.go b/cmd/protoc-gen-micro/main.go new file mode 100644 index 00000000..540b11aa --- /dev/null +++ b/cmd/protoc-gen-micro/main.go @@ -0,0 +1,105 @@ +// Go support for Protocol Buffers - Google's data interchange format +// +// Copyright 2010 The Go Authors. All rights reserved. +// https://github.com/golang/protobuf +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// protoc-gen-micro is a plugin for the Google protocol buffer compiler to generate +// Go code. Run it by building this program and putting it in your path with +// the name +// +// protoc-gen-micro +// +// That word 'micro' at the end becomes part of the option string set for the +// protocol compiler, so once the protocol compiler (protoc) is installed +// you can run +// +// protoc --micro_out=output_directory --go_out=output_directory input_directory/file.proto +// +// to generate go-micro code for the protocol defined by file.proto. +// With that input, the output will be written to +// +// output_directory/file.micro.go +// +// The generated code is documented in the package comment for +// the library. +// +// See the README and documentation for protocol buffers to learn more: +// +// https://developers.google.com/protocol-buffers/ +package main + +import ( + "io" + "os" + + "go-micro.dev/v5/cmd/protoc-gen-micro/generator" + _ "go-micro.dev/v5/cmd/protoc-gen-micro/plugin/micro" + "google.golang.org/protobuf/proto" +) + +func main() { + // Begin by allocating a generator. The request and response structures are stored there + // so we can do error handling easily - the response structure contains the field to + // report failure. + g := generator.New() + + data, err := io.ReadAll(os.Stdin) + if err != nil { + g.Error(err, "reading input") + } + + if err := proto.Unmarshal(data, g.Request); err != nil { + g.Error(err, "parsing input proto") + } + + if len(g.Request.FileToGenerate) == 0 { + g.Fail("no files to generate") + } + + g.CommandLineParameters(g.Request.GetParameter()) + + // Create a wrapped version of the Descriptors and EnumDescriptors that + // point to the file that defines them. + g.WrapTypes() + + g.SetPackageNames() + g.BuildTypeNameMap() + + g.GenerateAllFiles() + + // Send back the results. + data, err = proto.Marshal(g.Response) + if err != nil { + g.Error(err, "failed to marshal output proto") + } + _, err = os.Stdout.Write(data) + if err != nil { + g.Error(err, "failed to write output proto") + } +} diff --git a/cmd/protoc-gen-micro/plugin/micro/micro.go b/cmd/protoc-gen-micro/plugin/micro/micro.go new file mode 100644 index 00000000..665f77c9 --- /dev/null +++ b/cmd/protoc-gen-micro/plugin/micro/micro.go @@ -0,0 +1,531 @@ +package micro + +import ( + "fmt" + "path" + "strconv" + "strings" + + "go-micro.dev/v5/cmd/protoc-gen-micro/generator" + options "google.golang.org/genproto/googleapis/api/annotations" + "google.golang.org/protobuf/proto" + pb "google.golang.org/protobuf/types/descriptorpb" +) + +// Paths for packages used by code generated in this file, +// relative to the import_prefix of the generator.Generator. +const ( + contextPkgPath = "context" + clientPkgPath = "go-micro.dev/v5/client" + serverPkgPath = "go-micro.dev/v5/server" +) + +func init() { + generator.RegisterPlugin(new(micro)) +} + +// micro is an implementation of the Go protocol buffer compiler's +// plugin architecture. It generates bindings for go-micro support. +type micro struct { + gen *generator.Generator +} + +// Name returns the name of this plugin, "micro". +func (g *micro) Name() string { + return "micro" +} + +// The names for packages imported in the generated code. +// They may vary from the final path component of the import path +// if the name is used by other packages. +var ( + contextPkg string + clientPkg string + serverPkg string + pkgImports map[generator.GoPackageName]bool +) + +// Init initializes the plugin. +func (g *micro) Init(gen *generator.Generator) { + g.gen = gen + contextPkg = generator.RegisterUniquePackageName("context", nil) + clientPkg = generator.RegisterUniquePackageName("client", nil) + serverPkg = generator.RegisterUniquePackageName("server", nil) +} + +// Given a type name defined in a .proto, return its object. +// Also record that we're using it, to guarantee the associated import. +func (g *micro) objectNamed(name string) generator.Object { + g.gen.RecordTypeUse(name) + return g.gen.ObjectNamed(name) +} + +// Given a type name defined in a .proto, return its name as we will print it. +func (g *micro) typeName(str string) string { + return g.gen.TypeName(g.objectNamed(str)) +} + +// P forwards to g.gen.P. +func (g *micro) P(args ...interface{}) { g.gen.P(args...) } + +// Generate generates code for the services in the given file. +func (g *micro) Generate(file *generator.FileDescriptor) { + if len(file.FileDescriptorProto.Service) == 0 { + return + } + g.P("// Reference imports to suppress errors if they are not otherwise used.") + g.P("var _ ", contextPkg, ".Context") + g.P("var _ ", clientPkg, ".Option") + g.P("var _ ", serverPkg, ".Option") + g.P() + + for i, service := range file.FileDescriptorProto.Service { + g.generateService(file, service, i) + } +} + +// GenerateImports generates the import declaration for this file. +func (g *micro) GenerateImports(file *generator.FileDescriptor, imports map[generator.GoImportPath]generator.GoPackageName) { + if len(file.FileDescriptorProto.Service) == 0 { + return + } + g.P("import (") + g.P(contextPkg, " ", strconv.Quote(path.Join(g.gen.ImportPrefix, contextPkgPath))) + g.P(clientPkg, " ", strconv.Quote(path.Join(g.gen.ImportPrefix, clientPkgPath))) + g.P(serverPkg, " ", strconv.Quote(path.Join(g.gen.ImportPrefix, serverPkgPath))) + g.P(")") + g.P() + + // We need to keep track of imported packages to make sure we don't produce + // a name collision when generating types. + pkgImports = make(map[generator.GoPackageName]bool) + for _, name := range imports { + pkgImports[name] = true + } +} + +// reservedClientName records whether a client name is reserved on the client side. +var reservedClientName = map[string]bool{ + // TODO: do we need any in go-micro? +} + +func unexport(s string) string { + if len(s) == 0 { + return "" + } + name := strings.ToLower(s[:1]) + s[1:] + if pkgImports[generator.GoPackageName(name)] { + return name + "_" + } + return name +} + +// generateService generates all the code for the named service. +func (g *micro) generateService(file *generator.FileDescriptor, service *pb.ServiceDescriptorProto, index int) { + path := fmt.Sprintf("6,%d", index) // 6 means service. + + origServName := service.GetName() + serviceName := strings.ToLower(service.GetName()) + pkg := file.GetPackage() + if pkg != "" { + serviceName = pkg + } + servName := generator.CamelCase(origServName) + servAlias := servName + "Service" + + // strip suffix + if strings.HasSuffix(servAlias, "ServiceService") { + servAlias = strings.TrimSuffix(servAlias, "Service") + } + + g.P() + g.P("// Client API for ", servName, " service") + g.P() + + // Client interface. + g.P("type ", servAlias, " interface {") + for i, method := range service.Method { + g.gen.PrintComments(fmt.Sprintf("%s,2,%d", path, i)) // 2 means method in a service. + g.P(g.generateClientSignature(servName, method)) + } + g.P("}") + g.P() + + // Client structure. + g.P("type ", unexport(servAlias), " struct {") + g.P("c ", clientPkg, ".Client") + g.P("name string") + g.P("}") + g.P() + + // NewClient factory. + g.P("func New", servAlias, " (name string, c ", clientPkg, ".Client) ", servAlias, " {") + /* + g.P("if c == nil {") + g.P("c = ", clientPkg, ".NewClient()") + g.P("}") + g.P("if len(name) == 0 {") + g.P(`name = "`, serviceName, `"`) + g.P("}") + */ + g.P("return &", unexport(servAlias), "{") + g.P("c: c,") + g.P("name: name,") + g.P("}") + g.P("}") + g.P() + var methodIndex, streamIndex int + serviceDescVar := "_" + servName + "_serviceDesc" + // Client method implementations. + for _, method := range service.Method { + var descExpr string + if !method.GetServerStreaming() { + // Unary RPC method + descExpr = fmt.Sprintf("&%s.Methods[%d]", serviceDescVar, methodIndex) + methodIndex++ + } else { + // Streaming RPC method + descExpr = fmt.Sprintf("&%s.Streams[%d]", serviceDescVar, streamIndex) + streamIndex++ + } + g.generateClientMethod(pkg, serviceName, servName, serviceDescVar, method, descExpr) + } + + g.P("// Server API for ", servName, " service") + g.P() + + // Server interface. + serverType := servName + "Handler" + g.P("type ", serverType, " interface {") + for i, method := range service.Method { + g.gen.PrintComments(fmt.Sprintf("%s,2,%d", path, i)) // 2 means method in a service. + g.P(g.generateServerSignature(servName, method)) + } + g.P("}") + g.P() + + // Server registration. + g.P("func Register", servName, "Handler(s ", serverPkg, ".Server, hdlr ", serverType, ", opts ...", serverPkg, ".HandlerOption) error {") + g.P("type ", unexport(servName), " interface {") + + // generate interface methods + for _, method := range service.Method { + methName := generator.CamelCase(method.GetName()) + inType := g.typeName(method.GetInputType()) + outType := g.typeName(method.GetOutputType()) + + if !method.GetServerStreaming() && !method.GetClientStreaming() { + g.P(methName, "(ctx ", contextPkg, ".Context, in *", inType, ", out *", outType, ") error") + continue + } + g.P(methName, "(ctx ", contextPkg, ".Context, stream server.Stream) error") + } + g.P("}") + g.P("type ", servName, " struct {") + g.P(unexport(servName)) + g.P("}") + g.P("h := &", unexport(servName), "Handler{hdlr}") + g.P("return s.Handle(s.NewHandler(&", servName, "{h}, opts...))") + g.P("}") + g.P() + + g.P("type ", unexport(servName), "Handler struct {") + g.P(serverType) + g.P("}") + + // Server handler implementations. + var handlerNames []string + for _, method := range service.Method { + hname := g.generateServerMethod(servName, method) + handlerNames = append(handlerNames, hname) + } +} + +// generateEndpoint creates the api endpoint +func (g *micro) generateEndpoint(servName string, method *pb.MethodDescriptorProto) { + if method.Options == nil || !proto.HasExtension(method.Options, options.E_Http) { + return + } + // http rules + r := proto.GetExtension(method.Options, options.E_Http) + rule := r.(*options.HttpRule) + var meth string + var path string + switch { + case len(rule.GetDelete()) > 0: + meth = "DELETE" + path = rule.GetDelete() + case len(rule.GetGet()) > 0: + meth = "GET" + path = rule.GetGet() + case len(rule.GetPatch()) > 0: + meth = "PATCH" + path = rule.GetPatch() + case len(rule.GetPost()) > 0: + meth = "POST" + path = rule.GetPost() + case len(rule.GetPut()) > 0: + meth = "PUT" + path = rule.GetPut() + } + if len(meth) == 0 || len(path) == 0 { + return + } + // TODO: process additional bindings + g.P("Name:", fmt.Sprintf(`"%s.%s",`, servName, method.GetName())) + g.P("Path:", fmt.Sprintf(`[]string{"%s"},`, path)) + g.P("Method:", fmt.Sprintf(`[]string{"%s"},`, meth)) + if method.GetServerStreaming() || method.GetClientStreaming() { + g.P("Stream: true,") + } + g.P(`Handler: "rpc",`) +} + +// generateClientSignature returns the client-side signature for a method. +func (g *micro) generateClientSignature(servName string, method *pb.MethodDescriptorProto) string { + origMethName := method.GetName() + methName := generator.CamelCase(origMethName) + if reservedClientName[methName] { + methName += "_" + } + reqArg := ", in *" + g.typeName(method.GetInputType()) + if method.GetClientStreaming() { + reqArg = "" + } + respName := "*" + g.typeName(method.GetOutputType()) + if method.GetServerStreaming() || method.GetClientStreaming() { + respName = servName + "_" + generator.CamelCase(origMethName) + "Service" + } + + return fmt.Sprintf("%s(ctx %s.Context%s, opts ...%s.CallOption) (%s, error)", methName, contextPkg, reqArg, clientPkg, respName) +} + +func (g *micro) generateClientMethod(pkg, reqServ, servName, serviceDescVar string, method *pb.MethodDescriptorProto, descExpr string) { + reqMethod := fmt.Sprintf("%s.%s", servName, method.GetName()) + useGrpc := g.gen.Param["use_grpc"] + if useGrpc != "" { + reqMethod = fmt.Sprintf("/%s.%s/%s", pkg, servName, method.GetName()) + } + methName := generator.CamelCase(method.GetName()) + inType := g.typeName(method.GetInputType()) + outType := g.typeName(method.GetOutputType()) + + servAlias := servName + "Service" + + // strip suffix + if strings.HasSuffix(servAlias, "ServiceService") { + servAlias = strings.TrimSuffix(servAlias, "Service") + } + + g.P("func (c *", unexport(servAlias), ") ", g.generateClientSignature(servName, method), "{") + if !method.GetServerStreaming() && !method.GetClientStreaming() { + g.P(`req := c.c.NewRequest(c.name, "`, reqMethod, `", in)`) + g.P("out := new(", outType, ")") + // TODO: Pass descExpr to Invoke. + g.P("err := ", `c.c.Call(ctx, req, out, opts...)`) + g.P("if err != nil { return nil, err }") + g.P("return out, nil") + g.P("}") + g.P() + return + } + streamType := unexport(servAlias) + methName + g.P(`req := c.c.NewRequest(c.name, "`, reqMethod, `", &`, inType, `{})`) + g.P("stream, err := c.c.Stream(ctx, req, opts...)") + g.P("if err != nil { return nil, err }") + + if !method.GetClientStreaming() { + g.P("if err := stream.Send(in); err != nil { return nil, err }") + // TODO: currently only grpc support CloseSend + // g.P("if err := stream.CloseSend(); err != nil { return nil, err }") + } + + g.P("return &", streamType, "{stream}, nil") + g.P("}") + g.P() + + genSend := method.GetClientStreaming() + genRecv := method.GetServerStreaming() + + // Stream auxiliary types and methods. + g.P("type ", servName, "_", methName, "Service interface {") + g.P("Context() context.Context") + g.P("SendMsg(interface{}) error") + g.P("RecvMsg(interface{}) error") + g.P("CloseSend() error") + g.P("Close() error") + + if genSend { + g.P("Send(*", inType, ") error") + } + if genRecv { + g.P("Recv() (*", outType, ", error)") + } + g.P("}") + g.P() + + g.P("type ", streamType, " struct {") + g.P("stream ", clientPkg, ".Stream") + g.P("}") + g.P() + + g.P("func (x *", streamType, ") CloseSend() error {") + g.P("return x.stream.CloseSend()") + g.P("}") + g.P() + + g.P("func (x *", streamType, ") Close() error {") + g.P("return x.stream.Close()") + g.P("}") + g.P() + + g.P("func (x *", streamType, ") Context() context.Context {") + g.P("return x.stream.Context()") + g.P("}") + g.P() + + g.P("func (x *", streamType, ") SendMsg(m interface{}) error {") + g.P("return x.stream.Send(m)") + g.P("}") + g.P() + + g.P("func (x *", streamType, ") RecvMsg(m interface{}) error {") + g.P("return x.stream.Recv(m)") + g.P("}") + g.P() + + if genSend { + g.P("func (x *", streamType, ") Send(m *", inType, ") error {") + g.P("return x.stream.Send(m)") + g.P("}") + g.P() + + } + + if genRecv { + g.P("func (x *", streamType, ") Recv() (*", outType, ", error) {") + g.P("m := new(", outType, ")") + g.P("err := x.stream.Recv(m)") + g.P("if err != nil {") + g.P("return nil, err") + g.P("}") + g.P("return m, nil") + g.P("}") + g.P() + } +} + +// generateServerSignature returns the server-side signature for a method. +func (g *micro) generateServerSignature(servName string, method *pb.MethodDescriptorProto) string { + origMethName := method.GetName() + methName := generator.CamelCase(origMethName) + if reservedClientName[methName] { + methName += "_" + } + + var reqArgs []string + ret := "error" + reqArgs = append(reqArgs, contextPkg+".Context") + + if !method.GetClientStreaming() { + reqArgs = append(reqArgs, "*"+g.typeName(method.GetInputType())) + } + if method.GetServerStreaming() || method.GetClientStreaming() { + reqArgs = append(reqArgs, servName+"_"+generator.CamelCase(origMethName)+"Stream") + } + if !method.GetClientStreaming() && !method.GetServerStreaming() { + reqArgs = append(reqArgs, "*"+g.typeName(method.GetOutputType())) + } + return methName + "(" + strings.Join(reqArgs, ", ") + ") " + ret +} + +func (g *micro) generateServerMethod(servName string, method *pb.MethodDescriptorProto) string { + methName := generator.CamelCase(method.GetName()) + hname := fmt.Sprintf("_%s_%s_Handler", servName, methName) + serveType := servName + "Handler" + inType := g.typeName(method.GetInputType()) + outType := g.typeName(method.GetOutputType()) + + if !method.GetServerStreaming() && !method.GetClientStreaming() { + g.P("func (h *", unexport(servName), "Handler) ", methName, "(ctx ", contextPkg, ".Context, in *", inType, ", out *", outType, ") error {") + g.P("return h.", serveType, ".", methName, "(ctx, in, out)") + g.P("}") + g.P() + return hname + } + streamType := unexport(servName) + methName + "Stream" + g.P("func (h *", unexport(servName), "Handler) ", methName, "(ctx ", contextPkg, ".Context, stream server.Stream) error {") + if !method.GetClientStreaming() { + g.P("m := new(", inType, ")") + g.P("if err := stream.Recv(m); err != nil { return err }") + g.P("return h.", serveType, ".", methName, "(ctx, m, &", streamType, "{stream})") + } else { + g.P("return h.", serveType, ".", methName, "(ctx, &", streamType, "{stream})") + } + g.P("}") + g.P() + + genSend := method.GetServerStreaming() + genRecv := method.GetClientStreaming() + + // Stream auxiliary types and methods. + g.P("type ", servName, "_", methName, "Stream interface {") + g.P("Context() context.Context") + g.P("SendMsg(interface{}) error") + g.P("RecvMsg(interface{}) error") + g.P("Close() error") + + if genSend { + g.P("Send(*", outType, ") error") + } + + if genRecv { + g.P("Recv() (*", inType, ", error)") + } + + g.P("}") + g.P() + + g.P("type ", streamType, " struct {") + g.P("stream ", serverPkg, ".Stream") + g.P("}") + g.P() + + g.P("func (x *", streamType, ") Close() error {") + g.P("return x.stream.Close()") + g.P("}") + g.P() + + g.P("func (x *", streamType, ") Context() context.Context {") + g.P("return x.stream.Context()") + g.P("}") + g.P() + + g.P("func (x *", streamType, ") SendMsg(m interface{}) error {") + g.P("return x.stream.Send(m)") + g.P("}") + g.P() + + g.P("func (x *", streamType, ") RecvMsg(m interface{}) error {") + g.P("return x.stream.Recv(m)") + g.P("}") + g.P() + + if genSend { + g.P("func (x *", streamType, ") Send(m *", outType, ") error {") + g.P("return x.stream.Send(m)") + g.P("}") + g.P() + } + + if genRecv { + g.P("func (x *", streamType, ") Recv() (*", inType, ", error) {") + g.P("m := new(", inType, ")") + g.P("if err := x.stream.Recv(m); err != nil { return nil, err }") + g.P("return m, nil") + g.P("}") + g.P() + } + + return hname +} diff --git a/go.mod b/go.mod index ed5a073d..338129bc 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/fsnotify/fsnotify v1.6.0 github.com/go-redis/redis/v8 v8.11.5 github.com/go-sql-driver/mysql v1.9.2 + github.com/golang-jwt/jwt/v5 v5.3.0 github.com/golang/protobuf v1.5.4 github.com/google/uuid v1.6.0 github.com/hashicorp/consul/api v1.32.1 @@ -28,9 +29,11 @@ require ( github.com/patrickmn/go-cache v2.1.0+incompatible github.com/pkg/errors v0.9.1 github.com/streadway/amqp v1.1.0 + github.com/stretchr/objx v0.5.2 github.com/stretchr/testify v1.10.0 github.com/test-go/testify v1.1.4 github.com/urfave/cli/v2 v2.27.6 + github.com/xlab/treeprint v1.2.0 go.etcd.io/bbolt v1.4.0 go.etcd.io/etcd/api/v3 v3.5.21 go.etcd.io/etcd/client/v3 v3.5.21 @@ -40,16 +43,13 @@ require ( golang.org/x/crypto v0.37.0 golang.org/x/net v0.38.0 golang.org/x/sync v0.13.0 - google.golang.org/genai v1.12.0 + google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 google.golang.org/grpc v1.71.1 google.golang.org/grpc/examples v0.0.0-20250515150734-f2d3e11f3057 google.golang.org/protobuf v1.36.6 ) require ( - cloud.google.com/go v0.120.0 // indirect - cloud.google.com/go/auth v0.15.0 // indirect - cloud.google.com/go/compute/metadata v0.6.0 // indirect filippo.io/edwards25519 v1.1.0 // indirect github.com/armon/go-metrics v0.4.1 // indirect github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect @@ -59,16 +59,10 @@ require ( github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/fatih/color v1.16.0 // indirect - github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-tpm v0.9.3 // indirect - github.com/google/s2a-go v0.1.9 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect - github.com/googleapis/gax-go/v2 v2.14.1 // indirect - github.com/gorilla/websocket v1.5.3 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-hclog v1.5.0 // indirect @@ -101,7 +95,6 @@ require ( github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect go.etcd.io/etcd/client/pkg/v3 v3.5.21 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect go.opentelemetry.io/otel/metric v1.35.0 // indirect go.uber.org/multierr v1.10.0 // indirect golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect @@ -110,7 +103,6 @@ require ( golang.org/x/text v0.24.0 // indirect golang.org/x/time v0.11.0 // indirect golang.org/x/tools v0.31.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 0e17153c..b7d9d1d0 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,3 @@ -cloud.google.com/go v0.120.0 h1:wc6bgG9DHyKqF5/vQvX1CiZrtHnxJjBlKUyF9nP6meA= -cloud.google.com/go v0.120.0/go.mod h1:/beW32s8/pGRuj4IILWQNd4uuebeT4dkOhKmkfit64Q= -cloud.google.com/go/auth v0.15.0 h1:Ly0u4aA5vG/fsSsxu98qCQBemXtAtJf+95z9HK+cxps= -cloud.google.com/go/auth v0.15.0/go.mod h1:WJDGqZ1o9E9wKIL+IwStfyn/+s59zl4Bi+1KQNVXLZ8= -cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= -cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= @@ -61,8 +55,6 @@ github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= -github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= -github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= @@ -87,6 +79,8 @@ github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRx github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -103,16 +97,8 @@ github.com/google/go-tpm v0.9.3 h1:+yx0/anQuGzi+ssRqeD6WpXjW2L/V0dItUayO0i9sRc= github.com/google/go-tpm v0.9.3/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= -github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= -github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= -github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= -github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= -github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= -github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/consul/api v1.32.1 h1:0+osr/3t/aZNAdJX558crU3PEjVrG4x6715aZHRgceE= github.com/hashicorp/consul/api v1.32.1/go.mod h1:mXUWLnxftwTmDv4W3lzxYCPD199iNLLUyLfLGFJbtl4= github.com/hashicorp/consul/sdk v0.16.1 h1:V8TxTnImoPD5cj0U9Spl0TUxcytjcbbJeADFF07KdHg= @@ -355,6 +341,8 @@ github.com/test-go/testify v1.1.4/go.mod h1:rH7cfJo/47vWGdi4GPj16x3/t1xGOj2YxzmN github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g= github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= +github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= +github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -371,8 +359,6 @@ go.etcd.io/etcd/client/v3 v3.5.21 h1:T6b1Ow6fNjOLOtM0xSoKNQt1ASPCLWrF9XMHcH9pEyY go.etcd.io/etcd/client/v3 v3.5.21/go.mod h1:mFYy67IOqmbRf/kRUvsHixzo3iG+1OF2W2+jVIQRAnU= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= @@ -510,8 +496,6 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genai v1.12.0 h1:0JjAdwvEAha9ZpPH5hL6dVG8bpMnRbAMCgv2f2LDnz4= -google.golang.org/genai v1.12.0/go.mod h1:HFXR1zT3LCdLxd/NW6IOSCczOYyRAxwaShvYbgPSeVw= google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 h1:hE3bRWtU6uceqlh4fhrSnUyjKHMKB9KrTLLG+bc0ddM= google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463/go.mod h1:U90ffi8eUL9MwPcrJylN5+Mk2v3vuPDptd5yyNUiRR8= google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g= diff --git a/server/rpc_server.go b/server/rpc_server.go index 6d5d4ef8..2b62fb1b 100644 --- a/server/rpc_server.go +++ b/server/rpc_server.go @@ -327,49 +327,49 @@ func (s *rpcServer) ServeConn(sock transport.Socket) { wg.Add(2) // Process the outbound messages from the socket - go func(psock *socket.Socket) { - defer func() { - if r := recover(); r != nil { - logger.Log(log.ErrorLevel, "panic recovered in outbound goroutine: ", r) - logger.Log(log.ErrorLevel, string(debug.Stack())) - } - // TODO: don't hack this but if its grpc just break out of the stream - // We do this because the underlying connection is h2 and its a stream - if protocol == "grpc" { - if err := sock.Close(); err != nil { - logger.Logf(log.ErrorLevel, "Failed to close socket: %v", err) - } - } + go func(psock *socket.Socket) { + defer func() { + if r := recover(); r != nil { + logger.Log(log.ErrorLevel, "panic recovered in outbound goroutine: ", r) + logger.Log(log.ErrorLevel, string(debug.Stack())) + } + // TODO: don't hack this but if its grpc just break out of the stream + // We do this because the underlying connection is h2 and its a stream + if protocol == "grpc" { + if err := sock.Close(); err != nil { + logger.Logf(log.ErrorLevel, "Failed to close socket: %v", err) + } + } - s.deferer(pool, psock, wg) - }() + s.deferer(pool, psock, wg) + }() - for { - // Get the message from our internal handler/stream - m := new(transport.Message) - if err := psock.Process(m); err != nil { - return - } + for { + // Get the message from our internal handler/stream + m := new(transport.Message) + if err := psock.Process(m); err != nil { + return + } - // Send the message back over the socket - if err := sock.Send(m); err != nil { - return - } - } - }(psock) + // Send the message back over the socket + if err := sock.Send(m); err != nil { + return + } + } + }(psock) // Serve the request in a go routine as this may be a stream - go func(psock *socket.Socket) { - defer func() { - if r := recover(); r != nil { - logger.Log(log.ErrorLevel, "panic recovered in serveReq goroutine: ", r) - logger.Log(log.ErrorLevel, string(debug.Stack())) - } - s.deferer(pool, psock, wg) - }() + go func(psock *socket.Socket) { + defer func() { + if r := recover(); r != nil { + logger.Log(log.ErrorLevel, "panic recovered in serveReq goroutine: ", r) + logger.Log(log.ErrorLevel, string(debug.Stack())) + } + s.deferer(pool, psock, wg) + }() - s.serveReq(ctx, msg, &request, &response, rcodec) - }(psock) + s.serveReq(ctx, msg, &request, &response, rcodec) + }(psock) } } diff --git a/transport/http2_buf_pool.go b/transport/http2_buf_pool.go index 47e6edca..a7f1a540 100644 --- a/transport/http2_buf_pool.go +++ b/transport/http2_buf_pool.go @@ -3,11 +3,11 @@ package transport import "sync" var http2BufPool = sync.Pool{ - New: func() interface{} { - return make([]byte, DefaultBufSizeH2) - }, + New: func() interface{} { + return make([]byte, DefaultBufSizeH2) + }, } func getHTTP2BufPool() *sync.Pool { - return &http2BufPool -} \ No newline at end of file + return &http2BufPool +} diff --git a/transport/http_socket.go b/transport/http_socket.go index 5e0cc945..c086d3de 100644 --- a/transport/http_socket.go +++ b/transport/http_socket.go @@ -165,51 +165,51 @@ func (h *httpTransportSocket) recvHTTP1(msg *Message) error { } func (h *httpTransportSocket) recvHTTP2(msg *Message) error { - // only process if the socket is open - select { - case <-h.closed: - return io.EOF - default: - } + // only process if the socket is open + select { + case <-h.closed: + return io.EOF + default: + } - // buffer pool for reuse - var bufPool = getHTTP2BufPool() + // buffer pool for reuse + var bufPool = getHTTP2BufPool() - // set max buffer size - s := h.ht.opts.BuffSizeH2 - if s == 0 { - s = DefaultBufSizeH2 - } + // set max buffer size + s := h.ht.opts.BuffSizeH2 + if s == 0 { + s = DefaultBufSizeH2 + } - buf := bufPool.Get().([]byte) - if cap(buf) < s { - buf = make([]byte, s) - } - buf = buf[:s] + buf := bufPool.Get().([]byte) + if cap(buf) < s { + buf = make([]byte, s) + } + buf = buf[:s] - n, err := h.buf.Read(buf) - if err != nil { - bufPool.Put(buf) - return err - } + n, err := h.buf.Read(buf) + if err != nil { + bufPool.Put(buf) + return err + } - if n > 0 { - msg.Body = make([]byte, n) - copy(msg.Body, buf[:n]) - } - bufPool.Put(buf) + if n > 0 { + msg.Body = make([]byte, n) + copy(msg.Body, buf[:n]) + } + bufPool.Put(buf) - for k, v := range h.r.Header { - if len(v) > 0 { - msg.Header[k] = v[0] - } else { - msg.Header[k] = "" - } - } + for k, v := range h.r.Header { + if len(v) > 0 { + msg.Header[k] = v[0] + } else { + msg.Header[k] = "" + } + } - msg.Header[":path"] = h.r.URL.Path + msg.Header[":path"] = h.r.URL.Path - return nil + return nil } func (h *httpTransportSocket) sendHTTP1(msg *Message) error {