Skip to content

Commit 38a0b91

Browse files
committed
implement openrpc-codegen tool:
- create openrpc.json spec file that represent all zos types and api handlers - create a tool that generate go types/methods from the spec file with the limitation for the `net/rpc` package services
1 parent 5a9358e commit 38a0b91

File tree

12 files changed

+1579
-0
lines changed

12 files changed

+1579
-0
lines changed

openrpc.json

Lines changed: 1112 additions & 0 deletions
Large diffs are not rendered by default.

tools/openrpc-codegen/Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
2+
build:
3+
sudo go build -o /usr/local/bin/openrpc-codegen main.go
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package fileutils
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"os"
7+
8+
"github.com/threefoldtech/zos/tools/openrpc-codegen/schema"
9+
)
10+
11+
func Parse(filePath string) (spec schema.Spec, err error) {
12+
content, err := os.ReadFile(filePath)
13+
if err != nil {
14+
return spec, fmt.Errorf("failed to read file content filepath=%v: %w", filePath, err)
15+
}
16+
17+
if err := json.Unmarshal(content, &spec); err != nil {
18+
return spec, fmt.Errorf("failed to unmarshal file content to schema spec: %w", err)
19+
}
20+
21+
return spec, nil
22+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package fileutils
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"go/format"
7+
"os"
8+
)
9+
10+
func Write(buf bytes.Buffer, filePath string) error {
11+
formatted, err := format.Source(buf.Bytes())
12+
if err != nil {
13+
return fmt.Errorf("failed to format buffer content: %w", err)
14+
}
15+
16+
file, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755)
17+
if err != nil {
18+
return fmt.Errorf("failed to open file for writing filepath=%v: %w", filePath, err)
19+
}
20+
defer file.Close()
21+
22+
if _, err := file.Write(formatted); err != nil {
23+
return fmt.Errorf("failed to write on file: %w", err)
24+
}
25+
26+
return nil
27+
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package generator
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
7+
"github.com/threefoldtech/zos/tools/openrpc-codegen/schema"
8+
)
9+
10+
func generateStructs(buf *bytes.Buffer, name string, schema schema.Schema) error {
11+
fields := []field{}
12+
for n, prop := range schema.Properties {
13+
// Handle nested properties recursively
14+
propType, err := generateType(buf, n, prop)
15+
if err != nil {
16+
return err
17+
}
18+
fields = append(fields, field{
19+
Name: n,
20+
Type: propType,
21+
JSONName: prop.Tag,
22+
})
23+
}
24+
25+
return executeTemplate(buf, structTemplate, structType{
26+
Name: name,
27+
Fields: fields,
28+
})
29+
}
30+
31+
func generateType(buf *bytes.Buffer, name string, schema schema.Schema) (string, error) {
32+
if schema.Ref != "" {
33+
return invokeRef(schema.Ref), nil
34+
}
35+
36+
if schema.Format == "raw" {
37+
return convertToGoType(invokeRef(schema.Type), schema.Format), nil
38+
}
39+
40+
switch schema.Type {
41+
case "object":
42+
if err := generateStructs(buf, name, schema); err != nil {
43+
return "", err
44+
}
45+
return name, nil
46+
case "array":
47+
if schema.Items.Ref != "" {
48+
return "[]" + invokeRef(schema.Items.Ref), nil
49+
}
50+
itemType, err := generateType(buf, name, *schema.Items)
51+
if err != nil {
52+
return "", err
53+
}
54+
return "[]" + itemType, nil
55+
default:
56+
return convertToGoType(invokeRef(schema.Type), schema.Format), nil
57+
}
58+
}
59+
60+
func generateSchemas(buf *bytes.Buffer, schemas map[string]schema.Schema) error {
61+
for key, schema := range schemas {
62+
_, err := generateType(buf, key, schema)
63+
if err != nil {
64+
return err
65+
}
66+
}
67+
return nil
68+
}
69+
70+
func generateMethods(buf *bytes.Buffer, serviceName string, methods []schema.Method) error {
71+
ms := []methodType{}
72+
for _, method := range methods {
73+
methodName := extractMethodName(method.Name)
74+
arg, reply, err := getMethodTypes(method)
75+
if err != nil {
76+
return err
77+
}
78+
ms = append(ms, methodType{
79+
Name: methodName,
80+
ArgType: arg,
81+
ReplyType: reply,
82+
})
83+
}
84+
85+
return executeTemplate(buf, MethodTemplate, service{
86+
Name: serviceName,
87+
Methods: ms,
88+
})
89+
}
90+
91+
func getMethodTypes(method schema.Method) (string, string, error) {
92+
argType, replyType := "any", ""
93+
94+
if len(method.Params) == 1 {
95+
argType = getTypeFromSchema(method.Params[0].Schema)
96+
} else if len(method.Params) > 1 {
97+
return "", "", fmt.Errorf("multiple parameters not supported for method: %v", method.Name)
98+
}
99+
100+
if method.Result.Schema.Type != "" {
101+
replyType = convertToGoType(method.Result.Schema.Type, method.Result.Schema.Format)
102+
} else if method.Result.Schema.Ref != "" {
103+
replyType = invokeRef(method.Result.Schema.Ref)
104+
} else {
105+
return "", "", fmt.Errorf("no result defined for method: %v", method.Name)
106+
}
107+
108+
return argType, replyType, nil
109+
}
110+
111+
func getTypeFromSchema(schema schema.Schema) string {
112+
if schema.Type != "" {
113+
return convertToGoType(schema.Type, schema.Format)
114+
}
115+
return invokeRef(schema.Ref)
116+
}
117+
118+
func GenerateServerCode(buf *bytes.Buffer, spec schema.Spec, pkg string) error {
119+
if err := addPackageName(buf, pkg); err != nil {
120+
return fmt.Errorf("failed to write pkg name: %w", err)
121+
}
122+
123+
if err := generateMethods(buf, spec.Info.Title, spec.Methods); err != nil {
124+
return fmt.Errorf("failed to generate methods: %w", err)
125+
}
126+
127+
if err := generateSchemas(buf, spec.Components.Schemas); err != nil {
128+
return fmt.Errorf("failed to generate schema: %w", err)
129+
}
130+
131+
return nil
132+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package generator
2+
3+
type structType struct {
4+
Name string
5+
Fields []field
6+
}
7+
8+
type field struct {
9+
Name string
10+
Type string
11+
JSONName string
12+
}
13+
14+
const structTemplate = `
15+
type {{.Name}} struct {
16+
{{- range .Fields }}
17+
{{ .Name }} {{ .Type }} ` + "`json:\"{{ .JSONName }}\"`" + `
18+
{{- end }}
19+
}
20+
`
21+
22+
type service struct {
23+
Name string
24+
Methods []methodType
25+
}
26+
27+
type methodType struct {
28+
Name string
29+
ArgType string
30+
ReplyType string
31+
}
32+
33+
const MethodTemplate = `
34+
type {{.Name}} interface {
35+
{{- range .Methods }}
36+
{{.Name}}({{.ArgType}}, *{{.ReplyType}}) error
37+
{{- end}}
38+
}
39+
`
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package generator
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"path"
7+
"strings"
8+
"text/template"
9+
)
10+
11+
func convertToGoType(t string, f string) string {
12+
if f == "raw" {
13+
return "json.RawMessage"
14+
}
15+
16+
switch t {
17+
case "integer":
18+
return "uint64"
19+
case "number":
20+
return "float64"
21+
case "boolean":
22+
return "bool"
23+
case "null":
24+
return "any"
25+
default:
26+
return t
27+
}
28+
}
29+
30+
func invokeRef(t string) string {
31+
return path.Base(t)
32+
}
33+
34+
func extractMethodName(methodText string) string {
35+
return strings.Split(methodText, ".")[len(strings.Split(methodText, "."))-1]
36+
}
37+
38+
func executeTemplate(buf *bytes.Buffer, tmpl string, data interface{}) error {
39+
templ, err := template.New("").Parse(tmpl)
40+
if err != nil {
41+
return fmt.Errorf("failed to parse template: %w", err)
42+
}
43+
if err := templ.Execute(buf, data); err != nil {
44+
return fmt.Errorf("failed to execute template: %w", err)
45+
}
46+
return nil
47+
}
48+
49+
func addPackageName(buf *bytes.Buffer, pkg string) error {
50+
pkgLine := fmt.Sprintf("package %s\n", pkg)
51+
_, err := buf.Write([]byte(pkgLine))
52+
return err
53+
}

