Skip to content

Commit

Permalink
Add PHP JSON-RPC Client generator
Browse files Browse the repository at this point in the history
  • Loading branch information
sergeyfast authored Apr 6, 2021
2 parents 43cca24 + 0b56fad commit 9172559
Show file tree
Hide file tree
Showing 6 changed files with 471 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ func main () {

http.HandleFunc("/client.go", rpcgen.Handler(gen.GoClient()))
http.HandleFunc("/client.ts", rpcgen.Handler(gen.TSClient(nil)))
http.HandleFunc("/RpcClient.php", rpcgen.Handler(gen.PHPClient("")))
}
```

Expand Down
5 changes: 5 additions & 0 deletions generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"net/http"

"github.com/vmkteam/rpcgen/v2/golang"
"github.com/vmkteam/rpcgen/v2/php"
"github.com/vmkteam/rpcgen/v2/typescript"

smd1 "github.com/vmkteam/zenrpc/smd"
Expand All @@ -23,6 +24,10 @@ func (g RPCGen) GoClient() Generator {
return golang.NewClient(g.schema)
}

func (g RPCGen) PHPClient(phpNamespace string) Generator {
return php.NewClient(g.schema, phpNamespace)
}

func (g RPCGen) TSClient(typeMapper typescript.TypeMapper) Generator {
return typescript.NewClient(g.schema, typeMapper)
}
Expand Down
255 changes: 255 additions & 0 deletions php/php_client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
package php

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

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

const (
definitionsPrefix = "#/definitions/"

defaultPhpNamespace = "JsonRpcClient"

phpBoolean = "bool"
phpInt = "int"
phpFloat = "float"
phpString = "string"
phpObject = "object"
phpArray = "array"
)

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

type Generator struct {
schema smd.Schema
phpNamespace string
}

func NewClient(schema smd.Schema, phpNamespace string) *Generator {
ns := phpNamespace
if ns == "" {
ns = defaultPhpNamespace
}
return &Generator{schema: schema, phpNamespace: ns}
}

// Generate returns generate TypeScript client
func (g *Generator) Generate() ([]byte, error) {
m := g.PHPModels()
funcMap := template.FuncMap{
"now": time.Now,
}
tmpl, err := template.New("test").Funcs(funcMap).Parse(phpTpl)
if err != nil {
return nil, err
}

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

return buf.Bytes(), nil
}

type phpMethod struct {
Name string
SafeName string
Description []string
Parameters []Parameter
Returns Parameter
}

type phpClass struct {
Name string
Description string
Fields []Parameter
}

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

type phpModels struct {
Namespace string
Methods []phpMethod
Classes []phpClass
}

// PHPModels return converted schema to PHP.
func (g *Generator) PHPModels() phpModels {
var pModels phpModels

classesMap := make(map[string]phpClass, 0)

// iterate over all services
for serviceName, service := range g.schema.Services {
var (
params []Parameter
)
desc := linebreakRegex.ReplaceAllString(service.Description, "\n")
pMethod := phpMethod{
Name: serviceName,
SafeName: strings.ReplaceAll(serviceName, ".", "_"),
Description: strings.Split(desc, "\n"),
Returns: prepareParameter(service.Returns),
}

defToClassMap(classesMap, service.Returns.Definitions)
paramToClassMap(classesMap, pMethod.Returns)

for _, param := range service.Parameters {
p := prepareParameter(param)
defToClassMap(classesMap, param.Definitions)
paramToClassMap(classesMap, p)
params = append(params, p)
}
pMethod.Parameters = params
pModels.Methods = append(pModels.Methods, pMethod)
}

for _, v := range classesMap {
pModels.Classes = append(pModels.Classes, v)
}

// sort methods
sort.Slice(pModels.Methods, func(i, j int) bool {
return pModels.Methods[i].Name < pModels.Methods[j].Name
})

// sort classes
sort.Slice(pModels.Classes, func(i, j int) bool {
return pModels.Classes[i].Name < pModels.Classes[j].Name
})

// sort classes fields
for idx := range pModels.Classes {
sort.Slice(pModels.Classes[idx].Fields, func(i, j int) bool {
return pModels.Classes[idx].Fields[i].Name < pModels.Classes[idx].Fields[j].Name
})
}
pModels.Namespace = g.phpNamespace

return pModels
}

// prepareParameter create Parameter from smd.JSONSchema
func prepareParameter(param smd.JSONSchema) Parameter {
p := Parameter{
Name: param.Name,
Description: param.Description,
BaseType: phpType(param.Type),
Optional: param.Optional,
Properties: propertiesToParams(param.Properties),
}

pType := phpType(param.Type)
p.ReturnType = pType
if param.Type == smd.Object && param.Description != "" && !strings.Contains(param.Description, " ") {
pType = param.Description
p.ReturnType = pType
}
if param.Type == smd.Array {
pType = arrayType(param.Items)
p.ReturnType = phpArray
}
p.Type = pType

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

return p
}

// propertiesToParams convert smd.PropertyList to []Parameter
func propertiesToParams(list smd.PropertyList) []Parameter {
var parameters []Parameter
for _, prop := range list {
p := Parameter{
Name: prop.Name,
Optional: prop.Optional,
Description: prop.Description,
}
pType := phpType(prop.Type)
if prop.Type == smd.Object && prop.Ref != "" {
pType = objectType(prop.Ref)
}
if prop.Type == smd.Array {
pType = arrayType(prop.Items)
}

p.Type = pType
parameters = append(parameters, p)

}
return parameters
}

// defToClassMap convert smd.Definition to phpClass and add to classes map
func defToClassMap(classesMap map[string]phpClass, definitions map[string]smd.Definition) {
for name, def := range definitions {
classesMap[name] = phpClass{Name: name, Fields: propertiesToParams(def.Properties)}
}
}

// paramToClassMap add Parameter to class map if parameter is object
func paramToClassMap(classesMap map[string]phpClass, p Parameter) {
if p.BaseType == phpObject && p.BaseType != p.Type {
classesMap[p.Type] = phpClass{
Name: p.Type,
Fields: p.Properties,
}
}
}

// objectType return object type from $ref
func objectType(ref string) string {
if ref == "" {
return phpObject
}
return strings.TrimPrefix(ref, definitionsPrefix)
}

// arrayType return array type from $ref
func arrayType(ref map[string]string) string {
if r, ok := ref["$ref"]; ok {
return strings.TrimPrefix(r, definitionsPrefix) + "[]"
}
return phpArray
}

// phpType convert smd types to php types
func phpType(smdType string) string {
switch smdType {
case smd.String:
return phpString
case smd.Array:
return phpArray
case smd.Boolean:
return phpBoolean
case smd.Float:
return phpFloat
case smd.Integer:
return phpInt
case smd.Object:
return phpObject
}
return "mixed"
}
31 changes: 31 additions & 0 deletions php/php_client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package php

import (
"bytes"
"io/ioutil"
"testing"

"github.com/vmkteam/zenrpc/v2"
"github.com/vmkteam/zenrpc/v2/testdata"
)

func TestGeneratePHPClient(t *testing.T) {
rpc := zenrpc.NewServer(zenrpc.Options{})
rpc.Register("catalogue", testdata.CatalogueService{})

cl := NewClient(rpc.SMD(), "")

generated, err := cl.Generate()
if err != nil {
t.Fatalf("generate php client: %v", err)
}

testData, err := ioutil.ReadFile("./testdata/RpcClient.php")
if err != nil {
t.Fatalf("open test data file: %v", err)
}

if !bytes.Equal(generated, testData) {
t.Fatalf("bad generator output")
}
}
56 changes: 56 additions & 0 deletions php/php_template.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package php

const phpTpl = `<?php
/**
* PHP RPC Client by rpcgen
*/
namespace {{.Namespace}};
use EazyJsonRpc\BaseJsonRpcClient;
use EazyJsonRpc\BaseJsonRpcException;
use GuzzleHttp\Exception\GuzzleException;
use JsonMapper_Exception;
{{range .Classes}}
/** {{.Name}} **/
class {{.Name}} { {{range .Fields}}
/**{{if .Description}}
* {{.Description}}{{end}}
* @var {{.Type}}{{if .Optional}}|null{{end}}
*/
public ${{.Name}};{{end}}
}
{{end}}
/**
* RpcClient
*/
class RpcClient extends BaseJsonRpcClient {
{{range .Methods}}
/**
* <{{.Name}}> RPC method{{range $row := .Description}}
* {{$row}}{{end}}{{range .Parameters}}
* @param {{.Type}}{{if .Optional}}|null{{end}} ${{.Name}}{{if .Optional}} [optional]{{end}}{{if .Description}} {{.Description}}{{end}}{{end}}
* @param bool $isNotification [optional] set to true if call is notification
* @return {{.Returns.Type}}{{if .Returns.Optional}}|null{{end}}
* @throws BaseJsonRpcException
* @throws GuzzleException
* @throws JsonMapper_Exception
**/
public function {{.SafeName}}( {{range .Parameters}}{{if .Optional}}?{{end}}{{.ReturnType}} ${{.Name}}{{if .DefaultValue}} = {{.DefaultValue}}{{end}}, {{end}}$isNotification = false ){{if ne .Returns.Type "mixed"}}: {{if .Returns.Optional}}?{{end}}{{.Returns.ReturnType}}{{end}} {
return $this->call( __FUNCTION__, {{if ne .Returns.BaseType .Returns.Type}} __NAMESPACE__ . '\\'. {{end}}'{{.Returns.Type}}', [ {{range .Parameters}}'{{.Name}}' => ${{.Name}}, {{end}}], $this->getRequestId( $isNotification ) );
}
{{end}}
/**
* Get Instance
* @param $url string RPC server url
* @return RpcClient
*/
public static function GetInstance( $url ): RpcClient {
return new self( $url );
}
}
`
Loading

0 comments on commit 9172559

Please sign in to comment.