Skip to content

Commit

Permalink
Added independent communication module (#1)
Browse files Browse the repository at this point in the history
* use fetch from window instead of the previous module
* Added integration test for generated code
* Added github actions for test
  • Loading branch information
lyonlai authored Oct 29, 2020
1 parent a18c814 commit c767c51
Show file tree
Hide file tree
Showing 31 changed files with 4,905 additions and 140 deletions.
65 changes: 65 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# This is a basic workflow to help you get started with Actions

name: CI

# Controls when the action will run. Triggers the workflow on push or pull request
# events but only for the master branch
on: [push]

# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
test:
# The type of runner that the job will run on
runs-on: ubuntu-latest

strategy:
matrix:
node-version: [12.x]

# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2

- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}

- name: Set up Go 1.x
uses: actions/setup-go@v2
with:
go-version: ^1.14

- name: Install Protoc
uses: arduino/setup-protoc@v1

- name: golangci-lint
uses: golangci/golangci-lint-action@v2
with:
# Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
version: v1.29

# Runs a single command using the runners shell
- name: Get go dependencies
run: |
go get
# Runs check dependencies for integratino tests
- name: Get node dependencies
run: |
cd integration_tests && npm install
# Runs a set of commands using the runners shell
- name: Run unit test
run: |
cd testdata && make protos && cd ..
go test ./...
# Runs
- name: Run integration tests
run: |
cd integration_tests
make client-protos
./scripts/test-ci.sh
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*/node_modules/*
*/coverage/*
2 changes: 1 addition & 1 deletion data/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ type File struct {
// ExternalDependingTypes stores the external dependenciees fully qualified name,
ExternalDependingTypes []string
// Services stores the information to render service
Services []*Service
Services Services
// Name is the name of the file
Name string
// TSFileName is the name of the output file
Expand Down
27 changes: 27 additions & 0 deletions data/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,33 @@ type Service struct {
Methods []*Method
}

// Services is an alias of Service array
type Services []*Service

// HasServerStreamingMethod indicates whether there is server side streaming calls inside any of the services
func (s Services) HasServerStreamingMethod() bool {
for _, service := range s {
for _, method := range service.Methods {
if method.ServerStreaming {
return true
}
}
}
return false
}

// HasUnaryCallMethod indicates whether there is unary methods inside any of the services
func (s Services) HasUnaryCallMethod() bool {
for _, service := range s {
for _, method := range service.Methods {
if !method.ServerStreaming && !method.ClientStreaming {
return true
}
}
}
return false
}

// NewService returns an initialised service
func NewService() *Service {
return &Service{
Expand Down
135 changes: 131 additions & 4 deletions generator/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,12 @@ export type {{.Name}} = {
{{define "services"}}{{range .}}export class {{.Name}} {
{{- range .Methods}}
{{- if .ServerStreaming }}
static {{.Name}}(req: {{tsType .Input}}, entityNotifier?: gap.NotifyStreamEntityArrival<{{tsType .Output}}>): Promise<gap.FetchState<undefined>> {
return gap.gapFetchGRPCStream<{{tsType .Input}}, {{tsType .Output}}>("{{.URL}}", req, entityNotifier)
static {{.Name}}(req: {{tsType .Input}}, entityNotifier?: NotifyStreamEntityArrival<{{tsType .Output}}>, initReq?: InitReq): Promise<void> {
return fetchStreamingRequest<{{tsType .Input}}, {{tsType .Output}}>("{{.URL}}", req, entityNotifier, initReq)
}
{{- else }}
static {{.Name}}(req: {{tsType .Input}}): Promise<gap.FetchState<{{tsType .Output}}>> {
return gap.gapFetchGRPC<{{tsType .Input}}, {{tsType .Output}}>("{{.URL}}", req)
static {{.Name}}(req: {{tsType .Input}}, initReq?: InitReq): Promise<{{tsType .Output}}> {
return fetchReq<{{tsType .Input}}, {{tsType .Output}}>("{{.URL}}", req, initReq)
}
{{- end}}
{{- end}}
Expand All @@ -77,11 +77,137 @@ type OneOf<T> =
{{end}}
{{- if .Enums}}{{include "enums" .Enums}}{{end}}
{{- if .Messages}}{{include "messages" .Messages}}{{end}}
{{- if .Services}}{{include "comm" .Services}}{{end}}
{{- if .Services}}{{include "services" .Services}}{{end}}
`

const commTmpl = `
export interface InitReq extends RequestInit {
pathPrefix?: string
}
{{- if .HasUnaryCallMethod }}
function fetchReq<I, O>(path: string, body: I, init?: InitReq): Promise<O> {
const {pathPrefix, ...req} = init || {}
const url = pathPrefix ? ` + "`${pathPrefix}${path}`" + ` : path
const b = JSON.stringify(body)
return fetch(url, {
...req,
method: "POST",
body: b
}).then(r => r.json()) as Promise<O>
}
{{- end -}}
{{- if .HasServerStreamingMethod }}
// NotifyStreamEntityArrival is a callback that will be called on streaming entity arrival
export type NotifyStreamEntityArrival<T> = (resp: T) => void
/**
* fetchStreamingRequest is able to handle grpc-gateway server side streaming call
* it takes NotifyStreamEntityArrival that lets users respond to entity arrival during the call
* all entities will be returned as an array after the call finishes.
**/
async function fetchStreamingRequest<S, R>(path: string, body: S, callback?: NotifyStreamEntityArrival<R>, init?: InitReq) {
const {pathPrefix, ...req} = init || {}
const url = pathPrefix ?` + "`${pathPrefix}${path}`" + ` : path
const result = await fetch(url, {
...req,
method: "Post",
body: JSON.stringify(body),
})
// needs to use the .ok to check the status of HTTP status code
// http other than 200 will not throw an error, instead the .ok will become false.
// see https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#
if (!result.ok) {
const resp = await result.json()
const errMsg = resp.error && resp.error.message ? resp.error.message : ""
throw new Error(errMsg)
}
if (!result.body) {
throw new Error("response doesnt have a body")
}
await result.body
.pipeThrough(new TextDecoderStream())
.pipeThrough<R>(getNewLineDelimitedJSONDecodingStream<R>())
.pipeTo(getNotifyEntityArrivalSink((e: R) => {
if (callback) {
callback(e)
}
}))
// wait for the streaming to finish and return the success respond
return
}
/**
* JSONStringStreamController represents the transform controller that's able to transform the incoming
* new line delimited json content stream into entities and able to push the entity to the down stream
*/
interface JSONStringStreamController<T> extends TransformStreamDefaultController {
buf?: string
pos?: number
enqueue: (s: T) => void
}
/**
* getNewLineDelimitedJSONDecodingStream returns a TransformStream that's able to handle new line delimited json stream content into parsed entities
*/
function getNewLineDelimitedJSONDecodingStream<T>(): TransformStream<string, T> {
return new TransformStream({
start(controller: JSONStringStreamController<T>) {
controller.buf = ''
controller.pos = 0
},
transform(chunk: string, controller: JSONStringStreamController<T>) {
if (controller.buf === undefined) {
controller.buf = ''
}
if (controller.pos === undefined) {
controller.pos = 0
}
controller.buf += chunk
while (controller.pos < controller.buf.length) {
if (controller.buf[controller.pos] == '\n') {
const line = controller.buf.substring(0, controller.pos)
const response = JSON.parse(line)
controller.enqueue(response.result)
controller.buf = controller.buf.substring(controller.pos + 1)
controller.pos = 0
} else {
++controller.pos
}
}
}
})
}
/**
* getNotifyEntityArrivalSink takes the NotifyStreamEntityArrival callback and return
* a sink that will call the callback on entity arrival
* @param notifyCallback
*/
function getNotifyEntityArrivalSink<T>(notifyCallback: NotifyStreamEntityArrival<T>) {
return new WritableStream<T>({
write(entity: T) {
notifyCallback(entity)
}
})
}
{{- end -}}
`

// GetTemplate gets the templates to for the typescript file
func GetTemplate(r *registry.Registry) *template.Template {

t := template.New("file")
t = t.Funcs(sprig.TxtFuncMap())

Expand All @@ -93,6 +219,7 @@ func GetTemplate(r *registry.Registry) *template.Template {
})

t = template.Must(t.Parse(tmpl))
template.Must(t.New("comm").Parse(commTmpl))
return t
}

Expand Down
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ require (
github.com/Masterminds/semver v1.5.0 // indirect
github.com/Masterminds/sprig v2.22.0+incompatible
github.com/golang/protobuf v1.4.3
github.com/google/uuid v1.1.2 // indirect
github.com/grpc-ecosystem/grpc-gateway v1.15.2
github.com/huandu/xstrings v1.3.2 // indirect
github.com/imdario/mergo v0.3.11 // indirect
github.com/mitchellh/copystructure v1.0.0 // indirect
github.com/pkg/errors v0.9.1
github.com/sirupsen/logrus v1.7.0
github.com/stretchr/testify v1.6.1
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 // indirect
google.golang.org/grpc v1.33.1
google.golang.org/protobuf v1.25.0
)
Loading

0 comments on commit c767c51

Please sign in to comment.