tools/openrpc-codegen/go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module github.com/threefoldtech/zos/tools/openrpc-codegen
2+
3+
go 1.21.0

tools/openrpc-codegen/go.sum

Whitespace-only changes.

tools/openrpc-codegen/main.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"flag"
6+
"fmt"
7+
"log"
8+
9+
"github.com/threefoldtech/zos/tools/openrpc-codegen/fileutils"
10+
"github.com/threefoldtech/zos/tools/openrpc-codegen/generator"
11+
)
12+
13+
type flags struct {
14+
spec string
15+
output string
16+
pkg string
17+
}
18+
19+
func run() error {
20+
var f flags
21+
flag.StringVar(&f.spec, "spec", "", "openrpc spec file")
22+
flag.StringVar(&f.output, "output", "", "generated go code")
23+
flag.StringVar(&f.pkg, "pkg", "apirpc", "name of the go package")
24+
flag.Parse()
25+
26+
if f.spec == "" || f.output == "" {
27+
return fmt.Errorf("missing flag is required")
28+
}
29+
30+
spec, err := fileutils.Parse(f.spec)
31+
if err != nil {
32+
return err
33+
}
34+
35+
var buf bytes.Buffer
36+
if err := generator.GenerateServerCode(&buf, spec, f.pkg); err != nil {
37+
return err
38+
}
39+
40+
if err := fileutils.Write(buf, f.output); err != nil {
41+
return err
42+
}
43+
44+
return nil
45+
}
46+
47+
func main() {
48+
if err := run(); err != nil {
49+
log.Fatal(err)
50+
}
51+
}

