| // The cdpgen command generates the package cdp from the provided protocol definitions. |
| package main |
| |
| import ( |
| "bytes" |
| "context" |
| "encoding/json" |
| "flag" |
| "fmt" |
| "go/format" |
| "io/ioutil" |
| "log" |
| "os" |
| "os/exec" |
| "path" |
| "sort" |
| "strings" |
| |
| "github.com/mafredri/cdp/cmd/cdpgen/proto" |
| ) |
| |
| // Global constants. |
| const ( |
| OptionalPropPrefix = "" |
| realEnum = false |
| ) |
| |
| var ( |
| nonPtrMap = make(map[string]bool) |
| ) |
| |
| func panicErr(err error) { |
| if err != nil { |
| panic(err) |
| } |
| } |
| |
| func mkdir(name string) error { |
| err := os.Mkdir(name, 0755) |
| if os.IsExist(err) { |
| return nil |
| } |
| return err |
| } |
| |
| func main() { |
| var ( |
| destPkg string |
| browserProtoJSON string |
| jsProtoFileJSON string |
| ) |
| flag.StringVar(&destPkg, "dest-pkg", "", "Destination for generated cdp package (inside $GOPATH)") |
| flag.StringVar(&browserProtoJSON, "browser-proto", "./protodef/browser_protocol.json", "Path to browser protocol") |
| flag.StringVar(&jsProtoFileJSON, "js-proto", "./protodef/js_protocol.json", "Path to JS protocol") |
| flag.Parse() |
| |
| if destPkg == "" { |
| fmt.Fprintln(os.Stderr, "error: dest-pkg must be set") |
| os.Exit(1) |
| } |
| |
| var protocol, jsProtocol proto.Protocol |
| protocolData, err := ioutil.ReadFile(browserProtoJSON) |
| panicErr(err) |
| |
| err = json.Unmarshal(protocolData, &protocol) |
| panicErr(err) |
| |
| jsProtocolData, err := ioutil.ReadFile(jsProtoFileJSON) |
| panicErr(err) |
| |
| err = json.Unmarshal(jsProtocolData, &jsProtocol) |
| panicErr(err) |
| |
| protocol.Domains = append(protocol.Domains, jsProtocol.Domains...) |
| sort.Slice(protocol.Domains, func(i, j int) bool { |
| return protocol.Domains[i].Domain < protocol.Domains[j].Domain |
| }) |
| |
| protoDest := path.Join(destPkg, "protocol") |
| imports := []string{ |
| "github.com/mafredri/cdp/rpcc", |
| "github.com/mafredri/cdp/protocol/internal", |
| "github.com/mafredri/cdp/protocol", |
| } |
| for i, d := range protocol.Domains { |
| dLower := strings.ToLower(d.Name()) |
| imports = append(imports, path.Join(protoDest, dLower)) |
| |
| for ii, t := range d.Types { |
| nam := t.Name(d) |
| if isNonPointer(d.Domain, d, t) { |
| nonPtrMap[nam] = true |
| nonPtrMap[d.Domain+"."+nam] = true |
| } |
| |
| // Reference the FrameId in the Frame type. |
| if d.Domain == "Page" && t.IDName == "Frame" { |
| for iii, p := range t.Properties { |
| if p.NameName == "id" || p.NameName == "parentId" { |
| p.Ref = "FrameId" |
| t.Properties[iii] = p |
| } |
| } |
| } |
| d.Types[ii] = t |
| } |
| protocol.Domains[i] = d |
| } |
| nonPtrMap["Timestamp"] = true |
| nonPtrMap["protocol.Timestamp"] = true |
| nonPtrMap["TimeSinceEpoch"] = true |
| nonPtrMap["network.TimeSinceEpoch"] = true |
| nonPtrMap["MonotonicTime"] = true |
| nonPtrMap["network.MonotonicTime"] = true |
| |
| var cdp, g Generator |
| cdp.imports = imports |
| g.imports = imports |
| |
| // Define the cdp Client. |
| cdp.pkg = "cdp" |
| cdp.dir = destPkg |
| cdp.PackageHeader("") |
| cdp.CdpClient(protocol.Domains) |
| cdp.writeFile("cdp_client.go") |
| |
| // Package cdp/protocol. |
| g.pkg = "protocol" |
| g.dir = protoDest |
| err = mkdir(g.path()) |
| panicErr(err) |
| |
| // Package cdp/protocol/internal. |
| g.pkg = "internal" |
| g.dir = path.Join(protoDest, "internal") |
| for _, d := range protocol.Domains { |
| if d.Name() == "Page" { |
| d.Domain = "internal" |
| g.PackageHeader("") |
| for _, t := range d.Types { |
| if t.Name(d) == "FrameID" { |
| idName := t.Name(d) |
| t.IDName = "Page" + t.IDName |
| t.Description = fmt.Sprintf("%s\n\nThis type cannot be used directly. Use page.%s instead.", t.Description, idName) |
| g.DomainType(d, t) |
| } |
| } |
| g.writeFile("page.go") |
| } |
| } |
| |
| // Generate the protocol definitions. |
| for _, d := range protocol.Domains { |
| cdp.PackageHeader("") |
| cdp.DomainInterface(d) |
| |
| dLower := strings.ToLower(d.Domain) |
| g.dir = path.Join(protoDest, dLower) |
| g.pkg = dLower |
| err := mkdir(g.path()) |
| panicErr(err) |
| |
| comment := fmt.Sprintf("Package %s implements the %s domain. ", dLower, d.Name()) |
| g.PackageHeader(fmt.Sprintf("// %s%s", comment, d.Desc(0, len(comment)))) |
| g.DomainDefinition(d) |
| g.writeFile("domain.go") |
| |
| if len(d.Types) > 0 { |
| g.PackageHeader("") |
| for _, t := range d.Types { |
| g.DomainType(d, t) |
| } |
| g.writeFile("types.go") |
| } |
| |
| if len(d.Commands) > 0 { |
| g.PackageHeader("") |
| for _, c := range d.Commands { |
| if c.Redirect != "" { |
| continue |
| } |
| g.DomainCmd(d, c) |
| } |
| g.writeFile("command.go") |
| } |
| |
| if len(d.Events) > 0 { |
| g.PackageHeader("") |
| for _, e := range d.Events { |
| g.DomainEvent(d, e) |
| } |
| g.writeFile("event.go") |
| } |
| } |
| |
| cdp.writeFile(cdp.pkg + ".go") |
| |
| g.dir = destPkg |
| |
| ctx, cancel := context.WithCancel(context.Background()) |
| defer cancel() |
| |
| goimports := exec.CommandContext(ctx, "goimports", "-w", g.path()) |
| out, err := goimports.CombinedOutput() |
| if err != nil { |
| log.Printf("goimports failed: %s", out) |
| log.Println(err) |
| os.Exit(1) |
| } |
| |
| goinstall := exec.CommandContext(ctx, "go", "install", path.Join(destPkg, "...")) |
| out, err = goinstall.CombinedOutput() |
| if err != nil { |
| log.Printf("install failed: %s", out) |
| log.Println(err) |
| os.Exit(1) |
| } |
| } |
| |
| // Generator holds the state of the analysis. Primarily used to buffer |
| // the output for format.Source. |
| type Generator struct { |
| dir string |
| pkg string |
| imports []string |
| buf bytes.Buffer // Accumulated output. |
| testbuf bytes.Buffer // Accumulated test output. |
| hasContent bool |
| hasHeader bool |
| } |
| |
| func (g *Generator) path() string { |
| return path.Join(os.Getenv("GOPATH"), "src", g.dir) |
| } |
| |
| // Printf prints to the Generator buffer. |
| func (g *Generator) Printf(format string, args ...interface{}) { |
| fmt.Fprintf(&g.buf, format, args...) |
| } |
| |
| // TestPrintf prints to the Generator test buffer. |
| func (g *Generator) TestPrintf(format string, args ...interface{}) { |
| // No-op |
| // fmt.Fprintf(&g.testbuf, format, args...) |
| } |
| |
| func (g *Generator) writeFile(f string) { |
| fp := path.Join(g.path(), f) |
| if !g.hasContent { |
| log.Printf("No content, skipping %s...", fp) |
| g.clear() |
| return |
| } |
| if g.buf.Len() == 0 { |
| log.Printf("Empty buffer, skipping %s...", fp) |
| return |
| } |
| log.Printf("Writing %s...", fp) |
| err := ioutil.WriteFile(fp, g.format(), 0644) |
| panicErr(err) |
| |
| if g.testbuf.Len() > 0 { |
| g.buf.Reset() |
| g.Printf("package %s\n\n", g.pkg) |
| _, err = g.testbuf.WriteTo(&g.buf) |
| panicErr(err) |
| fptest := strings.Replace(fp, ".go", "_test.go", 1) |
| log.Printf("Writing %s...", fptest) |
| |
| err = ioutil.WriteFile(fptest, g.format(), 0644) |
| panicErr(err) |
| } |
| g.clear() |
| } |
| |
| func (g *Generator) clear() { |
| g.hasContent = false |
| g.hasHeader = false |
| g.buf.Reset() |
| g.testbuf.Reset() |
| } |
| |
| // format returns the gofmt-ed contents of the Generator's buffer. |
| func (g *Generator) format() []byte { |
| src, err := format.Source(g.buf.Bytes()) |
| if err != nil { |
| // Should never happen, but can arise when developing this code. |
| // The user can compile the output to see the error. |
| log.Printf("warning: internal error: invalid Go generated: %s", err) |
| log.Printf("warning: compile the package to analyze the error") |
| return g.buf.Bytes() |
| } |
| return src |
| } |
| |
| // CdpClient creates the cdp.Client type. |
| func (g *Generator) CdpClient(domains []proto.Domain) { |
| g.hasContent = true |
| var fields, newFields Generator |
| for _, d := range domains { |
| fields.Printf("\t%s %s\n", d.Name(), d.Type()) |
| newFields.Printf("\t\t%s: %s.NewClient(conn),\n", d.Name(), strings.ToLower(d.Name())) |
| } |
| g.Printf(` |
| // Client represents a Chrome DevTools Protocol client that can be used to |
| // invoke methods or listen to events in every CDP domain. The Client consumes |
| // a rpcc connection, used to invoke the methods. |
| type Client struct { |
| %s |
| } |
| |
| // NewClient returns a new Client that uses conn |
| // for communication with the debugging target. |
| func NewClient(conn *rpcc.Conn) *Client { |
| return &Client{ |
| %s |
| } |
| } |
| `, fields.buf.Bytes(), newFields.buf.Bytes()) |
| } |
| |
| // PackageHeader writes the header for a package. |
| func (g *Generator) PackageHeader(comment string) { |
| if g.hasHeader { |
| return |
| } |
| g.hasHeader = true |
| g.Printf(`// Code generated by cdpgen. DO NOT EDIT. |
| |
| %s |
| package %s |
| |
| import ( |
| "context" |
| "encoding/json" |
| "fmt" |
| |
| %s |
| ) |
| `, comment, g.pkg, quotedImports(g.imports)) |
| } |
| |
| // DomainInterface defines the domain interface. |
| func (g *Generator) DomainInterface(d proto.Domain) { |
| g.hasContent = true |
| |
| comment := "The " + d.Name() + " domain. " |
| desc := d.Desc(0, len(comment)) |
| if d.Deprecated { |
| desc = "\n//\n// Deprecated: " + desc |
| } |
| if d.Experimental { |
| desc += "\n//\n// Note: This domain is experimental." |
| } |
| g.Printf(` |
| // %[1]s%[2]s |
| type %[3]s interface{`, comment, desc, d.Name()) |
| for _, c := range d.Commands { |
| if c.Redirect != "" { |
| continue |
| } |
| request := "" |
| reply := "error" |
| if len(c.Parameters) > 0 { |
| request = ", *" + strings.ToLower(d.Name()) + "." + c.ArgsName(d) |
| } |
| if len(c.Returns) > 0 { |
| reply = fmt.Sprintf("(*%s.%s, error)", strings.ToLower(d.Name()), c.ReplyName(d)) |
| } |
| desc := c.Desc(true, 8, 0) |
| if c.Deprecated { |
| desc = strings.Replace(c.Desc(true, 8, 12), "Deprecated, ", "", 1) |
| desc = "Deprecated: " + strings.ToUpper(desc[0:1]) + desc[1:] |
| } |
| if desc != "" { |
| desc = "\n\t//\n\t// " + desc |
| } |
| if c.Experimental { |
| desc += "\n\t//\n\t// Note: This command is experimental." |
| } |
| g.Printf("\n\t// Command %s%s\n\t%s(context.Context%s) %s\n", c.Name(), desc, c.Name(), request, reply) |
| } |
| for _, e := range d.Events { |
| eventClient := fmt.Sprintf("%sClient", e.EventName(d)) |
| desc := e.Desc(true, 8, 0) |
| if e.Deprecated { |
| desc = strings.Replace(e.Desc(true, 8, 12), "Deprecated, ", "", 1) |
| desc = "Deprecated: " + strings.ToUpper(desc[0:1]) + desc[1:] |
| } |
| if desc != "" { |
| desc = "\n\t//\n\t// " + desc |
| } |
| if e.Experimental { |
| desc += "\n//\n// Note: This event is experimental." |
| } |
| g.Printf("\n\t// Event %s%s\n\t%s(context.Context) (%s.%s, error)\n", e.Name(), desc, e.Name(), strings.ToLower(d.Name()), eventClient) |
| } |
| g.Printf("}\n") |
| } |
| |
| // DomainDefinition defines the entire domain. |
| func (g *Generator) DomainDefinition(d proto.Domain) { |
| g.hasContent = true |
| |
| comment := fmt.Sprintf("domainClient is a client for the %s domain. ", d.Name()) |
| g.Printf(` |
| // %[1]s%[3]s |
| type domainClient struct{ conn *rpcc.Conn } |
| |
| // NewClient returns a client for the %[2]s domain with the connection set to conn. |
| func NewClient(conn *rpcc.Conn) *domainClient { |
| return &domainClient{conn: conn} |
| } |
| `, comment, d.Name(), d.Desc(0, len(comment))) |
| |
| for _, c := range d.Commands { |
| if c.Redirect != "" { |
| continue |
| } |
| request := "" |
| invokeReply := "nil" |
| if len(c.Parameters) > 0 { |
| request = ", args *" + c.ArgsName(d) |
| } |
| reply := "(err error)" |
| if len(c.Returns) > 0 { |
| reply = fmt.Sprintf("(reply *%s, err error)", c.ReplyName(d)) |
| } |
| comment := fmt.Sprintf("%[1]s invokes the %[2]s method. ", c.Name(), d.Name()) |
| g.Printf(` |
| // %[2]s%[5]s |
| func (d *domainClient) %[1]s(ctx context.Context%[3]s) %[4]s {`, c.Name(), comment, request, reply, c.Desc(true, 0, len(comment))) |
| if len(c.Returns) > 0 { |
| g.Printf(` |
| reply = new(%s)`, c.ReplyName(d)) |
| invokeReply = "reply" |
| } |
| if len(c.Parameters) > 0 { |
| g.Printf(` |
| if args != nil { |
| err = rpcc.Invoke(ctx, %[1]q, args, %[2]s, d.conn) |
| } else { |
| err = rpcc.Invoke(ctx, %[1]q, nil, %[2]s, d.conn) |
| } |
| if err != nil { |
| err = &internal.OpError{Domain: %[3]q, Op: %[4]q, Err: err} |
| } |
| return |
| } |
| `, d.Domain+"."+c.NameName, invokeReply, d.Name(), c.Name()) |
| } else { |
| g.Printf(` |
| err = rpcc.Invoke(ctx, %q, nil, %s, d.conn) |
| if err != nil { |
| err = &internal.OpError{Domain: %q, Op: %q, Err: err} |
| } |
| return |
| } |
| `, d.Domain+"."+c.NameName, invokeReply, d.Name(), c.Name()) |
| } |
| |
| // Generate method tests. |
| g.TestPrintf(` |
| func Test%[1]s_%[2]s(t *testing.T) { |
| conn, codec, cleanup := newTestConn(t) |
| defer cleanup() |
| |
| dom := New%[1]s(conn) |
| var err error |
| `, d.Name(), c.Name()) |
| assign := "err" |
| if len(c.Returns) > 0 { |
| assign = "_, err" |
| } |
| if len(c.Parameters) > 0 { |
| g.TestPrintf(` |
| // Test nil args. |
| %[1]s = dom.%[2]s(nil, nil) |
| if err != nil { |
| t.Error(err) |
| } |
| // Test args. |
| %[1]s = dom.%[2]s(nil, &%[3]s{})`, assign, c.Name(), c.ArgsName(d)) |
| } else { |
| g.TestPrintf(` |
| %[1]s = dom.%[2]s(nil)`, assign, c.Name()) |
| } |
| g.TestPrintf(` |
| if err != nil { |
| t.Error(err) |
| } |
| |
| // Test error. |
| codec.respErr = errors.New("bad request")`) |
| if len(c.Parameters) > 0 { |
| g.TestPrintf(` |
| %[1]s = dom.%[2]s(nil, &%[3]s{})`, assign, c.Name(), c.ArgsName(d)) |
| } else { |
| g.TestPrintf(` |
| %[1]s = dom.%[2]s(nil)`, assign, c.Name()) |
| } |
| g.TestPrintf(` |
| if err == nil || err.(*internal.OpError).Err.(*rpcc.ResponseError).Message != codec.respErr.Error() { |
| t.Errorf("unexpected error; got: %%v, want bad request", err) |
| }`) |
| g.TestPrintf(` |
| } |
| `) |
| |
| } |
| for _, e := range d.Events { |
| eventClient := fmt.Sprintf("%sClient", e.EventName(d)) |
| eventClientImpl := strings.ToLower(string(eventClient[0])) + eventClient[1:] |
| |
| // Implement event on domain. |
| g.Printf(` |
| func (d *domainClient) %s(ctx context.Context) (%s, error) { |
| s, err := rpcc.NewStream(ctx, %q, d.conn) |
| if err != nil { |
| return nil, err |
| } |
| return &%s{Stream: s}, nil |
| } |
| `, e.Name(), eventClient, d.Domain+"."+e.NameName, eventClientImpl) |
| |
| g.Printf(` |
| type %[4]s struct { rpcc.Stream } |
| |
| // GetStream returns the original Stream for use with cdp.Sync. |
| func (c *%[4]s) GetStream() rpcc.Stream { return c.Stream } |
| |
| func (c *%[4]s) Recv() (*%[3]s, error) { |
| event := new(%[3]s) |
| if err := c.RecvMsg(event); err != nil { |
| return nil, &internal.OpError{Domain: %[5]q, Op: "%[6]s Recv", Err: err} |
| } |
| return event, nil |
| } |
| `, eventClient, "", e.ReplyName(d), eventClientImpl, d.Name(), e.Name()) |
| |
| // Generate event tests. |
| g.TestPrintf(` |
| func Test%[1]s_%[2]s(t *testing.T) { |
| conn, codec, cleanup := newTestConn(t) |
| defer cleanup() |
| |
| dom := New%[1]s(conn) |
| |
| stream, err := dom.%[2]s(nil) |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer stream.Close() |
| |
| codec.event = %[3]s.String() |
| codec.conn <- nil |
| _, err = stream.Recv() |
| if err != nil { |
| t.Error(err) |
| } |
| |
| codec.eventArgs = []byte("invalid json") |
| codec.conn <- nil |
| _, err = stream.Recv() |
| if err, ok := err.(*internal.OpError); !ok { |
| t.Errorf("Recv() got %%v, want internal.OpError", err) |
| } |
| |
| conn.Close() |
| stream, err = dom.%[2]s(nil) |
| if err == nil { |
| t.Errorf("Open stream: got nil, want error") |
| } |
| `, d.Name(), e.Name(), e.EventName(d)) |
| g.TestPrintf(` |
| } |
| `) |
| } |
| } |
| |
| // DomainType creates the type definition. |
| func (g *Generator) DomainType(d proto.Domain, t proto.AnyType) { |
| g.hasContent = true |
| |
| var comment string |
| if d.Name() == "Page" && (t.Name(d) == "FrameID") { |
| comment = "//" |
| g.Printf(` |
| // %[1]s %[2]s |
| // |
| // Provided as an alias to prevent circular dependencies. |
| type %[1]s = internal.Page%[1]s |
| `, t.Name(d), t.Desc(0, len(t.Name(d))+1)) |
| } |
| desc := t.Desc(0, len(t.Name(d))+1) |
| if t.Deprecated { |
| desc = "\n//\n// Deprecated: " + t.Desc(0, 12) |
| } |
| if t.Experimental { |
| desc = desc + "\n//\n// Note: This type is experimental." |
| } |
| g.Printf(` |
| // %[1]s %[2]s |
| %[3]stype %[1]s `, t.Name(d), desc, comment) |
| |
| typ := t.GoType(g.pkg, d) |
| switch typ { |
| case "struct": |
| g.domainTypeStruct(d, t) |
| case "enum": |
| g.domainTypeEnum(d, t) |
| case "time.Time": |
| g.domainTypeTime(d, t) |
| case "RawMessage": |
| g.domainTypeRawMessage(d, t) |
| default: |
| g.Printf(typ) |
| } |
| g.Printf("\n\n") |
| } |
| |
| func (g *Generator) printStructProperties(d proto.Domain, name string, props []proto.AnyType, ptrOptional, renameOptional bool) { |
| for _, prop := range props { |
| jsontag := prop.NameName |
| ptype := prop.GoType(g.pkg, d) |
| |
| if ptype == "page.FrameID" { |
| if g.pkg == "network" || g.pkg == "dom" { |
| // Domain-local type (alias). |
| ptype = strings.Replace(ptype, "page.", "internal.Page", 1) |
| } |
| } |
| |
| // Make all optional properties into pointers, unless they are slices. |
| if prop.Optional { |
| isNonPtr := nonPtrMap[ptype] |
| if ptrOptional && !isNonPtr && !isNonPointer(g.pkg, d, prop) { |
| ptype = "*" + ptype |
| } |
| jsontag += ",omitempty" |
| } |
| |
| // Avoid recursive type definitions. |
| if ptype == name { |
| ptype = "*" + ptype |
| } |
| |
| exportedName := prop.ExportedName(d) |
| if renameOptional && prop.Optional { |
| exportedName = OptionalPropPrefix + exportedName |
| } |
| |
| var preDesc, postDesc string |
| |
| desc := prop.Desc(8, len(exportedName)+1) |
| var deprecated, localEnum, experimental string |
| if prop.Deprecated { |
| desc = prop.Desc(8, 12) |
| if desc == "" { |
| desc = "This property should not be used." |
| } |
| deprecated = "//\n// Deprecated: " + desc + "\n" |
| desc = "is deprecated." |
| } |
| if prop.IsLocalEnum() { |
| var enums []string |
| for _, e := range prop.Enum { |
| enums = append(enums, fmt.Sprintf("%q", e)) |
| } |
| localEnum = "//\n// Values: " + strings.Join(enums, ", ") + ".\n" |
| } |
| if prop.Experimental { |
| experimental = "//\n// Note: This property is experimental.\n" |
| } |
| if deprecated != "" || localEnum != "" || experimental != "" { |
| preDesc = "// " + exportedName + " " + desc + "\n" + deprecated + localEnum + experimental |
| } else { |
| if desc == "" { |
| desc = "No description." |
| } |
| postDesc = "// " + enforceSingleLine(desc) |
| } |
| |
| g.Printf("\t%s%s %s `json:\"%s\"` %s\n", preDesc, exportedName, ptype, jsontag, postDesc) |
| } |
| } |
| |
| func enforceSingleLine(s string) string { |
| return strings.Replace(s, "\n//", "", -1) |
| } |
| |
| func (g *Generator) domainTypeStruct(d proto.Domain, t proto.AnyType) { |
| g.Printf("struct{\n") |
| g.printStructProperties(d, t.Name(d), t.Properties, true, false) |
| g.Printf("}") |
| } |
| |
| func (g *Generator) domainTypeTime(d proto.Domain, t proto.AnyType) { |
| var div int |
| if d.Name() == "Runtime" { |
| // Runtime domain denotes timestamps in milliseconds. |
| div = 1000 |
| } else { |
| div = 1 |
| } |
| |
| g.Printf(`float64 |
| |
| // String calls (time.Time).String(). |
| func (t %[1]s) String() string { |
| return t.Time().String() |
| } |
| |
| // Time parses the Unix time. |
| func (t %[1]s) Time() time.Time { |
| ts := float64(t) / %[3]d |
| secs := int64(ts) |
| nsecs := int64((ts - float64(secs)) * 1000000000) |
| return time.Unix(secs, nsecs) |
| } |
| |
| // MarshalJSON implements json.Marshaler. Encodes to null if t is zero. |
| func (t %[1]s) MarshalJSON() ([]byte, error) { |
| if t == 0 { |
| return []byte("null"), nil |
| } |
| f := float64(t) |
| return json.Marshal(&f) |
| } |
| |
| // UnmarshalJSON implements json.Unmarshaler. |
| func (t *%[1]s) UnmarshalJSON(data []byte) error { |
| *t = 0 |
| if len(data) == 0 { |
| return nil |
| } |
| var f float64 |
| if err := json.Unmarshal(data, &f); err != nil { |
| return errors.New("%[2]s.%[1]s: " + err.Error()) |
| } |
| *t = %[1]s(f) |
| return nil |
| } |
| |
| var _ json.Marshaler = (*%[1]s)(nil) |
| var _ json.Unmarshaler = (*%[1]s)(nil) |
| `, t.Name(d), g.pkg, div) |
| |
| g.TestPrintf(` |
| func Test%[1]s_Marshal(t *testing.T) { |
| var v %[1]s |
| |
| // Test empty. |
| b, err := json.Marshal(&v) |
| if err != nil { |
| t.Errorf("Marshal() got %%v, want no error", err) |
| } |
| if string(b) != "null" { |
| t.Errorf("Marshal() got %%s, want null", b) |
| } |
| err = json.Unmarshal(b, &v) |
| if err != nil { |
| t.Errorf("Unmarshal() got %%v, want no error", err) |
| } |
| |
| // Test non-empty. |
| v = 1 |
| b, err = json.Marshal(&v) |
| if err != nil { |
| t.Errorf("Marshal() got %%v, want no error", err) |
| } |
| if string(b) != "1" { |
| t.Errorf("Marshal() got %%s, want 1", b) |
| } |
| err = json.Unmarshal(b, &v) |
| if err != nil { |
| t.Errorf("Unmarshal() got %%v, want no error", err) |
| } |
| } |
| `, t.Name(d)) |
| } |
| |
| func (g *Generator) domainTypeRawMessage(d proto.Domain, t proto.AnyType) { |
| g.Printf(`[]byte |
| |
| // MarshalJSON copies behavior of json.RawMessage. |
| func (%[3]s %[1]s) MarshalJSON() ([]byte, error) { |
| if %[3]s == nil { |
| return []byte("null"), nil |
| } |
| return %[3]s, nil |
| } |
| |
| // UnmarshalJSON copies behavior of json.RawMessage. |
| func (%[3]s *%[1]s) UnmarshalJSON(data []byte) error { |
| if %[3]s == nil { |
| return errors.New("%[2]s.%[1]s: UnmarshalJSON on nil pointer") |
| } |
| *%[3]s = append((*%[3]s)[0:0], data...) |
| return nil |
| } |
| |
| var _ json.Marshaler = (*%[1]s)(nil) |
| var _ json.Unmarshaler = (*%[1]s)(nil) |
| `, t.Name(d), g.pkg, t.Recvr(d)) |
| |
| g.TestPrintf(` |
| func Test%[1]s_Marshal(t *testing.T) { |
| var v %[1]s |
| |
| // Test empty. |
| b, err := json.Marshal(&v) |
| if err != nil { |
| t.Errorf("Marshal() got %%v, want no error", err) |
| } |
| if string(b) != "null" { |
| t.Errorf("Marshal() got %%s, want null", b) |
| } |
| err = json.Unmarshal(b, &v) |
| if err != nil { |
| t.Errorf("Unmarshal() got %%v, want no error", err) |
| } |
| |
| // Test non-empty. |
| v = []byte("\"test\"") |
| b, err = json.Marshal(&v) |
| if err != nil { |
| t.Errorf("Marshal() got %%v, want no error", err) |
| } |
| if !bytes.Equal(v, b) { |
| t.Errorf("Marshal() got %%s, want %%s", b, v) |
| } |
| v = nil |
| err = json.Unmarshal(b, &v) |
| if err != nil { |
| t.Errorf("Unmarshal() got %%v, want no error", err) |
| } |
| if !bytes.Equal(v, b) { |
| t.Errorf("Unmarshal() got %%s, want %%s", b, v) |
| } |
| } |
| `, t.Name(d)) |
| } |
| |
| func (g *Generator) domainTypeEnum(d proto.Domain, t proto.AnyType) { |
| if t.Type != "string" { |
| log.Panicf("unknown enum type: %s", t.Type) |
| } |
| |
| // Verify assumption about enums never being the empty string, this |
| // allows us to consider optional enums as type string instead of |
| // *string when encoding to JSON (omitempty). |
| for _, e := range t.Enum { |
| if e == "" { |
| panic("enum " + t.Name(d) + " has unexpected empty enum value") |
| } |
| } |
| |
| name := strings.Title(t.Name(d)) |
| if realEnum { |
| g.Printf("int\n\n") |
| |
| format := ` |
| // %s as enums. |
| const ( |
| %sNotSet %s = iota` |
| g.Printf(format, name, name, name) |
| for _, e := range t.Enum { |
| g.Printf("\n\t%s%s", name, e.Name()) |
| } |
| g.Printf(` |
| ) |
| `) |
| g.Printf(` |
| // Valid returns true if enum is set. |
| func (e %[1]s) Valid() bool { |
| return e >= 1 && e <= %[2]d |
| } |
| |
| func (e %[1]s) String() string { |
| switch e { |
| case 0: |
| return "%[1]sNotSet"`, name, len(t.Enum)) |
| for i, e := range t.Enum { |
| g.Printf(` |
| case %d: |
| return "%s"`, i+1, e) |
| } |
| g.Printf(` |
| } |
| return fmt.Sprintf("%[1]s(%%d)", e) |
| } |
| |
| // MarshalJSON encodes enum into a string or null when not set. |
| func (e %[1]s) MarshalJSON() ([]byte, error) { |
| if e == 0 { |
| return []byte("null"), nil |
| } |
| if !e.Valid() { |
| return nil, errors.New("%[2]s.%[1]s: MarshalJSON on bad enum value: " + e.String()) |
| } |
| return json.Marshal(e.String()) |
| } |
| |
| // UnmarshalJSON decodes a string value into a enum. |
| func (e *%[1]s) UnmarshalJSON(data []byte) error { |
| switch string(data) { |
| case "null": |
| *e = 0`, name, g.pkg) |
| for i, e := range t.Enum { |
| g.Printf(` |
| case "\"%s\"": |
| *e = %d`, e, i+1) |
| } |
| g.Printf(` |
| default: |
| return fmt.Errorf("%s.%s: UnmarshalJSON on bad input: %%s", data) |
| } |
| return nil |
| }`, g.pkg, name) |
| } else { |
| g.Printf("string\n\n") |
| |
| g.Printf(` |
| // %s as enums. |
| const (`, name) |
| format := "\n\t%s%s %s = %q" |
| g.Printf(format, name, "NotSet", name, "") |
| |
| for _, e := range t.Enum { |
| g.Printf(format, name, e.Name(), name, e) |
| } |
| g.Printf(` |
| ) |
| `) |
| |
| var enumValues []string |
| for _, e := range t.Enum { |
| enumValues = append(enumValues, fmt.Sprintf("%q", e)) |
| } |
| |
| g.Printf(` |
| func (e %[1]s) Valid() bool { |
| switch e { |
| case %[2]s: |
| return true |
| default: |
| return false |
| } |
| } |
| |
| func (e %[1]s) String() string { |
| return string(e) |
| } |
| `, t.Name(d), strings.Join(enumValues, ", ")) |
| } |
| } |
| |
| // CmdType generates the type for CDP methods names. |
| func (g *Generator) CmdType(doms []proto.Domain) { |
| g.hasContent = true |
| g.Printf(` |
| // CmdType is the type for CDP methods names. |
| type CmdType string |
| |
| func (c CmdType) String() string { |
| return string(c) |
| } |
| |
| // Cmd methods. |
| const (`) |
| for _, d := range doms { |
| for _, c := range d.Commands { |
| g.Printf("\n\t%s CmdType = %q", c.CmdName(d, true), d.Domain+"."+c.NameName) |
| } |
| } |
| g.Printf("\n)\n") |
| } |
| |
| // DomainCmd defines the command args and reply. |
| func (g *Generator) DomainCmd(d proto.Domain, c proto.Command) { |
| if len(c.Parameters) > 0 { |
| g.hasContent = true |
| g.domainCmdArgs(d, c) |
| } |
| if len(c.Returns) > 0 { |
| g.hasContent = true |
| g.domainCmdReply(d, c) |
| } |
| } |
| |
| func (g *Generator) domainCmdArgs(d proto.Domain, c proto.Command) { |
| g.Printf(` |
| // %[1]s represents the arguments for %[2]s in the %[3]s domain. |
| type %[1]s struct { |
| `, c.ArgsName(d), c.Name(), d.Name()) |
| g.printStructProperties(d, c.ArgsName(d), c.Parameters, true, true) |
| g.Printf("}\n\n") |
| |
| newfmt := ` |
| // New%[1]s initializes %[4]s with the required arguments. |
| func New%[1]s(%[2]s) *%[1]s { |
| args := new(%[1]s) |
| %[3]s |
| return args |
| } |
| ` |
| sig := c.ArgsSignature(d) |
| if d.Name() == "DOM" { |
| sig = strings.Replace(sig, "page.FrameID", "internal.PageFrameID", 1) |
| } |
| g.Printf(newfmt, c.ArgsName(d), sig, c.ArgsAssign("args", d), c.ArgsName(d)) |
| |
| // Test the new arguments. |
| testInit := "" |
| if c.ArgsSignature(d) != "" { |
| testInit = fmt.Sprintf("func() (%s) { return }()", c.ArgsSignature(d)) |
| } |
| g.TestPrintf(` |
| func TestNew%[1]s(t *testing.T) { |
| args := New%[1]s(%[2]s) |
| if args == nil { |
| t.Errorf("New%[1]s returned nil args") |
| } |
| } |
| `, c.ArgsName(d), testInit) |
| |
| for _, arg := range c.Parameters { |
| if !arg.Optional { |
| continue |
| } |
| typ := arg.GoType(g.pkg, d) |
| isNonPtr := nonPtrMap[typ] |
| ptr := "&" |
| if isNonPtr || isNonPointer(g.pkg, d, arg) { |
| ptr = "" |
| } |
| name := arg.Name(d) |
| if name == "range" || name == "type" { |
| name = name[0 : len(name)-1] |
| } |
| comment := fmt.Sprintf("Set%[1]s sets the %[1]s optional argument. ", arg.ExportedName(d)) |
| desc := arg.Desc(8, len(comment)) |
| if arg.Deprecated { |
| if desc == "" { |
| desc = "This property should not be used." |
| } |
| desc = "\n//\n// Deprecated: " + desc |
| } |
| if arg.IsLocalEnum() { |
| var enums []string |
| for _, e := range arg.Enum { |
| enums = append(enums, fmt.Sprintf("%q", e)) |
| } |
| desc += "\n//\n// Values: " + strings.Join(enums, ", ") + "." |
| } |
| if arg.Experimental { |
| desc += "\n//\n// Note: This property is experimental." |
| } |
| setMethodFmt := fmt.Sprintf(` |
| // %[7]s%[5]s |
| func (a *%[2]s) Set%[1]s(%[3]s %%[1]s) *%[2]s { |
| a.%[4]s%[1]s = %[6]s%[3]s |
| return a |
| } |
| `, arg.ExportedName(d), c.ArgsName(d), name, OptionalPropPrefix, desc, ptr, comment) |
| |
| argType := arg.GoType("cdp", d) |
| g.Printf(setMethodFmt, argType) |
| } |
| } |
| |
| func (g *Generator) domainCmdReply(d proto.Domain, c proto.Command) { |
| g.Printf(` |
| // %[1]s represents the return values for %[2]s in the %[3]s domain. |
| type %[1]s struct { |
| `, c.ReplyName(d), c.Name(), d.Name()) |
| g.printStructProperties(d, c.ReplyName(d), c.Returns, true, false) |
| g.Printf("}\n\n") |
| } |
| |
| // EventType generates the type for CDP event names. |
| func (g *Generator) EventType(doms []proto.Domain) { |
| g.hasContent = true |
| g.Printf(` |
| // EventType is the type for CDP event names. |
| type EventType string |
| |
| func (e EventType) String() string { |
| return string(e) |
| } |
| |
| // Event methods. |
| const (`) |
| for _, d := range doms { |
| for _, e := range d.Events { |
| g.Printf("\n\t%s EventType = %q", e.EventName(d), d.Domain+"."+e.NameName) |
| } |
| } |
| g.Printf("\n)\n") |
| } |
| |
| // DomainEvent defines the event client and reply. |
| func (g *Generator) DomainEvent(d proto.Domain, e proto.Event) { |
| g.hasContent = true |
| g.domainEventClient(d, e) |
| |
| g.domainEventReply(d, e) |
| } |
| |
| func (g *Generator) domainEventClient(d proto.Domain, e proto.Event) { |
| eventClient := fmt.Sprintf("%sClient", e.EventName(d)) |
| comment := fmt.Sprintf("%[1]s is a client for %[2]s events. ", eventClient, e.Name()) |
| g.Printf(` |
| // %[2]s%[4]s |
| type %[1]s interface { |
| // Recv calls RecvMsg on rpcc.Stream, blocks until the event is |
| // triggered, context canceled or connection closed. |
| Recv() (*%[3]s, error) |
| rpcc.Stream |
| } |
| `, eventClient, comment, e.ReplyName(d), e.Desc(true, 0, len(comment))) |
| } |
| |
| func (g *Generator) domainEventReply(d proto.Domain, e proto.Event) { |
| g.Printf(` |
| // %[1]s is the reply for %[2]s events. |
| type %[1]s struct { |
| `, e.ReplyName(d), e.Name()) |
| g.printStructProperties(d, e.ReplyName(d), e.Parameters, true, false) |
| g.Printf("}\n") |
| } |
| |
| func quotedImports(imports []string) string { |
| if len(imports) == 0 { |
| return "" |
| } |
| |
| return "\"" + strings.Join(imports, "\"\n\"") + "\"" |
| } |
| |
| func isNonPointer(pkg string, d proto.Domain, t proto.AnyType) bool { |
| typ := t.GoType(pkg, d) |
| switch { |
| case t.IsEnum(): |
| case strings.HasPrefix(typ, "[]"): |
| case strings.HasPrefix(typ, "map["): |
| case typ == "time.Time": |
| case typ == "json.RawMessage": |
| case typ == "RawMessage": |
| case typ == "interface{}": |
| default: |
| return false |
| } |
| return true |
| } |