Skip to content

Commit

Permalink
Add Dart rpc client generation, update dependencies, fix tests (#25)
Browse files Browse the repository at this point in the history
  • Loading branch information
sas1024 authored Oct 30, 2023
1 parent cd53383 commit 24ac781
Show file tree
Hide file tree
Showing 15 changed files with 700 additions and 10 deletions.
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
[![Go Report Card](https://goreportcard.com/badge/github.com/vmkteam/rpcgen)](https://goreportcard.com/report/github.com/vmkteam/rpcgen) [![Go Reference](https://pkg.go.dev/badge/github.com/vmkteam/rpcgen.svg)](https://pkg.go.dev/github.com/vmkteam/rpcgen)

`rpcgen` is a JSON-RPC 2.0 client library generator for [zenrpc](https://github.com/vmkteam/zenrpc). It supports client generation for following languages:
- Dart
- Golang
- PHP
- TypeScript
Expand Down Expand Up @@ -45,6 +46,7 @@ import (
"net/http"

"github.com/vmkteam/rpcgen/v2"
"github.com/vmkteam/rpcgen/v2/dart"
"github.com/vmkteam/rpcgen/v2/golang"
"github.com/vmkteam/rpcgen/v2/swift"
"github.com/vmkteam/zenrpc/v2"
Expand All @@ -59,6 +61,7 @@ func main() {
http.HandleFunc("/client.ts", rpcgen.Handler(gen.TSClient(nil)))
http.HandleFunc("/RpcClient.php", rpcgen.Handler(gen.PHPClient("")))
http.HandleFunc("/client.swift", rpcgen.Handler(gen.SwiftClient(swift.Settings{})))
http.HandleFunc("/client.dart", rpcgen.Handler(gen.DartClient(dart.Settings{ Part: "client"})))
}
```

Expand Down Expand Up @@ -131,3 +134,44 @@ func main() {
http.HandleFunc("/client.swift", rpcgen.Handler(gen.SwiftClient(swift.Settings{"", typeMapper})))
}
```

### Add custom Dart type mapper

```go
package main

import (
"net/http"

"github.com/vmkteam/rpcgen/v2"
"github.com/vmkteam/rpcgen/v2/dart"
"github.com/vmkteam/zenrpc/v2"
"github.com/vmkteam/zenrpc/v2/smd"
)

func main() {
rpc := zenrpc.NewServer(zenrpc.Options{})

gen := rpcgen.FromSMD(rpc.SMD())

typeMapper := func(in smd.JSONSchema, param dart.Parameter) dart.Parameter {
if in.Type == smd.Object {
switch in.TypeName {
case "Time", "Date":
param.Type = "String"
}
}
if in.Type == smd.Array {
switch in.TypeName {
case "[]Date", "[]Time":
param.Type = "List<String>"
param.ReturnType = "List<String>"
}
}

return param
}

http.HandleFunc("/client.dart", rpcgen.Handler(gen.DartClient(dart.Settings{Part: "client", TypeMapper: typeMapper})))
}
```
323 changes: 323 additions & 0 deletions dart/dart_client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,323 @@
package dart

import (
"bytes"
"fmt"
"regexp"
"sort"
"strings"
"text/template"

"github.com/vmkteam/rpcgen/v2/gen"
"github.com/vmkteam/zenrpc/v2/smd"
)

const (
defaultPart = "generated_rpc_client"

Bool = "bool"
Int = "int"
Double = "double"
String = "String"
)

var (
linebreakRegex = regexp.MustCompile("[\r\n]+")
)

type Namespaces []Namespace

type Namespace struct {
Name string
Methods []Method
}

type Method struct {
Name string
SafeName string
Description []string
Parameters []Parameter
Returns Parameter
// all params and return models, used in method
Models []Parameter
}

func (m Method) ParamsClass() string {
return m.SafeName + "Params"
}

type Parameter struct {
Name string
Description string
Type string
BaseType string
ReturnType string
Optional bool
DefaultValue string
IsArray bool
IsObject bool
Properties []Parameter
}

func (ns Namespaces) Models() []Parameter {
var out []Parameter
m := make(map[string]Parameter)
for _, namespace := range ns {
for _, method := range namespace.Methods {
for _, p := range method.Models {
m[p.Type] = p
}
}
}
for _, p := range m {
out = append(out, p)
}
sort.Slice(out, func(i, j int) bool {
return out[i].Type < out[j].Type
})
return out
}

type TypeMapper func(in smd.JSONSchema, dartType Parameter) Parameter

type templateData struct {
gen.GeneratorData
Part string
Namespaces Namespaces
Models []Parameter
}

type Generator struct {
schema smd.Schema

settings Settings
}

type Settings struct {
Part string
TypeMapper TypeMapper
}

func NewClient(schema smd.Schema, settings Settings) *Generator {
return &Generator{schema: schema, settings: settings}
}

// Generate returns generated Dart client
func (g *Generator) Generate() ([]byte, error) {
data := templateData{Part: defaultPart, GeneratorData: gen.DefaultGeneratorData()}

if g.settings.Part != "" {
data.Part = g.settings.Part
}

for _, namespaceName := range gen.GetNamespaceNames(g.schema) {
data.Namespaces = append(data.Namespaces, Namespace{
Name: namespaceName,
Methods: g.getNamespaceMethods(g.schema, namespaceName),
})
}
data.Models = data.Namespaces.Models()

tmpl, err := template.New("dart_client").Funcs(gen.TemplateFuncs).Parse(client)
if err != nil {
return nil, err
}

// compile template
var buf bytes.Buffer
if err := tmpl.Execute(&buf, data); err != nil {
return nil, err
}

return buf.Bytes(), nil

}

// getNamespaceMethods return all namespace methods
func (g *Generator) getNamespaceMethods(schema smd.Schema, namespace string) (res []Method) {
for name, service := range schema.Services {
if gen.GetNamespace(name) == namespace {
res = append(res, g.newMethod(name, service))
}
}

sort.Slice(res, func(i, j int) bool {
return res[i].Name < res[j].Name
})

return res
}

func (g *Generator) newMethod(methodName string, service smd.Service) Method {
desc := linebreakRegex.ReplaceAllString(service.Description, "\n")

method := Method{
Name: gen.GetMethodName(methodName),
SafeName: strings.ReplaceAll(gen.GetMethodName(methodName), ".", ""),
Description: strings.Split(desc, "\n"),
Returns: g.prepareParameter(service.Returns),
}
method.Parameters = g.prepareParameters(service.Parameters)
method.Models = g.prepareModels(service)

return method
}

func (g *Generator) prepareModels(service smd.Service) []Parameter {
var out []Parameter

params := g.definitionsToParams(service)
params = append(params, g.prepareParameter(service.Returns))
params = append(params, g.prepareParameters(service.Parameters)...)
// remove non-objects and empty objects
for i, p := range params {
if !params[i].IsObject || len(params[i].Properties) == 0 {
continue
}
out = append(out, p)
}

return out
}

func (g *Generator) definitionsToParams(service smd.Service) []Parameter {
var out []Parameter
for typeName, definition := range service.Returns.Definitions {
out = append(out, g.definitionToParam(typeName, definition))
}
for _, p := range service.Parameters {
for typeName, definition := range p.Definitions {
out = append(out, g.definitionToParam(typeName, definition))
}
}
return out
}

func (g *Generator) definitionToParam(typeName string, definition smd.Definition) Parameter {
return Parameter{
Name: typeName,
Type: typeName,
IsObject: definition.Type == smd.Object,
Properties: g.propertiesToParams(definition.Properties),
}
}

// propertiesToParams convert smd.PropertyList to []Parameter
func (g *Generator) propertiesToParams(list smd.PropertyList) []Parameter {
var parameters []Parameter
for _, prop := range list {
p := Parameter{
Name: prop.Name,
Optional: prop.Optional,
Description: prop.Description,
}

pType := dartType(prop.Type)
if prop.Type == smd.Object && prop.Ref != "" {
pType = strings.TrimPrefix(prop.Ref, gen.DefinitionsPrefix)
p.IsObject = true
}

if prop.Type == smd.Array {
pType = arrayType(prop.Items)
p.IsArray = true
}

p.Type = pType

if g.settings.TypeMapper != nil {
p = g.settings.TypeMapper(smd.JSONSchema{
Name: prop.Name,
Type: prop.Type,
TypeName: pType,
Description: prop.Description,
Optional: prop.Optional,
}, p)
}
parameters = append(parameters, p)
}

sort.Slice(parameters, func(i, j int) bool {
return parameters[i].Name < parameters[j].Name
})

return parameters
}

func (g *Generator) prepareParameters(in []smd.JSONSchema) []Parameter {
var out []Parameter
for _, param := range in {
out = append(out, g.prepareParameter(param))
}

return out
}

// prepareParameter create Parameter from smd.JSONSchema
func (g *Generator) prepareParameter(in smd.JSONSchema) Parameter {
out := Parameter{
Name: in.Name,
Description: in.Description,
BaseType: dartType(in.Type),
Optional: in.Optional,
Properties: g.propertiesToParams(in.Properties),
}

pType := dartType(in.Type)
if in.Type == smd.Object {
typeName := in.TypeName
if typeName == "" && in.Description != "" && smd.IsSMDTypeName(in.Description, in.Type) {
typeName = in.Description
}

if typeName != "" {
pType = typeName
}
out.IsObject = true
}

if in.Type == smd.Array {
pType = arrayType(in.Items)
out.IsArray = true
}
out.Type = pType
out.ReturnType = pType

defaultValue := ""
if in.Default != nil {
defaultValue = string(*in.Default)
}
out.DefaultValue = defaultValue

if g.settings.TypeMapper != nil {
out = g.settings.TypeMapper(in, out)
}

return out
}

// dartType convert smd types to dart types
func dartType(smdType string) string {
switch smdType {
case smd.String:
return String
case smd.Boolean:
return Bool
case smd.Float:
return Double
case smd.Integer:
return Int
}
return "void"
}

func arrayType(items map[string]string) string {
var subType string
if scalar, ok := items["type"]; ok {
subType = dartType(scalar)
}
if ref, ok := items["$ref"]; ok {
subType = strings.TrimPrefix(ref, gen.DefinitionsPrefix)
}
return fmt.Sprintf("List<%s>", subType)
}
Loading

0 comments on commit 24ac781

Please sign in to comment.