tools/openrpc-codegen/readme.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# OpenRPC CodeGen
2+
3+
this tools generate server side code in go from an openrpc spec file
4+
5+
## Usage
6+
7+
Manually generate code
8+
9+
```bash
10+
go run main.go -spec </path/to/specfile> -output </path/to/generated/code>
11+
```
12+
13+
Use it to generate the api code
14+
15+
```bash
16+
make build
17+
# go to root
18+
go generate
19+
```
20+
21+
## Limitations
22+
23+
Any openrpc file that passes the linting on the [playground](https://playground.open-rpc.org/) should be valid for this tool. with just some limitations:
24+
25+
- Methods must have only one arg/reply: since we use `net/rpc` package it requires to have only a single arg and reply.
26+
- Methods can have arg/reply defined only for a primitive types. but for custom types array/objects it must be defined on the components schema and referenced in the method.
27+
- Array is not a valid reply type, you need to define an object in the components that have a field of this array. and reference it on the method.
28+
- Method Name, component Name, and component fields name must be a `PascalCase`
29+
- Component fields must have a tag field, it is interpreted to a json tag on the generated struct and it is necessary in the conversion to the zos types.
30+
31+
## Extensions
32+
33+
- for compatibility with gridtypes we needed to configure some extra formats like
34+
35+
```json
36+
"Data": {
37+
"tag": "data",
38+
"type": "object",
39+
"format": "raw"
40+
}
41+
```
42+
43+
which will generate a json.RawMessage type
44+
45+
## Notes
46+
47+
- All types and fields should be upper case.
48+
49+
## Enhancements
50+
51+
- [ ] write structs in order
52+
- [ ] extend the spec file to have errors and examples and docs

0 commit comments

Comments
 (0)