diff --git a/.github/.goreleaser.yml b/.github/.goreleaser.yml new file mode 100644 index 00000000..5b92dc60 --- /dev/null +++ b/.github/.goreleaser.yml @@ -0,0 +1,106 @@ +# See documentation at https://goreleaser.com/customization/build. +# yaml-language-server: $schema=https://goreleaser.com/static/schema.json + +version: 2 + +project_name: webrpc + +builds: + - id: webrpc-gen + main: ./cmd/webrpc-gen + binary: webrpc-gen + goos: + - darwin + - linux + - windows + goarch: + - amd64 + - arm64 + ldflags: + - -s -w -X github.com/webrpc/webrpc.VERSION=v{{.Version}} + - id: webrpc-test + main: ./cmd/webrpc-test + binary: webrpc-test + goos: + - darwin + - linux + - windows + goarch: + - amd64 + - arm64 + ldflags: + - -s -w -X github.com/webrpc/webrpc.VERSION=v{{.Version}} + +archives: + - id: webrpc-gen + builds: + - webrpc-gen + name_template: "{{ .Binary }}.{{ .Os }}-{{ .Arch }}" + format: binary + - id: webrpc-test + builds: + - webrpc-test + name_template: "{{ .Binary }}.{{ .Os }}-{{ .Arch }}" + format: binary + +checksum: + name_template: "checksums.txt" + +release: + footer: | + ## Docker + ``` + docker pull ghcr.io/webrpc/webrpc-gen:v{{.Version}} + ``` + + Example: `docker run -v $PWD:$PWD ghcr.io/webrpc/webrpc-gen:v{{.Version}} -schema=$PWD/api.ridl -target=golang` + + ## Homebrew + ``` + brew tap webrpc/tap + brew install webrpc-gen + ``` + + ## Build from source + ``` + go install -ldflags="-s -w -X github.com/webrpc/webrpc.VERSION=v{{.Version}}" github.com/webrpc/webrpc/cmd/webrpc-gen@v{{.Version}} + ``` + + ## Download binaries + macOS: [amd64](https://github.com/webrpc/webrpc/releases/download/v{{.Version}}/webrpc-gen.darwin-amd64), [arm64](https://github.com/webrpc/webrpc/releases/download/v{{.Version}}/webrpc-gen.darwin-arm64) (Apple Silicon) + Linux: [amd64](https://github.com/webrpc/webrpc/releases/download/v{{.Version}}/webrpc-gen.linux-amd64), [arm64](https://github.com/webrpc/webrpc/releases/download/v{{.Version}}/webrpc-gen.linux-arm64) + Windows: [amd64](https://github.com/webrpc/webrpc/releases/download/v{{.Version}}/webrpc-gen.windows-amd64.exe), [arm64](https://github.com/webrpc/webrpc/releases/download/v{{.Version}}/webrpc-gen.windows-arm64.exe) + +changelog: + use: github-native + sort: asc + +brews: + - name: webrpc-gen + ids: + - webrpc-gen + repository: + owner: webrpc + name: homebrew-tap + token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}" + commit_author: + name: goreleaserbot + email: bot@goreleaser.com + commit_msg_template: "Brew formula update for {{ .ProjectName }} version {{ .Tag }}" + homepage: "https://github.com/webrpc/webrpc" + description: "generate source code for your target language from webrpc schema" + license: "MIT" + - name: webrpc-test + ids: + - webrpc-test + repository: + owner: webrpc + name: homebrew-tap + token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}" + commit_author: + name: goreleaserbot + email: bot@goreleaser.com + commit_msg_template: "Brew formula update for {{ .ProjectName }} version {{ .Tag }}" + homepage: "https://github.com/webrpc/webrpc" + description: "generate source code for your target language from webrpc schema" + license: "MIT" diff --git a/.github/workflows/cache.yml b/.github/workflows/cache.yml new file mode 100644 index 00000000..22313697 --- /dev/null +++ b/.github/workflows/cache.yml @@ -0,0 +1,17 @@ +name: Clean cache +on: + schedule: + - cron: "0 0 * * *" # every day (min hour dayOfMonth month dayOfWeek) + +jobs: + cleanup: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Cleanup + run: | + gh extension install actions/gh-actions-cache + gh actions-cache list + gh actions-cache delete webrpc-cache --confirm || true + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..2339625b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,55 @@ +name: Test + +on: + push: + branches: + - master + pull_request: + branches: + - "**" + +jobs: + test: + strategy: + matrix: + go-version: ["1.22", "1.19"] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 20 + fetch-tags: true + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: ${{ matrix.go-version }} + - name: Set up webrpc cache folder + uses: actions/cache@v4 + with: + key: webrpc-cache + path: /tmp/webrpc-cache + - name: Test + run: make test + + examples: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 20 + fetch-tags: true + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: 1.19 + - name: Set up webrpc cache folder + uses: actions/cache@v4 + with: + key: webrpc-cache + path: /tmp/webrpc-cache + - name: Build + run: make install + - name: Regenerate examples + run: make generate + - name: Git diff of regenerated examples + run: make diff diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..dbebae38 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,37 @@ +name: Release tag + +on: + push: + tags: + - "v*" + +jobs: + goreleaser: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: 1.19 + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v6 + with: + distribution: goreleaser + version: "~> v2" + args: release -f .github/.goreleaser.yml --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }} + + docker: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Log into Github registry + run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin + - name: Docker build + run: | + docker build --build-arg VERSION=${GITHUB_REF##*/} -t ghcr.io/webrpc/webrpc-gen:${GITHUB_REF##*/} -t ghcr.io/webrpc/webrpc-gen:latest . + docker push ghcr.io/webrpc/webrpc-gen:${GITHUB_REF##*/} + docker push ghcr.io/webrpc/webrpc-gen:latest diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index cc9c1905..00000000 --- a/.travis.yml +++ /dev/null @@ -1,8 +0,0 @@ -language: go - -go: - - 1.13 - -script: - - make tools - - make test diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..eb26edfb --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,398 @@ +- [webrpc v0.12.0](#webrpc-v0120) + - [Breaking change](#breaking-change) + - [Stricter json fields](#stricter-json-fields) +- [webrpc v0.11.0](#webrpc-v0110) + - [Feature: Define webrpc schema errors](#feature-define-webrpc-schema-errors) + - [typescript@v0.11.0 breaking changes](#typescriptv0110-breaking-changes) + - [golang@v0.11.0 breaking changes](#golangv0110-breaking-changes) +- [webrpc v0.10.0](#webrpc-v0100) + - [Interoperability tests](#interoperability-tests) + - [Breaking changes in webrpc-gen Go API](#breaking-changes-in-webrpc-gen-go-api) +- [webrpc v0.9.0](#webrpc-v090) + - [Breaking changes](#breaking-changes) + - [RIDL v0.9.0 changes](#ridl-v090-changes) + - [JSON schema v0.9.0 changes](#json-schema-v090-changes) + - [Template changes](#template-changes) + - [Migration guide](#migration-guide) + - [RIDL v0.9.0 migration guide](#ridl-v090-migration-guide) + - [JSON schema v0.9.0 migration guide](#json-schema-v090-migration-guide) + - [Generator templates v0.9.0 migration guide](#generator-templates-v090-migration-guide) + +# webrpc v0.12.0 + +## Breaking change + +Go users are expected to use `go.tag.json` in RIDL instead of `json` for special tags like `,omitempty` or `,string`. +```diff + - FieldName +- + json = field_name,omitempty ++ + json = field_name ++ + go.tag.json = field_name,omitempty +``` + +## Stricter json fields + +Fixes #66 duplicate or invalid json fields (#218). + +The following invalid/duplicate fields (in JSON) will now error out: +``` +struct Simple + - Field1: string + + json = _invalid +``` + +``` +struct Simple + - Field1: string + + json = field_1 + - Field2: string + + json = field_1 +``` + +``` +struct Simple + - Field1: string + - Field2: string + + json = Field1 +``` + +# webrpc v0.11.0 + +## Feature: Define webrpc schema errors + +You can now define your own custom schema errors in RIDL file, for example: + +```ridl +error 1 Unauthorized "unauthorized" HTTP 401 +error 2 ExpiredToken "expired token" HTTP 401 +error 3 InvalidToken "invalid token" HTTP 401 +error 4 Deactivated "account deactivated" HTTP 403 +error 5 ConfirmAccount "confirm your email" HTTP 403 +error 6 AccessDenied "access denied" HTTP 403 +error 7 MissingArgument "missing argument" HTTP 400 +error 8 UnexpectedValue "unexpected value" HTTP 400 +error 100 RateLimited "too many requests" HTTP 429 +error 101 DatabaseDown "service outage" HTTP 503 +error 102 ElasticDown "search is degraded" HTTP 503 +error 103 NotImplemented "not implemented" HTTP 501 +error 200 UserNotFound "user not found" +error 201 UserBusy "user busy" +error 202 InvalidUsername "invalid username" +error 300 FileTooBig "file is too big (max 1GB)" +error 301 FileInfected "file is infected" +error 302 FileType "unsupported file type" +``` + +Note: Unless specified, the default HTTP status for webrpc errors is `HTTP 400`. + +## typescript@v0.11.0 breaking changes + +- All errors thrown by webrpc client are now instance of `WebrpcError`, which extends JavaScript `Error`. No need to re-throw errors anymore. +- ~`error.msg`~ `error.message` +- by default, the error messages are "human-friendly", they don't contain any details about the backend error cause +- underlying backend error (for developers) is optionally available as `error.cause?` +- `error.code` or `error.message` can be used as input for user-friendly error i18n translations + +You can now check for explicit error class instance (as defined in RIDL schema) or against a generic `WebrpcError` class. + +```js +try { + const resp = await testApiClient.getUser(); + // setUser(resp.user) +} catch (error) { + if (error instanceof RateLimitedError) { + // retry with back-off time + } + + if (error instanceof UnauthorizedError) { + // render sign-in page + } + + if (error instanceof WebrpcError) { + console.log(error.status) // print response HTTP status code (ie. 4xx or 5xx) + console.log(error.code) // print unique schema error code; generic endpoint errors are 0 + console.log(error.message) // print error message + console.log(error.cause) // print the underlying backend error -- ie. "DB error" - useful for debugging / reporting to Sentry + } + + // setError(error.message) +} +``` + +## golang@v0.11.0 breaking changes + +Note: You can turn on `-legacyErrors=true` flag on golang generator (ie. `webrpc-gen -target=golang -legacyErrors=true -pkg=proto`) in order to preserve the deprecated functions and sentinel errors (see below). This will allow you to migrate your codebase to the new custom schema errors gradually. + +The following werbrpc error functions and sentinel errors are now deprecated or ~removed~: +- `proto.WrapError() // Deprecated.` +- `proto.Errorf() // Deprecated.` +- ~`proto.HTTPStatusFromErrorCode()`~ +- ~`proto.IsErrorCode()`~ +- `proto.ErrCanceled // Deprecated.` +- `proto.ErrUnknown // Deprecated.` +- `proto.ErrFail // Deprecated.` +- `proto.ErrInvalidArgument // Deprecated.` +- `proto.ErrDeadlineExceeded // Deprecated.` +- `proto.ErrNotFound // Deprecated.` +- `proto.ErrBadRoute // Deprecated.` +- `proto.ErrAlreadyExists // Deprecated.` +- `proto.ErrPermissionDenied // Deprecated.` +- `proto.ErrUnauthenticated // Deprecated.` +- `proto.ErrResourceExhausted // Deprecated.` +- `proto.ErrFailedPrecondition // Deprecated.` +- `proto.ErrAborted // Deprecated.` +- `proto.ErrOutOfRange // Deprecated.` +- `proto.ErrUnimplemented // Deprecated.` +- `proto.ErrInternal // Deprecated.` +- `proto.ErrUnavailable // Deprecated.` +- `proto.ErrDataLoss // Deprecated.` +- `proto.ErrNone // Deprecated.` + +The schema errors can now be returned from the RPC endpoints via: + +```diff +func (s *RPC) RemoveUser(ctx context.Context, userID int64) (bool, error) { + r, _ := ctx.Value(proto.HTTPRequestCtxKey).(*http.Request) + if s.IsRateLimited(r) { +- return false, proto.Errorf(proto.ErrUnavailable, "rate limited") ++ return false, proto.ErrRateLimited // HTTP 429 per RIDL schema + } + + _, err := s.DB.RemoveUser(ctx, userID) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { +- return false, proto.Errorf(proto.ErrNotFound, "no such user(%v)", userID) ++ return false, proto.ErrUserNotFound + } +- return false, proto.WrapError(proto.ErrInternal, err, "") ++ return false, proto.ErrorWithCause(proto.ErrDatabaseDown, err) + } + + return true, nil +} +``` + +You can also return any other Go error and webrpc will render generic `proto.ErrWebrpcEndpoint` error automatically along with `HTTP 400` status code. +```go +return fmt.Errorf("some error") +``` + +The RPC client(s) can now assert the schema error type by their unique error code: + +```go +if err, ok := rpc.RemoveUser(ctx, userID); err != nil { + if errors.Is(proto.ErrRateLimited) { + // slow down; retry with back-off strategy + } + if errors.Is(proto.ErrUserNotFound) { + // handle + } + // etc. +} +``` + +# webrpc v0.10.0 + +## Interoperability tests + +We have defined a new interoperability test suite implementing the following schema: + +``` +webrpc = v1 + +name = Test +version = v0.10.0 + +service TestApi + - GetEmpty() + - GetError() + + - GetOne() => (one: Simple) + - SendOne(one: Simple) + + - GetMulti() => (one: Simple, two: Simple, three: Simple) + - SendMulti(one: Simple, two: Simple, three: Simple) + + - GetComplex() => (complex: Complex) + - SendComplex(complex: Complex) +``` + +All generators are expected to implement [TestApi schema](./tests/schema/test.ridl) and run client/server interoperability tests against a reference [webrpc-test binaries)](https://github.com/webrpc/webrpc/releases). + +For more info, see [typescript](https://github.com/webrpc/gen-typescript/tree/master/tests) or [golang](https://github.com/webrpc/gen-golang/tree/master/tests) tests. + +## Breaking changes in webrpc-gen Go API + +```diff +-func NewParser(r *schema.Reader) *Parser ++func NewParser(fsys fs.FS, path string) *Parser +``` + +```diff +- func NewTemplateSource(proto *schema.WebRPCSchema, target string, config *Config) (*TemplateSource, error) ++ func NewTemplateSource(target string, config *Config) (*TemplateSource, error) +``` + +# webrpc v0.9.0 + +Towards reaching webrpc@v1.0.0, we have decided to make some breaking changes to webrpc schema and RIDL file format. + +## Breaking changes + +### RIDL v0.9.0 changes + +Keyword "message" was renamed to "struct". + +```diff + webrpc = v1 + + name = your-app + version = v0.1.0 + +- message User ++ struct User + - id: uint64 + - username: string + - createdAt?: timestamp +``` + +### JSON schema v0.9.0 changes + +- Field "messages" was renamed to "types" +- Field "type" was renamed to "kind" +- Enum type was moved from enum.fields[] to enum object + +```diff + { + "webrpc": "v1", + "name": "Test", + "version": "v0.0.1", + "imports": [], +- "messages": [ ++ "types": [ + { + "name": "Status", +- "type": "enum", ++ "kind": "enum", ++ "type": "uint32", + "fields": [ + { + "name": "AVAILABLE", +- "type": "uint32", +- "optional": false, + "value": "0", +- "meta": null + }, + { + "name": "NOT_AVAILABLE", +- "type": "uint32", +- "optional": false, + "value": "1", +- "meta": null + } + }, + { + "name": "Empty", +- "type": "struct", ++ "kind": "struct", +- "fields": null + }] + } +``` + +### Template changes + +You might see the following error when running your webrpc generator templates against webrpc-gen@v0.9.0+: + +``` +template: main.go.tmpl:88:57: executing "main" at <.Messages>: can't evaluate field Messages in type struct { *schema.WebRPCSchema; SchemaHash string; WebrpcGenVersion string; WebrpcGenCommand string; WebrpcTarget string; Opts map[string]interface {} } +``` + +To fix this, rename `{{.Messages}}` variable to `{{.Types}}` in your `*.go.tmpl` template files. + +## Migration guide + +### RIDL v0.9.0 migration guide + +Run this command to migrate your RIDL files to webrpc@v0.9.0+: + +```bash +#!/bin/bash + +find . -name '*.ridl' -exec sed -i -e 's/^message /struct /g' {} \; +``` + +### JSON schema v0.9.0 migration guide + +Run this Node.js script to migrate your `*webrpc.json` schema files to webrpc@v0.9.0+: + +`node migrate.js schema.webrpc.json` + +Contents of `migrate.js` file: +```javascript +const fs = require("fs"); + +if (process.argv.length != 3) { + throw Error(`Usage: node ${process.argv[1]} `); +} + +const filePath = process.argv[2]; + +console.log(filePath); + +fs.readFile(filePath, "utf8", (e, data) => { + if (e) { + throw e; + } + + let schema = JSON.parse(data); + schema = { + webrpc: schema.webrpc, + name: schema.name, + version: schema.version, + types: schema.messages.map((orig) => { + let type = { + name: orig.name, + kind: orig.type, + fields: orig.fields, + }; + + if (type.kind == "enum") { + type = { + name: orig.name, + kind: orig.type, + type: orig.fields[0].type, + fields: orig.fields.map((field) => { + return { name: field.name, value: field.value }; + }), + }; + } + return type; + }), + services: schema.services, + }; + + schema.types = fs.writeFile( + filePath, + JSON.stringify(schema, null, "\t"), + (err) => { + if (err) { + console.error(err); + console.log(schema); + } + } + ); +}); +``` + +### Generator templates v0.9.0 migration guide + +Run this command to migrate your `.go.tmpl` templates to webrpc@v0.9.0+: + +```bash +#!/bin/bash + +find . -name '*.go.tmpl' -exec sed -i -e 's/\.Messages/.Types/g' {} \; +find . -name '*.go.tmpl' -exec sed -i -e 's/"Messages"/"Types"/g' {} \; +``` + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..7cd190f8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +# ----------------------------------------------------------------- +# Builder +# ----------------------------------------------------------------- +FROM golang:1.19-alpine3.16 as builder +ARG VERSION + +RUN apk add --update git + +ADD ./ /src + +WORKDIR /src +RUN go build -ldflags="-s -w -X github.com/webrpc/webrpc.VERSION=${VERSION}" -o /usr/bin/webrpc-gen ./cmd/webrpc-gen + +# ----------------------------------------------------------------- +# Runner +# ----------------------------------------------------------------- +FROM alpine:3.16 + +ENV TZ=UTC + +RUN apk add --no-cache --update ca-certificates + +COPY --from=builder /usr/bin/webrpc-gen /usr/bin/ + +ENTRYPOINT ["/usr/bin/webrpc-gen"] diff --git a/Makefile b/Makefile index 67c799de..ba447a62 100644 --- a/Makefile +++ b/Makefile @@ -1,48 +1,63 @@ +export PATH = $(shell echo $$PWD/bin:$$PATH) + all: - @echo "*****************************************" - @echo "** WebRPC Dev **" - @echo "*****************************************" + @echo "****************************************" + @echo "** webrpc **" + @echo "****************************************" @echo "make " @echo "" @echo "commands:" - @echo "" - @echo " + Testing:" - @echo " - test" - @echo "" - @echo " + Builds:" - @echo " - build" - @echo " - clean" - @echo " - generate" - @echo "" - @echo " + Dep management:" - @echo " - dep" - @echo " - dep-upgrade-all" - @echo "" - -tools: - GO111MODULE=off go get -u github.com/goware/statik + @awk -F'[ :]' '/^#+/ {comment=$$0; gsub(/^#+[ ]*/, "", comment)} !/^(_|all:)/ && /^([A-Za-z_-]+):/ && !seen[$$1]++ {printf " %-24s %s\n", $$1, (comment ? "- " comment : ""); comment=""} !/^#+/ {comment=""}' Makefile +# Build webrpc-gen build: - go generate ./gen/... - go build -o ./bin/webrpc-gen ./cmd/webrpc-gen - go generate ./... + go build -ldflags="-s -w -X github.com/webrpc/webrpc.VERSION=$$(git describe --tags)" -o ./bin/webrpc-gen ./cmd/webrpc-gen + +# Build webrpc-test +build-test: + go build -ldflags="-s -w -X github.com/webrpc/webrpc.VERSION=$$(git describe --tags)" -o ./bin/webrpc-test ./cmd/webrpc-test + +# Install webrpc-gen and webrpc-test binaries locally +install: + go install -ldflags="-s -w -X github.com/webrpc/webrpc.VERSION=$$(git describe --tags)" ./cmd/webrpc-gen + go install -ldflags="-s -w -X github.com/webrpc/webrpc.VERSION=$$(git describe --tags)" ./cmd/webrpc-test clean: rm -rf ./bin -install: build - go install ./cmd/webrpc-gen - -test: generate - go test -v ./... +# Regenerate examples and tests using latest templates (see go.mod) +generate: build + go generate -v -x ./... + cd _examples/ && go generate -x ./... + @for i in _examples/*/Makefile; do \ + echo; echo $$ cd $$(dirname $$i) \&\& make generate; \ + cd $$(dirname $$i); \ + make generate || exit 1; \ + cd ../../; \ + done + # Replace webrpc version in all generated files to avoid git conflicts. + git grep -l "$$(git describe --tags)" | xargs sed -i -e "s/@$$(git describe --tags)//g" + sed -i "/$$(git describe --tags)/d" tests/schema/test.debug.gen.txt + +# Upgrade Go dependencies +dep-upgrade-all: + go get -u go@1.19 ./... -generate: - go generate ./... +# Run git diff and fail on any local changes +diff: + git diff --color --ignore-all-space --ignore-blank-lines --exit-code -dep: - @export GO111MODULE=on && go mod tidy +# Run all tests +test: test-go test-interoperability -dep-upgrade-all: - @GO111MODULE=on go get -u - @$(MAKE) dep +# Run Go tests +test-go: generate + go test -v ./... +# Run interoperability test suite +test-interoperability: build-test + echo "Running interoperability test suite"; \ + ./bin/webrpc-test -server -port=9988 -timeout=2s & \ + until nc -z localhost 9988; do sleep 0.1; done; \ + ./bin/webrpc-test -client -url=http://localhost:9988; \ + wait diff --git a/README.md b/README.md index 1226241c..37b2904c 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,58 @@ webrpc webrpc is a schema-driven approach to writing backend servers for the Web. Write your server's -api interface in a schema format of [RIDL](./_examples/golang-basics/example.ridl) or [JSON](./_examples/golang-basics/example.webrpc.json), +API interface in a schema format of [RIDL](./_examples/golang-basics/example.ridl) or [JSON](./_examples/golang-basics/example.webrpc.json), and then run `webrpc-gen` to generate the networking source code for your server and client apps. From the schema, -webrpc-gen will generate application base class types/interfaces, JSON encoders, and networking code. In doing +`webrpc-gen` will generate application base class types/interfaces, JSON encoders, and networking code. In doing so, it's able to generate fully functioning and typed client libraries to communicate with your server. Enjoy strongly-typed Web services and never having to write an API client library again. Under the hood, webrpc is a Web service meta-protocol, schema and code-generator tool for simplifying the development of backend services for modern Web applications. -Current code-generation language targets: -* [Go](./gen/golang) -* [Typescript](./gen/typescript) -* [Javascript](./gen/javascript) -* .. contribute more! they're just templates +- [Getting started](#getting-started) +- [Code generators](#code-generators) +- [Quick example](#quick-example) + - [Example apps](#example-apps) +- [Why](#why) +- [Design / architecture](#design--architecture) +- [Schema](#schema) +- [Development](#development) + - [Building from source](#building-from-source) + - [Writing your own code-generator](#writing-your-own-code-generator) + - [Format ridl files](#format-ridl) +- [Authors](#authors) +- [Credits](#credits) +- [License](#license) + +# Getting started + +1. Install [webrpc-gen](https://github.com/webrpc/webrpc/releases) +2. Write+design a [webrpc schema file](./_examples/golang-basics/example.ridl) for your Web service +3. Run the code-generator to create your server interface and client, ie. + - `webrpc-gen -schema=example.ridl -target=golang -pkg=service -server -client -out=./service/proto.gen.go` + - `webrpc-gen -schema=example.ridl -target=typescript -client -out=./web/client.ts` +4. Implement the handlers for your server -- of course, it can't guess the server logic :) + +another option is to copy the [hello-webrpc](./_examples/hello-webrpc) example, and adapt for your own webapp and server. + +Btw, check out https://marketplace.visualstudio.com/items?itemName=XanderAppWorks.vscode-webrpc-ridl-syntax for VSCode +plugin for RIDL synx highlighting. + +# Code generators +| Generator | Description | Schema | Client | Server | +|--------------------------------------------------------|-----------------------------------|--------|--------|--------| +| [golang](https://github.com/webrpc/gen-golang) | Go 1.16+ | v1 | ✅ | ✅ | +| [typescript](https://github.com/webrpc/gen-typescript) | TypeScript | v1 | ✅ | ✅ | +| [javascript](https://github.com/webrpc/gen-javascript) | JavaScript (ES6) | v1 | ✅ | ✅ | +| [kotlin](https://github.com/webrpc/gen-kotlin) | Kotlin (coroutines, moshi, ktor) | v1 | ✅ | | +| [dart](https://github.com/webrpc/gen-dart) | Dart 3.1+ | v1 | ✅ | | +| [openapi](https://github.com/webrpc/gen-openapi) | OpenAPI 3.x (Swagger) | v1 | ✅ [*](https://github.com/swagger-api/swagger-codegen#overview) | ✅ [*](https://github.com/swagger-api/swagger-codegen#overview) | -## Quick example +..contribute more! [webrpc generators](./gen/) are just Go templates (similar to [Hugo](https://gohugo.io/templates/) or [Helm](https://helm.sh/docs/chart_best_practices/templates/)). + +# Quick example Here is an example webrpc schema in RIDL format (a new documentation-like format introduced by webrpc) @@ -27,12 +62,12 @@ webrpc = v1 name = your-app version = v0.1.0 -message User +struct User - id: uint64 - username: string - createdAt?: timestamp -message UsersQueryFilter +struct UsersQueryFilter - page?: uint32 - name?: string - location?: string @@ -43,38 +78,48 @@ service ExampleService - GetUserByID(userID: uint64) => (user: User) - IsOnline(user: User) => (online: bool) - ListUsers(q?: UsersQueryFilter) => (page: uint32, users: []User) -``` -WebRPC is a design/schema-driven approach to writing backend servers. Write your server's -api interface in a schema format of [RIDL](./_examples/golang-basics/example.ridl) or -[JSON](./_examples/golang-basics/example.webrpc.json) format and run `webrpc-gen` to generate -source code for your target language. +error 100 RateLimited "too many requests" HTTP 429 +error 101 DatabaseDown "service outage" HTTP 503 +``` -For example, to generate webrpc server+client code -- run: +Generate webrpc Go server+client code: ``` -bin/webrpc-gen -schema=example.ridl -target=go -pkg=main -server -client -out=./example.gen.go +webrpc-gen -schema=example.ridl -target=golang -pkg=main -server -client -out=./example.gen.go ``` and see the generated `./example.gen.go` file of types, server and client in Go. This is essentially -how the `golang-basics` example was built. +how the [golang-basics](./_examples/golang-basics) example was built. + +## Example apps -### More example apps +| Example | Description | +|------------------------------------------------|-----------------------------------------------| +| [hello-webrpc](./_examples/hello-webrpc) | Go server <=> Javascript webapp | +| [hello-webrpc-ts](./_examples/hello-webrpc-ts) | Go server <=> Typescript webapp | +| [golang-basics](./_examples/golang-basics) | Go server <=> Go client | +| [golang-nodejs](./_examples/golang-nodejs) | Go server <=> Node.js (Javascript ES6) client | +| [node-ts](./_examples/node-ts) | Node.js server <=> Typescript webapp client | -* [hello-webrpc](./_examples/hello-webrpc) - webrpc service with Go server and Javascript webapp -* [hello-webrpc-ts](./_examples/hello-webrpc-ts) - webrpc service with Go server and Typescript webapp -* [golang-basics](./_examples/golang-basics) - webrpc service with Go server and Go client -* [golang-nodejs](./_examples/golang-nodejs) - webrpc service with Go server and nodejs (Javascript ES6) client -* [node-ts](./_examples/node-ts) - webrpc service with nodejs server and Typescript webapp client +# Why +**TLDR;** it's much simpler + faster to write and consume a webrpc service than traditional approaches +like a REST API or gRPC service. -## Why + 1. Code-generate your client libraries in full -- never write another API client again + 2. Compatible with the Web. A Webrpc server is just a HTTP/HTTPS server that speaks JSON, and thus + all existing browsers, http clients, load balancers, proxies, caches, and tools work + out of the box (versus gRPC). cURL "just works". + 3. Be more productive, write more correct systems. -Writing a Web service / microservice takes a lot of work and time. There are many pieces -to build -- designing the routes of your service, agreeing on conventions for the routes -with your team, the request payloads, the response payloads, writing the actual server logic, -routing the methods and requests to the server handlers, implementing the handlers, and +--- + +Writing a Web service / microservice takes a lot of work and time. REST is making me tired. +There are many pieces to build -- designing the routes of your service, agreeing on conventions +for the routes with your team, the request payloads, the response payloads, writing the actual server +logic, routing the methods and requests to the server handlers, implementing the handlers, and then writing a client library for your desired language so it can speak to your Web service. Yikes, it's a lot of work. Want to add an additional field or handler? yea, you have to go through the entire cycle. And what about type-safety across the wire? @@ -86,7 +131,7 @@ you can use the `webrpc-gen` cli to generate source code for: * Complete client library to communicate with the web service -## Design / architecture +# Design / architecture webrpc services speak JSON, as our goals are to build services that communicate with webapps. We optimize for developer experience, ease of use and productivity when building backends @@ -96,7 +141,7 @@ of cases that this would be a bottleneck or costly tradeoff. webrpc is heavily inspired by gRPC and Twirp. It is architecturally the same and has a similar workflow, but simpler. In fact, the webrpc schema is similar in design to protobuf, as -in we have messages and rpc methods, but the type system is arguably more flexible and +in we have messages (structs) and RPC methods, but the type system is arguably more flexible and code-gen tooling is simpler. The [webrpc schema](./schema/README.md) is a documentation-like language for describing a server's api interface and the type system within is inspired by Go, Typescript and WASM. @@ -134,20 +179,7 @@ Future goals/work: 1. Add RPC streaming support for client/server 2. More code generators.. for Rust, Python, .. - -## Getting started - -1. `go get -u github.com/webrpc/webrpc/cmd/webrpc-gen` -2. Write+design a [webrpc schema file](./_examples/golang-basics/example.ridl) for your Web service -3. Run the code-generator to create your server interface and client, ie. - * `webrpc-gen -schema=example.ridl -target=go -pkg=service -server -client -out=./service/proto.gen.go` - * `webrpc-gen -schema=example.ridl -target=ts -pkg=client -client -out=./web/client.ts` -4. Implement the handlers for your server -- of course, it can't guess the server logic :) - -another option is to copy the [hello-webrpc](./_examples/hello-webrpc) example, and adapt for your own webapp and server. - - -## Schema +# Schema The webrpc schema type system is inspired by Go and TypeScript, and is simple and flexible enough to cover the wide variety of language targets, designed to target RPC communication with Web @@ -169,34 +201,34 @@ High-level features: For more information please see the [schema readme](./schema/README.md). -## Building from source / making your own code-generator +# Development -### Dev +## Building from source -1. Install Go 1.11+ -2. $ `go get -u github.com/webrpc/webrpc/...` -3. $ `make tools` -4. $ `make build` -5. $ `make test` -6. $ `go install ./cmd/webrpc-gen` +1. Install Go 1.16+ +2. $ `make build` +3. $ `make test` +4. $ `make install` -### Writing your own code-generator +## Writing your own code-generator -Some tips.. +See [webrpc-gen documentation](./gen). -1. Copy `gen/golang` to `gen/` and start writing templates -2. Write an example service and use `make build` to regenerate -3. Write tests, TDD is a great approach to confirm things work +## Format ridl +- Use [Ridlfmt](https://github.com/webrpc/ridlfmt) +- Supports same arguments as `gofmt` +- See: [Example](https://github.com/webrpc/webrpc/blob/master/_examples/golang-basics/Makefile#L4) - -## Authors +# Authors * [Peter Kieltyka](https://github.com/pkieltyka) * [José Carlos Nieto](https://github.com/xiam) +* [Vojtech Vitek](https://github.com/VojtechVitek) * ..and full list of [contributors](https://github.com/webrpc/webrpc/graphs/contributors)! -## Credits + +# Credits * [Twirp authors](https://github.com/twitchtv/twirp) for making twirp. Much of the webrpc-go library comes from the twirp project. @@ -204,16 +236,6 @@ library comes from the twirp project. for code-generating the bindings between client and server from a common IDL. -## We're hiring! - -Our team at https://horizon.io is building [Arcadeum.net](https://arcadeum.net), a distributed -network and platform for blockchain based video games :) built for Ethereum. - -If you're passionate about distributed systems, cryptography, privacy, and -writing awesome network infrastructure to help power the Arcadeum network, please -write to us, hello at arcadeum.net - - -## License +# License MIT diff --git a/_examples/golang-basics/Makefile b/_examples/golang-basics/Makefile new file mode 100644 index 00000000..548f235c --- /dev/null +++ b/_examples/golang-basics/Makefile @@ -0,0 +1,20 @@ +all: + @awk -F'[ :]' '!/^all:/ && /^([A-z_-]+):/ {print "make " $$1}' Makefile + +format: + go run github.com/webrpc/ridlfmt@v0.2.0 -w example.ridl + +generate: + webrpc-gen -schema=example.ridl -target=golang -pkg=main -server -client -service="ExampleService" -out=./example.gen.go + webrpc-gen -schema=example.ridl -target=golang -pkg=admin -server -client -service="AdminService" -ignore="@deprecated" -out=./admin/admin.gen.go + webrpc-gen -schema=example.ridl -target=golang -pkg=internal -server -client -service="ExampleService,AdminService" -ignore="@public,@deprecated" -out=./internal/internal.gen.go + +dev-generate: + ../../bin/webrpc-gen -schema=example.ridl -target=golang -pkg=main -server -client -service="ExampleService" -out=./example.gen.go + ../../bin/webrpc-gen -schema=example.ridl -target=golang -pkg=admin -server -client -service="AdminService" -ignore="@deprecated" -out=./admin/admin.gen.go + ../../bin/webrpc-gen -schema=example.ridl -target=golang -pkg=internal -server -client -service="ExampleService,AdminService" -ignore="@public,@deprecated" -out=./internal/internal.gen.go + +dev-generate-local-templates: + ../../bin/webrpc-gen -schema=example.ridl -target=golang -pkg=main -server -client -service="ExampleService" -ignore="@deprecated" -out=./example.gen.go + ../../bin/webrpc-gen -schema=example.ridl -target=golang -pkg=admin -server -client -service="AdminService" -ignore="@deprecated" -out=./admin/admin.gen.go + ../../bin/webrpc-gen -schema=example.ridl -target=golang -pkg=internal -server -client -service="ExampleService,AdminService" -ignore="@public,@deprecated" -out=./internal/internal.gen.go diff --git a/_examples/golang-basics/README.md b/_examples/golang-basics/README.md index c572435a..f5b6490f 100644 --- a/_examples/golang-basics/README.md +++ b/_examples/golang-basics/README.md @@ -13,18 +13,18 @@ you can also write your schema in JSON format like so, [./example.webrpc.json](. 2. Design your schema file and think about the methods calls clients will need to make to your service 3. Write the "services" section of the schema file -4. From the inputs and outputs for the function definitions, start writing the "messages" +4. From the inputs and outputs for the function definitions, start writing the "structs" section of the data types needed in your program. 5. Run the code generator to build the server and client: - * `webrpc-gen -schema=example.ridl -target=go -pkg=main -server -client -out=./example.gen.go` - * or... * `webrpc-gen -schema=example.webrpc.json -target=go -pkg=main -server -client -out=./example.gen.go` + * `webrpc-gen -schema=example.ridl -target=golang -pkg=main -server -client -out=./example.gen.go` + * or... * `webrpc-gen -schema=example.webrpc.json -target=golang -pkg=main -server -client -out=./example.gen.go` * however, in this example we put it inside a `go:generate`, so you can run `go generate .` 6. Write your server ([./main.go](./main.go)) and implement the `ExampleServiceRPC` interface type that was created by the code generator, and located in the [gen'd file](./example.gen.go). 7. Enjoy! Next steps, you can generate a Typescript client by running: -* `webrpc-gen -schema=example.ridl -target=ts -pkg=example -client -out=./example-client.ts` +* `webrpc-gen -schema=example.ridl -target=typescript -client -out=./example-client.ts` * check out the [hello-webrpc](../hello-webrpc) for an example with a Webapp client talking to a webrpc backend diff --git a/_examples/golang-basics/admin/admin.gen.go b/_examples/golang-basics/admin/admin.gen.go new file mode 100644 index 00000000..89610d24 --- /dev/null +++ b/_examples/golang-basics/admin/admin.gen.go @@ -0,0 +1,781 @@ +// example v0.0.1 bda7f73f32984f82d76cced03f4577a31dbcb1a6 +// -- +// Code generated by webrpc-gen with golang generator. DO NOT EDIT. +// +// webrpc-gen -schema=example.ridl -target=golang -pkg=admin -server -client -service=AdminService -ignore=@deprecated -out=./admin/admin.gen.go +package admin + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "strings" +) + +const WebrpcHeader = "Webrpc" + +const WebrpcHeaderValue = "webrpc;gen-golang@v0.17.0;example@v0.0.1" + +// WebRPC description and code-gen version +func WebRPCVersion() string { + return "v1" +} + +// Schema version of your RIDL schema +func WebRPCSchemaVersion() string { + return "v0.0.1" +} + +// Schema hash generated from your RIDL schema +func WebRPCSchemaHash() string { + return "bda7f73f32984f82d76cced03f4577a31dbcb1a6" +} + +type WebrpcGenVersions struct { + WebrpcGenVersion string + CodeGenName string + CodeGenVersion string + SchemaName string + SchemaVersion string +} + +func VersionFromHeader(h http.Header) (*WebrpcGenVersions, error) { + if h.Get(WebrpcHeader) == "" { + return nil, fmt.Errorf("header is empty or missing") + } + + versions, err := parseWebrpcGenVersions(h.Get(WebrpcHeader)) + if err != nil { + return nil, fmt.Errorf("webrpc header is invalid: %w", err) + } + + return versions, nil +} + +func parseWebrpcGenVersions(header string) (*WebrpcGenVersions, error) { + versions := strings.Split(header, ";") + if len(versions) < 3 { + return nil, fmt.Errorf("expected at least 3 parts while parsing webrpc header: %v", header) + } + + _, webrpcGenVersion, ok := strings.Cut(versions[0], "@") + if !ok { + return nil, fmt.Errorf("webrpc gen version could not be parsed from: %s", versions[0]) + } + + tmplTarget, tmplVersion, ok := strings.Cut(versions[1], "@") + if !ok { + return nil, fmt.Errorf("tmplTarget and tmplVersion could not be parsed from: %s", versions[1]) + } + + schemaName, schemaVersion, ok := strings.Cut(versions[2], "@") + if !ok { + return nil, fmt.Errorf("schema name and schema version could not be parsed from: %s", versions[2]) + } + + return &WebrpcGenVersions{ + WebrpcGenVersion: webrpcGenVersion, + CodeGenName: tmplTarget, + CodeGenVersion: tmplVersion, + SchemaName: schemaName, + SchemaVersion: schemaVersion, + }, nil +} + +// +// Common types +// + +type Kind uint32 + +const ( + Kind_USER Kind = 0 + Kind_ADMIN Kind = 1 +) + +var Kind_name = map[uint32]string{ + 0: "USER", + 1: "ADMIN", +} + +var Kind_value = map[string]uint32{ + "USER": 0, + "ADMIN": 1, +} + +func (x Kind) String() string { + return Kind_name[uint32(x)] +} + +func (x Kind) MarshalText() ([]byte, error) { + return []byte(Kind_name[uint32(x)]), nil +} + +func (x *Kind) UnmarshalText(b []byte) error { + *x = Kind(Kind_value[string(b)]) + return nil +} + +func (x *Kind) Is(values ...Kind) bool { + if x == nil { + return false + } + for _, v := range values { + if *x == v { + return true + } + } + return false +} + +type Intent string + +const ( + Intent_openSession Intent = "openSession" + Intent_closeSession Intent = "closeSession" + Intent_validateSession Intent = "validateSession" +) + +func (x Intent) MarshalText() ([]byte, error) { + return []byte(x), nil +} + +func (x *Intent) UnmarshalText(b []byte) error { + *x = Intent(string(b)) + return nil +} + +func (x *Intent) Is(values ...Intent) bool { + if x == nil { + return false + } + for _, v := range values { + if *x == v { + return true + } + } + return false +} + +type Empty struct { +} + +// User struct +// +// More information about user struct +type User struct { + ID uint64 `json:"id" db:"id"` + Username string `json:"USERNAME" db:"username"` + Role string `json:"role" db:"-"` + Kind Kind `json:"kind"` + Intent Intent `json:"intent"` +} + +type SearchFilter struct { + Q string `json:"q"` +} + +type Version struct { + WebrpcVersion string `json:"webrpcVersion"` + SchemaVersion string `json:"schemaVersion"` + SchemaHash string `json:"schemaHash"` +} + +type ComplexType struct { + Meta map[string]interface{} `json:"meta"` + MetaNestedExample map[string]map[string]uint32 `json:"metaNestedExample"` + NamesList []string `json:"namesList"` + NumsList []int64 `json:"numsList"` + DoubleArray [][]string `json:"doubleArray"` + ListOfMaps []map[string]uint32 `json:"listOfMaps"` + ListOfUsers []*User `json:"listOfUsers"` + MapOfUsers map[string]*User `json:"mapOfUsers"` + User *User `json:"user"` +} + +var methods = map[string]method{ + "/rpc/AdminService/Auth": { + Name: "Auth", + Service: "AdminService", + Annotations: map[string]string{"public": ""}, + }, + "/rpc/AdminService/Status": { + Name: "Status", + Service: "AdminService", + Annotations: map[string]string{"internal": ""}, + }, + "/rpc/AdminService/Version": { + Name: "Version", + Service: "AdminService", + Annotations: map[string]string{"internal": ""}, + }, +} + +func WebrpcMethods() map[string]method { + res := make(map[string]method, len(methods)) + for k, v := range methods { + res[k] = v + } + + return res +} + +var WebRPCServices = map[string][]string{ + "AdminService": { + "Auth", + "Status", + "Version", + }, +} + +// +// Server types +// + +type AdminService interface { + Auth(ctx context.Context) (string, string, error) + Status(ctx context.Context) (bool, error) + Version(ctx context.Context) (*Version, error) +} + +// +// Client types +// + +type AdminServiceClient interface { + Auth(ctx context.Context) (string, string, error) + Status(ctx context.Context) (bool, error) + Version(ctx context.Context) (*Version, error) +} + +// +// Server +// + +type WebRPCServer interface { + http.Handler +} + +type adminServiceServer struct { + AdminService + OnError func(r *http.Request, rpcErr *WebRPCError) + OnRequest func(w http.ResponseWriter, r *http.Request) error +} + +func NewAdminServiceServer(svc AdminService) *adminServiceServer { + return &adminServiceServer{ + AdminService: svc, + } +} + +func (s *adminServiceServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { + defer func() { + // In case of a panic, serve a HTTP 500 error and then panic. + if rr := recover(); rr != nil { + s.sendErrorJSON(w, r, ErrWebrpcServerPanic.WithCausef("%v", rr)) + panic(rr) + } + }() + + w.Header().Set(WebrpcHeader, WebrpcHeaderValue) + + ctx := r.Context() + ctx = context.WithValue(ctx, HTTPResponseWriterCtxKey, w) + ctx = context.WithValue(ctx, HTTPRequestCtxKey, r) + ctx = context.WithValue(ctx, ServiceNameCtxKey, "AdminService") + + r = r.WithContext(ctx) + + var handler func(ctx context.Context, w http.ResponseWriter, r *http.Request) + switch r.URL.Path { + case "/rpc/AdminService/Auth": + handler = s.serveAuthJSON + case "/rpc/AdminService/Status": + handler = s.serveStatusJSON + case "/rpc/AdminService/Version": + handler = s.serveVersionJSON + default: + err := ErrWebrpcBadRoute.WithCausef("no webrpc method defined for path %v", r.URL.Path) + s.sendErrorJSON(w, r, err) + return + } + + if r.Method != "POST" { + w.Header().Add("Allow", "POST") // RFC 9110. + err := ErrWebrpcBadMethod.WithCausef("unsupported HTTP method %v (only POST is allowed)", r.Method) + s.sendErrorJSON(w, r, err) + return + } + + contentType := r.Header.Get("Content-Type") + if i := strings.Index(contentType, ";"); i >= 0 { + contentType = contentType[:i] + } + contentType = strings.TrimSpace(strings.ToLower(contentType)) + + switch contentType { + case "application/json": + if s.OnRequest != nil { + if err := s.OnRequest(w, r); err != nil { + rpcErr, ok := err.(WebRPCError) + if !ok { + rpcErr = ErrWebrpcEndpoint.WithCause(err) + } + s.sendErrorJSON(w, r, rpcErr) + return + } + } + + handler(ctx, w, r) + default: + err := ErrWebrpcBadRequest.WithCausef("unsupported Content-Type %q (only application/json is allowed)", r.Header.Get("Content-Type")) + s.sendErrorJSON(w, r, err) + } +} + +func (s *adminServiceServer) serveAuthJSON(ctx context.Context, w http.ResponseWriter, r *http.Request) { + ctx = context.WithValue(ctx, MethodNameCtxKey, "Auth") + + // Call service method implementation. + ret0, ret1, err := s.AdminService.Auth(ctx) + if err != nil { + rpcErr, ok := err.(WebRPCError) + if !ok { + rpcErr = ErrWebrpcEndpoint.WithCause(err) + } + s.sendErrorJSON(w, r, rpcErr) + return + } + + respPayload := struct { + Ret0 string `json:"jwt"` + Ret1 string `json:"role"` + }{ret0, ret1} + respBody, err := json.Marshal(respPayload) + if err != nil { + s.sendErrorJSON(w, r, ErrWebrpcBadResponse.WithCausef("failed to marshal json response: %w", err)) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write(respBody) +} + +func (s *adminServiceServer) serveStatusJSON(ctx context.Context, w http.ResponseWriter, r *http.Request) { + ctx = context.WithValue(ctx, MethodNameCtxKey, "Status") + + // Call service method implementation. + ret0, err := s.AdminService.Status(ctx) + if err != nil { + rpcErr, ok := err.(WebRPCError) + if !ok { + rpcErr = ErrWebrpcEndpoint.WithCause(err) + } + s.sendErrorJSON(w, r, rpcErr) + return + } + + respPayload := struct { + Ret0 bool `json:"status"` + }{ret0} + respBody, err := json.Marshal(respPayload) + if err != nil { + s.sendErrorJSON(w, r, ErrWebrpcBadResponse.WithCausef("failed to marshal json response: %w", err)) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write(respBody) +} + +func (s *adminServiceServer) serveVersionJSON(ctx context.Context, w http.ResponseWriter, r *http.Request) { + ctx = context.WithValue(ctx, MethodNameCtxKey, "Version") + + // Call service method implementation. + ret0, err := s.AdminService.Version(ctx) + if err != nil { + rpcErr, ok := err.(WebRPCError) + if !ok { + rpcErr = ErrWebrpcEndpoint.WithCause(err) + } + s.sendErrorJSON(w, r, rpcErr) + return + } + + respPayload := struct { + Ret0 *Version `json:"version"` + }{ret0} + respBody, err := json.Marshal(respPayload) + if err != nil { + s.sendErrorJSON(w, r, ErrWebrpcBadResponse.WithCausef("failed to marshal json response: %w", err)) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write(respBody) +} + +func (s *adminServiceServer) sendErrorJSON(w http.ResponseWriter, r *http.Request, rpcErr WebRPCError) { + if s.OnError != nil { + s.OnError(r, &rpcErr) + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(rpcErr.HTTPStatus) + + respBody, _ := json.Marshal(rpcErr) + w.Write(respBody) +} + +func RespondWithError(w http.ResponseWriter, err error) { + rpcErr, ok := err.(WebRPCError) + if !ok { + rpcErr = ErrWebrpcEndpoint.WithCause(err) + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(rpcErr.HTTPStatus) + + respBody, _ := json.Marshal(rpcErr) + w.Write(respBody) +} + +// +// Client +// + +const AdminServicePathPrefix = "/rpc/AdminService/" + +type adminServiceClient struct { + client HTTPClient + urls [3]string +} + +func NewAdminServiceClient(addr string, client HTTPClient) AdminServiceClient { + prefix := urlBase(addr) + AdminServicePathPrefix + urls := [3]string{ + prefix + "Auth", + prefix + "Status", + prefix + "Version", + } + return &adminServiceClient{ + client: client, + urls: urls, + } +} + +func (c *adminServiceClient) Auth(ctx context.Context) (string, string, error) { + out := struct { + Ret0 string `json:"jwt"` + Ret1 string `json:"role"` + }{} + + resp, err := doHTTPRequest(ctx, c.client, c.urls[0], nil, &out) + if resp != nil { + cerr := resp.Body.Close() + if err == nil && cerr != nil { + err = ErrWebrpcRequestFailed.WithCausef("failed to close response body: %w", cerr) + } + } + + return out.Ret0, out.Ret1, err +} + +func (c *adminServiceClient) Status(ctx context.Context) (bool, error) { + out := struct { + Ret0 bool `json:"status"` + }{} + + resp, err := doHTTPRequest(ctx, c.client, c.urls[1], nil, &out) + if resp != nil { + cerr := resp.Body.Close() + if err == nil && cerr != nil { + err = ErrWebrpcRequestFailed.WithCausef("failed to close response body: %w", cerr) + } + } + + return out.Ret0, err +} + +func (c *adminServiceClient) Version(ctx context.Context) (*Version, error) { + out := struct { + Ret0 *Version `json:"version"` + }{} + + resp, err := doHTTPRequest(ctx, c.client, c.urls[2], nil, &out) + if resp != nil { + cerr := resp.Body.Close() + if err == nil && cerr != nil { + err = ErrWebrpcRequestFailed.WithCausef("failed to close response body: %w", cerr) + } + } + + return out.Ret0, err +} + +// HTTPClient is the interface used by generated clients to send HTTP requests. +// It is fulfilled by *(net/http).Client, which is sufficient for most users. +// Users can provide their own implementation for special retry policies. +type HTTPClient interface { + Do(req *http.Request) (*http.Response, error) +} + +// urlBase helps ensure that addr specifies a scheme. If it is unparsable +// as a URL, it returns addr unchanged. +func urlBase(addr string) string { + // If the addr specifies a scheme, use it. If not, default to + // http. If url.Parse fails on it, return it unchanged. + url, err := url.Parse(addr) + if err != nil { + return addr + } + if url.Scheme == "" { + url.Scheme = "http" + } + return url.String() +} + +// newRequest makes an http.Request from a client, adding common headers. +func newRequest(ctx context.Context, url string, reqBody io.Reader, contentType string) (*http.Request, error) { + req, err := http.NewRequestWithContext(ctx, "POST", url, reqBody) + if err != nil { + return nil, err + } + req.Header.Set("Accept", contentType) + req.Header.Set("Content-Type", contentType) + req.Header.Set(WebrpcHeader, WebrpcHeaderValue) + if headers, ok := HTTPRequestHeaders(ctx); ok { + for k := range headers { + for _, v := range headers[k] { + req.Header.Add(k, v) + } + } + } + return req, nil +} + +// doHTTPRequest is common code to make a request to the remote service. +func doHTTPRequest(ctx context.Context, client HTTPClient, url string, in, out interface{}) (*http.Response, error) { + reqBody, err := json.Marshal(in) + if err != nil { + return nil, ErrWebrpcRequestFailed.WithCausef("failed to marshal JSON body: %w", err) + } + if err = ctx.Err(); err != nil { + return nil, ErrWebrpcRequestFailed.WithCausef("aborted because context was done: %w", err) + } + + req, err := newRequest(ctx, url, bytes.NewBuffer(reqBody), "application/json") + if err != nil { + return nil, ErrWebrpcRequestFailed.WithCausef("could not build request: %w", err) + } + + resp, err := client.Do(req) + if err != nil { + return nil, ErrWebrpcRequestFailed.WithCause(err) + } + + if resp.StatusCode != 200 { + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, ErrWebrpcBadResponse.WithCausef("failed to read server error response body: %w", err) + } + + var rpcErr WebRPCError + if err := json.Unmarshal(respBody, &rpcErr); err != nil { + return nil, ErrWebrpcBadResponse.WithCausef("failed to unmarshal server error: %w", err) + } + if rpcErr.Cause != "" { + rpcErr.cause = errors.New(rpcErr.Cause) + } + return nil, rpcErr + } + + if out != nil { + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, ErrWebrpcBadResponse.WithCausef("failed to read response body: %w", err) + } + + err = json.Unmarshal(respBody, &out) + if err != nil { + return nil, ErrWebrpcBadResponse.WithCausef("failed to unmarshal JSON response body: %w", err) + } + } + + return resp, nil +} + +func WithHTTPRequestHeaders(ctx context.Context, h http.Header) (context.Context, error) { + if _, ok := h["Accept"]; ok { + return nil, errors.New("provided header cannot set Accept") + } + if _, ok := h["Content-Type"]; ok { + return nil, errors.New("provided header cannot set Content-Type") + } + + copied := make(http.Header, len(h)) + for k, vv := range h { + if vv == nil { + copied[k] = nil + continue + } + copied[k] = make([]string, len(vv)) + copy(copied[k], vv) + } + + return context.WithValue(ctx, HTTPClientRequestHeadersCtxKey, copied), nil +} + +func HTTPRequestHeaders(ctx context.Context) (http.Header, bool) { + h, ok := ctx.Value(HTTPClientRequestHeadersCtxKey).(http.Header) + return h, ok +} + +// +// Helpers +// + +type method struct { + Name string + Service string + Annotations map[string]string +} + +type contextKey struct { + name string +} + +func (k *contextKey) String() string { + return "webrpc context value " + k.name +} + +var ( + HTTPClientRequestHeadersCtxKey = &contextKey{"HTTPClientRequestHeaders"} + HTTPResponseWriterCtxKey = &contextKey{"HTTPResponseWriter"} + + HTTPRequestCtxKey = &contextKey{"HTTPRequest"} + + ServiceNameCtxKey = &contextKey{"ServiceName"} + + MethodNameCtxKey = &contextKey{"MethodName"} +) + +func ServiceNameFromContext(ctx context.Context) string { + service, _ := ctx.Value(ServiceNameCtxKey).(string) + return service +} + +func MethodNameFromContext(ctx context.Context) string { + method, _ := ctx.Value(MethodNameCtxKey).(string) + return method +} + +func RequestFromContext(ctx context.Context) *http.Request { + r, _ := ctx.Value(HTTPRequestCtxKey).(*http.Request) + return r +} + +func MethodCtx(ctx context.Context) (method, bool) { + req := RequestFromContext(ctx) + if req == nil { + return method{}, false + } + + m, ok := methods[req.URL.Path] + if !ok { + return method{}, false + } + + return m, true +} + +func ResponseWriterFromContext(ctx context.Context) http.ResponseWriter { + w, _ := ctx.Value(HTTPResponseWriterCtxKey).(http.ResponseWriter) + return w +} + +// +// Errors +// + +type WebRPCError struct { + Name string `json:"error"` + Code int `json:"code"` + Message string `json:"msg"` + Cause string `json:"cause,omitempty"` + HTTPStatus int `json:"status"` + cause error +} + +var _ error = WebRPCError{} + +func (e WebRPCError) Error() string { + if e.cause != nil { + return fmt.Sprintf("%s %d: %s: %v", e.Name, e.Code, e.Message, e.cause) + } + return fmt.Sprintf("%s %d: %s", e.Name, e.Code, e.Message) +} + +func (e WebRPCError) Is(target error) bool { + if target == nil { + return false + } + if rpcErr, ok := target.(WebRPCError); ok { + return rpcErr.Code == e.Code + } + return errors.Is(e.cause, target) +} + +func (e WebRPCError) Unwrap() error { + return e.cause +} + +func (e WebRPCError) WithCause(cause error) WebRPCError { + err := e + err.cause = cause + err.Cause = cause.Error() + return err +} + +func (e WebRPCError) WithCausef(format string, args ...interface{}) WebRPCError { + cause := fmt.Errorf(format, args...) + err := e + err.cause = cause + err.Cause = cause.Error() + return err +} + +// Deprecated: Use .WithCause() method on WebRPCError. +func ErrorWithCause(rpcErr WebRPCError, cause error) WebRPCError { + return rpcErr.WithCause(cause) +} + +// Webrpc errors +var ( + ErrWebrpcEndpoint = WebRPCError{Code: 0, Name: "WebrpcEndpoint", Message: "endpoint error", HTTPStatus: 400} + ErrWebrpcRequestFailed = WebRPCError{Code: -1, Name: "WebrpcRequestFailed", Message: "request failed", HTTPStatus: 400} + ErrWebrpcBadRoute = WebRPCError{Code: -2, Name: "WebrpcBadRoute", Message: "bad route", HTTPStatus: 404} + ErrWebrpcBadMethod = WebRPCError{Code: -3, Name: "WebrpcBadMethod", Message: "bad method", HTTPStatus: 405} + ErrWebrpcBadRequest = WebRPCError{Code: -4, Name: "WebrpcBadRequest", Message: "bad request", HTTPStatus: 400} + ErrWebrpcBadResponse = WebRPCError{Code: -5, Name: "WebrpcBadResponse", Message: "bad response", HTTPStatus: 500} + ErrWebrpcServerPanic = WebRPCError{Code: -6, Name: "WebrpcServerPanic", Message: "server panic", HTTPStatus: 500} + ErrWebrpcInternalError = WebRPCError{Code: -7, Name: "WebrpcInternalError", Message: "internal error", HTTPStatus: 500} + ErrWebrpcClientDisconnected = WebRPCError{Code: -8, Name: "WebrpcClientDisconnected", Message: "client disconnected", HTTPStatus: 400} + ErrWebrpcStreamLost = WebRPCError{Code: -9, Name: "WebrpcStreamLost", Message: "stream lost", HTTPStatus: 400} + ErrWebrpcStreamFinished = WebRPCError{Code: -10, Name: "WebrpcStreamFinished", Message: "stream finished", HTTPStatus: 200} +) + +// Schema errors +var ( + ErrUserNotFound = WebRPCError{Code: 1000, Name: "UserNotFound", Message: "User not found", HTTPStatus: 404} + ErrUnauthorized = WebRPCError{Code: 2000, Name: "Unauthorized", Message: "Unauthorized access", HTTPStatus: 401} + ErrPermissionDenied = WebRPCError{Code: 3000, Name: "PermissionDenied", Message: "Permission denied", HTTPStatus: 403} +) diff --git a/_examples/golang-basics/example.gen.go b/_examples/golang-basics/example.gen.go index 77a126eb..9a27814a 100644 --- a/_examples/golang-basics/example.gen.go +++ b/_examples/golang-basics/example.gen.go @@ -1,7 +1,8 @@ -// example v0.0.1 b421904d19b997e555df17cba464343c3ed53e03 +// example v0.0.1 56c49483c480fd08c4dc4451dd1c8a09fd0c5c82 // -- -// This file has been generated by https://github.com/webrpc/webrpc using gen/golang -// Do not edit by hand. Update your webrpc schema and re-generate. +// Code generated by webrpc-gen with golang generator. DO NOT EDIT. +// +// webrpc-gen -schema=example.ridl -target=golang -pkg=main -server -client -service=ExampleService -out=./example.gen.go package main import ( @@ -11,12 +12,15 @@ import ( "errors" "fmt" "io" - "io/ioutil" "net/http" "net/url" "strings" ) +const WebrpcHeader = "Webrpc" + +const WebrpcHeaderValue = "webrpc;gen-golang@v0.17.0;example@v0.0.1" + // WebRPC description and code-gen version func WebRPCVersion() string { return "v1" @@ -29,11 +33,62 @@ func WebRPCSchemaVersion() string { // Schema hash generated from your RIDL schema func WebRPCSchemaHash() string { - return "b421904d19b997e555df17cba464343c3ed53e03" + return "56c49483c480fd08c4dc4451dd1c8a09fd0c5c82" +} + +type WebrpcGenVersions struct { + WebrpcGenVersion string + CodeGenName string + CodeGenVersion string + SchemaName string + SchemaVersion string +} + +func VersionFromHeader(h http.Header) (*WebrpcGenVersions, error) { + if h.Get(WebrpcHeader) == "" { + return nil, fmt.Errorf("header is empty or missing") + } + + versions, err := parseWebrpcGenVersions(h.Get(WebrpcHeader)) + if err != nil { + return nil, fmt.Errorf("webrpc header is invalid: %w", err) + } + + return versions, nil +} + +func parseWebrpcGenVersions(header string) (*WebrpcGenVersions, error) { + versions := strings.Split(header, ";") + if len(versions) < 3 { + return nil, fmt.Errorf("expected at least 3 parts while parsing webrpc header: %v", header) + } + + _, webrpcGenVersion, ok := strings.Cut(versions[0], "@") + if !ok { + return nil, fmt.Errorf("webrpc gen version could not be parsed from: %s", versions[0]) + } + + tmplTarget, tmplVersion, ok := strings.Cut(versions[1], "@") + if !ok { + return nil, fmt.Errorf("tmplTarget and tmplVersion could not be parsed from: %s", versions[1]) + } + + schemaName, schemaVersion, ok := strings.Cut(versions[2], "@") + if !ok { + return nil, fmt.Errorf("schema name and schema version could not be parsed from: %s", versions[2]) + } + + return &WebrpcGenVersions{ + WebrpcGenVersion: webrpcGenVersion, + CodeGenName: tmplTarget, + CodeGenVersion: tmplVersion, + SchemaName: schemaName, + SchemaVersion: schemaVersion, + }, nil } // -// Types +// Common types // type Kind uint32 @@ -57,30 +112,68 @@ func (x Kind) String() string { return Kind_name[uint32(x)] } -func (x Kind) MarshalJSON() ([]byte, error) { - buf := bytes.NewBufferString(`"`) - buf.WriteString(Kind_name[uint32(x)]) - buf.WriteString(`"`) - return buf.Bytes(), nil +func (x Kind) MarshalText() ([]byte, error) { + return []byte(Kind_name[uint32(x)]), nil } -func (x *Kind) UnmarshalJSON(b []byte) error { - var j string - err := json.Unmarshal(b, &j) - if err != nil { - return err +func (x *Kind) UnmarshalText(b []byte) error { + *x = Kind(Kind_value[string(b)]) + return nil +} + +func (x *Kind) Is(values ...Kind) bool { + if x == nil { + return false + } + for _, v := range values { + if *x == v { + return true + } } - *x = Kind(Kind_value[j]) + return false +} + +type Intent string + +const ( + Intent_openSession Intent = "openSession" + Intent_closeSession Intent = "closeSession" + Intent_validateSession Intent = "validateSession" +) + +func (x Intent) MarshalText() ([]byte, error) { + return []byte(x), nil +} + +func (x *Intent) UnmarshalText(b []byte) error { + *x = Intent(string(b)) return nil } +func (x *Intent) Is(values ...Intent) bool { + if x == nil { + return false + } + for _, v := range values { + if *x == v { + return true + } + } + return false +} + type Empty struct { } +// User struct +// +// More information about user struct type User struct { ID uint64 `json:"id" db:"id"` Username string `json:"USERNAME" db:"username"` Role string `json:"role" db:"-"` + Kind Kind `json:"kind"` + Intent Intent `json:"intent"` } type SearchFilter struct { @@ -105,12 +198,56 @@ type ComplexType struct { User *User `json:"user"` } -type ExampleService interface { - Ping(ctx context.Context) error - Status(ctx context.Context) (bool, error) - Version(ctx context.Context) (*Version, error) - GetUser(ctx context.Context, header map[string]string, userID uint64) (uint32, *User, error) - FindUser(ctx context.Context, s *SearchFilter) (string, *User, error) +var methods = map[string]method{ + "/rpc/ExampleService/Ping": { + Name: "Ping", + Service: "ExampleService", + Annotations: map[string]string{"internal": ""}, + }, + "/rpc/ExampleService/Status": { + Name: "Status", + Service: "ExampleService", + Annotations: map[string]string{"internal": ""}, + }, + "/rpc/ExampleService/Version": { + Name: "Version", + Service: "ExampleService", + Annotations: map[string]string{"internal": ""}, + }, + "/rpc/ExampleService/GetUser": { + Name: "GetUser", + Service: "ExampleService", + Annotations: map[string]string{"deprecated": "GetUserV2", "internal": ""}, + }, + "/rpc/ExampleService/GetUserV2": { + Name: "GetUserV2", + Service: "ExampleService", + Annotations: map[string]string{"auth": "X-Access-Key,S2S,Cookies", "public": ""}, + }, + "/rpc/ExampleService/FindUser": { + Name: "FindUser", + Service: "ExampleService", + Annotations: map[string]string{"public": ""}, + }, + "/rpc/ExampleService/GetIntents": { + Name: "GetIntents", + Service: "ExampleService", + Annotations: map[string]string{"public": ""}, + }, + "/rpc/ExampleService/CountIntents": { + Name: "CountIntents", + Service: "ExampleService", + Annotations: map[string]string{"public": ""}, + }, +} + +func WebrpcMethods() map[string]method { + res := make(map[string]method, len(methods)) + for k, v := range methods { + res[k] = v + } + + return res } var WebRPCServices = map[string][]string{ @@ -119,10 +256,45 @@ var WebRPCServices = map[string][]string{ "Status", "Version", "GetUser", + "GetUserV2", "FindUser", + "GetIntents", + "CountIntents", }, } +// +// Server types +// + +type ExampleService interface { + Ping(ctx context.Context) error + Status(ctx context.Context) (bool, error) + Version(ctx context.Context) (*Version, error) + // Deprecated: + GetUser(ctx context.Context, header map[string]string, userID uint64) (uint32, *User, error) + GetUserV2(ctx context.Context, header map[string]string, userID uint64) (uint32, *User, string, error) + FindUser(ctx context.Context, s *SearchFilter) (string, *User, error) + GetIntents(ctx context.Context) ([]Intent, error) + CountIntents(ctx context.Context, userId uint64) (map[Intent]uint32, error) +} + +// +// Client types +// + +type ExampleServiceClient interface { + Ping(ctx context.Context) error + Status(ctx context.Context) (bool, error) + Version(ctx context.Context) (*Version, error) + // Deprecated: + GetUser(ctx context.Context, header map[string]string, userID uint64) (uint32, *User, error) + GetUserV2(ctx context.Context, header map[string]string, userID uint64) (uint32, *User, string, error) + FindUser(ctx context.Context, s *SearchFilter) (string, *User, error) + GetIntents(ctx context.Context) ([]Intent, error) + CountIntents(ctx context.Context, userId uint64) (map[Intent]uint32, error) +} + // // Server // @@ -133,134 +305,130 @@ type WebRPCServer interface { type exampleServiceServer struct { ExampleService + OnError func(r *http.Request, rpcErr *WebRPCError) + OnRequest func(w http.ResponseWriter, r *http.Request) error } -func NewExampleServiceServer(svc ExampleService) WebRPCServer { +func NewExampleServiceServer(svc ExampleService) *exampleServiceServer { return &exampleServiceServer{ ExampleService: svc, } } func (s *exampleServiceServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { + defer func() { + // In case of a panic, serve a HTTP 500 error and then panic. + if rr := recover(); rr != nil { + s.sendErrorJSON(w, r, ErrWebrpcServerPanic.WithCausef("%v", rr)) + panic(rr) + } + }() + + w.Header().Set(WebrpcHeader, WebrpcHeaderValue) + ctx := r.Context() ctx = context.WithValue(ctx, HTTPResponseWriterCtxKey, w) ctx = context.WithValue(ctx, HTTPRequestCtxKey, r) ctx = context.WithValue(ctx, ServiceNameCtxKey, "ExampleService") - if r.Method != "POST" { - err := Errorf(ErrBadRoute, "unsupported method %q (only POST is allowed)", r.Method) - RespondWithError(w, err) - return - } + r = r.WithContext(ctx) + var handler func(ctx context.Context, w http.ResponseWriter, r *http.Request) switch r.URL.Path { case "/rpc/ExampleService/Ping": - s.servePing(ctx, w, r) - return + handler = s.servePingJSON case "/rpc/ExampleService/Status": - s.serveStatus(ctx, w, r) - return + handler = s.serveStatusJSON case "/rpc/ExampleService/Version": - s.serveVersion(ctx, w, r) - return + handler = s.serveVersionJSON case "/rpc/ExampleService/GetUser": - s.serveGetUser(ctx, w, r) - return + handler = s.serveGetUserJSON + case "/rpc/ExampleService/GetUserV2": + handler = s.serveGetUserV2JSON case "/rpc/ExampleService/FindUser": - s.serveFindUser(ctx, w, r) - return + handler = s.serveFindUserJSON + case "/rpc/ExampleService/GetIntents": + handler = s.serveGetIntentsJSON + case "/rpc/ExampleService/CountIntents": + handler = s.serveCountIntentsJSON default: - err := Errorf(ErrBadRoute, "no handler for path %q", r.URL.Path) - RespondWithError(w, err) + err := ErrWebrpcBadRoute.WithCausef("no webrpc method defined for path %v", r.URL.Path) + s.sendErrorJSON(w, r, err) + return + } + + if r.Method != "POST" { + w.Header().Add("Allow", "POST") // RFC 9110. + err := ErrWebrpcBadMethod.WithCausef("unsupported HTTP method %v (only POST is allowed)", r.Method) + s.sendErrorJSON(w, r, err) return } -} -func (s *exampleServiceServer) servePing(ctx context.Context, w http.ResponseWriter, r *http.Request) { - header := r.Header.Get("Content-Type") - i := strings.Index(header, ";") - if i == -1 { - i = len(header) + contentType := r.Header.Get("Content-Type") + if i := strings.Index(contentType, ";"); i >= 0 { + contentType = contentType[:i] } + contentType = strings.TrimSpace(strings.ToLower(contentType)) - switch strings.TrimSpace(strings.ToLower(header[:i])) { + switch contentType { case "application/json": - s.servePingJSON(ctx, w, r) + if s.OnRequest != nil { + if err := s.OnRequest(w, r); err != nil { + rpcErr, ok := err.(WebRPCError) + if !ok { + rpcErr = ErrWebrpcEndpoint.WithCause(err) + } + s.sendErrorJSON(w, r, rpcErr) + return + } + } + + handler(ctx, w, r) default: - err := Errorf(ErrBadRoute, "unexpected Content-Type: %q", r.Header.Get("Content-Type")) - RespondWithError(w, err) + err := ErrWebrpcBadRequest.WithCausef("unsupported Content-Type %q (only application/json is allowed)", r.Header.Get("Content-Type")) + s.sendErrorJSON(w, r, err) } } func (s *exampleServiceServer) servePingJSON(ctx context.Context, w http.ResponseWriter, r *http.Request) { - var err error ctx = context.WithValue(ctx, MethodNameCtxKey, "Ping") - // Call service method - func() { - defer func() { - // In case of a panic, serve a 500 error and then panic. - if rr := recover(); rr != nil { - RespondWithError(w, ErrorInternal("internal service panic")) - panic(rr) - } - }() - err = s.ExampleService.Ping(ctx) - }() - + // Call service method implementation. + err := s.ExampleService.Ping(ctx) if err != nil { - RespondWithError(w, err) + rpcErr, ok := err.(WebRPCError) + if !ok { + rpcErr = ErrWebrpcEndpoint.WithCause(err) + } + s.sendErrorJSON(w, r, rpcErr) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) -} - -func (s *exampleServiceServer) serveStatus(ctx context.Context, w http.ResponseWriter, r *http.Request) { - header := r.Header.Get("Content-Type") - i := strings.Index(header, ";") - if i == -1 { - i = len(header) - } - - switch strings.TrimSpace(strings.ToLower(header[:i])) { - case "application/json": - s.serveStatusJSON(ctx, w, r) - default: - err := Errorf(ErrBadRoute, "unexpected Content-Type: %q", r.Header.Get("Content-Type")) - RespondWithError(w, err) - } + w.Write([]byte("{}")) } func (s *exampleServiceServer) serveStatusJSON(ctx context.Context, w http.ResponseWriter, r *http.Request) { - var err error ctx = context.WithValue(ctx, MethodNameCtxKey, "Status") - // Call service method - var ret0 bool - func() { - defer func() { - // In case of a panic, serve a 500 error and then panic. - if rr := recover(); rr != nil { - RespondWithError(w, ErrorInternal("internal service panic")) - panic(rr) - } - }() - ret0, err = s.ExampleService.Status(ctx) - }() - respContent := struct { - Ret0 bool `json:"status"` - }{ret0} - + // Call service method implementation. + ret0, err := s.ExampleService.Status(ctx) if err != nil { - RespondWithError(w, err) + rpcErr, ok := err.(WebRPCError) + if !ok { + rpcErr = ErrWebrpcEndpoint.WithCause(err) + } + s.sendErrorJSON(w, r, rpcErr) return } - respBody, err := json.Marshal(respContent) + + respPayload := struct { + Ret0 bool `json:"status"` + }{ret0} + respBody, err := json.Marshal(respPayload) if err != nil { - err = WrapError(ErrInternal, err, "failed to marshal json response") - RespondWithError(w, err) + s.sendErrorJSON(w, r, ErrWebrpcBadResponse.WithCausef("failed to marshal json response: %w", err)) return } @@ -269,50 +437,26 @@ func (s *exampleServiceServer) serveStatusJSON(ctx context.Context, w http.Respo w.Write(respBody) } -func (s *exampleServiceServer) serveVersion(ctx context.Context, w http.ResponseWriter, r *http.Request) { - header := r.Header.Get("Content-Type") - i := strings.Index(header, ";") - if i == -1 { - i = len(header) - } - - switch strings.TrimSpace(strings.ToLower(header[:i])) { - case "application/json": - s.serveVersionJSON(ctx, w, r) - default: - err := Errorf(ErrBadRoute, "unexpected Content-Type: %q", r.Header.Get("Content-Type")) - RespondWithError(w, err) - } -} - func (s *exampleServiceServer) serveVersionJSON(ctx context.Context, w http.ResponseWriter, r *http.Request) { - var err error ctx = context.WithValue(ctx, MethodNameCtxKey, "Version") - // Call service method - var ret0 *Version - func() { - defer func() { - // In case of a panic, serve a 500 error and then panic. - if rr := recover(); rr != nil { - RespondWithError(w, ErrorInternal("internal service panic")) - panic(rr) - } - }() - ret0, err = s.ExampleService.Version(ctx) - }() - respContent := struct { - Ret0 *Version `json:"version"` - }{ret0} - + // Call service method implementation. + ret0, err := s.ExampleService.Version(ctx) if err != nil { - RespondWithError(w, err) + rpcErr, ok := err.(WebRPCError) + if !ok { + rpcErr = ErrWebrpcEndpoint.WithCause(err) + } + s.sendErrorJSON(w, r, rpcErr) return } - respBody, err := json.Marshal(respContent) + + respPayload := struct { + Ret0 *Version `json:"version"` + }{ret0} + respBody, err := json.Marshal(respPayload) if err != nil { - err = WrapError(ErrInternal, err, "failed to marshal json response") - RespondWithError(w, err) + s.sendErrorJSON(w, r, ErrWebrpcBadResponse.WithCausef("failed to marshal json response: %w", err)) return } @@ -321,71 +465,43 @@ func (s *exampleServiceServer) serveVersionJSON(ctx context.Context, w http.Resp w.Write(respBody) } -func (s *exampleServiceServer) serveGetUser(ctx context.Context, w http.ResponseWriter, r *http.Request) { - header := r.Header.Get("Content-Type") - i := strings.Index(header, ";") - if i == -1 { - i = len(header) - } +func (s *exampleServiceServer) serveGetUserJSON(ctx context.Context, w http.ResponseWriter, r *http.Request) { + ctx = context.WithValue(ctx, MethodNameCtxKey, "GetUser") - switch strings.TrimSpace(strings.ToLower(header[:i])) { - case "application/json": - s.serveGetUserJSON(ctx, w, r) - default: - err := Errorf(ErrBadRoute, "unexpected Content-Type: %q", r.Header.Get("Content-Type")) - RespondWithError(w, err) + reqBody, err := io.ReadAll(r.Body) + if err != nil { + s.sendErrorJSON(w, r, ErrWebrpcBadRequest.WithCausef("failed to read request data: %w", err)) + return } -} + defer r.Body.Close() -func (s *exampleServiceServer) serveGetUserJSON(ctx context.Context, w http.ResponseWriter, r *http.Request) { - var err error - ctx = context.WithValue(ctx, MethodNameCtxKey, "GetUser") - reqContent := struct { + reqPayload := struct { Arg0 map[string]string `json:"header"` Arg1 uint64 `json:"userID"` }{} - - reqBody, err := ioutil.ReadAll(r.Body) - if err != nil { - err = WrapError(ErrInternal, err, "failed to read request data") - RespondWithError(w, err) + if err := json.Unmarshal(reqBody, &reqPayload); err != nil { + s.sendErrorJSON(w, r, ErrWebrpcBadRequest.WithCausef("failed to unmarshal request data: %w", err)) return } - defer r.Body.Close() - err = json.Unmarshal(reqBody, &reqContent) + // Call service method implementation. + ret0, ret1, err := s.ExampleService.GetUser(ctx, reqPayload.Arg0, reqPayload.Arg1) if err != nil { - err = WrapError(ErrInvalidArgument, err, "failed to unmarshal request data") - RespondWithError(w, err) + rpcErr, ok := err.(WebRPCError) + if !ok { + rpcErr = ErrWebrpcEndpoint.WithCause(err) + } + s.sendErrorJSON(w, r, rpcErr) return } - // Call service method - var ret0 uint32 - var ret1 *User - func() { - defer func() { - // In case of a panic, serve a 500 error and then panic. - if rr := recover(); rr != nil { - RespondWithError(w, ErrorInternal("internal service panic")) - panic(rr) - } - }() - ret0, ret1, err = s.ExampleService.GetUser(ctx, reqContent.Arg0, reqContent.Arg1) - }() - respContent := struct { + respPayload := struct { Ret0 uint32 `json:"code"` Ret1 *User `json:"user"` }{ret0, ret1} - - if err != nil { - RespondWithError(w, err) - return - } - respBody, err := json.Marshal(respContent) + respBody, err := json.Marshal(respPayload) if err != nil { - err = WrapError(ErrInternal, err, "failed to marshal json response") - RespondWithError(w, err) + s.sendErrorJSON(w, r, ErrWebrpcBadResponse.WithCausef("failed to marshal json response: %w", err)) return } @@ -394,70 +510,116 @@ func (s *exampleServiceServer) serveGetUserJSON(ctx context.Context, w http.Resp w.Write(respBody) } -func (s *exampleServiceServer) serveFindUser(ctx context.Context, w http.ResponseWriter, r *http.Request) { - header := r.Header.Get("Content-Type") - i := strings.Index(header, ";") - if i == -1 { - i = len(header) +func (s *exampleServiceServer) serveGetUserV2JSON(ctx context.Context, w http.ResponseWriter, r *http.Request) { + ctx = context.WithValue(ctx, MethodNameCtxKey, "GetUserV2") + + reqBody, err := io.ReadAll(r.Body) + if err != nil { + s.sendErrorJSON(w, r, ErrWebrpcBadRequest.WithCausef("failed to read request data: %w", err)) + return } + defer r.Body.Close() - switch strings.TrimSpace(strings.ToLower(header[:i])) { - case "application/json": - s.serveFindUserJSON(ctx, w, r) - default: - err := Errorf(ErrBadRoute, "unexpected Content-Type: %q", r.Header.Get("Content-Type")) - RespondWithError(w, err) + reqPayload := struct { + Arg0 map[string]string `json:"header"` + Arg1 uint64 `json:"userID"` + }{} + if err := json.Unmarshal(reqBody, &reqPayload); err != nil { + s.sendErrorJSON(w, r, ErrWebrpcBadRequest.WithCausef("failed to unmarshal request data: %w", err)) + return + } + + // Call service method implementation. + ret0, ret1, ret2, err := s.ExampleService.GetUserV2(ctx, reqPayload.Arg0, reqPayload.Arg1) + if err != nil { + rpcErr, ok := err.(WebRPCError) + if !ok { + rpcErr = ErrWebrpcEndpoint.WithCause(err) + } + s.sendErrorJSON(w, r, rpcErr) + return + } + + respPayload := struct { + Ret0 uint32 `json:"code"` + Ret1 *User `json:"user"` + Ret2 string `json:"profile"` + }{ret0, ret1, ret2} + respBody, err := json.Marshal(respPayload) + if err != nil { + s.sendErrorJSON(w, r, ErrWebrpcBadResponse.WithCausef("failed to marshal json response: %w", err)) + return } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write(respBody) } func (s *exampleServiceServer) serveFindUserJSON(ctx context.Context, w http.ResponseWriter, r *http.Request) { - var err error ctx = context.WithValue(ctx, MethodNameCtxKey, "FindUser") - reqContent := struct { - Arg0 *SearchFilter `json:"s"` - }{} - reqBody, err := ioutil.ReadAll(r.Body) + reqBody, err := io.ReadAll(r.Body) if err != nil { - err = WrapError(ErrInternal, err, "failed to read request data") - RespondWithError(w, err) + s.sendErrorJSON(w, r, ErrWebrpcBadRequest.WithCausef("failed to read request data: %w", err)) return } defer r.Body.Close() - err = json.Unmarshal(reqBody, &reqContent) + reqPayload := struct { + Arg0 *SearchFilter `json:"s"` + }{} + if err := json.Unmarshal(reqBody, &reqPayload); err != nil { + s.sendErrorJSON(w, r, ErrWebrpcBadRequest.WithCausef("failed to unmarshal request data: %w", err)) + return + } + + // Call service method implementation. + ret0, ret1, err := s.ExampleService.FindUser(ctx, reqPayload.Arg0) if err != nil { - err = WrapError(ErrInvalidArgument, err, "failed to unmarshal request data") - RespondWithError(w, err) + rpcErr, ok := err.(WebRPCError) + if !ok { + rpcErr = ErrWebrpcEndpoint.WithCause(err) + } + s.sendErrorJSON(w, r, rpcErr) return } - // Call service method - var ret0 string - var ret1 *User - func() { - defer func() { - // In case of a panic, serve a 500 error and then panic. - if rr := recover(); rr != nil { - RespondWithError(w, ErrorInternal("internal service panic")) - panic(rr) - } - }() - ret0, ret1, err = s.ExampleService.FindUser(ctx, reqContent.Arg0) - }() - respContent := struct { + respPayload := struct { Ret0 string `json:"name"` Ret1 *User `json:"user"` }{ret0, ret1} + respBody, err := json.Marshal(respPayload) + if err != nil { + s.sendErrorJSON(w, r, ErrWebrpcBadResponse.WithCausef("failed to marshal json response: %w", err)) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write(respBody) +} + +func (s *exampleServiceServer) serveGetIntentsJSON(ctx context.Context, w http.ResponseWriter, r *http.Request) { + ctx = context.WithValue(ctx, MethodNameCtxKey, "GetIntents") + // Call service method implementation. + ret0, err := s.ExampleService.GetIntents(ctx) if err != nil { - RespondWithError(w, err) + rpcErr, ok := err.(WebRPCError) + if !ok { + rpcErr = ErrWebrpcEndpoint.WithCause(err) + } + s.sendErrorJSON(w, r, rpcErr) return } - respBody, err := json.Marshal(respContent) + + respPayload := struct { + Ret0 []Intent `json:"intents"` + }{ret0} + respBody, err := json.Marshal(respPayload) if err != nil { - err = WrapError(ErrInternal, err, "failed to marshal json response") - RespondWithError(w, err) + s.sendErrorJSON(w, r, ErrWebrpcBadResponse.WithCausef("failed to marshal json response: %w", err)) return } @@ -466,18 +628,71 @@ func (s *exampleServiceServer) serveFindUserJSON(ctx context.Context, w http.Res w.Write(respBody) } +func (s *exampleServiceServer) serveCountIntentsJSON(ctx context.Context, w http.ResponseWriter, r *http.Request) { + ctx = context.WithValue(ctx, MethodNameCtxKey, "CountIntents") + + reqBody, err := io.ReadAll(r.Body) + if err != nil { + s.sendErrorJSON(w, r, ErrWebrpcBadRequest.WithCausef("failed to read request data: %w", err)) + return + } + defer r.Body.Close() + + reqPayload := struct { + Arg0 uint64 `json:"userId"` + }{} + if err := json.Unmarshal(reqBody, &reqPayload); err != nil { + s.sendErrorJSON(w, r, ErrWebrpcBadRequest.WithCausef("failed to unmarshal request data: %w", err)) + return + } + + // Call service method implementation. + ret0, err := s.ExampleService.CountIntents(ctx, reqPayload.Arg0) + if err != nil { + rpcErr, ok := err.(WebRPCError) + if !ok { + rpcErr = ErrWebrpcEndpoint.WithCause(err) + } + s.sendErrorJSON(w, r, rpcErr) + return + } + + respPayload := struct { + Ret0 map[Intent]uint32 `json:"count"` + }{ret0} + respBody, err := json.Marshal(respPayload) + if err != nil { + s.sendErrorJSON(w, r, ErrWebrpcBadResponse.WithCausef("failed to marshal json response: %w", err)) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write(respBody) +} + +func (s *exampleServiceServer) sendErrorJSON(w http.ResponseWriter, r *http.Request, rpcErr WebRPCError) { + if s.OnError != nil { + s.OnError(r, &rpcErr) + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(rpcErr.HTTPStatus) + + respBody, _ := json.Marshal(rpcErr) + w.Write(respBody) +} + func RespondWithError(w http.ResponseWriter, err error) { - rpcErr, ok := err.(Error) + rpcErr, ok := err.(WebRPCError) if !ok { - rpcErr = WrapError(ErrInternal, err, "webrpc error") + rpcErr = ErrWebrpcEndpoint.WithCause(err) } - statusCode := HTTPStatusFromErrorCode(rpcErr.Code()) - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(statusCode) + w.WriteHeader(rpcErr.HTTPStatus) - respBody, _ := json.Marshal(rpcErr.Payload()) + respBody, _ := json.Marshal(rpcErr) w.Write(respBody) } @@ -489,17 +704,20 @@ const ExampleServicePathPrefix = "/rpc/ExampleService/" type exampleServiceClient struct { client HTTPClient - urls [5]string + urls [8]string } -func NewExampleServiceClient(addr string, client HTTPClient) ExampleService { +func NewExampleServiceClient(addr string, client HTTPClient) ExampleServiceClient { prefix := urlBase(addr) + ExampleServicePathPrefix - urls := [5]string{ + urls := [8]string{ prefix + "Ping", prefix + "Status", prefix + "Version", prefix + "GetUser", + prefix + "GetUserV2", prefix + "FindUser", + prefix + "GetIntents", + prefix + "CountIntents", } return &exampleServiceClient{ client: client, @@ -509,7 +727,14 @@ func NewExampleServiceClient(addr string, client HTTPClient) ExampleService { func (c *exampleServiceClient) Ping(ctx context.Context) error { - err := doJSONRequest(ctx, c.client, c.urls[0], nil, nil) + resp, err := doHTTPRequest(ctx, c.client, c.urls[0], nil, nil) + if resp != nil { + cerr := resp.Body.Close() + if err == nil && cerr != nil { + err = ErrWebrpcRequestFailed.WithCausef("failed to close response body: %w", cerr) + } + } + return err } @@ -518,7 +743,14 @@ func (c *exampleServiceClient) Status(ctx context.Context) (bool, error) { Ret0 bool `json:"status"` }{} - err := doJSONRequest(ctx, c.client, c.urls[1], nil, &out) + resp, err := doHTTPRequest(ctx, c.client, c.urls[1], nil, &out) + if resp != nil { + cerr := resp.Body.Close() + if err == nil && cerr != nil { + err = ErrWebrpcRequestFailed.WithCausef("failed to close response body: %w", cerr) + } + } + return out.Ret0, err } @@ -527,7 +759,14 @@ func (c *exampleServiceClient) Version(ctx context.Context) (*Version, error) { Ret0 *Version `json:"version"` }{} - err := doJSONRequest(ctx, c.client, c.urls[2], nil, &out) + resp, err := doHTTPRequest(ctx, c.client, c.urls[2], nil, &out) + if resp != nil { + cerr := resp.Body.Close() + if err == nil && cerr != nil { + err = ErrWebrpcRequestFailed.WithCausef("failed to close response body: %w", cerr) + } + } + return out.Ret0, err } @@ -541,10 +780,39 @@ func (c *exampleServiceClient) GetUser(ctx context.Context, header map[string]st Ret1 *User `json:"user"` }{} - err := doJSONRequest(ctx, c.client, c.urls[3], in, &out) + resp, err := doHTTPRequest(ctx, c.client, c.urls[3], in, &out) + if resp != nil { + cerr := resp.Body.Close() + if err == nil && cerr != nil { + err = ErrWebrpcRequestFailed.WithCausef("failed to close response body: %w", cerr) + } + } + return out.Ret0, out.Ret1, err } +func (c *exampleServiceClient) GetUserV2(ctx context.Context, header map[string]string, userID uint64) (uint32, *User, string, error) { + in := struct { + Arg0 map[string]string `json:"header"` + Arg1 uint64 `json:"userID"` + }{header, userID} + out := struct { + Ret0 uint32 `json:"code"` + Ret1 *User `json:"user"` + Ret2 string `json:"profile"` + }{} + + resp, err := doHTTPRequest(ctx, c.client, c.urls[4], in, &out) + if resp != nil { + cerr := resp.Body.Close() + if err == nil && cerr != nil { + err = ErrWebrpcRequestFailed.WithCausef("failed to close response body: %w", cerr) + } + } + + return out.Ret0, out.Ret1, out.Ret2, err +} + func (c *exampleServiceClient) FindUser(ctx context.Context, s *SearchFilter) (string, *User, error) { in := struct { Arg0 *SearchFilter `json:"s"` @@ -554,10 +822,52 @@ func (c *exampleServiceClient) FindUser(ctx context.Context, s *SearchFilter) (s Ret1 *User `json:"user"` }{} - err := doJSONRequest(ctx, c.client, c.urls[4], in, &out) + resp, err := doHTTPRequest(ctx, c.client, c.urls[5], in, &out) + if resp != nil { + cerr := resp.Body.Close() + if err == nil && cerr != nil { + err = ErrWebrpcRequestFailed.WithCausef("failed to close response body: %w", cerr) + } + } + return out.Ret0, out.Ret1, err } +func (c *exampleServiceClient) GetIntents(ctx context.Context) ([]Intent, error) { + out := struct { + Ret0 []Intent `json:"intents"` + }{} + + resp, err := doHTTPRequest(ctx, c.client, c.urls[6], nil, &out) + if resp != nil { + cerr := resp.Body.Close() + if err == nil && cerr != nil { + err = ErrWebrpcRequestFailed.WithCausef("failed to close response body: %w", cerr) + } + } + + return out.Ret0, err +} + +func (c *exampleServiceClient) CountIntents(ctx context.Context, userId uint64) (map[Intent]uint32, error) { + in := struct { + Arg0 uint64 `json:"userId"` + }{userId} + out := struct { + Ret0 map[Intent]uint32 `json:"count"` + }{} + + resp, err := doHTTPRequest(ctx, c.client, c.urls[7], in, &out) + if resp != nil { + cerr := resp.Body.Close() + if err == nil && cerr != nil { + err = ErrWebrpcRequestFailed.WithCausef("failed to close response body: %w", cerr) + } + } + + return out.Ret0, err +} + // HTTPClient is the interface used by generated clients to send HTTP requests. // It is fulfilled by *(net/http).Client, which is sufficient for most users. // Users can provide their own implementation for special retry policies. @@ -582,12 +892,13 @@ func urlBase(addr string) string { // newRequest makes an http.Request from a client, adding common headers. func newRequest(ctx context.Context, url string, reqBody io.Reader, contentType string) (*http.Request, error) { - req, err := http.NewRequest("POST", url, reqBody) + req, err := http.NewRequestWithContext(ctx, "POST", url, reqBody) if err != nil { return nil, err } req.Header.Set("Accept", contentType) req.Header.Set("Content-Type", contentType) + req.Header.Set(WebrpcHeader, WebrpcHeaderValue) if headers, ok := HTTPRequestHeaders(ctx); ok { for k := range headers { for _, v := range headers[k] { @@ -598,85 +909,55 @@ func newRequest(ctx context.Context, url string, reqBody io.Reader, contentType return req, nil } -// doJSONRequest is common code to make a request to the remote service. -func doJSONRequest(ctx context.Context, client HTTPClient, url string, in, out interface{}) error { +// doHTTPRequest is common code to make a request to the remote service. +func doHTTPRequest(ctx context.Context, client HTTPClient, url string, in, out interface{}) (*http.Response, error) { reqBody, err := json.Marshal(in) if err != nil { - return clientError("failed to marshal json request", err) + return nil, ErrWebrpcRequestFailed.WithCausef("failed to marshal JSON body: %w", err) } if err = ctx.Err(); err != nil { - return clientError("aborted because context was done", err) + return nil, ErrWebrpcRequestFailed.WithCausef("aborted because context was done: %w", err) } req, err := newRequest(ctx, url, bytes.NewBuffer(reqBody), "application/json") if err != nil { - return clientError("could not build request", err) + return nil, ErrWebrpcRequestFailed.WithCausef("could not build request: %w", err) } + resp, err := client.Do(req) if err != nil { - return clientError("request failed", err) + return nil, ErrWebrpcRequestFailed.WithCause(err) } - defer func() { - cerr := resp.Body.Close() - if err == nil && cerr != nil { - err = clientError("failed to close response body", cerr) + if resp.StatusCode != 200 { + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, ErrWebrpcBadResponse.WithCausef("failed to read server error response body: %w", err) } - }() - - if err = ctx.Err(); err != nil { - return clientError("aborted because context was done", err) - } - if resp.StatusCode != 200 { - return errorFromResponse(resp) + var rpcErr WebRPCError + if err := json.Unmarshal(respBody, &rpcErr); err != nil { + return nil, ErrWebrpcBadResponse.WithCausef("failed to unmarshal server error: %w", err) + } + if rpcErr.Cause != "" { + rpcErr.cause = errors.New(rpcErr.Cause) + } + return nil, rpcErr } if out != nil { - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return clientError("failed to read response body", err) + return nil, ErrWebrpcBadResponse.WithCausef("failed to read response body: %w", err) } err = json.Unmarshal(respBody, &out) if err != nil { - return clientError("failed to unmarshal json response body", err) + return nil, ErrWebrpcBadResponse.WithCausef("failed to unmarshal JSON response body: %w", err) } - if err = ctx.Err(); err != nil { - return clientError("aborted because context was done", err) - } - } - - return nil -} - -// errorFromResponse builds a webrpc Error from a non-200 HTTP response. -func errorFromResponse(resp *http.Response) Error { - respBody, err := ioutil.ReadAll(resp.Body) - if err != nil { - return clientError("failed to read server error response body", err) - } - - var respErr ErrorPayload - if err := json.Unmarshal(respBody, &respErr); err != nil { - return clientError("failed unmarshal error response", err) } - errCode := ErrorCode(respErr.Code) - - if HTTPStatusFromErrorCode(errCode) == 0 { - return ErrorInternal("invalid code returned from server error response: %s", respErr.Code) - } - - return &rpcErr{ - code: errCode, - msg: respErr.Msg, - cause: errors.New(respErr.Cause), - } -} - -func clientError(desc string, err error) Error { - return WrapError(ErrInternal, err, desc) + return resp, nil } func WithHTTPRequestHeaders(ctx context.Context, h http.Header) (context.Context, error) { @@ -709,274 +990,139 @@ func HTTPRequestHeaders(ctx context.Context) (http.Header, bool) { // Helpers // -type ErrorPayload struct { - Status int `json:"status"` - Code string `json:"code"` - Cause string `json:"cause,omitempty"` - Msg string `json:"msg"` - Error string `json:"error"` +type method struct { + Name string + Service string + Annotations map[string]string } -type Error interface { - // Code is of the valid error codes - Code() ErrorCode - - // Msg returns a human-readable, unstructured messages describing the error - Msg() string - - // Cause is reason for the error - Cause() error - - // Error returns a string of the form "webrpc error : " - Error() string - - // Error response payload - Payload() ErrorPayload +type contextKey struct { + name string } -func Errorf(code ErrorCode, msgf string, args ...interface{}) Error { - msg := fmt.Sprintf(msgf, args...) - if IsValidErrorCode(code) { - return &rpcErr{code: code, msg: msg} - } - return &rpcErr{code: ErrInternal, msg: "invalid error type " + string(code)} +func (k *contextKey) String() string { + return "webrpc context value " + k.name } -func WrapError(code ErrorCode, cause error, format string, args ...interface{}) Error { - msg := fmt.Sprintf(format, args...) - if IsValidErrorCode(code) { - return &rpcErr{code: code, msg: msg, cause: cause} - } - return &rpcErr{code: ErrInternal, msg: "invalid error type " + string(code), cause: cause} -} +var ( + HTTPClientRequestHeadersCtxKey = &contextKey{"HTTPClientRequestHeaders"} + HTTPResponseWriterCtxKey = &contextKey{"HTTPResponseWriter"} -func ErrorNotFound(format string, args ...interface{}) Error { - return Errorf(ErrNotFound, format, args...) -} + HTTPRequestCtxKey = &contextKey{"HTTPRequest"} -func ErrorInvalidArgument(argument string, validationMsg string) Error { - return Errorf(ErrInvalidArgument, argument+" "+validationMsg) -} + ServiceNameCtxKey = &contextKey{"ServiceName"} -func ErrorRequiredArgument(argument string) Error { - return ErrorInvalidArgument(argument, "is required") -} + MethodNameCtxKey = &contextKey{"MethodName"} +) -func ErrorInternal(format string, args ...interface{}) Error { - return Errorf(ErrInternal, format, args...) +func ServiceNameFromContext(ctx context.Context) string { + service, _ := ctx.Value(ServiceNameCtxKey).(string) + return service } -type ErrorCode string +func MethodNameFromContext(ctx context.Context) string { + method, _ := ctx.Value(MethodNameCtxKey).(string) + return method +} -const ( - // Canceled indicates the operation was cancelled (typically by the caller). - ErrCanceled ErrorCode = "canceled" - - // Unknown error. For example when handling errors raised by APIs that do not - // return enough error information. - ErrUnknown ErrorCode = "unknown" - - // InvalidArgument indicates client specified an invalid argument. It - // indicates arguments that are problematic regardless of the state of the - // system (i.e. a malformed file name, required argument, number out of range, - // etc.). - ErrInvalidArgument ErrorCode = "invalid argument" - - // DeadlineExceeded means operation expired before completion. For operations - // that change the state of the system, this error may be returned even if the - // operation has completed successfully (timeout). - ErrDeadlineExceeded ErrorCode = "deadline exceeded" - - // NotFound means some requested entity was not found. - ErrNotFound ErrorCode = "not found" - - // BadRoute means that the requested URL path wasn't routable to a webrpc - // service and method. This is returned by the generated server, and usually - // shouldn't be returned by applications. Instead, applications should use - // NotFound or Unimplemented. - ErrBadRoute ErrorCode = "bad route" - - // AlreadyExists means an attempt to create an entity failed because one - // already exists. - ErrAlreadyExists ErrorCode = "already exists" - - // PermissionDenied indicates the caller does not have permission to execute - // the specified operation. It must not be used if the caller cannot be - // identified (Unauthenticated). - ErrPermissionDenied ErrorCode = "permission denied" - - // Unauthenticated indicates the request does not have valid authentication - // credentials for the operation. - ErrUnauthenticated ErrorCode = "unauthenticated" - - // ResourceExhausted indicates some resource has been exhausted, perhaps a - // per-user quota, or perhaps the entire file system is out of space. - ErrResourceExhausted ErrorCode = "resource exhausted" - - // FailedPrecondition indicates operation was rejected because the system is - // not in a state required for the operation's execution. For example, doing - // an rmdir operation on a directory that is non-empty, or on a non-directory - // object, or when having conflicting read-modify-write on the same resource. - ErrFailedPrecondition ErrorCode = "failed precondition" - - // Aborted indicates the operation was aborted, typically due to a concurrency - // issue like sequencer check failures, transaction aborts, etc. - ErrAborted ErrorCode = "aborted" - - // OutOfRange means operation was attempted past the valid range. For example, - // seeking or reading past end of a paginated collection. - // - // Unlike InvalidArgument, this error indicates a problem that may be fixed if - // the system state changes (i.e. adding more items to the collection). - // - // There is a fair bit of overlap between FailedPrecondition and OutOfRange. - // We recommend using OutOfRange (the more specific error) when it applies so - // that callers who are iterating through a space can easily look for an - // OutOfRange error to detect when they are done. - ErrOutOfRange ErrorCode = "out of range" - - // Unimplemented indicates operation is not implemented or not - // supported/enabled in this service. - ErrUnimplemented ErrorCode = "unimplemented" - - // Internal errors. When some invariants expected by the underlying system - // have been broken. In other words, something bad happened in the library or - // backend service. Do not confuse with HTTP Internal Server Error; an - // Internal error could also happen on the client code, i.e. when parsing a - // server response. - ErrInternal ErrorCode = "internal" - - // Unavailable indicates the service is currently unavailable. This is a most - // likely a transient condition and may be corrected by retrying with a - // backoff. - ErrUnavailable ErrorCode = "unavailable" - - // DataLoss indicates unrecoverable data loss or corruption. - ErrDataLoss ErrorCode = "data loss" - - // ErrNone is the zero-value, is considered an empty error and should not be - // used. - ErrNone ErrorCode = "" -) +func RequestFromContext(ctx context.Context) *http.Request { + r, _ := ctx.Value(HTTPRequestCtxKey).(*http.Request) + return r +} -func HTTPStatusFromErrorCode(code ErrorCode) int { - switch code { - case ErrCanceled: - return 408 // RequestTimeout - case ErrUnknown: - return 500 // Internal Server Error - case ErrInvalidArgument: - return 400 // BadRequest - case ErrDeadlineExceeded: - return 408 // RequestTimeout - case ErrNotFound: - return 404 // Not Found - case ErrBadRoute: - return 404 // Not Found - case ErrAlreadyExists: - return 409 // Conflict - case ErrPermissionDenied: - return 403 // Forbidden - case ErrUnauthenticated: - return 401 // Unauthorized - case ErrResourceExhausted: - return 403 // Forbidden - case ErrFailedPrecondition: - return 412 // Precondition Failed - case ErrAborted: - return 409 // Conflict - case ErrOutOfRange: - return 400 // Bad Request - case ErrUnimplemented: - return 501 // Not Implemented - case ErrInternal: - return 500 // Internal Server Error - case ErrUnavailable: - return 503 // Service Unavailable - case ErrDataLoss: - return 500 // Internal Server Error - case ErrNone: - return 200 // OK - default: - return 0 // Invalid! +func MethodCtx(ctx context.Context) (method, bool) { + req := RequestFromContext(ctx) + if req == nil { + return method{}, false } -} -func IsErrorCode(err error, code ErrorCode) bool { - if rpcErr, ok := err.(Error); ok { - if rpcErr.Code() == code { - return true - } + m, ok := methods[req.URL.Path] + if !ok { + return method{}, false } - return false -} -func IsValidErrorCode(code ErrorCode) bool { - return HTTPStatusFromErrorCode(code) != 0 + return m, true } -type rpcErr struct { - code ErrorCode - msg string - cause error +func ResponseWriterFromContext(ctx context.Context) http.ResponseWriter { + w, _ := ctx.Value(HTTPResponseWriterCtxKey).(http.ResponseWriter) + return w } -func (e *rpcErr) Code() ErrorCode { - return e.code -} +// +// Errors +// -func (e *rpcErr) Msg() string { - return e.msg +type WebRPCError struct { + Name string `json:"error"` + Code int `json:"code"` + Message string `json:"msg"` + Cause string `json:"cause,omitempty"` + HTTPStatus int `json:"status"` + cause error } -func (e *rpcErr) Cause() error { - return e.cause -} +var _ error = WebRPCError{} -func (e *rpcErr) Error() string { - if e.cause != nil && e.cause.Error() != "" { - if e.msg != "" { - return fmt.Sprintf("webrpc %s error: %s -- %s", e.code, e.cause.Error(), e.msg) - } else { - return fmt.Sprintf("webrpc %s error: %s", e.code, e.cause.Error()) - } - } else { - return fmt.Sprintf("webrpc %s error: %s", e.code, e.msg) +func (e WebRPCError) Error() string { + if e.cause != nil { + return fmt.Sprintf("%s %d: %s: %v", e.Name, e.Code, e.Message, e.cause) } + return fmt.Sprintf("%s %d: %s", e.Name, e.Code, e.Message) } -func (e *rpcErr) Payload() ErrorPayload { - statusCode := HTTPStatusFromErrorCode(e.Code()) - errPayload := ErrorPayload{ - Status: statusCode, - Code: string(e.Code()), - Msg: e.Msg(), - Error: e.Error(), +func (e WebRPCError) Is(target error) bool { + if target == nil { + return false } - if e.Cause() != nil { - errPayload.Cause = e.Cause().Error() + if rpcErr, ok := target.(WebRPCError); ok { + return rpcErr.Code == e.Code } - return errPayload + return errors.Is(e.cause, target) } -type contextKey struct { - name string +func (e WebRPCError) Unwrap() error { + return e.cause } -func (k *contextKey) String() string { - return "webrpc context value " + k.name +func (e WebRPCError) WithCause(cause error) WebRPCError { + err := e + err.cause = cause + err.Cause = cause.Error() + return err } -var ( - // For Client - HTTPClientRequestHeadersCtxKey = &contextKey{"HTTPClientRequestHeaders"} - - // For Server - HTTPResponseWriterCtxKey = &contextKey{"HTTPResponseWriter"} +func (e WebRPCError) WithCausef(format string, args ...interface{}) WebRPCError { + cause := fmt.Errorf(format, args...) + err := e + err.cause = cause + err.Cause = cause.Error() + return err +} - HTTPRequestCtxKey = &contextKey{"HTTPRequest"} +// Deprecated: Use .WithCause() method on WebRPCError. +func ErrorWithCause(rpcErr WebRPCError, cause error) WebRPCError { + return rpcErr.WithCause(cause) +} - ServiceNameCtxKey = &contextKey{"ServiceName"} +// Webrpc errors +var ( + ErrWebrpcEndpoint = WebRPCError{Code: 0, Name: "WebrpcEndpoint", Message: "endpoint error", HTTPStatus: 400} + ErrWebrpcRequestFailed = WebRPCError{Code: -1, Name: "WebrpcRequestFailed", Message: "request failed", HTTPStatus: 400} + ErrWebrpcBadRoute = WebRPCError{Code: -2, Name: "WebrpcBadRoute", Message: "bad route", HTTPStatus: 404} + ErrWebrpcBadMethod = WebRPCError{Code: -3, Name: "WebrpcBadMethod", Message: "bad method", HTTPStatus: 405} + ErrWebrpcBadRequest = WebRPCError{Code: -4, Name: "WebrpcBadRequest", Message: "bad request", HTTPStatus: 400} + ErrWebrpcBadResponse = WebRPCError{Code: -5, Name: "WebrpcBadResponse", Message: "bad response", HTTPStatus: 500} + ErrWebrpcServerPanic = WebRPCError{Code: -6, Name: "WebrpcServerPanic", Message: "server panic", HTTPStatus: 500} + ErrWebrpcInternalError = WebRPCError{Code: -7, Name: "WebrpcInternalError", Message: "internal error", HTTPStatus: 500} + ErrWebrpcClientDisconnected = WebRPCError{Code: -8, Name: "WebrpcClientDisconnected", Message: "client disconnected", HTTPStatus: 400} + ErrWebrpcStreamLost = WebRPCError{Code: -9, Name: "WebrpcStreamLost", Message: "stream lost", HTTPStatus: 400} + ErrWebrpcStreamFinished = WebRPCError{Code: -10, Name: "WebrpcStreamFinished", Message: "stream finished", HTTPStatus: 200} +) - MethodNameCtxKey = &contextKey{"MethodName"} +// Schema errors +var ( + ErrUserNotFound = WebRPCError{Code: 1000, Name: "UserNotFound", Message: "User not found", HTTPStatus: 404} + ErrUnauthorized = WebRPCError{Code: 2000, Name: "Unauthorized", Message: "Unauthorized access", HTTPStatus: 401} + ErrPermissionDenied = WebRPCError{Code: 3000, Name: "PermissionDenied", Message: "Permission denied", HTTPStatus: 403} ) diff --git a/_examples/golang-basics/example.ridl b/_examples/golang-basics/example.ridl index b4f9030b..a98beff9 100644 --- a/_examples/golang-basics/example.ridl +++ b/_examples/golang-basics/example.ridl @@ -1,18 +1,23 @@ -webrpc = v1 # version of webrpc schema format (ridl or json) +webrpc = v1 # version of webrpc schema format (ridl or json) -name = example # name if your backend app -version = v0.0.1 # version of your schema - +name = example # name of your backend app +version = v0.0.1 # version of your schema enum Kind: uint32 - USER - ADMIN +enum Intent: string + - openSession + - closeSession + - validateSession -message Empty +struct Empty - -message User +# User struct +# +# More information about user struct +struct User - id: uint64 + json = id + go.field.name = ID @@ -25,15 +30,21 @@ message User - role: string + go.tag.db = - -message SearchFilter + - kind: Kind + + json = kind + + - intent: Intent + + json = intent + +struct SearchFilter - q: string -message Version +struct Version - webrpcVersion: string - schemaVersion: string - schemaHash: string -message ComplexType +struct ComplexType - meta: map - metaNestedExample: map> - namesList: []string @@ -44,10 +55,41 @@ message ComplexType - mapOfUsers: map - user: User +error 1000 UserNotFound "User not found" HTTP 404 +error 2000 Unauthorized "Unauthorized access" HTTP 401 +error 3000 PermissionDenied "Permission denied" HTTP 403 service ExampleService + @internal - Ping() + @internal - Status() => (status: bool) + @internal - Version() => (version: Version) + @deprecated:GetUserV2 + @internal - GetUser(header: map, userID: uint64) => (code: uint32, user: User) + @public + @auth:"X-Access-Key,S2S,Cookies" + - GetUserV2(header: map, userID: uint64) => (code: uint32, user: User, profile: string) + @public - FindUser(s: SearchFilter) => (name: string, user: User) + @public + - GetIntents() => (intents: []Intent) + @public + - CountIntents(userId: uint64) => (count: map) + +service ExampleServiceV2 + - Ping() + - Status() => (status: bool) + +service AdminService + @public + @deprecated:Auth + - AuthOld() => (jwt: string) + @public + - Auth() => (jwt: string, role: string) + @internal + - Status() => (status: bool) + @internal + - Version() => (version: Version) diff --git a/_examples/golang-basics/example.webrpc.json b/_examples/golang-basics/example.webrpc.json index d046689c..d94b85ff 100644 --- a/_examples/golang-basics/example.webrpc.json +++ b/_examples/golang-basics/example.webrpc.json @@ -1,159 +1,172 @@ { - "webrpc": "v1", - "name": "example", - "version": "v0.0.1", - "messages": [ - { - "name": "Kind", - "type": "enum", - "fields": [ - { - "name": "USER", - "type": "uint32", - "value": "0" - }, - { - "name": "ADMIN", - "type": "uint32", - "value": "1" - } - ] - }, - { - "name": "Empty", - "type": "struct", - "fields": [ - ] - }, - { - "name": "User", - "type": "struct", - "fields": [ - { - "name": "ID", - "type": "uint64", - "optional": false, - "meta": [ - { "json": "id" }, - { "go.tag.db": "id" } - ] - }, - { - "name": "username", - "type": "string", - "optional": false, - "meta": [ - { "json": "USERNAME" }, - { "go.tag.db": "username" } - ] - }, - { - "name": "role", - "type": "string", - "optional": false, - "meta": [ - { "go.tag.db": "-" } - ] - }, - { - "name": "createdAt", - "type": "timestamp", - "optional": true, - "meta": [ - { "json": "created_at" }, - { "go.tag.json": "created_at,omitempty" }, - { "go.tag.db": "created_at" } - ] - } - - ] - }, - { - "name": "ComplexType", - "type": "struct", - "fields": [ - { - "name": "meta", - "type": "map" - }, - { - "name": "metaNestedExample", - "type": "map>" - }, - { - "name": "namesList", - "type": "[]string" - }, - { - "name": "numsList", - "type": "[]int64" - }, - { - "name": "doubleArray", - "type": "[][]string" - }, - { - "name": "listOfMaps", - "type": "[]map" - }, - { - "name": "listOfUsers", - "type": "[]User" - }, - { - "name": "mapOfUsers", - "type": "map" - }, - { - "name": "user", - "type": "User" - } - ] - } - ], - "services": [ - { - "name": "ExampleService", - "methods": [ - { - "name": "Ping", - "inputs": [], - "outputs": [] - }, - { - "name": "Status", - "inputs": [], - "outputs": [ - { - "name": "status", - "type": "bool" - } - ] - }, - { - "name": "GetUser", - "inputs": [ - { - "name": "header", - "type": "map" - }, - { - "name": "userID", - "type": "uint64" - } - ], - "outputs": [ - { - "name": "code", - "type": "uint32" - }, - { - "name": "user", - "type": "User" - } - ] - } - ] - } - ] -} + "webrpc": "v1", + "name": "example", + "version": "v0.0.1", + "types": [ + { + "name": "Kind", + "kind": "enum", + "type": "uint32", + "fields": [ + { + "name": "USER", + "value": "0" + }, + { + "name": "ADMIN", + "value": "1" + } + ] + }, + { + "name": "Empty", + "kind": "struct", + "fields": [] + }, + { + "name": "User", + "kind": "struct", + "fields": [ + { + "name": "ID", + "type": "uint64", + "optional": false, + "meta": [ + { + "json": "id" + }, + { + "go.tag.db": "id" + } + ] + }, + { + "name": "username", + "type": "string", + "optional": false, + "meta": [ + { + "json": "USERNAME" + }, + { + "go.tag.db": "username" + } + ] + }, + { + "name": "role", + "type": "string", + "optional": false, + "meta": [ + { + "go.tag.db": "-" + } + ] + }, + { + "name": "createdAt", + "type": "timestamp", + "optional": true, + "meta": [ + { + "json": "created_at" + }, + { + "go.tag.json": "created_at,omitempty" + }, + { + "go.tag.db": "created_at" + } + ] + } + ] + }, + { + "name": "ComplexType", + "kind": "struct", + "fields": [ + { + "name": "meta", + "type": "map" + }, + { + "name": "metaNestedExample", + "type": "map>" + }, + { + "name": "namesList", + "type": "[]string" + }, + { + "name": "numsList", + "type": "[]int64" + }, + { + "name": "doubleArray", + "type": "[][]string" + }, + { + "name": "listOfMaps", + "type": "[]map" + }, + { + "name": "listOfUsers", + "type": "[]User" + }, + { + "name": "mapOfUsers", + "type": "map" + }, + { + "name": "user", + "type": "User" + } + ] + } + ], + "services": [ + { + "name": "ExampleService", + "methods": [ + { + "name": "Ping", + "inputs": [], + "outputs": [] + }, + { + "name": "Status", + "inputs": [], + "outputs": [ + { + "name": "status", + "type": "bool" + } + ] + }, + { + "name": "GetUser", + "inputs": [ + { + "name": "header", + "type": "map" + }, + { + "name": "userID", + "type": "uint64" + } + ], + "outputs": [ + { + "name": "code", + "type": "uint32" + }, + { + "name": "user", + "type": "User" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/_examples/golang-basics/example_test.go b/_examples/golang-basics/example_test.go index ef88ad1f..700de6e7 100644 --- a/_examples/golang-basics/example_test.go +++ b/_examples/golang-basics/example_test.go @@ -38,20 +38,31 @@ func TestStatus(t *testing.T) { assert.NoError(t, err) } +func TestDeprecatedUserEndpoint(t *testing.T) { + arg1 := map[string]string{"a": "1"} + + _, _, err := client.GetUser(context.Background(), arg1, 12) + + assert.Error(t, err) +} + func TestGetUser(t *testing.T) { { arg1 := map[string]string{"a": "1"} - code, user, err := client.GetUser(context.Background(), arg1, 12) + code, user, _, err := client.GetUserV2(context.Background(), arg1, 12) + intent := Intent_openSession + kind := Kind_ADMIN + assert.Equal(t, uint32(200), code) - assert.Equal(t, &User{ID: 12, Username: "hihi"}, user) + assert.Equal(t, &User{ID: 12, Username: "hihi", Intent: intent, Kind: kind}, user) assert.NoError(t, err) } { // Error case, expecting to receive an error - code, user, err := client.GetUser(context.Background(), nil, 911) + code, user, _, err := client.GetUserV2(context.Background(), nil, 911) - assert.True(t, IsErrorCode(err, ErrNotFound)) + assert.ErrorAs(t, err, &ErrUserNotFound) assert.Nil(t, user) assert.Equal(t, uint32(0), code) assert.Error(t, err) diff --git a/_examples/golang-basics/go.mod b/_examples/golang-basics/go.mod new file mode 100644 index 00000000..48ffb177 --- /dev/null +++ b/_examples/golang-basics/go.mod @@ -0,0 +1,14 @@ +module github.com/webrpc/webrpc/_example/golang-basics + +go 1.22 + +require ( + github.com/go-chi/chi/v5 v5.0.12 + github.com/stretchr/testify v1.9.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/_examples/golang-basics/go.sum b/_examples/golang-basics/go.sum new file mode 100644 index 00000000..26aeb980 --- /dev/null +++ b/_examples/golang-basics/go.sum @@ -0,0 +1,12 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= +github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/_examples/golang-basics/internal/internal.gen.go b/_examples/golang-basics/internal/internal.gen.go new file mode 100644 index 00000000..e1c8b674 --- /dev/null +++ b/_examples/golang-basics/internal/internal.gen.go @@ -0,0 +1,984 @@ +// example v0.0.1 54e24d88b51908586cd13f6871d7a415cb968cdc +// -- +// Code generated by webrpc-gen with golang generator. DO NOT EDIT. +// +// webrpc-gen -schema=example.ridl -target=golang -pkg=internal -server -client -service=ExampleService,AdminService -ignore=@public,@deprecated -out=./internal/internal.gen.go +package internal + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "strings" +) + +const WebrpcHeader = "Webrpc" + +const WebrpcHeaderValue = "webrpc;gen-golang@v0.17.0;example@v0.0.1" + +// WebRPC description and code-gen version +func WebRPCVersion() string { + return "v1" +} + +// Schema version of your RIDL schema +func WebRPCSchemaVersion() string { + return "v0.0.1" +} + +// Schema hash generated from your RIDL schema +func WebRPCSchemaHash() string { + return "54e24d88b51908586cd13f6871d7a415cb968cdc" +} + +type WebrpcGenVersions struct { + WebrpcGenVersion string + CodeGenName string + CodeGenVersion string + SchemaName string + SchemaVersion string +} + +func VersionFromHeader(h http.Header) (*WebrpcGenVersions, error) { + if h.Get(WebrpcHeader) == "" { + return nil, fmt.Errorf("header is empty or missing") + } + + versions, err := parseWebrpcGenVersions(h.Get(WebrpcHeader)) + if err != nil { + return nil, fmt.Errorf("webrpc header is invalid: %w", err) + } + + return versions, nil +} + +func parseWebrpcGenVersions(header string) (*WebrpcGenVersions, error) { + versions := strings.Split(header, ";") + if len(versions) < 3 { + return nil, fmt.Errorf("expected at least 3 parts while parsing webrpc header: %v", header) + } + + _, webrpcGenVersion, ok := strings.Cut(versions[0], "@") + if !ok { + return nil, fmt.Errorf("webrpc gen version could not be parsed from: %s", versions[0]) + } + + tmplTarget, tmplVersion, ok := strings.Cut(versions[1], "@") + if !ok { + return nil, fmt.Errorf("tmplTarget and tmplVersion could not be parsed from: %s", versions[1]) + } + + schemaName, schemaVersion, ok := strings.Cut(versions[2], "@") + if !ok { + return nil, fmt.Errorf("schema name and schema version could not be parsed from: %s", versions[2]) + } + + return &WebrpcGenVersions{ + WebrpcGenVersion: webrpcGenVersion, + CodeGenName: tmplTarget, + CodeGenVersion: tmplVersion, + SchemaName: schemaName, + SchemaVersion: schemaVersion, + }, nil +} + +// +// Common types +// + +type Kind uint32 + +const ( + Kind_USER Kind = 0 + Kind_ADMIN Kind = 1 +) + +var Kind_name = map[uint32]string{ + 0: "USER", + 1: "ADMIN", +} + +var Kind_value = map[string]uint32{ + "USER": 0, + "ADMIN": 1, +} + +func (x Kind) String() string { + return Kind_name[uint32(x)] +} + +func (x Kind) MarshalText() ([]byte, error) { + return []byte(Kind_name[uint32(x)]), nil +} + +func (x *Kind) UnmarshalText(b []byte) error { + *x = Kind(Kind_value[string(b)]) + return nil +} + +func (x *Kind) Is(values ...Kind) bool { + if x == nil { + return false + } + for _, v := range values { + if *x == v { + return true + } + } + return false +} + +type Intent string + +const ( + Intent_openSession Intent = "openSession" + Intent_closeSession Intent = "closeSession" + Intent_validateSession Intent = "validateSession" +) + +func (x Intent) MarshalText() ([]byte, error) { + return []byte(x), nil +} + +func (x *Intent) UnmarshalText(b []byte) error { + *x = Intent(string(b)) + return nil +} + +func (x *Intent) Is(values ...Intent) bool { + if x == nil { + return false + } + for _, v := range values { + if *x == v { + return true + } + } + return false +} + +type Empty struct { +} + +// User struct +// +// More information about user struct +type User struct { + ID uint64 `json:"id" db:"id"` + Username string `json:"USERNAME" db:"username"` + Role string `json:"role" db:"-"` + Kind Kind `json:"kind"` + Intent Intent `json:"intent"` +} + +type SearchFilter struct { + Q string `json:"q"` +} + +type Version struct { + WebrpcVersion string `json:"webrpcVersion"` + SchemaVersion string `json:"schemaVersion"` + SchemaHash string `json:"schemaHash"` +} + +type ComplexType struct { + Meta map[string]interface{} `json:"meta"` + MetaNestedExample map[string]map[string]uint32 `json:"metaNestedExample"` + NamesList []string `json:"namesList"` + NumsList []int64 `json:"numsList"` + DoubleArray [][]string `json:"doubleArray"` + ListOfMaps []map[string]uint32 `json:"listOfMaps"` + ListOfUsers []*User `json:"listOfUsers"` + MapOfUsers map[string]*User `json:"mapOfUsers"` + User *User `json:"user"` +} + +var methods = map[string]method{ + "/rpc/ExampleService/Ping": { + Name: "Ping", + Service: "ExampleService", + Annotations: map[string]string{"internal": ""}, + }, + "/rpc/ExampleService/Status": { + Name: "Status", + Service: "ExampleService", + Annotations: map[string]string{"internal": ""}, + }, + "/rpc/ExampleService/Version": { + Name: "Version", + Service: "ExampleService", + Annotations: map[string]string{"internal": ""}, + }, + "/rpc/AdminService/Status": { + Name: "Status", + Service: "AdminService", + Annotations: map[string]string{"internal": ""}, + }, + "/rpc/AdminService/Version": { + Name: "Version", + Service: "AdminService", + Annotations: map[string]string{"internal": ""}, + }, +} + +func WebrpcMethods() map[string]method { + res := make(map[string]method, len(methods)) + for k, v := range methods { + res[k] = v + } + + return res +} + +var WebRPCServices = map[string][]string{ + "ExampleService": { + "Ping", + "Status", + "Version", + }, + "AdminService": { + "Status", + "Version", + }, +} + +// +// Server types +// + +type ExampleService interface { + Ping(ctx context.Context) error + Status(ctx context.Context) (bool, error) + Version(ctx context.Context) (*Version, error) +} + +type AdminService interface { + Status(ctx context.Context) (bool, error) + Version(ctx context.Context) (*Version, error) +} + +// +// Client types +// + +type ExampleServiceClient interface { + Ping(ctx context.Context) error + Status(ctx context.Context) (bool, error) + Version(ctx context.Context) (*Version, error) +} + +type AdminServiceClient interface { + Status(ctx context.Context) (bool, error) + Version(ctx context.Context) (*Version, error) +} + +// +// Server +// + +type WebRPCServer interface { + http.Handler +} + +type exampleServiceServer struct { + ExampleService + OnError func(r *http.Request, rpcErr *WebRPCError) + OnRequest func(w http.ResponseWriter, r *http.Request) error +} + +func NewExampleServiceServer(svc ExampleService) *exampleServiceServer { + return &exampleServiceServer{ + ExampleService: svc, + } +} + +func (s *exampleServiceServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { + defer func() { + // In case of a panic, serve a HTTP 500 error and then panic. + if rr := recover(); rr != nil { + s.sendErrorJSON(w, r, ErrWebrpcServerPanic.WithCausef("%v", rr)) + panic(rr) + } + }() + + w.Header().Set(WebrpcHeader, WebrpcHeaderValue) + + ctx := r.Context() + ctx = context.WithValue(ctx, HTTPResponseWriterCtxKey, w) + ctx = context.WithValue(ctx, HTTPRequestCtxKey, r) + ctx = context.WithValue(ctx, ServiceNameCtxKey, "ExampleService") + + r = r.WithContext(ctx) + + var handler func(ctx context.Context, w http.ResponseWriter, r *http.Request) + switch r.URL.Path { + case "/rpc/ExampleService/Ping": + handler = s.servePingJSON + case "/rpc/ExampleService/Status": + handler = s.serveStatusJSON + case "/rpc/ExampleService/Version": + handler = s.serveVersionJSON + default: + err := ErrWebrpcBadRoute.WithCausef("no webrpc method defined for path %v", r.URL.Path) + s.sendErrorJSON(w, r, err) + return + } + + if r.Method != "POST" { + w.Header().Add("Allow", "POST") // RFC 9110. + err := ErrWebrpcBadMethod.WithCausef("unsupported HTTP method %v (only POST is allowed)", r.Method) + s.sendErrorJSON(w, r, err) + return + } + + contentType := r.Header.Get("Content-Type") + if i := strings.Index(contentType, ";"); i >= 0 { + contentType = contentType[:i] + } + contentType = strings.TrimSpace(strings.ToLower(contentType)) + + switch contentType { + case "application/json": + if s.OnRequest != nil { + if err := s.OnRequest(w, r); err != nil { + rpcErr, ok := err.(WebRPCError) + if !ok { + rpcErr = ErrWebrpcEndpoint.WithCause(err) + } + s.sendErrorJSON(w, r, rpcErr) + return + } + } + + handler(ctx, w, r) + default: + err := ErrWebrpcBadRequest.WithCausef("unsupported Content-Type %q (only application/json is allowed)", r.Header.Get("Content-Type")) + s.sendErrorJSON(w, r, err) + } +} + +func (s *exampleServiceServer) servePingJSON(ctx context.Context, w http.ResponseWriter, r *http.Request) { + ctx = context.WithValue(ctx, MethodNameCtxKey, "Ping") + + // Call service method implementation. + err := s.ExampleService.Ping(ctx) + if err != nil { + rpcErr, ok := err.(WebRPCError) + if !ok { + rpcErr = ErrWebrpcEndpoint.WithCause(err) + } + s.sendErrorJSON(w, r, rpcErr) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte("{}")) +} + +func (s *exampleServiceServer) serveStatusJSON(ctx context.Context, w http.ResponseWriter, r *http.Request) { + ctx = context.WithValue(ctx, MethodNameCtxKey, "Status") + + // Call service method implementation. + ret0, err := s.ExampleService.Status(ctx) + if err != nil { + rpcErr, ok := err.(WebRPCError) + if !ok { + rpcErr = ErrWebrpcEndpoint.WithCause(err) + } + s.sendErrorJSON(w, r, rpcErr) + return + } + + respPayload := struct { + Ret0 bool `json:"status"` + }{ret0} + respBody, err := json.Marshal(respPayload) + if err != nil { + s.sendErrorJSON(w, r, ErrWebrpcBadResponse.WithCausef("failed to marshal json response: %w", err)) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write(respBody) +} + +func (s *exampleServiceServer) serveVersionJSON(ctx context.Context, w http.ResponseWriter, r *http.Request) { + ctx = context.WithValue(ctx, MethodNameCtxKey, "Version") + + // Call service method implementation. + ret0, err := s.ExampleService.Version(ctx) + if err != nil { + rpcErr, ok := err.(WebRPCError) + if !ok { + rpcErr = ErrWebrpcEndpoint.WithCause(err) + } + s.sendErrorJSON(w, r, rpcErr) + return + } + + respPayload := struct { + Ret0 *Version `json:"version"` + }{ret0} + respBody, err := json.Marshal(respPayload) + if err != nil { + s.sendErrorJSON(w, r, ErrWebrpcBadResponse.WithCausef("failed to marshal json response: %w", err)) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write(respBody) +} + +func (s *exampleServiceServer) sendErrorJSON(w http.ResponseWriter, r *http.Request, rpcErr WebRPCError) { + if s.OnError != nil { + s.OnError(r, &rpcErr) + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(rpcErr.HTTPStatus) + + respBody, _ := json.Marshal(rpcErr) + w.Write(respBody) +} + +type adminServiceServer struct { + AdminService + OnError func(r *http.Request, rpcErr *WebRPCError) + OnRequest func(w http.ResponseWriter, r *http.Request) error +} + +func NewAdminServiceServer(svc AdminService) *adminServiceServer { + return &adminServiceServer{ + AdminService: svc, + } +} + +func (s *adminServiceServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { + defer func() { + // In case of a panic, serve a HTTP 500 error and then panic. + if rr := recover(); rr != nil { + s.sendErrorJSON(w, r, ErrWebrpcServerPanic.WithCausef("%v", rr)) + panic(rr) + } + }() + + w.Header().Set(WebrpcHeader, WebrpcHeaderValue) + + ctx := r.Context() + ctx = context.WithValue(ctx, HTTPResponseWriterCtxKey, w) + ctx = context.WithValue(ctx, HTTPRequestCtxKey, r) + ctx = context.WithValue(ctx, ServiceNameCtxKey, "AdminService") + + r = r.WithContext(ctx) + + var handler func(ctx context.Context, w http.ResponseWriter, r *http.Request) + switch r.URL.Path { + case "/rpc/AdminService/Status": + handler = s.serveStatusJSON + case "/rpc/AdminService/Version": + handler = s.serveVersionJSON + default: + err := ErrWebrpcBadRoute.WithCausef("no webrpc method defined for path %v", r.URL.Path) + s.sendErrorJSON(w, r, err) + return + } + + if r.Method != "POST" { + w.Header().Add("Allow", "POST") // RFC 9110. + err := ErrWebrpcBadMethod.WithCausef("unsupported HTTP method %v (only POST is allowed)", r.Method) + s.sendErrorJSON(w, r, err) + return + } + + contentType := r.Header.Get("Content-Type") + if i := strings.Index(contentType, ";"); i >= 0 { + contentType = contentType[:i] + } + contentType = strings.TrimSpace(strings.ToLower(contentType)) + + switch contentType { + case "application/json": + if s.OnRequest != nil { + if err := s.OnRequest(w, r); err != nil { + rpcErr, ok := err.(WebRPCError) + if !ok { + rpcErr = ErrWebrpcEndpoint.WithCause(err) + } + s.sendErrorJSON(w, r, rpcErr) + return + } + } + + handler(ctx, w, r) + default: + err := ErrWebrpcBadRequest.WithCausef("unsupported Content-Type %q (only application/json is allowed)", r.Header.Get("Content-Type")) + s.sendErrorJSON(w, r, err) + } +} + +func (s *adminServiceServer) serveStatusJSON(ctx context.Context, w http.ResponseWriter, r *http.Request) { + ctx = context.WithValue(ctx, MethodNameCtxKey, "Status") + + // Call service method implementation. + ret0, err := s.AdminService.Status(ctx) + if err != nil { + rpcErr, ok := err.(WebRPCError) + if !ok { + rpcErr = ErrWebrpcEndpoint.WithCause(err) + } + s.sendErrorJSON(w, r, rpcErr) + return + } + + respPayload := struct { + Ret0 bool `json:"status"` + }{ret0} + respBody, err := json.Marshal(respPayload) + if err != nil { + s.sendErrorJSON(w, r, ErrWebrpcBadResponse.WithCausef("failed to marshal json response: %w", err)) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write(respBody) +} + +func (s *adminServiceServer) serveVersionJSON(ctx context.Context, w http.ResponseWriter, r *http.Request) { + ctx = context.WithValue(ctx, MethodNameCtxKey, "Version") + + // Call service method implementation. + ret0, err := s.AdminService.Version(ctx) + if err != nil { + rpcErr, ok := err.(WebRPCError) + if !ok { + rpcErr = ErrWebrpcEndpoint.WithCause(err) + } + s.sendErrorJSON(w, r, rpcErr) + return + } + + respPayload := struct { + Ret0 *Version `json:"version"` + }{ret0} + respBody, err := json.Marshal(respPayload) + if err != nil { + s.sendErrorJSON(w, r, ErrWebrpcBadResponse.WithCausef("failed to marshal json response: %w", err)) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write(respBody) +} + +func (s *adminServiceServer) sendErrorJSON(w http.ResponseWriter, r *http.Request, rpcErr WebRPCError) { + if s.OnError != nil { + s.OnError(r, &rpcErr) + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(rpcErr.HTTPStatus) + + respBody, _ := json.Marshal(rpcErr) + w.Write(respBody) +} + +func RespondWithError(w http.ResponseWriter, err error) { + rpcErr, ok := err.(WebRPCError) + if !ok { + rpcErr = ErrWebrpcEndpoint.WithCause(err) + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(rpcErr.HTTPStatus) + + respBody, _ := json.Marshal(rpcErr) + w.Write(respBody) +} + +// +// Client +// + +const ExampleServicePathPrefix = "/rpc/ExampleService/" +const AdminServicePathPrefix = "/rpc/AdminService/" + +type exampleServiceClient struct { + client HTTPClient + urls [3]string +} + +func NewExampleServiceClient(addr string, client HTTPClient) ExampleServiceClient { + prefix := urlBase(addr) + ExampleServicePathPrefix + urls := [3]string{ + prefix + "Ping", + prefix + "Status", + prefix + "Version", + } + return &exampleServiceClient{ + client: client, + urls: urls, + } +} + +func (c *exampleServiceClient) Ping(ctx context.Context) error { + + resp, err := doHTTPRequest(ctx, c.client, c.urls[0], nil, nil) + if resp != nil { + cerr := resp.Body.Close() + if err == nil && cerr != nil { + err = ErrWebrpcRequestFailed.WithCausef("failed to close response body: %w", cerr) + } + } + + return err +} + +func (c *exampleServiceClient) Status(ctx context.Context) (bool, error) { + out := struct { + Ret0 bool `json:"status"` + }{} + + resp, err := doHTTPRequest(ctx, c.client, c.urls[1], nil, &out) + if resp != nil { + cerr := resp.Body.Close() + if err == nil && cerr != nil { + err = ErrWebrpcRequestFailed.WithCausef("failed to close response body: %w", cerr) + } + } + + return out.Ret0, err +} + +func (c *exampleServiceClient) Version(ctx context.Context) (*Version, error) { + out := struct { + Ret0 *Version `json:"version"` + }{} + + resp, err := doHTTPRequest(ctx, c.client, c.urls[2], nil, &out) + if resp != nil { + cerr := resp.Body.Close() + if err == nil && cerr != nil { + err = ErrWebrpcRequestFailed.WithCausef("failed to close response body: %w", cerr) + } + } + + return out.Ret0, err +} + +type adminServiceClient struct { + client HTTPClient + urls [2]string +} + +func NewAdminServiceClient(addr string, client HTTPClient) AdminServiceClient { + prefix := urlBase(addr) + AdminServicePathPrefix + urls := [2]string{ + prefix + "Status", + prefix + "Version", + } + return &adminServiceClient{ + client: client, + urls: urls, + } +} + +func (c *adminServiceClient) Status(ctx context.Context) (bool, error) { + out := struct { + Ret0 bool `json:"status"` + }{} + + resp, err := doHTTPRequest(ctx, c.client, c.urls[0], nil, &out) + if resp != nil { + cerr := resp.Body.Close() + if err == nil && cerr != nil { + err = ErrWebrpcRequestFailed.WithCausef("failed to close response body: %w", cerr) + } + } + + return out.Ret0, err +} + +func (c *adminServiceClient) Version(ctx context.Context) (*Version, error) { + out := struct { + Ret0 *Version `json:"version"` + }{} + + resp, err := doHTTPRequest(ctx, c.client, c.urls[1], nil, &out) + if resp != nil { + cerr := resp.Body.Close() + if err == nil && cerr != nil { + err = ErrWebrpcRequestFailed.WithCausef("failed to close response body: %w", cerr) + } + } + + return out.Ret0, err +} + +// HTTPClient is the interface used by generated clients to send HTTP requests. +// It is fulfilled by *(net/http).Client, which is sufficient for most users. +// Users can provide their own implementation for special retry policies. +type HTTPClient interface { + Do(req *http.Request) (*http.Response, error) +} + +// urlBase helps ensure that addr specifies a scheme. If it is unparsable +// as a URL, it returns addr unchanged. +func urlBase(addr string) string { + // If the addr specifies a scheme, use it. If not, default to + // http. If url.Parse fails on it, return it unchanged. + url, err := url.Parse(addr) + if err != nil { + return addr + } + if url.Scheme == "" { + url.Scheme = "http" + } + return url.String() +} + +// newRequest makes an http.Request from a client, adding common headers. +func newRequest(ctx context.Context, url string, reqBody io.Reader, contentType string) (*http.Request, error) { + req, err := http.NewRequestWithContext(ctx, "POST", url, reqBody) + if err != nil { + return nil, err + } + req.Header.Set("Accept", contentType) + req.Header.Set("Content-Type", contentType) + req.Header.Set(WebrpcHeader, WebrpcHeaderValue) + if headers, ok := HTTPRequestHeaders(ctx); ok { + for k := range headers { + for _, v := range headers[k] { + req.Header.Add(k, v) + } + } + } + return req, nil +} + +// doHTTPRequest is common code to make a request to the remote service. +func doHTTPRequest(ctx context.Context, client HTTPClient, url string, in, out interface{}) (*http.Response, error) { + reqBody, err := json.Marshal(in) + if err != nil { + return nil, ErrWebrpcRequestFailed.WithCausef("failed to marshal JSON body: %w", err) + } + if err = ctx.Err(); err != nil { + return nil, ErrWebrpcRequestFailed.WithCausef("aborted because context was done: %w", err) + } + + req, err := newRequest(ctx, url, bytes.NewBuffer(reqBody), "application/json") + if err != nil { + return nil, ErrWebrpcRequestFailed.WithCausef("could not build request: %w", err) + } + + resp, err := client.Do(req) + if err != nil { + return nil, ErrWebrpcRequestFailed.WithCause(err) + } + + if resp.StatusCode != 200 { + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, ErrWebrpcBadResponse.WithCausef("failed to read server error response body: %w", err) + } + + var rpcErr WebRPCError + if err := json.Unmarshal(respBody, &rpcErr); err != nil { + return nil, ErrWebrpcBadResponse.WithCausef("failed to unmarshal server error: %w", err) + } + if rpcErr.Cause != "" { + rpcErr.cause = errors.New(rpcErr.Cause) + } + return nil, rpcErr + } + + if out != nil { + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, ErrWebrpcBadResponse.WithCausef("failed to read response body: %w", err) + } + + err = json.Unmarshal(respBody, &out) + if err != nil { + return nil, ErrWebrpcBadResponse.WithCausef("failed to unmarshal JSON response body: %w", err) + } + } + + return resp, nil +} + +func WithHTTPRequestHeaders(ctx context.Context, h http.Header) (context.Context, error) { + if _, ok := h["Accept"]; ok { + return nil, errors.New("provided header cannot set Accept") + } + if _, ok := h["Content-Type"]; ok { + return nil, errors.New("provided header cannot set Content-Type") + } + + copied := make(http.Header, len(h)) + for k, vv := range h { + if vv == nil { + copied[k] = nil + continue + } + copied[k] = make([]string, len(vv)) + copy(copied[k], vv) + } + + return context.WithValue(ctx, HTTPClientRequestHeadersCtxKey, copied), nil +} + +func HTTPRequestHeaders(ctx context.Context) (http.Header, bool) { + h, ok := ctx.Value(HTTPClientRequestHeadersCtxKey).(http.Header) + return h, ok +} + +// +// Helpers +// + +type method struct { + Name string + Service string + Annotations map[string]string +} + +type contextKey struct { + name string +} + +func (k *contextKey) String() string { + return "webrpc context value " + k.name +} + +var ( + HTTPClientRequestHeadersCtxKey = &contextKey{"HTTPClientRequestHeaders"} + HTTPResponseWriterCtxKey = &contextKey{"HTTPResponseWriter"} + + HTTPRequestCtxKey = &contextKey{"HTTPRequest"} + + ServiceNameCtxKey = &contextKey{"ServiceName"} + + MethodNameCtxKey = &contextKey{"MethodName"} +) + +func ServiceNameFromContext(ctx context.Context) string { + service, _ := ctx.Value(ServiceNameCtxKey).(string) + return service +} + +func MethodNameFromContext(ctx context.Context) string { + method, _ := ctx.Value(MethodNameCtxKey).(string) + return method +} + +func RequestFromContext(ctx context.Context) *http.Request { + r, _ := ctx.Value(HTTPRequestCtxKey).(*http.Request) + return r +} + +func MethodCtx(ctx context.Context) (method, bool) { + req := RequestFromContext(ctx) + if req == nil { + return method{}, false + } + + m, ok := methods[req.URL.Path] + if !ok { + return method{}, false + } + + return m, true +} + +func ResponseWriterFromContext(ctx context.Context) http.ResponseWriter { + w, _ := ctx.Value(HTTPResponseWriterCtxKey).(http.ResponseWriter) + return w +} + +// +// Errors +// + +type WebRPCError struct { + Name string `json:"error"` + Code int `json:"code"` + Message string `json:"msg"` + Cause string `json:"cause,omitempty"` + HTTPStatus int `json:"status"` + cause error +} + +var _ error = WebRPCError{} + +func (e WebRPCError) Error() string { + if e.cause != nil { + return fmt.Sprintf("%s %d: %s: %v", e.Name, e.Code, e.Message, e.cause) + } + return fmt.Sprintf("%s %d: %s", e.Name, e.Code, e.Message) +} + +func (e WebRPCError) Is(target error) bool { + if target == nil { + return false + } + if rpcErr, ok := target.(WebRPCError); ok { + return rpcErr.Code == e.Code + } + return errors.Is(e.cause, target) +} + +func (e WebRPCError) Unwrap() error { + return e.cause +} + +func (e WebRPCError) WithCause(cause error) WebRPCError { + err := e + err.cause = cause + err.Cause = cause.Error() + return err +} + +func (e WebRPCError) WithCausef(format string, args ...interface{}) WebRPCError { + cause := fmt.Errorf(format, args...) + err := e + err.cause = cause + err.Cause = cause.Error() + return err +} + +// Deprecated: Use .WithCause() method on WebRPCError. +func ErrorWithCause(rpcErr WebRPCError, cause error) WebRPCError { + return rpcErr.WithCause(cause) +} + +// Webrpc errors +var ( + ErrWebrpcEndpoint = WebRPCError{Code: 0, Name: "WebrpcEndpoint", Message: "endpoint error", HTTPStatus: 400} + ErrWebrpcRequestFailed = WebRPCError{Code: -1, Name: "WebrpcRequestFailed", Message: "request failed", HTTPStatus: 400} + ErrWebrpcBadRoute = WebRPCError{Code: -2, Name: "WebrpcBadRoute", Message: "bad route", HTTPStatus: 404} + ErrWebrpcBadMethod = WebRPCError{Code: -3, Name: "WebrpcBadMethod", Message: "bad method", HTTPStatus: 405} + ErrWebrpcBadRequest = WebRPCError{Code: -4, Name: "WebrpcBadRequest", Message: "bad request", HTTPStatus: 400} + ErrWebrpcBadResponse = WebRPCError{Code: -5, Name: "WebrpcBadResponse", Message: "bad response", HTTPStatus: 500} + ErrWebrpcServerPanic = WebRPCError{Code: -6, Name: "WebrpcServerPanic", Message: "server panic", HTTPStatus: 500} + ErrWebrpcInternalError = WebRPCError{Code: -7, Name: "WebrpcInternalError", Message: "internal error", HTTPStatus: 500} + ErrWebrpcClientDisconnected = WebRPCError{Code: -8, Name: "WebrpcClientDisconnected", Message: "client disconnected", HTTPStatus: 400} + ErrWebrpcStreamLost = WebRPCError{Code: -9, Name: "WebrpcStreamLost", Message: "stream lost", HTTPStatus: 400} + ErrWebrpcStreamFinished = WebRPCError{Code: -10, Name: "WebrpcStreamFinished", Message: "stream finished", HTTPStatus: 200} +) + +// Schema errors +var ( + ErrUserNotFound = WebRPCError{Code: 1000, Name: "UserNotFound", Message: "User not found", HTTPStatus: 404} + ErrUnauthorized = WebRPCError{Code: 2000, Name: "Unauthorized", Message: "Unauthorized access", HTTPStatus: 401} + ErrPermissionDenied = WebRPCError{Code: 3000, Name: "PermissionDenied", Message: "Permission denied", HTTPStatus: 403} +) diff --git a/_examples/golang-basics/main.go b/_examples/golang-basics/main.go index fa2ec9b2..bb32899e 100644 --- a/_examples/golang-basics/main.go +++ b/_examples/golang-basics/main.go @@ -1,14 +1,15 @@ -//go:generate ../../bin/webrpc-gen -schema=example.ridl -target=go -pkg=main -server -client -out=./example.gen.go package main import ( "context" - "errors" + "fmt" "log" "net/http" - "github.com/go-chi/chi" - "github.com/go-chi/chi/middleware" + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" + + "github.com/webrpc/webrpc/_example/golang-basics/admin" ) func main() { @@ -29,14 +30,60 @@ func startServer() error { }) webrpcHandler := NewExampleServiceServer(&ExampleServiceRPC{}) + webrpcHandler.OnError = func(r *http.Request, err *WebRPCError) { + m, ok := MethodCtx(r.Context()) + + if ok { + _, ok = m.Annotations["deprecated"] + if ok { + fmt.Println(r.URL.Path, "deprecated") + } + } + } + webrpcHandler.OnRequest = func(w http.ResponseWriter, r *http.Request) error { + m, ok := MethodCtx(r.Context()) + if !ok { + return fmt.Errorf("could not find method context for request method: %s\n", r.URL.Path) + } + + newEndpoint, ok := m.Annotations["deprecated"] + if ok { + return fmt.Errorf( + "endpoint %s has been deprecated in favor of endpoint %s\n", + r.URL.Path, + newEndpoint, + ) + } + + return nil + } + + r.Handle("/admin/*", admin.NewAdminServiceServer(&AdminServiceRPC{})) r.Handle("/*", webrpcHandler) return http.ListenAndServe(":4242", r) } -type ExampleServiceRPC struct { +type AdminServiceRPC struct{} + +func (*AdminServiceRPC) Auth(ctx context.Context) (string, string, error) { + return "jwt", "admin", nil +} + +func (s *AdminServiceRPC) Status(ctx context.Context) (bool, error) { + return true, nil } +func (s *AdminServiceRPC) Version(ctx context.Context) (*admin.Version, error) { + return &admin.Version{ + WebrpcVersion: WebRPCVersion(), + SchemaVersion: WebRPCSchemaVersion(), + SchemaHash: WebRPCSchemaHash(), + }, nil +} + +type ExampleServiceRPC struct{} + func (s *ExampleServiceRPC) Ping(ctx context.Context) error { return nil } @@ -55,21 +102,58 @@ func (s *ExampleServiceRPC) Version(ctx context.Context) (*Version, error) { func (s *ExampleServiceRPC) GetUser(ctx context.Context, header map[string]string, userID uint64) (uint32, *User, error) { if userID == 911 { - return 0, nil, WrapError(ErrInternal, errors.New("bad"), "app msg here") - // return 0, nil, ErrorNotFound("unknown userID %d", 911) - // return 0, nil, Errorf(ErrNotFound, "unknown userID %d", 911) - // return 0, nil, WrapError(ErrNotFound, nil, "unknown userID %d", 911) + return 0, nil, ErrUserNotFound } + if userID == 31337 { + return 0, nil, ErrorWithCause(ErrUserNotFound, fmt.Errorf("unknown user id %d", userID)) + } + + kind := Kind_ADMIN + intent := Intent_openSession + return 200, &User{ ID: userID, Username: "hihi", + Kind: kind, + Intent: intent, }, nil } +func (s *ExampleServiceRPC) GetUserV2(ctx context.Context, header map[string]string, userID uint64) (uint32, *User, string, error) { + if userID == 911 { + return 0, nil, "", ErrUserNotFound + } + if userID == 31337 { + return 0, nil, "", ErrorWithCause(ErrUserNotFound, fmt.Errorf("unknown user id %d", userID)) + } + + kind := Kind_ADMIN + intent := Intent_openSession + + return 200, &User{ + ID: userID, + Username: "hihi", + Kind: kind, + Intent: intent, + }, "https://www.google.com/images/john-doe.jpg", nil +} + func (s *ExampleServiceRPC) FindUser(ctx context.Context, f *SearchFilter) (string, *User, error) { name := f.Q return f.Q, &User{ ID: 123, Username: name, }, nil } + +func (s *ExampleServiceRPC) GetIntents(ctx context.Context) ([]Intent, error) { + return []Intent{Intent_openSession, Intent_closeSession, Intent_validateSession}, nil +} + +func (s *ExampleServiceRPC) CountIntents(ctx context.Context, userID uint64) (map[Intent]uint32, error) { + return map[Intent]uint32{ + Intent_openSession: 1, + Intent_closeSession: 2, + Intent_validateSession: 3, + }, nil +} diff --git a/_examples/golang-imports/api.gen.go b/_examples/golang-imports/api.gen.go index bb35daa7..1a14d7bf 100644 --- a/_examples/golang-imports/api.gen.go +++ b/_examples/golang-imports/api.gen.go @@ -1,7 +1,8 @@ -// example-api-service v1.0.0 0d503533eece70559645300ec8c6afd39be48810 +// example-api-service v1.0.0 f1b366018b1650b6a0a4f09ff793089ec62830a6 // -- -// This file has been generated by https://github.com/webrpc/webrpc using gen/golang -// Do not edit by hand. Update your webrpc schema and re-generate. +// Code generated by webrpc-gen@v0.14.0-dev with golang generator. DO NOT EDIT. +// +// webrpc-gen -schema=./proto/api.ridl -target=golang -pkg=main -server -client -out=./api.gen.go package main import ( @@ -11,7 +12,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "net/http" "net/url" "strings" @@ -29,11 +29,11 @@ func WebRPCSchemaVersion() string { // Schema hash generated from your RIDL schema func WebRPCSchemaHash() string { - return "0d503533eece70559645300ec8c6afd39be48810" + return "f1b366018b1650b6a0a4f09ff793089ec62830a6" } // -// Types +// Common types // type User struct { @@ -62,23 +62,27 @@ func (x Location) String() string { return Location_name[uint32(x)] } -func (x Location) MarshalJSON() ([]byte, error) { - buf := bytes.NewBufferString(`"`) - buf.WriteString(Location_name[uint32(x)]) - buf.WriteString(`"`) - return buf.Bytes(), nil +func (x Location) MarshalText() ([]byte, error) { + return []byte(Location_name[uint32(x)]), nil } -func (x *Location) UnmarshalJSON(b []byte) error { - var j string - err := json.Unmarshal(b, &j) - if err != nil { - return err - } - *x = Location(Location_value[j]) +func (x *Location) UnmarshalText(b []byte) error { + *x = Location(Location_value[string(b)]) return nil } +func (x *Location) Is(values ...Location) bool { + if x == nil { + return false + } + for _, v := range values { + if *x == v { + return true + } + } + return false +} + type ExampleAPI interface { Ping(ctx context.Context) error Status(ctx context.Context) (bool, error) @@ -103,128 +107,104 @@ type WebRPCServer interface { type exampleAPIServer struct { ExampleAPI + OnError func(r *http.Request, rpcErr *WebRPCError) } -func NewExampleAPIServer(svc ExampleAPI) WebRPCServer { +func NewExampleAPIServer(svc ExampleAPI) *exampleAPIServer { return &exampleAPIServer{ ExampleAPI: svc, } } func (s *exampleAPIServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { + defer func() { + // In case of a panic, serve a HTTP 500 error and then panic. + if rr := recover(); rr != nil { + s.sendErrorJSON(w, r, ErrWebrpcServerPanic.WithCause(fmt.Errorf("%v", rr))) + panic(rr) + } + }() + ctx := r.Context() ctx = context.WithValue(ctx, HTTPResponseWriterCtxKey, w) ctx = context.WithValue(ctx, HTTPRequestCtxKey, r) ctx = context.WithValue(ctx, ServiceNameCtxKey, "ExampleAPI") - if r.Method != "POST" { - err := Errorf(ErrBadRoute, "unsupported method %q (only POST is allowed)", r.Method) - RespondWithError(w, err) - return - } - + var handler func(ctx context.Context, w http.ResponseWriter, r *http.Request) switch r.URL.Path { case "/rpc/ExampleAPI/Ping": - s.servePing(ctx, w, r) - return + handler = s.servePingJSON case "/rpc/ExampleAPI/Status": - s.serveStatus(ctx, w, r) - return + handler = s.serveStatusJSON case "/rpc/ExampleAPI/GetUsers": - s.serveGetUsers(ctx, w, r) - return + handler = s.serveGetUsersJSON default: - err := Errorf(ErrBadRoute, "no handler for path %q", r.URL.Path) - RespondWithError(w, err) + err := ErrWebrpcBadRoute.WithCause(fmt.Errorf("no handler for path %q", r.URL.Path)) + s.sendErrorJSON(w, r, err) + return + } + + if r.Method != "POST" { + w.Header().Add("Allow", "POST") // RFC 9110. + err := ErrWebrpcBadMethod.WithCause(fmt.Errorf("unsupported method %q (only POST is allowed)", r.Method)) + s.sendErrorJSON(w, r, err) return } -} -func (s *exampleAPIServer) servePing(ctx context.Context, w http.ResponseWriter, r *http.Request) { - header := r.Header.Get("Content-Type") - i := strings.Index(header, ";") - if i == -1 { - i = len(header) + contentType := r.Header.Get("Content-Type") + if i := strings.Index(contentType, ";"); i >= 0 { + contentType = contentType[:i] } + contentType = strings.TrimSpace(strings.ToLower(contentType)) - switch strings.TrimSpace(strings.ToLower(header[:i])) { + switch contentType { case "application/json": - s.servePingJSON(ctx, w, r) + handler(ctx, w, r) default: - err := Errorf(ErrBadRoute, "unexpected Content-Type: %q", r.Header.Get("Content-Type")) - RespondWithError(w, err) + err := ErrWebrpcBadRequest.WithCause(fmt.Errorf("unexpected Content-Type: %q", r.Header.Get("Content-Type"))) + s.sendErrorJSON(w, r, err) } } func (s *exampleAPIServer) servePingJSON(ctx context.Context, w http.ResponseWriter, r *http.Request) { - var err error ctx = context.WithValue(ctx, MethodNameCtxKey, "Ping") - // Call service method - func() { - defer func() { - // In case of a panic, serve a 500 error and then panic. - if rr := recover(); rr != nil { - RespondWithError(w, ErrorInternal("internal service panic")) - panic(rr) - } - }() - err = s.ExampleAPI.Ping(ctx) - }() - + // Call service method implementation. + err := s.ExampleAPI.Ping(ctx) if err != nil { - RespondWithError(w, err) + rpcErr, ok := err.(WebRPCError) + if !ok { + rpcErr = ErrWebrpcEndpoint.WithCause(err) + } + s.sendErrorJSON(w, r, rpcErr) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) -} - -func (s *exampleAPIServer) serveStatus(ctx context.Context, w http.ResponseWriter, r *http.Request) { - header := r.Header.Get("Content-Type") - i := strings.Index(header, ";") - if i == -1 { - i = len(header) - } - - switch strings.TrimSpace(strings.ToLower(header[:i])) { - case "application/json": - s.serveStatusJSON(ctx, w, r) - default: - err := Errorf(ErrBadRoute, "unexpected Content-Type: %q", r.Header.Get("Content-Type")) - RespondWithError(w, err) - } + w.Write([]byte("{}")) } func (s *exampleAPIServer) serveStatusJSON(ctx context.Context, w http.ResponseWriter, r *http.Request) { - var err error ctx = context.WithValue(ctx, MethodNameCtxKey, "Status") - // Call service method - var ret0 bool - func() { - defer func() { - // In case of a panic, serve a 500 error and then panic. - if rr := recover(); rr != nil { - RespondWithError(w, ErrorInternal("internal service panic")) - panic(rr) - } - }() - ret0, err = s.ExampleAPI.Status(ctx) - }() - respContent := struct { - Ret0 bool `json:"status"` - }{ret0} - + // Call service method implementation. + ret0, err := s.ExampleAPI.Status(ctx) if err != nil { - RespondWithError(w, err) + rpcErr, ok := err.(WebRPCError) + if !ok { + rpcErr = ErrWebrpcEndpoint.WithCause(err) + } + s.sendErrorJSON(w, r, rpcErr) return } - respBody, err := json.Marshal(respContent) + + respPayload := struct { + Ret0 bool `json:"status"` + }{ret0} + respBody, err := json.Marshal(respPayload) if err != nil { - err = WrapError(ErrInternal, err, "failed to marshal json response") - RespondWithError(w, err) + s.sendErrorJSON(w, r, ErrWebrpcBadResponse.WithCause(fmt.Errorf("failed to marshal json response: %w", err))) return } @@ -233,52 +213,27 @@ func (s *exampleAPIServer) serveStatusJSON(ctx context.Context, w http.ResponseW w.Write(respBody) } -func (s *exampleAPIServer) serveGetUsers(ctx context.Context, w http.ResponseWriter, r *http.Request) { - header := r.Header.Get("Content-Type") - i := strings.Index(header, ";") - if i == -1 { - i = len(header) - } - - switch strings.TrimSpace(strings.ToLower(header[:i])) { - case "application/json": - s.serveGetUsersJSON(ctx, w, r) - default: - err := Errorf(ErrBadRoute, "unexpected Content-Type: %q", r.Header.Get("Content-Type")) - RespondWithError(w, err) - } -} - func (s *exampleAPIServer) serveGetUsersJSON(ctx context.Context, w http.ResponseWriter, r *http.Request) { - var err error ctx = context.WithValue(ctx, MethodNameCtxKey, "GetUsers") - // Call service method - var ret0 []*User - var ret1 *Location - func() { - defer func() { - // In case of a panic, serve a 500 error and then panic. - if rr := recover(); rr != nil { - RespondWithError(w, ErrorInternal("internal service panic")) - panic(rr) - } - }() - ret0, ret1, err = s.ExampleAPI.GetUsers(ctx) - }() - respContent := struct { - Ret0 []*User `json:"users"` - Ret1 *Location `json:"location"` - }{ret0, ret1} - + // Call service method implementation. + ret0, ret1, err := s.ExampleAPI.GetUsers(ctx) if err != nil { - RespondWithError(w, err) + rpcErr, ok := err.(WebRPCError) + if !ok { + rpcErr = ErrWebrpcEndpoint.WithCause(err) + } + s.sendErrorJSON(w, r, rpcErr) return } - respBody, err := json.Marshal(respContent) + + respPayload := struct { + Ret0 []*User `json:"users"` + Ret1 *Location `json:"location"` + }{ret0, ret1} + respBody, err := json.Marshal(respPayload) if err != nil { - err = WrapError(ErrInternal, err, "failed to marshal json response") - RespondWithError(w, err) + s.sendErrorJSON(w, r, ErrWebrpcBadResponse.WithCause(fmt.Errorf("failed to marshal json response: %w", err))) return } @@ -287,18 +242,27 @@ func (s *exampleAPIServer) serveGetUsersJSON(ctx context.Context, w http.Respons w.Write(respBody) } +func (s *exampleAPIServer) sendErrorJSON(w http.ResponseWriter, r *http.Request, rpcErr WebRPCError) { + if s.OnError != nil { + s.OnError(r, &rpcErr) + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(rpcErr.HTTPStatus) + + respBody, _ := json.Marshal(rpcErr) + w.Write(respBody) +} func RespondWithError(w http.ResponseWriter, err error) { - rpcErr, ok := err.(Error) + rpcErr, ok := err.(WebRPCError) if !ok { - rpcErr = WrapError(ErrInternal, err, "webrpc error") + rpcErr = ErrWebrpcEndpoint.WithCause(err) } - statusCode := HTTPStatusFromErrorCode(rpcErr.Code()) - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(statusCode) + w.WriteHeader(rpcErr.HTTPStatus) - respBody, _ := json.Marshal(rpcErr.Payload()) + respBody, _ := json.Marshal(rpcErr) w.Write(respBody) } @@ -327,7 +291,6 @@ func NewExampleAPIClient(addr string, client HTTPClient) ExampleAPI { } func (c *exampleAPIClient) Ping(ctx context.Context) error { - err := doJSONRequest(ctx, c.client, c.urls[0], nil, nil) return err } @@ -395,83 +358,63 @@ func newRequest(ctx context.Context, url string, reqBody io.Reader, contentType func doJSONRequest(ctx context.Context, client HTTPClient, url string, in, out interface{}) error { reqBody, err := json.Marshal(in) if err != nil { - return clientError("failed to marshal json request", err) + return ErrWebrpcRequestFailed.WithCause(fmt.Errorf("failed to marshal JSON body: %w", err)) } if err = ctx.Err(); err != nil { - return clientError("aborted because context was done", err) + return ErrWebrpcRequestFailed.WithCause(fmt.Errorf("aborted because context was done: %w", err)) } req, err := newRequest(ctx, url, bytes.NewBuffer(reqBody), "application/json") if err != nil { - return clientError("could not build request", err) + return ErrWebrpcRequestFailed.WithCause(fmt.Errorf("could not build request: %w", err)) } resp, err := client.Do(req) if err != nil { - return clientError("request failed", err) + return ErrWebrpcRequestFailed.WithCause(err) } defer func() { cerr := resp.Body.Close() if err == nil && cerr != nil { - err = clientError("failed to close response body", cerr) + err = ErrWebrpcRequestFailed.WithCause(fmt.Errorf("failed to close response body: %w", cerr)) } }() if err = ctx.Err(); err != nil { - return clientError("aborted because context was done", err) + return ErrWebrpcRequestFailed.WithCause(fmt.Errorf("aborted because context was done: %w", err)) } if resp.StatusCode != 200 { - return errorFromResponse(resp) + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return ErrWebrpcBadResponse.WithCause(fmt.Errorf("failed to read server error response body: %w", err)) + } + + var rpcErr WebRPCError + if err := json.Unmarshal(respBody, &rpcErr); err != nil { + return ErrWebrpcBadResponse.WithCause(fmt.Errorf("failed to unmarshal server error: %w", err)) + } + if rpcErr.Cause != "" { + rpcErr.cause = errors.New(rpcErr.Cause) + } + return rpcErr } if out != nil { - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return clientError("failed to read response body", err) + return ErrWebrpcBadResponse.WithCause(fmt.Errorf("failed to read response body: %w", err)) } err = json.Unmarshal(respBody, &out) if err != nil { - return clientError("failed to unmarshal json response body", err) - } - if err = ctx.Err(); err != nil { - return clientError("aborted because context was done", err) + return ErrWebrpcBadResponse.WithCause(fmt.Errorf("failed to unmarshal JSON response body: %w", err)) } } return nil } -// errorFromResponse builds a webrpc Error from a non-200 HTTP response. -func errorFromResponse(resp *http.Response) Error { - respBody, err := ioutil.ReadAll(resp.Body) - if err != nil { - return clientError("failed to read server error response body", err) - } - - var respErr ErrorPayload - if err := json.Unmarshal(respBody, &respErr); err != nil { - return clientError("failed unmarshal error response", err) - } - - errCode := ErrorCode(respErr.Code) - - if HTTPStatusFromErrorCode(errCode) == 0 { - return ErrorInternal("invalid code returned from server error response: %s", respErr.Code) - } - - return &rpcErr{ - code: errCode, - msg: respErr.Msg, - cause: errors.New(respErr.Cause), - } -} - -func clientError(desc string, err error) Error { - return WrapError(ErrInternal, err, desc) -} - func WithHTTPRequestHeaders(ctx context.Context, h http.Header) (context.Context, error) { if _, ok := h["Accept"]; ok { return nil, errors.New("provided header cannot set Accept") @@ -502,287 +445,97 @@ func HTTPRequestHeaders(ctx context.Context) (http.Header, bool) { // Helpers // -type ErrorPayload struct { - Status int `json:"status"` - Code string `json:"code"` - Cause string `json:"cause,omitempty"` - Msg string `json:"msg"` - Error string `json:"error"` +type contextKey struct { + name string } -type Error interface { - // Code is of the valid error codes - Code() ErrorCode - - // Msg returns a human-readable, unstructured messages describing the error - Msg() string - - // Cause is reason for the error - Cause() error +func (k *contextKey) String() string { + return "webrpc context value " + k.name +} - // Error returns a string of the form "webrpc error : " - Error() string +var ( + HTTPClientRequestHeadersCtxKey = &contextKey{"HTTPClientRequestHeaders"} + HTTPResponseWriterCtxKey = &contextKey{"HTTPResponseWriter"} - // Error response payload - Payload() ErrorPayload -} + HTTPRequestCtxKey = &contextKey{"HTTPRequest"} -func Errorf(code ErrorCode, msgf string, args ...interface{}) Error { - msg := fmt.Sprintf(msgf, args...) - if IsValidErrorCode(code) { - return &rpcErr{code: code, msg: msg} - } - return &rpcErr{code: ErrInternal, msg: "invalid error type " + string(code)} -} + ServiceNameCtxKey = &contextKey{"ServiceName"} -func WrapError(code ErrorCode, cause error, format string, args ...interface{}) Error { - msg := fmt.Sprintf(format, args...) - if IsValidErrorCode(code) { - return &rpcErr{code: code, msg: msg, cause: cause} - } - return &rpcErr{code: ErrInternal, msg: "invalid error type " + string(code), cause: cause} -} + MethodNameCtxKey = &contextKey{"MethodName"} +) -func Failf(format string, args ...interface{}) Error { - return Errorf(ErrFail, format, args...) +func ServiceNameFromContext(ctx context.Context) string { + service, _ := ctx.Value(ServiceNameCtxKey).(string) + return service } -func WrapFailf(cause error, format string, args ...interface{}) Error { - return WrapError(ErrFail, cause, format, args...) +func MethodNameFromContext(ctx context.Context) string { + method, _ := ctx.Value(MethodNameCtxKey).(string) + return method } -func ErrorNotFound(format string, args ...interface{}) Error { - return Errorf(ErrNotFound, format, args...) +func RequestFromContext(ctx context.Context) *http.Request { + r, _ := ctx.Value(HTTPRequestCtxKey).(*http.Request) + return r } - -func ErrorInvalidArgument(argument string, validationMsg string) Error { - return Errorf(ErrInvalidArgument, argument+" "+validationMsg) +func ResponseWriterFromContext(ctx context.Context) http.ResponseWriter { + w, _ := ctx.Value(HTTPResponseWriterCtxKey).(http.ResponseWriter) + return w } -func ErrorRequiredArgument(argument string) Error { - return ErrorInvalidArgument(argument, "is required") -} +// +// Errors +// -func ErrorInternal(format string, args ...interface{}) Error { - return Errorf(ErrInternal, format, args...) +type WebRPCError struct { + Name string `json:"error"` + Code int `json:"code"` + Message string `json:"msg"` + Cause string `json:"cause,omitempty"` + HTTPStatus int `json:"status"` + cause error } -type ErrorCode string +var _ error = WebRPCError{} -const ( - // Unknown error. For example when handling errors raised by APIs that do not - // return enough error information. - ErrUnknown ErrorCode = "unknown" - - // Fail error. General failure error type. - ErrFail ErrorCode = "fail" - - // Canceled indicates the operation was cancelled (typically by the caller). - ErrCanceled ErrorCode = "canceled" - - // InvalidArgument indicates client specified an invalid argument. It - // indicates arguments that are problematic regardless of the state of the - // system (i.e. a malformed file name, required argument, number out of range, - // etc.). - ErrInvalidArgument ErrorCode = "invalid argument" - - // DeadlineExceeded means operation expired before completion. For operations - // that change the state of the system, this error may be returned even if the - // operation has completed successfully (timeout). - ErrDeadlineExceeded ErrorCode = "deadline exceeded" - - // NotFound means some requested entity was not found. - ErrNotFound ErrorCode = "not found" - - // BadRoute means that the requested URL path wasn't routable to a webrpc - // service and method. This is returned by the generated server, and usually - // shouldn't be returned by applications. Instead, applications should use - // NotFound or Unimplemented. - ErrBadRoute ErrorCode = "bad route" - - // AlreadyExists means an attempt to create an entity failed because one - // already exists. - ErrAlreadyExists ErrorCode = "already exists" - - // PermissionDenied indicates the caller does not have permission to execute - // the specified operation. It must not be used if the caller cannot be - // identified (Unauthenticated). - ErrPermissionDenied ErrorCode = "permission denied" - - // Unauthenticated indicates the request does not have valid authentication - // credentials for the operation. - ErrUnauthenticated ErrorCode = "unauthenticated" - - // ResourceExhausted indicates some resource has been exhausted, perhaps a - // per-user quota, or perhaps the entire file system is out of space. - ErrResourceExhausted ErrorCode = "resource exhausted" - - // FailedPrecondition indicates operation was rejected because the system is - // not in a state required for the operation's execution. For example, doing - // an rmdir operation on a directory that is non-empty, or on a non-directory - // object, or when having conflicting read-modify-write on the same resource. - ErrFailedPrecondition ErrorCode = "failed precondition" - - // Aborted indicates the operation was aborted, typically due to a concurrency - // issue like sequencer check failures, transaction aborts, etc. - ErrAborted ErrorCode = "aborted" - - // OutOfRange means operation was attempted past the valid range. For example, - // seeking or reading past end of a paginated collection. - // - // Unlike InvalidArgument, this error indicates a problem that may be fixed if - // the system state changes (i.e. adding more items to the collection). - // - // There is a fair bit of overlap between FailedPrecondition and OutOfRange. - // We recommend using OutOfRange (the more specific error) when it applies so - // that callers who are iterating through a space can easily look for an - // OutOfRange error to detect when they are done. - ErrOutOfRange ErrorCode = "out of range" - - // Unimplemented indicates operation is not implemented or not - // supported/enabled in this service. - ErrUnimplemented ErrorCode = "unimplemented" - - // Internal errors. When some invariants expected by the underlying system - // have been broken. In other words, something bad happened in the library or - // backend service. Do not confuse with HTTP Internal Server Error; an - // Internal error could also happen on the client code, i.e. when parsing a - // server response. - ErrInternal ErrorCode = "internal" - - // Unavailable indicates the service is currently unavailable. This is a most - // likely a transient condition and may be corrected by retrying with a - // backoff. - ErrUnavailable ErrorCode = "unavailable" - - // DataLoss indicates unrecoverable data loss or corruption. - ErrDataLoss ErrorCode = "data loss" - - // ErrNone is the zero-value, is considered an empty error and should not be - // used. - ErrNone ErrorCode = "" -) - -func HTTPStatusFromErrorCode(code ErrorCode) int { - switch code { - case ErrCanceled: - return 408 // RequestTimeout - case ErrUnknown: - return 400 // Bad Request - case ErrFail: - return 422 // Unprocessable Entity - case ErrInvalidArgument: - return 400 // BadRequest - case ErrDeadlineExceeded: - return 408 // RequestTimeout - case ErrNotFound: - return 404 // Not Found - case ErrBadRoute: - return 404 // Not Found - case ErrAlreadyExists: - return 409 // Conflict - case ErrPermissionDenied: - return 403 // Forbidden - case ErrUnauthenticated: - return 401 // Unauthorized - case ErrResourceExhausted: - return 403 // Forbidden - case ErrFailedPrecondition: - return 412 // Precondition Failed - case ErrAborted: - return 409 // Conflict - case ErrOutOfRange: - return 400 // Bad Request - case ErrUnimplemented: - return 501 // Not Implemented - case ErrInternal: - return 500 // Internal Server Error - case ErrUnavailable: - return 503 // Service Unavailable - case ErrDataLoss: - return 500 // Internal Server Error - case ErrNone: - return 200 // OK - default: - return 0 // Invalid! +func (e WebRPCError) Error() string { + if e.cause != nil { + return fmt.Sprintf("%s %d: %s: %v", e.Name, e.Code, e.Message, e.cause) } + return fmt.Sprintf("%s %d: %s", e.Name, e.Code, e.Message) } -func IsErrorCode(err error, code ErrorCode) bool { - if rpcErr, ok := err.(Error); ok { - if rpcErr.Code() == code { - return true - } +func (e WebRPCError) Is(target error) bool { + if rpcErr, ok := target.(WebRPCError); ok { + return rpcErr.Code == e.Code } - return false -} - -func IsValidErrorCode(code ErrorCode) bool { - return HTTPStatusFromErrorCode(code) != 0 + return errors.Is(e.cause, target) } -type rpcErr struct { - code ErrorCode - msg string - cause error -} - -func (e *rpcErr) Code() ErrorCode { - return e.code -} - -func (e *rpcErr) Msg() string { - return e.msg -} - -func (e *rpcErr) Cause() error { +func (e WebRPCError) Unwrap() error { return e.cause } -func (e *rpcErr) Error() string { - if e.cause != nil && e.cause.Error() != "" { - if e.msg != "" { - return fmt.Sprintf("webrpc %s error: %s -- %s", e.code, e.cause.Error(), e.msg) - } else { - return fmt.Sprintf("webrpc %s error: %s", e.code, e.cause.Error()) - } - } else { - return fmt.Sprintf("webrpc %s error: %s", e.code, e.msg) - } -} - -func (e *rpcErr) Payload() ErrorPayload { - statusCode := HTTPStatusFromErrorCode(e.Code()) - errPayload := ErrorPayload{ - Status: statusCode, - Code: string(e.Code()), - Msg: e.Msg(), - Error: e.Error(), - } - if e.Cause() != nil { - errPayload.Cause = e.Cause().Error() - } - return errPayload -} - -type contextKey struct { - name string +func (e WebRPCError) WithCause(cause error) WebRPCError { + err := e + err.cause = cause + err.Cause = cause.Error() + return err } -func (k *contextKey) String() string { - return "webrpc context value " + k.name +// Deprecated: Use .WithCause() method on WebRPCError. +func ErrorWithCause(rpcErr WebRPCError, cause error) WebRPCError { + return rpcErr.WithCause(cause) } +// Webrpc errors var ( - // For Client - HTTPClientRequestHeadersCtxKey = &contextKey{"HTTPClientRequestHeaders"} - - // For Server - HTTPResponseWriterCtxKey = &contextKey{"HTTPResponseWriter"} - - HTTPRequestCtxKey = &contextKey{"HTTPRequest"} - - ServiceNameCtxKey = &contextKey{"ServiceName"} - - MethodNameCtxKey = &contextKey{"MethodName"} + ErrWebrpcEndpoint = WebRPCError{Code: 0, Name: "WebrpcEndpoint", Message: "endpoint error", HTTPStatus: 400} + ErrWebrpcRequestFailed = WebRPCError{Code: -1, Name: "WebrpcRequestFailed", Message: "request failed", HTTPStatus: 400} + ErrWebrpcBadRoute = WebRPCError{Code: -2, Name: "WebrpcBadRoute", Message: "bad route", HTTPStatus: 404} + ErrWebrpcBadMethod = WebRPCError{Code: -3, Name: "WebrpcBadMethod", Message: "bad method", HTTPStatus: 405} + ErrWebrpcBadRequest = WebRPCError{Code: -4, Name: "WebrpcBadRequest", Message: "bad request", HTTPStatus: 400} + ErrWebrpcBadResponse = WebRPCError{Code: -5, Name: "WebrpcBadResponse", Message: "bad response", HTTPStatus: 500} + ErrWebrpcServerPanic = WebRPCError{Code: -6, Name: "WebrpcServerPanic", Message: "server panic", HTTPStatus: 500} + ErrWebrpcInternalError = WebRPCError{Code: -7, Name: "WebrpcInternalError", Message: "internal error", HTTPStatus: 500} ) diff --git a/_examples/golang-imports/go.mod b/_examples/golang-imports/go.mod new file mode 100644 index 00000000..f58d79da --- /dev/null +++ b/_examples/golang-imports/go.mod @@ -0,0 +1,14 @@ +module github.com/webrpc/webrpc/_example/golang-imports + +go 1.19 + +require ( + github.com/go-chi/chi/v5 v5.0.12 + github.com/stretchr/testify v1.8.1 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/_examples/golang-imports/go.sum b/_examples/golang-imports/go.sum new file mode 100644 index 00000000..3009b183 --- /dev/null +++ b/_examples/golang-imports/go.sum @@ -0,0 +1,19 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= +github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/_examples/golang-imports/golang-imports b/_examples/golang-imports/golang-imports deleted file mode 100755 index 5d43a359..00000000 Binary files a/_examples/golang-imports/golang-imports and /dev/null differ diff --git a/_examples/golang-imports/main.go b/_examples/golang-imports/main.go index af794724..0f01b739 100644 --- a/_examples/golang-imports/main.go +++ b/_examples/golang-imports/main.go @@ -1,4 +1,4 @@ -//go:generate ../../bin/webrpc-gen -schema=./proto/api.ridl -target=go -pkg=main -server -client -out=./api.gen.go +//go:generate webrpc-gen -schema=./proto/api.ridl -target=golang -pkg=main -server -client -out=./api.gen.go package main import ( @@ -6,8 +6,8 @@ import ( "log" "net/http" - "github.com/go-chi/chi" - "github.com/go-chi/chi/middleware" + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" ) func main() { diff --git a/_examples/golang-imports/proto/types.ridl b/_examples/golang-imports/proto/types.ridl index 16f2223d..24f1e5be 100644 --- a/_examples/golang-imports/proto/types.ridl +++ b/_examples/golang-imports/proto/types.ridl @@ -1,5 +1,5 @@ webrpc = v1 -message User +struct User - username: string - age: uint32 diff --git a/_examples/golang-imports/proto/util.ridl b/_examples/golang-imports/proto/util.ridl index 55ceac20..ab0e73d4 100644 --- a/_examples/golang-imports/proto/util.ridl +++ b/_examples/golang-imports/proto/util.ridl @@ -4,5 +4,5 @@ enum Location: uint32 - TORONTO - NEW_YORK -message Setting +struct Setting - config: map diff --git a/_examples/golang-nodejs/.gitignore b/_examples/golang-nodejs/.gitignore index 5be2eea6..47a03034 100644 --- a/_examples/golang-nodejs/.gitignore +++ b/_examples/golang-nodejs/.gitignore @@ -1,3 +1,4 @@ node_modules/ package-lock.json yarn.lock +vendor/ diff --git a/_examples/golang-nodejs/Makefile b/_examples/golang-nodejs/Makefile index 06366123..4c5f93b3 100644 --- a/_examples/golang-nodejs/Makefile +++ b/_examples/golang-nodejs/Makefile @@ -1,12 +1,16 @@ all: - @echo "please read Makefile source or README to see available commands" + @awk -F'[ :]' '!/^all:/ && /^([A-z_-]+):/ {print "make " $$1}' Makefile + +install: + go mod vendor && go mod tidy + cd client && npm install generate: - ../../bin/webrpc-gen -schema=example.webrpc.json -target=go -pkg=main -server -out=./server/server.gen.go - ../../bin/webrpc-gen -schema=example.webrpc.json -target=js -client -out=./client/client.gen.mjs + webrpc-gen -schema=example.webrpc.json -target=golang -pkg=main -server -out=./server/server.gen.go + webrpc-gen -schema=example.webrpc.json -target=javascript -client -out=./client/client.gen.mjs run-server: - go run ./server + go run lsserver/cmd/api.go run-client: cd ./client && npm start diff --git a/_examples/golang-nodejs/client/client.gen.mjs b/_examples/golang-nodejs/client/client.gen.mjs index b5dd4e90..ee628217 100644 --- a/_examples/golang-nodejs/client/client.gen.mjs +++ b/_examples/golang-nodejs/client/client.gen.mjs @@ -1,7 +1,8 @@ -// example v0.0.1 07d79ad7e0e7bc2320ac29fdd065307ce932cf47 +// example v0.0.1 84e6fe32d953376911ab8964f8438e051844efb9 // -- -// This file has been generated by https://github.com/webrpc/webrpc using gen/javascript -// Do not edit by hand. Update your webrpc schema and re-generate. +// Code generated by webrpc-gen with javascript generator. DO NOT EDIT. +// +// webrpc-gen -schema=example.webrpc.json -target=javascript -client -out=./client/client.gen.mjs // WebRPC description and code-gen version export const WebRPCVersion = "v1" @@ -10,8 +11,7 @@ export const WebRPCVersion = "v1" export const WebRPCSchemaVersion = " v0.0.1" // Schema hash generated from your RIDL schema -export const WebRPCSchemaHash = "07d79ad7e0e7bc2320ac29fdd065307ce932cf47" - +export const WebRPCSchemaHash = "84e6fe32d953376911ab8964f8438e051844efb9" // // Types @@ -27,7 +27,6 @@ export class Empty { constructor(_data) { this._data = {} if (_data) { - } } @@ -41,9 +40,9 @@ export class GetUserRequest { this._data = {} if (_data) { this._data['userID'] = _data['userID'] - } } + get userID() { return this._data['userID'] } @@ -63,21 +62,23 @@ export class User { this._data['id'] = _data['id'] this._data['USERNAME'] = _data['USERNAME'] this._data['created_at'] = _data['created_at'] - } } + get id() { return this._data['id'] } set id(value) { this._data['id'] = value } + get USERNAME() { return this._data['USERNAME'] } set USERNAME(value) { this._data['USERNAME'] = value } + get created_at() { return this._data['created_at'] } @@ -103,57 +104,65 @@ export class RandomStuff { this._data['listOfUsers'] = _data['listOfUsers'] this._data['mapOfUsers'] = _data['mapOfUsers'] this._data['user'] = _data['user'] - } } + get meta() { return this._data['meta'] } set meta(value) { this._data['meta'] = value } + get metaNestedExample() { return this._data['metaNestedExample'] } set metaNestedExample(value) { this._data['metaNestedExample'] = value } + get namesList() { return this._data['namesList'] } set namesList(value) { this._data['namesList'] = value } + get numsList() { return this._data['numsList'] } set numsList(value) { this._data['numsList'] = value } + get doubleArray() { return this._data['doubleArray'] } set doubleArray(value) { this._data['doubleArray'] = value } + get listOfMaps() { return this._data['listOfMaps'] } set listOfMaps(value) { this._data['listOfMaps'] = value } + get listOfUsers() { return this._data['listOfUsers'] } set listOfUsers(value) { this._data['listOfUsers'] = value } + get mapOfUsers() { return this._data['mapOfUsers'] } set mapOfUsers(value) { this._data['mapOfUsers'] = value } + get user() { return this._data['user'] } @@ -175,7 +184,7 @@ export class ExampleService { constructor(hostname, fetch) { this.path = '/rpc/ExampleService/' this.hostname = hostname - this.fetch = fetch + this.fetch = (input, init) => fetch(input, init) } url(name) { diff --git a/_examples/golang-nodejs/example.webrpc.json b/_examples/golang-nodejs/example.webrpc.json index 63993713..68848e0d 100644 --- a/_examples/golang-nodejs/example.webrpc.json +++ b/_examples/golang-nodejs/example.webrpc.json @@ -1,148 +1,168 @@ { - "webrpc": "v1", - "name": "example", - "version":" v0.0.1", - "messages": [ - { - "name": "Kind", - "type": "enum", - "fields": [ - { - "name": "USER", - "type": "uint32", - "value": "1" - }, - { - "name": "ADMIN", - "type": "uint32", - "value": "2" - } - ] - }, - { - "name": "Empty", - "type": "struct", - "fields": [ - ] - }, - { - "name": "GetUserRequest", - "type": "struct", - "fields": [ - { - "name": "userID", - "type": "uint64", - "optional": false - } - ] - }, - { - "name": "User", - "type": "struct", - "fields": [ - { - "name": "ID", - "type": "uint64", - "optional": false, - "meta": [ - { "json": "id" }, - { "go.tag.db": "id" } - ] - }, - { - "name": "username", - "type": "string", - "optional": false, - "meta": [ - { "json": "USERNAME" }, - { "go.tag.db": "username" } - ] - }, - { - "name": "createdAt", - "type": "timestamp", - "optional": true, - "meta": [ - { "json": "created_at,omitempty" }, - { "go.tag.db": "created_at" } - ] - } - - ] - }, - { - "name": "RandomStuff", - "type": "struct", - "fields": [ - { - "name": "meta", - "type": "map" - }, - { - "name": "metaNestedExample", - "type": "map>" - }, - { - "name": "namesList", - "type": "[]string" - }, - { - "name": "numsList", - "type": "[]int64" - }, - { - "name": "doubleArray", - "type": "[][]string" - }, - { - "name": "listOfMaps", - "type": "[]map" - }, - { - "name": "listOfUsers", - "type": "[]User" - }, - { - "name": "mapOfUsers", - "type": "map" - }, - { - "name": "user", - "type": "User" - } - ] - } - ], - "services": [ - { - "name": "ExampleService", - "methods": [ - { - "name": "Ping", - "inputs": [], - "outputs": [ - { - "name": "status", - "type": "bool" - } - ] - }, - { - "name": "GetUser", - "inputs": [ - { - "name": "req", - "type": "GetUserRequest" - } - ], - "outputs": [ - { - "name": "user", - "type": "User" - } - ] - } - ] - } - ] + "webrpc": "v1", + "name": "example", + "version": " v0.0.1", + "errors": [ + { + "code": 1000, + "name": "UserNotFound", + "message": "user not found", + "httpStatus": 404 + } + ], + "types": [ + { + "name": "Kind", + "kind": "enum", + "type": "uint32", + "fields": [ + { + "name": "USER", + "value": "1" + }, + { + "name": "ADMIN", + "value": "2" + } + ] + }, + { + "name": "Empty", + "kind": "struct", + "fields": [] + }, + { + "name": "GetUserRequest", + "kind": "struct", + "fields": [ + { + "name": "userID", + "type": "uint64", + "optional": false + } + ] + }, + { + "name": "User", + "kind": "struct", + "fields": [ + { + "name": "ID", + "type": "uint64", + "optional": false, + "meta": [ + { + "json": "id" + }, + { + "go.tag.db": "id" + } + ] + }, + { + "name": "username", + "type": "string", + "optional": false, + "meta": [ + { + "json": "USERNAME" + }, + { + "go.tag.db": "username" + } + ] + }, + { + "name": "createdAt", + "type": "timestamp", + "optional": true, + "meta": [ + { + "json": "created_at" + }, + { + "go.tag.json": "created_at,omitempty" + }, + { + "go.tag.db": "created_at" + } + ] + } + ] + }, + { + "name": "RandomStuff", + "kind": "struct", + "fields": [ + { + "name": "meta", + "type": "map" + }, + { + "name": "metaNestedExample", + "type": "map>" + }, + { + "name": "namesList", + "type": "[]string" + }, + { + "name": "numsList", + "type": "[]int64" + }, + { + "name": "doubleArray", + "type": "[][]string" + }, + { + "name": "listOfMaps", + "type": "[]map" + }, + { + "name": "listOfUsers", + "type": "[]User" + }, + { + "name": "mapOfUsers", + "type": "map" + }, + { + "name": "user", + "type": "User" + } + ] + } + ], + "services": [ + { + "name": "ExampleService", + "methods": [ + { + "name": "Ping", + "inputs": [], + "outputs": [ + { + "name": "status", + "type": "bool" + } + ] + }, + { + "name": "GetUser", + "inputs": [ + { + "name": "req", + "type": "GetUserRequest" + } + ], + "outputs": [ + { + "name": "user", + "type": "User" + } + ] + } + ] + } + ] } diff --git a/_examples/golang-nodejs/go.mod b/_examples/golang-nodejs/go.mod new file mode 100644 index 00000000..1057f7db --- /dev/null +++ b/_examples/golang-nodejs/go.mod @@ -0,0 +1,5 @@ +module github.com/webrpc/webrpc/_example/golang-nodejs + +go 1.19 + +require github.com/go-chi/chi/v5 v5.0.12 diff --git a/_examples/golang-nodejs/go.sum b/_examples/golang-nodejs/go.sum new file mode 100644 index 00000000..bfc91747 --- /dev/null +++ b/_examples/golang-nodejs/go.sum @@ -0,0 +1,2 @@ +github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= +github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= diff --git a/_examples/golang-nodejs/server/cmd/api.go b/_examples/golang-nodejs/server/cmd/api.go new file mode 100644 index 00000000..651d5fd5 --- /dev/null +++ b/_examples/golang-nodejs/server/cmd/api.go @@ -0,0 +1,35 @@ +package main + +import ( + "log" + "net/http" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" + "github.com/webrpc/webrpc/_example/golang-nodejs/server/pkg/rpc" +) + +func main() { + err := startServer() + if err != nil { + log.Fatal(err) + } +} + +func startServer() error { + r := chi.NewRouter() + r.Use(middleware.RequestID) + r.Use(middleware.Logger) + r.Use(middleware.Recoverer) + + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(".")) + }) + + webrpcHandler := rpc.NewExampleServiceServer(&rpc.ExampleServiceRPC{}) + r.Handle("/*", webrpcHandler) + + log.Printf("Starting webrpc server on localhost:4242") + + return http.ListenAndServe(":4242", r) +} diff --git a/_examples/golang-nodejs/server/pkg/rpc/server.go b/_examples/golang-nodejs/server/pkg/rpc/server.go new file mode 100644 index 00000000..2fdfb50f --- /dev/null +++ b/_examples/golang-nodejs/server/pkg/rpc/server.go @@ -0,0 +1,23 @@ +package rpc + +import ( + "context" +) + +type ExampleServiceRPC struct { +} + +func (s *ExampleServiceRPC) Ping(ctx context.Context) (bool, error) { + return true, nil +} + +func (s *ExampleServiceRPC) GetUser(ctx context.Context, req *GetUserRequest) (*User, error) { + if req.UserID == 911 { + return nil, ErrUserNotFound + } + + return &User{ + ID: req.UserID, + Username: "hihi", + }, nil +} diff --git a/_examples/golang-nodejs/server/server.gen.go b/_examples/golang-nodejs/server/server.gen.go index e814679f..70e2f285 100644 --- a/_examples/golang-nodejs/server/server.gen.go +++ b/_examples/golang-nodejs/server/server.gen.go @@ -1,20 +1,25 @@ -// example v0.0.1 07d79ad7e0e7bc2320ac29fdd065307ce932cf47 +// example v0.0.1 84e6fe32d953376911ab8964f8438e051844efb9 // -- -// This file has been generated by https://github.com/webrpc/webrpc using gen/golang -// Do not edit by hand. Update your webrpc schema and re-generate. +// Code generated by webrpc-gen with golang generator. DO NOT EDIT. +// +// webrpc-gen -schema=example.webrpc.json -target=golang -pkg=main -server -out=./server/server.gen.go package main import ( - "bytes" "context" "encoding/json" + "errors" "fmt" - "io/ioutil" + "io" "net/http" "strings" "time" ) +const WebrpcHeader = "Webrpc" + +const WebrpcHeaderValue = "webrpc;gen-golang@v0.17.0;example@ v0.0.1" + // WebRPC description and code-gen version func WebRPCVersion() string { return "v1" @@ -27,11 +32,62 @@ func WebRPCSchemaVersion() string { // Schema hash generated from your RIDL schema func WebRPCSchemaHash() string { - return "07d79ad7e0e7bc2320ac29fdd065307ce932cf47" + return "84e6fe32d953376911ab8964f8438e051844efb9" +} + +type WebrpcGenVersions struct { + WebrpcGenVersion string + CodeGenName string + CodeGenVersion string + SchemaName string + SchemaVersion string +} + +func VersionFromHeader(h http.Header) (*WebrpcGenVersions, error) { + if h.Get(WebrpcHeader) == "" { + return nil, fmt.Errorf("header is empty or missing") + } + + versions, err := parseWebrpcGenVersions(h.Get(WebrpcHeader)) + if err != nil { + return nil, fmt.Errorf("webrpc header is invalid: %w", err) + } + + return versions, nil +} + +func parseWebrpcGenVersions(header string) (*WebrpcGenVersions, error) { + versions := strings.Split(header, ";") + if len(versions) < 3 { + return nil, fmt.Errorf("expected at least 3 parts while parsing webrpc header: %v", header) + } + + _, webrpcGenVersion, ok := strings.Cut(versions[0], "@") + if !ok { + return nil, fmt.Errorf("webrpc gen version could not be parsed from: %s", versions[0]) + } + + tmplTarget, tmplVersion, ok := strings.Cut(versions[1], "@") + if !ok { + return nil, fmt.Errorf("tmplTarget and tmplVersion could not be parsed from: %s", versions[1]) + } + + schemaName, schemaVersion, ok := strings.Cut(versions[2], "@") + if !ok { + return nil, fmt.Errorf("schema name and schema version could not be parsed from: %s", versions[2]) + } + + return &WebrpcGenVersions{ + WebrpcGenVersion: webrpcGenVersion, + CodeGenName: tmplTarget, + CodeGenVersion: tmplVersion, + SchemaName: schemaName, + SchemaVersion: schemaVersion, + }, nil } // -// Types +// Common types // type Kind uint32 @@ -55,23 +111,27 @@ func (x Kind) String() string { return Kind_name[uint32(x)] } -func (x Kind) MarshalJSON() ([]byte, error) { - buf := bytes.NewBufferString(`"`) - buf.WriteString(Kind_name[uint32(x)]) - buf.WriteString(`"`) - return buf.Bytes(), nil +func (x Kind) MarshalText() ([]byte, error) { + return []byte(Kind_name[uint32(x)]), nil } -func (x *Kind) UnmarshalJSON(b []byte) error { - var j string - err := json.Unmarshal(b, &j) - if err != nil { - return err - } - *x = Kind(Kind_value[j]) +func (x *Kind) UnmarshalText(b []byte) error { + *x = Kind(Kind_value[string(b)]) return nil } +func (x *Kind) Is(values ...Kind) bool { + if x == nil { + return false + } + for _, v := range values { + if *x == v { + return true + } + } + return false +} + type Empty struct { } @@ -97,9 +157,26 @@ type RandomStuff struct { User *User `json:"user"` } -type ExampleService interface { - Ping(ctx context.Context) (bool, error) - GetUser(ctx context.Context, req *GetUserRequest) (*User, error) +var methods = map[string]method{ + "/rpc/ExampleService/Ping": { + Name: "Ping", + Service: "ExampleService", + Annotations: map[string]string{}, + }, + "/rpc/ExampleService/GetUser": { + Name: "GetUser", + Service: "ExampleService", + Annotations: map[string]string{}, + }, +} + +func WebrpcMethods() map[string]method { + res := make(map[string]method, len(methods)) + for k, v := range methods { + res[k] = v + } + + return res } var WebRPCServices = map[string][]string{ @@ -109,6 +186,24 @@ var WebRPCServices = map[string][]string{ }, } +// +// Server types +// + +type ExampleService interface { + Ping(ctx context.Context) (bool, error) + GetUser(ctx context.Context, req *GetUserRequest) (*User, error) +} + +// +// Client types +// + +type ExampleServiceClient interface { + Ping(ctx context.Context) (bool, error) + GetUser(ctx context.Context, req *GetUserRequest) (*User, error) +} + // // Server // @@ -119,84 +214,99 @@ type WebRPCServer interface { type exampleServiceServer struct { ExampleService + OnError func(r *http.Request, rpcErr *WebRPCError) + OnRequest func(w http.ResponseWriter, r *http.Request) error } -func NewExampleServiceServer(svc ExampleService) WebRPCServer { +func NewExampleServiceServer(svc ExampleService) *exampleServiceServer { return &exampleServiceServer{ ExampleService: svc, } } func (s *exampleServiceServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { + defer func() { + // In case of a panic, serve a HTTP 500 error and then panic. + if rr := recover(); rr != nil { + s.sendErrorJSON(w, r, ErrWebrpcServerPanic.WithCausef("%v", rr)) + panic(rr) + } + }() + + w.Header().Set(WebrpcHeader, WebrpcHeaderValue) + ctx := r.Context() ctx = context.WithValue(ctx, HTTPResponseWriterCtxKey, w) ctx = context.WithValue(ctx, HTTPRequestCtxKey, r) ctx = context.WithValue(ctx, ServiceNameCtxKey, "ExampleService") - if r.Method != "POST" { - err := Errorf(ErrBadRoute, "unsupported method %q (only POST is allowed)", r.Method) - RespondWithError(w, err) - return - } + r = r.WithContext(ctx) + var handler func(ctx context.Context, w http.ResponseWriter, r *http.Request) switch r.URL.Path { case "/rpc/ExampleService/Ping": - s.servePing(ctx, w, r) - return + handler = s.servePingJSON case "/rpc/ExampleService/GetUser": - s.serveGetUser(ctx, w, r) - return + handler = s.serveGetUserJSON default: - err := Errorf(ErrBadRoute, "no handler for path %q", r.URL.Path) - RespondWithError(w, err) + err := ErrWebrpcBadRoute.WithCausef("no webrpc method defined for path %v", r.URL.Path) + s.sendErrorJSON(w, r, err) return } -} -func (s *exampleServiceServer) servePing(ctx context.Context, w http.ResponseWriter, r *http.Request) { - header := r.Header.Get("Content-Type") - i := strings.Index(header, ";") - if i == -1 { - i = len(header) + if r.Method != "POST" { + w.Header().Add("Allow", "POST") // RFC 9110. + err := ErrWebrpcBadMethod.WithCausef("unsupported HTTP method %v (only POST is allowed)", r.Method) + s.sendErrorJSON(w, r, err) + return } - switch strings.TrimSpace(strings.ToLower(header[:i])) { + contentType := r.Header.Get("Content-Type") + if i := strings.Index(contentType, ";"); i >= 0 { + contentType = contentType[:i] + } + contentType = strings.TrimSpace(strings.ToLower(contentType)) + + switch contentType { case "application/json": - s.servePingJSON(ctx, w, r) + if s.OnRequest != nil { + if err := s.OnRequest(w, r); err != nil { + rpcErr, ok := err.(WebRPCError) + if !ok { + rpcErr = ErrWebrpcEndpoint.WithCause(err) + } + s.sendErrorJSON(w, r, rpcErr) + return + } + } + + handler(ctx, w, r) default: - err := Errorf(ErrBadRoute, "unexpected Content-Type: %q", r.Header.Get("Content-Type")) - RespondWithError(w, err) + err := ErrWebrpcBadRequest.WithCausef("unsupported Content-Type %q (only application/json is allowed)", r.Header.Get("Content-Type")) + s.sendErrorJSON(w, r, err) } } func (s *exampleServiceServer) servePingJSON(ctx context.Context, w http.ResponseWriter, r *http.Request) { - var err error ctx = context.WithValue(ctx, MethodNameCtxKey, "Ping") - // Call service method - var ret0 bool - func() { - defer func() { - // In case of a panic, serve a 500 error and then panic. - if rr := recover(); rr != nil { - RespondWithError(w, ErrorInternal("internal service panic")) - panic(rr) - } - }() - ret0, err = s.ExampleService.Ping(ctx) - }() - respContent := struct { - Ret0 bool `json:"status"` - }{ret0} - + // Call service method implementation. + ret0, err := s.ExampleService.Ping(ctx) if err != nil { - RespondWithError(w, err) + rpcErr, ok := err.(WebRPCError) + if !ok { + rpcErr = ErrWebrpcEndpoint.WithCause(err) + } + s.sendErrorJSON(w, r, rpcErr) return } - respBody, err := json.Marshal(respContent) + + respPayload := struct { + Ret0 bool `json:"status"` + }{ret0} + respBody, err := json.Marshal(respPayload) if err != nil { - err = WrapError(ErrInternal, err, "failed to marshal json response") - RespondWithError(w, err) + s.sendErrorJSON(w, r, ErrWebrpcBadResponse.WithCausef("failed to marshal json response: %w", err)) return } @@ -205,68 +315,41 @@ func (s *exampleServiceServer) servePingJSON(ctx context.Context, w http.Respons w.Write(respBody) } -func (s *exampleServiceServer) serveGetUser(ctx context.Context, w http.ResponseWriter, r *http.Request) { - header := r.Header.Get("Content-Type") - i := strings.Index(header, ";") - if i == -1 { - i = len(header) - } - - switch strings.TrimSpace(strings.ToLower(header[:i])) { - case "application/json": - s.serveGetUserJSON(ctx, w, r) - default: - err := Errorf(ErrBadRoute, "unexpected Content-Type: %q", r.Header.Get("Content-Type")) - RespondWithError(w, err) - } -} - func (s *exampleServiceServer) serveGetUserJSON(ctx context.Context, w http.ResponseWriter, r *http.Request) { - var err error ctx = context.WithValue(ctx, MethodNameCtxKey, "GetUser") - reqContent := struct { - Arg0 *GetUserRequest `json:"req"` - }{} - reqBody, err := ioutil.ReadAll(r.Body) + reqBody, err := io.ReadAll(r.Body) if err != nil { - err = WrapError(ErrInternal, err, "failed to read request data") - RespondWithError(w, err) + s.sendErrorJSON(w, r, ErrWebrpcBadRequest.WithCausef("failed to read request data: %w", err)) return } defer r.Body.Close() - err = json.Unmarshal(reqBody, &reqContent) - if err != nil { - err = WrapError(ErrInvalidArgument, err, "failed to unmarshal request data") - RespondWithError(w, err) + reqPayload := struct { + Arg0 *GetUserRequest `json:"req"` + }{} + if err := json.Unmarshal(reqBody, &reqPayload); err != nil { + s.sendErrorJSON(w, r, ErrWebrpcBadRequest.WithCausef("failed to unmarshal request data: %w", err)) return } - // Call service method - var ret0 *User - func() { - defer func() { - // In case of a panic, serve a 500 error and then panic. - if rr := recover(); rr != nil { - RespondWithError(w, ErrorInternal("internal service panic")) - panic(rr) - } - }() - ret0, err = s.ExampleService.GetUser(ctx, reqContent.Arg0) - }() - respContent := struct { - Ret0 *User `json:"user"` - }{ret0} - + // Call service method implementation. + ret0, err := s.ExampleService.GetUser(ctx, reqPayload.Arg0) if err != nil { - RespondWithError(w, err) + rpcErr, ok := err.(WebRPCError) + if !ok { + rpcErr = ErrWebrpcEndpoint.WithCause(err) + } + s.sendErrorJSON(w, r, rpcErr) return } - respBody, err := json.Marshal(respContent) + + respPayload := struct { + Ret0 *User `json:"user"` + }{ret0} + respBody, err := json.Marshal(respPayload) if err != nil { - err = WrapError(ErrInternal, err, "failed to marshal json response") - RespondWithError(w, err) + s.sendErrorJSON(w, r, ErrWebrpcBadResponse.WithCausef("failed to marshal json response: %w", err)) return } @@ -275,18 +358,28 @@ func (s *exampleServiceServer) serveGetUserJSON(ctx context.Context, w http.Resp w.Write(respBody) } +func (s *exampleServiceServer) sendErrorJSON(w http.ResponseWriter, r *http.Request, rpcErr WebRPCError) { + if s.OnError != nil { + s.OnError(r, &rpcErr) + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(rpcErr.HTTPStatus) + + respBody, _ := json.Marshal(rpcErr) + w.Write(respBody) +} + func RespondWithError(w http.ResponseWriter, err error) { - rpcErr, ok := err.(Error) + rpcErr, ok := err.(WebRPCError) if !ok { - rpcErr = WrapError(ErrInternal, err, "webrpc error") + rpcErr = ErrWebrpcEndpoint.WithCause(err) } - statusCode := HTTPStatusFromErrorCode(rpcErr.Code()) - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(statusCode) + w.WriteHeader(rpcErr.HTTPStatus) - respBody, _ := json.Marshal(rpcErr.Payload()) + respBody, _ := json.Marshal(rpcErr) w.Write(respBody) } @@ -294,274 +387,136 @@ func RespondWithError(w http.ResponseWriter, err error) { // Helpers // -type ErrorPayload struct { - Status int `json:"status"` - Code string `json:"code"` - Cause string `json:"cause,omitempty"` - Msg string `json:"msg"` - Error string `json:"error"` +type method struct { + Name string + Service string + Annotations map[string]string } -type Error interface { - // Code is of the valid error codes - Code() ErrorCode - - // Msg returns a human-readable, unstructured messages describing the error - Msg() string - - // Cause is reason for the error - Cause() error - - // Error returns a string of the form "webrpc error : " - Error() string - - // Error response payload - Payload() ErrorPayload +type contextKey struct { + name string } -func Errorf(code ErrorCode, msgf string, args ...interface{}) Error { - msg := fmt.Sprintf(msgf, args...) - if IsValidErrorCode(code) { - return &rpcErr{code: code, msg: msg} - } - return &rpcErr{code: ErrInternal, msg: "invalid error type " + string(code)} +func (k *contextKey) String() string { + return "webrpc context value " + k.name } -func WrapError(code ErrorCode, cause error, format string, args ...interface{}) Error { - msg := fmt.Sprintf(format, args...) - if IsValidErrorCode(code) { - return &rpcErr{code: code, msg: msg, cause: cause} - } - return &rpcErr{code: ErrInternal, msg: "invalid error type " + string(code), cause: cause} -} +var ( + HTTPResponseWriterCtxKey = &contextKey{"HTTPResponseWriter"} -func ErrorNotFound(format string, args ...interface{}) Error { - return Errorf(ErrNotFound, format, args...) -} + HTTPRequestCtxKey = &contextKey{"HTTPRequest"} -func ErrorInvalidArgument(argument string, validationMsg string) Error { - return Errorf(ErrInvalidArgument, argument+" "+validationMsg) -} + ServiceNameCtxKey = &contextKey{"ServiceName"} -func ErrorRequiredArgument(argument string) Error { - return ErrorInvalidArgument(argument, "is required") -} + MethodNameCtxKey = &contextKey{"MethodName"} +) -func ErrorInternal(format string, args ...interface{}) Error { - return Errorf(ErrInternal, format, args...) +func ServiceNameFromContext(ctx context.Context) string { + service, _ := ctx.Value(ServiceNameCtxKey).(string) + return service } -type ErrorCode string +func MethodNameFromContext(ctx context.Context) string { + method, _ := ctx.Value(MethodNameCtxKey).(string) + return method +} -const ( - // Canceled indicates the operation was cancelled (typically by the caller). - ErrCanceled ErrorCode = "canceled" - - // Unknown error. For example when handling errors raised by APIs that do not - // return enough error information. - ErrUnknown ErrorCode = "unknown" - - // InvalidArgument indicates client specified an invalid argument. It - // indicates arguments that are problematic regardless of the state of the - // system (i.e. a malformed file name, required argument, number out of range, - // etc.). - ErrInvalidArgument ErrorCode = "invalid argument" - - // DeadlineExceeded means operation expired before completion. For operations - // that change the state of the system, this error may be returned even if the - // operation has completed successfully (timeout). - ErrDeadlineExceeded ErrorCode = "deadline exceeded" - - // NotFound means some requested entity was not found. - ErrNotFound ErrorCode = "not found" - - // BadRoute means that the requested URL path wasn't routable to a webrpc - // service and method. This is returned by the generated server, and usually - // shouldn't be returned by applications. Instead, applications should use - // NotFound or Unimplemented. - ErrBadRoute ErrorCode = "bad route" - - // AlreadyExists means an attempt to create an entity failed because one - // already exists. - ErrAlreadyExists ErrorCode = "already exists" - - // PermissionDenied indicates the caller does not have permission to execute - // the specified operation. It must not be used if the caller cannot be - // identified (Unauthenticated). - ErrPermissionDenied ErrorCode = "permission denied" - - // Unauthenticated indicates the request does not have valid authentication - // credentials for the operation. - ErrUnauthenticated ErrorCode = "unauthenticated" - - // ResourceExhausted indicates some resource has been exhausted, perhaps a - // per-user quota, or perhaps the entire file system is out of space. - ErrResourceExhausted ErrorCode = "resource exhausted" - - // FailedPrecondition indicates operation was rejected because the system is - // not in a state required for the operation's execution. For example, doing - // an rmdir operation on a directory that is non-empty, or on a non-directory - // object, or when having conflicting read-modify-write on the same resource. - ErrFailedPrecondition ErrorCode = "failed precondition" - - // Aborted indicates the operation was aborted, typically due to a concurrency - // issue like sequencer check failures, transaction aborts, etc. - ErrAborted ErrorCode = "aborted" - - // OutOfRange means operation was attempted past the valid range. For example, - // seeking or reading past end of a paginated collection. - // - // Unlike InvalidArgument, this error indicates a problem that may be fixed if - // the system state changes (i.e. adding more items to the collection). - // - // There is a fair bit of overlap between FailedPrecondition and OutOfRange. - // We recommend using OutOfRange (the more specific error) when it applies so - // that callers who are iterating through a space can easily look for an - // OutOfRange error to detect when they are done. - ErrOutOfRange ErrorCode = "out of range" - - // Unimplemented indicates operation is not implemented or not - // supported/enabled in this service. - ErrUnimplemented ErrorCode = "unimplemented" - - // Internal errors. When some invariants expected by the underlying system - // have been broken. In other words, something bad happened in the library or - // backend service. Do not confuse with HTTP Internal Server Error; an - // Internal error could also happen on the client code, i.e. when parsing a - // server response. - ErrInternal ErrorCode = "internal" - - // Unavailable indicates the service is currently unavailable. This is a most - // likely a transient condition and may be corrected by retrying with a - // backoff. - ErrUnavailable ErrorCode = "unavailable" - - // DataLoss indicates unrecoverable data loss or corruption. - ErrDataLoss ErrorCode = "data loss" - - // ErrNone is the zero-value, is considered an empty error and should not be - // used. - ErrNone ErrorCode = "" -) +func RequestFromContext(ctx context.Context) *http.Request { + r, _ := ctx.Value(HTTPRequestCtxKey).(*http.Request) + return r +} -func HTTPStatusFromErrorCode(code ErrorCode) int { - switch code { - case ErrCanceled: - return 408 // RequestTimeout - case ErrUnknown: - return 500 // Internal Server Error - case ErrInvalidArgument: - return 400 // BadRequest - case ErrDeadlineExceeded: - return 408 // RequestTimeout - case ErrNotFound: - return 404 // Not Found - case ErrBadRoute: - return 404 // Not Found - case ErrAlreadyExists: - return 409 // Conflict - case ErrPermissionDenied: - return 403 // Forbidden - case ErrUnauthenticated: - return 401 // Unauthorized - case ErrResourceExhausted: - return 403 // Forbidden - case ErrFailedPrecondition: - return 412 // Precondition Failed - case ErrAborted: - return 409 // Conflict - case ErrOutOfRange: - return 400 // Bad Request - case ErrUnimplemented: - return 501 // Not Implemented - case ErrInternal: - return 500 // Internal Server Error - case ErrUnavailable: - return 503 // Service Unavailable - case ErrDataLoss: - return 500 // Internal Server Error - case ErrNone: - return 200 // OK - default: - return 0 // Invalid! +func MethodCtx(ctx context.Context) (method, bool) { + req := RequestFromContext(ctx) + if req == nil { + return method{}, false } -} -func IsErrorCode(err error, code ErrorCode) bool { - if rpcErr, ok := err.(Error); ok { - if rpcErr.Code() == code { - return true - } + m, ok := methods[req.URL.Path] + if !ok { + return method{}, false } - return false -} -func IsValidErrorCode(code ErrorCode) bool { - return HTTPStatusFromErrorCode(code) != 0 + return m, true } -type rpcErr struct { - code ErrorCode - msg string - cause error +func ResponseWriterFromContext(ctx context.Context) http.ResponseWriter { + w, _ := ctx.Value(HTTPResponseWriterCtxKey).(http.ResponseWriter) + return w } -func (e *rpcErr) Code() ErrorCode { - return e.code -} +// +// Errors +// -func (e *rpcErr) Msg() string { - return e.msg +type WebRPCError struct { + Name string `json:"error"` + Code int `json:"code"` + Message string `json:"msg"` + Cause string `json:"cause,omitempty"` + HTTPStatus int `json:"status"` + cause error } -func (e *rpcErr) Cause() error { - return e.cause -} +var _ error = WebRPCError{} -func (e *rpcErr) Error() string { - if e.cause != nil && e.cause.Error() != "" { - if e.msg != "" { - return fmt.Sprintf("webrpc %s error: %s -- %s", e.code, e.cause.Error(), e.msg) - } else { - return fmt.Sprintf("webrpc %s error: %s", e.code, e.cause.Error()) - } - } else { - return fmt.Sprintf("webrpc %s error: %s", e.code, e.msg) +func (e WebRPCError) Error() string { + if e.cause != nil { + return fmt.Sprintf("%s %d: %s: %v", e.Name, e.Code, e.Message, e.cause) } + return fmt.Sprintf("%s %d: %s", e.Name, e.Code, e.Message) } -func (e *rpcErr) Payload() ErrorPayload { - statusCode := HTTPStatusFromErrorCode(e.Code()) - errPayload := ErrorPayload{ - Status: statusCode, - Code: string(e.Code()), - Msg: e.Msg(), - Error: e.Error(), +func (e WebRPCError) Is(target error) bool { + if target == nil { + return false } - if e.Cause() != nil { - errPayload.Cause = e.Cause().Error() + if rpcErr, ok := target.(WebRPCError); ok { + return rpcErr.Code == e.Code } - return errPayload + return errors.Is(e.cause, target) } -type contextKey struct { - name string +func (e WebRPCError) Unwrap() error { + return e.cause } -func (k *contextKey) String() string { - return "webrpc context value " + k.name +func (e WebRPCError) WithCause(cause error) WebRPCError { + err := e + err.cause = cause + err.Cause = cause.Error() + return err } -var ( - // For Client - HTTPClientRequestHeadersCtxKey = &contextKey{"HTTPClientRequestHeaders"} - - // For Server - HTTPResponseWriterCtxKey = &contextKey{"HTTPResponseWriter"} +func (e WebRPCError) WithCausef(format string, args ...interface{}) WebRPCError { + cause := fmt.Errorf(format, args...) + err := e + err.cause = cause + err.Cause = cause.Error() + return err +} - HTTPRequestCtxKey = &contextKey{"HTTPRequest"} +// Deprecated: Use .WithCause() method on WebRPCError. +func ErrorWithCause(rpcErr WebRPCError, cause error) WebRPCError { + return rpcErr.WithCause(cause) +} - ServiceNameCtxKey = &contextKey{"ServiceName"} +// Webrpc errors +var ( + ErrWebrpcEndpoint = WebRPCError{Code: 0, Name: "WebrpcEndpoint", Message: "endpoint error", HTTPStatus: 400} + ErrWebrpcRequestFailed = WebRPCError{Code: -1, Name: "WebrpcRequestFailed", Message: "request failed", HTTPStatus: 400} + ErrWebrpcBadRoute = WebRPCError{Code: -2, Name: "WebrpcBadRoute", Message: "bad route", HTTPStatus: 404} + ErrWebrpcBadMethod = WebRPCError{Code: -3, Name: "WebrpcBadMethod", Message: "bad method", HTTPStatus: 405} + ErrWebrpcBadRequest = WebRPCError{Code: -4, Name: "WebrpcBadRequest", Message: "bad request", HTTPStatus: 400} + ErrWebrpcBadResponse = WebRPCError{Code: -5, Name: "WebrpcBadResponse", Message: "bad response", HTTPStatus: 500} + ErrWebrpcServerPanic = WebRPCError{Code: -6, Name: "WebrpcServerPanic", Message: "server panic", HTTPStatus: 500} + ErrWebrpcInternalError = WebRPCError{Code: -7, Name: "WebrpcInternalError", Message: "internal error", HTTPStatus: 500} + ErrWebrpcClientDisconnected = WebRPCError{Code: -8, Name: "WebrpcClientDisconnected", Message: "client disconnected", HTTPStatus: 400} + ErrWebrpcStreamLost = WebRPCError{Code: -9, Name: "WebrpcStreamLost", Message: "stream lost", HTTPStatus: 400} + ErrWebrpcStreamFinished = WebRPCError{Code: -10, Name: "WebrpcStreamFinished", Message: "stream finished", HTTPStatus: 200} +) - MethodNameCtxKey = &contextKey{"MethodName"} +// Schema errors +var ( + ErrUserNotFound = WebRPCError{Code: 1000, Name: "UserNotFound", Message: "user not found", HTTPStatus: 404} ) diff --git a/_examples/golang-nodejs/server/server.go b/_examples/golang-nodejs/server/server.go deleted file mode 100644 index 4a38ea89..00000000 --- a/_examples/golang-nodejs/server/server.go +++ /dev/null @@ -1,53 +0,0 @@ -package main - -import ( - "context" - "log" - "net/http" - - "github.com/go-chi/chi" - "github.com/go-chi/chi/middleware" -) - -func main() { - err := startServer() - if err != nil { - log.Fatal(err) - } -} - -func startServer() error { - r := chi.NewRouter() - r.Use(middleware.RequestID) - r.Use(middleware.Logger) - r.Use(middleware.Recoverer) - - r.Get("/", func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte(".")) - }) - - webrpcHandler := NewExampleServiceServer(&ExampleServiceRPC{}) - r.Handle("/*", webrpcHandler) - - log.Printf("Starting webrpc server on localhost:4242") - - return http.ListenAndServe(":4242", r) -} - -type ExampleServiceRPC struct { -} - -func (s *ExampleServiceRPC) Ping(ctx context.Context) (bool, error) { - return true, nil -} - -func (s *ExampleServiceRPC) GetUser(ctx context.Context, req *GetUserRequest) (*User, error) { - if req.UserID == 911 { - return nil, ErrorNotFound("unknown userID %d", 911) - } - - return &User{ - ID: req.UserID, - Username: "hihi", - }, nil -} diff --git a/_examples/golang-sse/Makefile b/_examples/golang-sse/Makefile new file mode 100644 index 00000000..f2e35303 --- /dev/null +++ b/_examples/golang-sse/Makefile @@ -0,0 +1,23 @@ +all: + @awk -F'[ :]' '!/^all:/ && /^([A-z_-]+):/ {print "make " $$1}' Makefile + +generate: + webrpc-gen -schema=proto/chat.ridl -target=golang -pkg=proto -server -client -out=proto/chat.gen.go + webrpc-gen -schema=proto/chat.ridl -target=typescript -client -out=webapp/src/rpc.gen.ts + +dev-generate: + ../../bin/webrpc-gen -schema=proto/chat.ridl -target=golang -pkg=proto -server -client -out=proto/chat.gen.go + ../../bin/webrpc-gen -schema=proto/chat.ridl -target=typescript -client -out=webapp/src/rpc.gen.ts + +dev-generate-local-templates: + ../../bin/webrpc-gen -schema=proto/chat.ridl -target=golang -pkg=proto -server -client -out=proto/chat.gen.go + ../../bin/webrpc-gen -schema=proto/chat.ridl -target=typescript -client -out=webapp/src/rpc.gen.ts + +run: + go run ./ + +test: + go test -v ./ + +curl: + curl -v -X POST -H "Content-Type: application/json" -H "Accept: application/x-ndjson" --data '{"username": "'$(shell whoami)'", "serverTimeoutSec": 2}' http://localhost:4848/rpc/Chat/SubscribeMessages diff --git a/_examples/golang-sse/chat_test.go b/_examples/golang-sse/chat_test.go new file mode 100644 index 00000000..d8f57f2e --- /dev/null +++ b/_examples/golang-sse/chat_test.go @@ -0,0 +1,149 @@ +package main + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/webrpc/webrpc/_example/golang-sse/proto" +) + +var ( + client proto.ChatClient +) + +func TestMain(m *testing.M) { + rpc := NewChatServer() + srv := httptest.NewServer(rpc.Router()) + defer srv.Close() + + // opts := &devslog.Options{ + // MaxSlicePrintSize: 4, + // SortKeys: true, + // TimeFormat: "[04:05:06]", + // NewLineAfterLog: true, + // DebugColor: devslog.Magenta, + // } + + // logger := slog.New(devslog.NewHandler(os.Stdout, opts)) + // slog.SetDefault(logger) + + client = proto.NewChatClient(srv.URL, &http.Client{}) + time.Sleep(time.Millisecond * 500) + + os.Exit(m.Run()) +} + +func TestStream10k(t *testing.T) { + t.Parallel() + + rpc := NewChatServer() + srv := httptest.NewServer(rpc.Router()) + defer srv.Close() + + client := proto.NewChatClient(srv.URL, &http.Client{}) + + ctx, cancel := context.WithCancel(context.Background()) + + stream, err := client.SubscribeMessages(ctx, t.Name()) + require.Nil(t, err) + + msgCount := 10000 + go func() { + for i := 0; i < msgCount; i++ { + if err := client.SendMessage(ctx, t.Name(), fmt.Sprintf("Hello %v", i)); err != nil { + t.Fatal(err) + } + } + }() + + for i := 0; i < msgCount; i++ { + _, err := stream.Read() + require.Nil(t, err) + } + + cancel() // stop subscription + + _, err = stream.Read() + if err != nil { + assert.ErrorIs(t, err, proto.ErrWebrpcClientDisconnected) + } +} + +func TestStreamServerConnectionLost(t *testing.T) { + t.Parallel() + + rpc := NewChatServer() + srv := httptest.NewServer(rpc.Router()) + defer srv.Close() + + go func() { + <-time.After(2 * time.Second) + srv.Config.Close() + }() + + client := proto.NewChatClient(srv.URL, &http.Client{}) + time.Sleep(time.Millisecond * 500) + + ctx := context.Background() + + stream, err := client.SubscribeMessages(ctx, t.Name()) + require.Nil(t, err) + + for { + msg, err := stream.Read() + if err != nil { + assert.ErrorIs(t, err, proto.ErrWebrpcStreamLost) + break + } + t.Log(msg.Text) + } +} + +func TestStreamCustomError(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + stream, err := client.SubscribeMessages(ctx, "") // empty username + require.Nil(t, err) // only network/connection errors should come back here + + _, err = stream.Read() // we should receive RPC handler errors (e.g. EmptyUsername) only when we start reading the data + require.Error(t, err) + require.ErrorIs(t, err, proto.ErrEmptyUsername) +} + +func TestStreamClientTimeout(t *testing.T) { + t.Parallel() + + t.Run("0.1s", testStreamClientTimeout(t, 100*time.Millisecond)) + t.Run("6s", testStreamClientTimeout(t, 5*time.Second)) + t.Run("12s", testStreamClientTimeout(t, 12*time.Second)) // over 10s (ping-alive) +} + +func testStreamClientTimeout(t *testing.T, timeout time.Duration) func(t *testing.T) { + return func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + stream, err := client.SubscribeMessages(ctx, t.Name()) + require.Nil(t, err) + + for { + msg, err := stream.Read() + if err != nil { + assert.ErrorIs(t, err, proto.ErrWebrpcClientDisconnected) + break + } + t.Log(msg.Text) + } + } +} diff --git a/_examples/golang-sse/go.mod b/_examples/golang-sse/go.mod new file mode 100644 index 00000000..851d191b --- /dev/null +++ b/_examples/golang-sse/go.mod @@ -0,0 +1,15 @@ +module github.com/webrpc/webrpc/_example/golang-sse + +go 1.21.1 + +require ( + github.com/go-chi/chi/v5 v5.0.12 + github.com/go-chi/cors v1.2.1 + github.com/stretchr/testify v1.8.1 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/_examples/golang-sse/go.sum b/_examples/golang-sse/go.sum new file mode 100644 index 00000000..b8e634fe --- /dev/null +++ b/_examples/golang-sse/go.sum @@ -0,0 +1,21 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= +github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= +github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/_examples/golang-sse/main.go b/_examples/golang-sse/main.go new file mode 100644 index 00000000..c24bb1c4 --- /dev/null +++ b/_examples/golang-sse/main.go @@ -0,0 +1,80 @@ +package main + +import ( + "bytes" + "fmt" + "io" + "log" + "log/slog" + "net/http" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" + "github.com/go-chi/cors" + "github.com/webrpc/webrpc/_example/golang-sse/proto" +) + +func main() { + port := 4848 + slog.Info(fmt.Sprintf("serving at http://localhost:%v", port)) + + rpc := NewChatServer() + + err := http.ListenAndServe(fmt.Sprintf("0.0.0.0:%v", port), rpc.Router()) + if err != nil { + log.Fatal(err) + } +} + +func (s *ChatServer) Router() http.Handler { + r := chi.NewRouter() + r.Use(middleware.RequestID) + r.Use(requestDebugger) + r.Use(middleware.Recoverer) + + cors := cors.New(cors.Options{ + // AllowedOrigins: []string{"https://foo.com"}, // Use this to allow specific origin hosts + AllowedOrigins: []string{"*"}, + // AllowOriginFunc: func(r *http.Request, origin string) bool { return true }, + AllowedMethods: []string{"POST", "OPTIONS"}, + AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token", "Webrpc"}, + ExposedHeaders: []string{"Link", "Webrpc"}, + AllowCredentials: true, + MaxAge: 300, // Maximum value not ignored by any of major browsers + }) + r.Use(cors.Handler) + + webrpcHandler := proto.NewChatServer(s) + r.Handle("/*", webrpcHandler) + + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(".")) + }) + + return r +} + +func requestDebugger(next http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + var reqBody bytes.Buffer + r.Body = io.NopCloser(io.TeeReader(r.Body, &reqBody)) + + var respBody bytes.Buffer + ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor) + ww.Tee(&respBody) + + slog.Info(fmt.Sprintf("req started"), + slog.String("url", fmt.Sprintf("%v %v", r.Method, r.URL.String()))) + + defer func() { + slog.Info(fmt.Sprintf("req finished HTTP %v", ww.Status()), + slog.String("url", fmt.Sprintf("%v %v", r.Method, r.URL.String())), + slog.String("reqBody", reqBody.String()), + slog.String("respBody", respBody.String()), + ) + }() + + next.ServeHTTP(ww, r) + } + return http.HandlerFunc(fn) +} diff --git a/_examples/golang-sse/proto/chat.gen.go b/_examples/golang-sse/proto/chat.gen.go new file mode 100644 index 00000000..6a1cdfcd --- /dev/null +++ b/_examples/golang-sse/proto/chat.gen.go @@ -0,0 +1,800 @@ +// webrpc-sse-chat v1.0.0 21faef7a7920c42e730d4a5df1dae90fde9f2e5b +// -- +// Code generated by webrpc-gen with golang generator. DO NOT EDIT. +// +// webrpc-gen -schema=proto/chat.ridl -target=golang -pkg=proto -server -client -out=proto/chat.gen.go +package proto + +import ( + "bufio" + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "sync" + "time" +) + +const WebrpcHeader = "Webrpc" + +const WebrpcHeaderValue = "webrpc;gen-golang@v0.17.0;webrpc-sse-chat@v1.0.0" + +// WebRPC description and code-gen version +func WebRPCVersion() string { + return "v1" +} + +// Schema version of your RIDL schema +func WebRPCSchemaVersion() string { + return "v1.0.0" +} + +// Schema hash generated from your RIDL schema +func WebRPCSchemaHash() string { + return "21faef7a7920c42e730d4a5df1dae90fde9f2e5b" +} + +type WebrpcGenVersions struct { + WebrpcGenVersion string + CodeGenName string + CodeGenVersion string + SchemaName string + SchemaVersion string +} + +func VersionFromHeader(h http.Header) (*WebrpcGenVersions, error) { + if h.Get(WebrpcHeader) == "" { + return nil, fmt.Errorf("header is empty or missing") + } + + versions, err := parseWebrpcGenVersions(h.Get(WebrpcHeader)) + if err != nil { + return nil, fmt.Errorf("webrpc header is invalid: %w", err) + } + + return versions, nil +} + +func parseWebrpcGenVersions(header string) (*WebrpcGenVersions, error) { + versions := strings.Split(header, ";") + if len(versions) < 3 { + return nil, fmt.Errorf("expected at least 3 parts while parsing webrpc header: %v", header) + } + + _, webrpcGenVersion, ok := strings.Cut(versions[0], "@") + if !ok { + return nil, fmt.Errorf("webrpc gen version could not be parsed from: %s", versions[0]) + } + + tmplTarget, tmplVersion, ok := strings.Cut(versions[1], "@") + if !ok { + return nil, fmt.Errorf("tmplTarget and tmplVersion could not be parsed from: %s", versions[1]) + } + + schemaName, schemaVersion, ok := strings.Cut(versions[2], "@") + if !ok { + return nil, fmt.Errorf("schema name and schema version could not be parsed from: %s", versions[2]) + } + + return &WebrpcGenVersions{ + WebrpcGenVersion: webrpcGenVersion, + CodeGenName: tmplTarget, + CodeGenVersion: tmplVersion, + SchemaName: schemaName, + SchemaVersion: schemaVersion, + }, nil +} + +// +// Common types +// + +type Message struct { + Id uint64 `json:"id"` + Username string `json:"username"` + Text string `json:"text"` + CreatedAt time.Time `json:"createdAt"` +} + +var methods = map[string]method{ + "/rpc/Chat/SendMessage": { + Name: "SendMessage", + Service: "Chat", + Annotations: map[string]string{}, + }, + "/rpc/Chat/SubscribeMessages": { + Name: "SubscribeMessages", + Service: "Chat", + Annotations: map[string]string{}, + }, +} + +func WebrpcMethods() map[string]method { + res := make(map[string]method, len(methods)) + for k, v := range methods { + res[k] = v + } + + return res +} + +var WebRPCServices = map[string][]string{ + "Chat": { + "SendMessage", + "SubscribeMessages", + }, +} + +// +// Server types +// + +type Chat interface { + SendMessage(ctx context.Context, username string, text string) error + SubscribeMessages(ctx context.Context, username string, stream SubscribeMessagesStreamWriter) error +} + +type SubscribeMessagesStreamWriter interface { + Write(message *Message) error +} + +type subscribeMessagesStreamWriter struct { + streamWriter +} + +func (w *subscribeMessagesStreamWriter) Write(message *Message) error { + out := struct { + Ret0 *Message `json:"message"` + }{ + Ret0: message, + } + + return w.streamWriter.write(out) +} + +type streamWriter struct { + mu sync.Mutex // Guards concurrent writes to w. + w http.ResponseWriter + f http.Flusher + e *json.Encoder + + sendError func(w http.ResponseWriter, r *http.Request, rpcErr WebRPCError) +} + +const StreamKeepAliveInterval = 10 * time.Second + +func (w *streamWriter) keepAlive(ctx context.Context) { + for { + select { + case <-time.After(StreamKeepAliveInterval): + err := w.ping() + if err != nil { + return + } + case <-ctx.Done(): + return + } + } +} + +func (w *streamWriter) ping() error { + defer w.f.Flush() + + w.mu.Lock() + defer w.mu.Unlock() + + _, err := w.w.Write([]byte("\n")) + return err +} + +func (w *streamWriter) write(respPayload interface{}) error { + defer w.f.Flush() + + w.mu.Lock() + defer w.mu.Unlock() + + return w.e.Encode(respPayload) +} + +// +// Client types +// + +type ChatClient interface { + SendMessage(ctx context.Context, username string, text string) error + SubscribeMessages(ctx context.Context, username string) (SubscribeMessagesStreamReader, error) +} + +type SubscribeMessagesStreamReader interface { + Read() (message *Message, err error) +} + +// +// Server +// + +type WebRPCServer interface { + http.Handler +} + +type chatServer struct { + Chat + OnError func(r *http.Request, rpcErr *WebRPCError) + OnRequest func(w http.ResponseWriter, r *http.Request) error +} + +func NewChatServer(svc Chat) *chatServer { + return &chatServer{ + Chat: svc, + } +} + +func (s *chatServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { + defer func() { + // In case of a panic, serve a HTTP 500 error and then panic. + if rr := recover(); rr != nil { + s.sendErrorJSON(w, r, ErrWebrpcServerPanic.WithCausef("%v", rr)) + panic(rr) + } + }() + + w.Header().Set(WebrpcHeader, WebrpcHeaderValue) + + ctx := r.Context() + ctx = context.WithValue(ctx, HTTPResponseWriterCtxKey, w) + ctx = context.WithValue(ctx, HTTPRequestCtxKey, r) + ctx = context.WithValue(ctx, ServiceNameCtxKey, "Chat") + + r = r.WithContext(ctx) + + var handler func(ctx context.Context, w http.ResponseWriter, r *http.Request) + switch r.URL.Path { + case "/rpc/Chat/SendMessage": + handler = s.serveSendMessageJSON + case "/rpc/Chat/SubscribeMessages": + handler = s.serveSubscribeMessagesJSONStream + default: + err := ErrWebrpcBadRoute.WithCausef("no webrpc method defined for path %v", r.URL.Path) + s.sendErrorJSON(w, r, err) + return + } + + if r.Method != "POST" { + w.Header().Add("Allow", "POST") // RFC 9110. + err := ErrWebrpcBadMethod.WithCausef("unsupported HTTP method %v (only POST is allowed)", r.Method) + s.sendErrorJSON(w, r, err) + return + } + + contentType := r.Header.Get("Content-Type") + if i := strings.Index(contentType, ";"); i >= 0 { + contentType = contentType[:i] + } + contentType = strings.TrimSpace(strings.ToLower(contentType)) + + switch contentType { + case "application/json": + if s.OnRequest != nil { + if err := s.OnRequest(w, r); err != nil { + rpcErr, ok := err.(WebRPCError) + if !ok { + rpcErr = ErrWebrpcEndpoint.WithCause(err) + } + s.sendErrorJSON(w, r, rpcErr) + return + } + } + + handler(ctx, w, r) + default: + err := ErrWebrpcBadRequest.WithCausef("unsupported Content-Type %q (only application/json is allowed)", r.Header.Get("Content-Type")) + s.sendErrorJSON(w, r, err) + } +} + +func (s *chatServer) serveSendMessageJSON(ctx context.Context, w http.ResponseWriter, r *http.Request) { + ctx = context.WithValue(ctx, MethodNameCtxKey, "SendMessage") + + reqBody, err := io.ReadAll(r.Body) + if err != nil { + s.sendErrorJSON(w, r, ErrWebrpcBadRequest.WithCausef("failed to read request data: %w", err)) + return + } + defer r.Body.Close() + + reqPayload := struct { + Arg0 string `json:"username"` + Arg1 string `json:"text"` + }{} + if err := json.Unmarshal(reqBody, &reqPayload); err != nil { + s.sendErrorJSON(w, r, ErrWebrpcBadRequest.WithCausef("failed to unmarshal request data: %w", err)) + return + } + + // Call service method implementation. + err = s.Chat.SendMessage(ctx, reqPayload.Arg0, reqPayload.Arg1) + if err != nil { + rpcErr, ok := err.(WebRPCError) + if !ok { + rpcErr = ErrWebrpcEndpoint.WithCause(err) + } + s.sendErrorJSON(w, r, rpcErr) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte("{}")) +} + +func (s *chatServer) serveSubscribeMessagesJSONStream(ctx context.Context, w http.ResponseWriter, r *http.Request) { + ctx = context.WithValue(ctx, MethodNameCtxKey, "SubscribeMessages") + + reqBody, err := io.ReadAll(r.Body) + if err != nil { + s.sendErrorJSON(w, r, ErrWebrpcBadRequest.WithCausef("failed to read request data: %w", err)) + return + } + defer r.Body.Close() + + reqPayload := struct { + Arg0 string `json:"username"` + }{} + if err := json.Unmarshal(reqBody, &reqPayload); err != nil { + s.sendErrorJSON(w, r, ErrWebrpcBadRequest.WithCausef("failed to unmarshal request data: %w", err)) + return + } + + f, ok := w.(http.Flusher) + if !ok { + s.sendErrorJSON(w, r, ErrWebrpcInternalError.WithCausef("server http.ResponseWriter doesn't support .Flush() method")) + return + } + + w.Header().Set("Cache-Control", "no-store") + w.Header().Set("Connection", "keep-alive") + w.Header().Set("Content-Type", "application/x-ndjson") + w.Header().Set("X-Content-Type-Options", "nosniff") + w.WriteHeader(http.StatusOK) + + streamWriter := &subscribeMessagesStreamWriter{streamWriter{w: w, f: f, e: json.NewEncoder(w), sendError: s.sendErrorJSON}} + if err := streamWriter.ping(); err != nil { + s.sendErrorJSON(w, r, ErrWebrpcStreamLost.WithCausef("failed to establish SSE stream: %w", err)) + return + } + + ctx, cancel := context.WithCancel(r.Context()) + defer cancel() + + go streamWriter.keepAlive(ctx) + + // Call service method implementation. + if err := s.Chat.SubscribeMessages(ctx, reqPayload.Arg0, streamWriter); err != nil { + rpcErr, ok := err.(WebRPCError) + if !ok { + rpcErr = ErrWebrpcEndpoint.WithCause(err) + } + streamWriter.sendError(w, r, rpcErr) + return + } +} + +func (s *chatServer) sendErrorJSON(w http.ResponseWriter, r *http.Request, rpcErr WebRPCError) { + if s.OnError != nil { + s.OnError(r, &rpcErr) + } + + if w.Header().Get("Content-Type") == "application/x-ndjson" { + out := struct { + WebRPCError WebRPCError `json:"webrpcError"` + }{WebRPCError: rpcErr} + json.NewEncoder(w).Encode(out) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(rpcErr.HTTPStatus) + + respBody, _ := json.Marshal(rpcErr) + w.Write(respBody) +} + +func RespondWithError(w http.ResponseWriter, err error) { + rpcErr, ok := err.(WebRPCError) + if !ok { + rpcErr = ErrWebrpcEndpoint.WithCause(err) + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(rpcErr.HTTPStatus) + + respBody, _ := json.Marshal(rpcErr) + w.Write(respBody) +} + +// +// Client +// + +const ChatPathPrefix = "/rpc/Chat/" + +type chatClient struct { + client HTTPClient + urls [2]string +} + +func NewChatClient(addr string, client HTTPClient) ChatClient { + prefix := urlBase(addr) + ChatPathPrefix + urls := [2]string{ + prefix + "SendMessage", + prefix + "SubscribeMessages", + } + return &chatClient{ + client: client, + urls: urls, + } +} + +func (c *chatClient) SendMessage(ctx context.Context, username string, text string) error { + in := struct { + Arg0 string `json:"username"` + Arg1 string `json:"text"` + }{username, text} + + resp, err := doHTTPRequest(ctx, c.client, c.urls[0], in, nil) + if resp != nil { + cerr := resp.Body.Close() + if err == nil && cerr != nil { + err = ErrWebrpcRequestFailed.WithCausef("failed to close response body: %w", cerr) + } + } + + return err +} + +func (c *chatClient) SubscribeMessages(ctx context.Context, username string) (SubscribeMessagesStreamReader, error) { + in := struct { + Arg0 string `json:"username"` + }{username} + + resp, err := doHTTPRequest(ctx, c.client, c.urls[1], in, nil) + if err != nil { + if resp != nil { + resp.Body.Close() + } + return nil, err + } + + buf := bufio.NewReader(resp.Body) + return &subscribeMessagesStreamReader{streamReader{ctx: ctx, c: resp.Body, r: buf}}, nil +} + +type subscribeMessagesStreamReader struct { + streamReader +} + +func (r *subscribeMessagesStreamReader) Read() (*Message, error) { + out := struct { + Ret0 *Message `json:"message"` + WebRPCError *WebRPCError `json:"webrpcError"` + }{} + + err := r.streamReader.read(&out) + if err != nil { + return out.Ret0, err + } + + if out.WebRPCError != nil { + return out.Ret0, out.WebRPCError + } + + return out.Ret0, nil +} + +type streamReader struct { + ctx context.Context + c io.Closer + r *bufio.Reader +} + +func (r *streamReader) read(v interface{}) error { + for { + select { + case <-r.ctx.Done(): + r.c.Close() + return ErrWebrpcClientDisconnected.WithCause(r.ctx.Err()) + default: + } + + line, err := r.r.ReadBytes('\n') + if err != nil { + return r.handleReadError(err) + } + + // Eat newlines (keep-alive pings). + if len(line) == 1 && line[0] == '\n' { + continue + } + + if err := json.Unmarshal(line, &v); err != nil { + return r.handleReadError(err) + } + return nil + } +} + +func (r *streamReader) handleReadError(err error) error { + defer r.c.Close() + if errors.Is(err, io.EOF) { + return ErrWebrpcStreamFinished.WithCause(err) + } + if errors.Is(err, io.ErrUnexpectedEOF) { + return ErrWebrpcStreamLost.WithCause(err) + } + if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { + return ErrWebrpcClientDisconnected.WithCause(err) + } + return ErrWebrpcBadResponse.WithCausef("reading stream: %w", err) +} + +// HTTPClient is the interface used by generated clients to send HTTP requests. +// It is fulfilled by *(net/http).Client, which is sufficient for most users. +// Users can provide their own implementation for special retry policies. +type HTTPClient interface { + Do(req *http.Request) (*http.Response, error) +} + +// urlBase helps ensure that addr specifies a scheme. If it is unparsable +// as a URL, it returns addr unchanged. +func urlBase(addr string) string { + // If the addr specifies a scheme, use it. If not, default to + // http. If url.Parse fails on it, return it unchanged. + url, err := url.Parse(addr) + if err != nil { + return addr + } + if url.Scheme == "" { + url.Scheme = "http" + } + return url.String() +} + +// newRequest makes an http.Request from a client, adding common headers. +func newRequest(ctx context.Context, url string, reqBody io.Reader, contentType string) (*http.Request, error) { + req, err := http.NewRequestWithContext(ctx, "POST", url, reqBody) + if err != nil { + return nil, err + } + req.Header.Set("Accept", contentType) + req.Header.Set("Content-Type", contentType) + req.Header.Set(WebrpcHeader, WebrpcHeaderValue) + if headers, ok := HTTPRequestHeaders(ctx); ok { + for k := range headers { + for _, v := range headers[k] { + req.Header.Add(k, v) + } + } + } + return req, nil +} + +// doHTTPRequest is common code to make a request to the remote service. +func doHTTPRequest(ctx context.Context, client HTTPClient, url string, in, out interface{}) (*http.Response, error) { + reqBody, err := json.Marshal(in) + if err != nil { + return nil, ErrWebrpcRequestFailed.WithCausef("failed to marshal JSON body: %w", err) + } + if err = ctx.Err(); err != nil { + return nil, ErrWebrpcRequestFailed.WithCausef("aborted because context was done: %w", err) + } + + req, err := newRequest(ctx, url, bytes.NewBuffer(reqBody), "application/json") + if err != nil { + return nil, ErrWebrpcRequestFailed.WithCausef("could not build request: %w", err) + } + + resp, err := client.Do(req) + if err != nil { + return nil, ErrWebrpcRequestFailed.WithCause(err) + } + + if resp.StatusCode != 200 { + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, ErrWebrpcBadResponse.WithCausef("failed to read server error response body: %w", err) + } + + var rpcErr WebRPCError + if err := json.Unmarshal(respBody, &rpcErr); err != nil { + return nil, ErrWebrpcBadResponse.WithCausef("failed to unmarshal server error: %w", err) + } + if rpcErr.Cause != "" { + rpcErr.cause = errors.New(rpcErr.Cause) + } + return nil, rpcErr + } + + if out != nil { + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, ErrWebrpcBadResponse.WithCausef("failed to read response body: %w", err) + } + + err = json.Unmarshal(respBody, &out) + if err != nil { + return nil, ErrWebrpcBadResponse.WithCausef("failed to unmarshal JSON response body: %w", err) + } + } + + return resp, nil +} + +func WithHTTPRequestHeaders(ctx context.Context, h http.Header) (context.Context, error) { + if _, ok := h["Accept"]; ok { + return nil, errors.New("provided header cannot set Accept") + } + if _, ok := h["Content-Type"]; ok { + return nil, errors.New("provided header cannot set Content-Type") + } + + copied := make(http.Header, len(h)) + for k, vv := range h { + if vv == nil { + copied[k] = nil + continue + } + copied[k] = make([]string, len(vv)) + copy(copied[k], vv) + } + + return context.WithValue(ctx, HTTPClientRequestHeadersCtxKey, copied), nil +} + +func HTTPRequestHeaders(ctx context.Context) (http.Header, bool) { + h, ok := ctx.Value(HTTPClientRequestHeadersCtxKey).(http.Header) + return h, ok +} + +// +// Helpers +// + +type method struct { + Name string + Service string + Annotations map[string]string +} + +type contextKey struct { + name string +} + +func (k *contextKey) String() string { + return "webrpc context value " + k.name +} + +var ( + HTTPClientRequestHeadersCtxKey = &contextKey{"HTTPClientRequestHeaders"} + HTTPResponseWriterCtxKey = &contextKey{"HTTPResponseWriter"} + + HTTPRequestCtxKey = &contextKey{"HTTPRequest"} + + ServiceNameCtxKey = &contextKey{"ServiceName"} + + MethodNameCtxKey = &contextKey{"MethodName"} +) + +func ServiceNameFromContext(ctx context.Context) string { + service, _ := ctx.Value(ServiceNameCtxKey).(string) + return service +} + +func MethodNameFromContext(ctx context.Context) string { + method, _ := ctx.Value(MethodNameCtxKey).(string) + return method +} + +func RequestFromContext(ctx context.Context) *http.Request { + r, _ := ctx.Value(HTTPRequestCtxKey).(*http.Request) + return r +} + +func MethodCtx(ctx context.Context) (method, bool) { + req := RequestFromContext(ctx) + if req == nil { + return method{}, false + } + + m, ok := methods[req.URL.Path] + if !ok { + return method{}, false + } + + return m, true +} + +func ResponseWriterFromContext(ctx context.Context) http.ResponseWriter { + w, _ := ctx.Value(HTTPResponseWriterCtxKey).(http.ResponseWriter) + return w +} + +// +// Errors +// + +type WebRPCError struct { + Name string `json:"error"` + Code int `json:"code"` + Message string `json:"msg"` + Cause string `json:"cause,omitempty"` + HTTPStatus int `json:"status"` + cause error +} + +var _ error = WebRPCError{} + +func (e WebRPCError) Error() string { + if e.cause != nil { + return fmt.Sprintf("%s %d: %s: %v", e.Name, e.Code, e.Message, e.cause) + } + return fmt.Sprintf("%s %d: %s", e.Name, e.Code, e.Message) +} + +func (e WebRPCError) Is(target error) bool { + if target == nil { + return false + } + if rpcErr, ok := target.(WebRPCError); ok { + return rpcErr.Code == e.Code + } + return errors.Is(e.cause, target) +} + +func (e WebRPCError) Unwrap() error { + return e.cause +} + +func (e WebRPCError) WithCause(cause error) WebRPCError { + err := e + err.cause = cause + err.Cause = cause.Error() + return err +} + +func (e WebRPCError) WithCausef(format string, args ...interface{}) WebRPCError { + cause := fmt.Errorf(format, args...) + err := e + err.cause = cause + err.Cause = cause.Error() + return err +} + +// Deprecated: Use .WithCause() method on WebRPCError. +func ErrorWithCause(rpcErr WebRPCError, cause error) WebRPCError { + return rpcErr.WithCause(cause) +} + +// Webrpc errors +var ( + ErrWebrpcEndpoint = WebRPCError{Code: 0, Name: "WebrpcEndpoint", Message: "endpoint error", HTTPStatus: 400} + ErrWebrpcRequestFailed = WebRPCError{Code: -1, Name: "WebrpcRequestFailed", Message: "request failed", HTTPStatus: 400} + ErrWebrpcBadRoute = WebRPCError{Code: -2, Name: "WebrpcBadRoute", Message: "bad route", HTTPStatus: 404} + ErrWebrpcBadMethod = WebRPCError{Code: -3, Name: "WebrpcBadMethod", Message: "bad method", HTTPStatus: 405} + ErrWebrpcBadRequest = WebRPCError{Code: -4, Name: "WebrpcBadRequest", Message: "bad request", HTTPStatus: 400} + ErrWebrpcBadResponse = WebRPCError{Code: -5, Name: "WebrpcBadResponse", Message: "bad response", HTTPStatus: 500} + ErrWebrpcServerPanic = WebRPCError{Code: -6, Name: "WebrpcServerPanic", Message: "server panic", HTTPStatus: 500} + ErrWebrpcInternalError = WebRPCError{Code: -7, Name: "WebrpcInternalError", Message: "internal error", HTTPStatus: 500} + ErrWebrpcClientDisconnected = WebRPCError{Code: -8, Name: "WebrpcClientDisconnected", Message: "client disconnected", HTTPStatus: 400} + ErrWebrpcStreamLost = WebRPCError{Code: -9, Name: "WebrpcStreamLost", Message: "stream lost", HTTPStatus: 400} + ErrWebrpcStreamFinished = WebRPCError{Code: -10, Name: "WebrpcStreamFinished", Message: "stream finished", HTTPStatus: 200} +) + +// Schema errors +var ( + ErrEmptyUsername = WebRPCError{Code: 100, Name: "EmptyUsername", Message: "Username must be provided.", HTTPStatus: 400} +) diff --git a/_examples/golang-sse/proto/chat.ridl b/_examples/golang-sse/proto/chat.ridl new file mode 100644 index 00000000..4c02260c --- /dev/null +++ b/_examples/golang-sse/proto/chat.ridl @@ -0,0 +1,16 @@ +webrpc = v1 + +name = webrpc-sse-chat +version = v1.0.0 + +struct Message + - id: uint64 + - username: string + - text: string + - createdAt: timestamp + +service Chat + - SendMessage(username: string, text: string) + - SubscribeMessages(username: string) => stream (message: Message) + +error 100 EmptyUsername "Username must be provided." HTTP 400 diff --git a/_examples/golang-sse/rpc.go b/_examples/golang-sse/rpc.go new file mode 100644 index 00000000..7ad21674 --- /dev/null +++ b/_examples/golang-sse/rpc.go @@ -0,0 +1,92 @@ +package main + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/webrpc/webrpc/_example/golang-sse/proto" +) + +type ChatServer struct { + mu sync.Mutex + msgId uint64 + lastSubId uint64 + subs map[uint64]chan *proto.Message +} + +func NewChatServer() *ChatServer { + return &ChatServer{ + subs: map[uint64]chan *proto.Message{}, + } +} + +func (s *ChatServer) SendMessage(ctx context.Context, username string, text string) error { + s.mu.Lock() + defer s.mu.Unlock() + + msg := &proto.Message{ + Id: s.msgId, + Username: username, + Text: text, + CreatedAt: time.Now(), + } + s.msgId++ + + for _, sub := range s.subs { + sub := sub + go func() { + sub <- msg + }() + } + + return nil +} + +func (s *ChatServer) SubscribeMessages(ctx context.Context, username string, stream proto.SubscribeMessagesStreamWriter) error { + if username == "" { + return proto.ErrEmptyUsername + } + + s.SendMessage(ctx, "SYSTEM", fmt.Sprintf("%v joined", username)) + defer s.SendMessage(ctx, "SYSTEM", fmt.Sprintf("%v left", username)) + + msgs := make(chan *proto.Message, 10) + defer s.unsubscribe(s.subscribe(msgs)) + + for { + select { + case <-ctx.Done(): + switch err := ctx.Err(); err { + case context.Canceled: + return proto.ErrWebrpcClientDisconnected + default: + return proto.ErrWebrpcInternalError + } + + case msg := <-msgs: + if err := stream.Write(msg); err != nil { + return err + } + } + } +} + +func (s *ChatServer) subscribe(c chan *proto.Message) uint64 { + s.mu.Lock() + defer s.mu.Unlock() + + id := s.lastSubId + s.subs[id] = c + s.lastSubId++ + + return id +} + +func (s *ChatServer) unsubscribe(subscriptionId uint64) { + s.mu.Lock() + defer s.mu.Unlock() + + delete(s.subs, subscriptionId) +} diff --git a/_examples/golang-sse/webapp/.gitignore b/_examples/golang-sse/webapp/.gitignore new file mode 100644 index 00000000..a547bf36 --- /dev/null +++ b/_examples/golang-sse/webapp/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/_examples/golang-sse/webapp/index.html b/_examples/golang-sse/webapp/index.html new file mode 100644 index 00000000..52d3c695 --- /dev/null +++ b/_examples/golang-sse/webapp/index.html @@ -0,0 +1,30 @@ + + + + + + Chat + + +
+
+
Chat
+
    +
    +
    + + +
    +
    +
    + +
    + + + diff --git a/_examples/golang-sse/webapp/package.json b/_examples/golang-sse/webapp/package.json new file mode 100644 index 00000000..aca81ce0 --- /dev/null +++ b/_examples/golang-sse/webapp/package.json @@ -0,0 +1,20 @@ +{ + "name": "web-rpc-sse-ts", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "test": "vitest" + }, + "devDependencies": { + "typescript": "^5.2.2", + "vite": "^5.0.13", + "vitest": "^1.0.2" + }, + "dependencies": { + "@preact/signals-core": "^1.5.0" + } +} diff --git a/_examples/golang-sse/webapp/pnpm-lock.yaml b/_examples/golang-sse/webapp/pnpm-lock.yaml new file mode 100644 index 00000000..66910558 --- /dev/null +++ b/_examples/golang-sse/webapp/pnpm-lock.yaml @@ -0,0 +1,881 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +dependencies: + '@preact/signals-core': + specifier: ^1.5.0 + version: 1.5.0 + +devDependencies: + typescript: + specifier: ^5.2.2 + version: 5.3.2 + vite: + specifier: ^5.0.13 + version: 5.0.13 + vitest: + specifier: ^1.0.2 + version: 1.0.2 + +packages: + + /@esbuild/android-arm64@0.19.8: + resolution: {integrity: sha512-B8JbS61bEunhfx8kasogFENgQfr/dIp+ggYXwTqdbMAgGDhRa3AaPpQMuQU0rNxDLECj6FhDzk1cF9WHMVwrtA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm@0.19.8: + resolution: {integrity: sha512-31E2lxlGM1KEfivQl8Yf5aYU/mflz9g06H6S15ITUFQueMFtFjESRMoDSkvMo8thYvLBax+VKTPlpnx+sPicOA==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-x64@0.19.8: + resolution: {integrity: sha512-rdqqYfRIn4jWOp+lzQttYMa2Xar3OK9Yt2fhOhzFXqg0rVWEfSclJvZq5fZslnz6ypHvVf3CT7qyf0A5pM682A==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-arm64@0.19.8: + resolution: {integrity: sha512-RQw9DemMbIq35Bprbboyf8SmOr4UXsRVxJ97LgB55VKKeJOOdvsIPy0nFyF2l8U+h4PtBx/1kRf0BelOYCiQcw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-x64@0.19.8: + resolution: {integrity: sha512-3sur80OT9YdeZwIVgERAysAbwncom7b4bCI2XKLjMfPymTud7e/oY4y+ci1XVp5TfQp/bppn7xLw1n/oSQY3/Q==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-arm64@0.19.8: + resolution: {integrity: sha512-WAnPJSDattvS/XtPCTj1tPoTxERjcTpH6HsMr6ujTT+X6rylVe8ggxk8pVxzf5U1wh5sPODpawNicF5ta/9Tmw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-x64@0.19.8: + resolution: {integrity: sha512-ICvZyOplIjmmhjd6mxi+zxSdpPTKFfyPPQMQTK/w+8eNK6WV01AjIztJALDtwNNfFhfZLux0tZLC+U9nSyA5Zg==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm64@0.19.8: + resolution: {integrity: sha512-z1zMZivxDLHWnyGOctT9JP70h0beY54xDDDJt4VpTX+iwA77IFsE1vCXWmprajJGa+ZYSqkSbRQ4eyLCpCmiCQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm@0.19.8: + resolution: {integrity: sha512-H4vmI5PYqSvosPaTJuEppU9oz1dq2A7Mr2vyg5TF9Ga+3+MGgBdGzcyBP7qK9MrwFQZlvNyJrvz6GuCaj3OukQ==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ia32@0.19.8: + resolution: {integrity: sha512-1a8suQiFJmZz1khm/rDglOc8lavtzEMRo0v6WhPgxkrjcU0LkHj+TwBrALwoz/OtMExvsqbbMI0ChyelKabSvQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-loong64@0.19.8: + resolution: {integrity: sha512-fHZWS2JJxnXt1uYJsDv9+b60WCc2RlvVAy1F76qOLtXRO+H4mjt3Tr6MJ5l7Q78X8KgCFudnTuiQRBhULUyBKQ==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-mips64el@0.19.8: + resolution: {integrity: sha512-Wy/z0EL5qZYLX66dVnEg9riiwls5IYnziwuju2oUiuxVc+/edvqXa04qNtbrs0Ukatg5HEzqT94Zs7J207dN5Q==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ppc64@0.19.8: + resolution: {integrity: sha512-ETaW6245wK23YIEufhMQ3HSeHO7NgsLx8gygBVldRHKhOlD1oNeNy/P67mIh1zPn2Hr2HLieQrt6tWrVwuqrxg==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-riscv64@0.19.8: + resolution: {integrity: sha512-T2DRQk55SgoleTP+DtPlMrxi/5r9AeFgkhkZ/B0ap99zmxtxdOixOMI570VjdRCs9pE4Wdkz7JYrsPvsl7eESg==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-s390x@0.19.8: + resolution: {integrity: sha512-NPxbdmmo3Bk7mbNeHmcCd7R7fptJaczPYBaELk6NcXxy7HLNyWwCyDJ/Xx+/YcNH7Im5dHdx9gZ5xIwyliQCbg==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-x64@0.19.8: + resolution: {integrity: sha512-lytMAVOM3b1gPypL2TRmZ5rnXl7+6IIk8uB3eLsV1JwcizuolblXRrc5ShPrO9ls/b+RTp+E6gbsuLWHWi2zGg==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/netbsd-x64@0.19.8: + resolution: {integrity: sha512-hvWVo2VsXz/8NVt1UhLzxwAfo5sioj92uo0bCfLibB0xlOmimU/DeAEsQILlBQvkhrGjamP0/el5HU76HAitGw==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/openbsd-x64@0.19.8: + resolution: {integrity: sha512-/7Y7u77rdvmGTxR83PgaSvSBJCC2L3Kb1M/+dmSIvRvQPXXCuC97QAwMugBNG0yGcbEGfFBH7ojPzAOxfGNkwQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/sunos-x64@0.19.8: + resolution: {integrity: sha512-9Lc4s7Oi98GqFA4HzA/W2JHIYfnXbUYgekUP/Sm4BG9sfLjyv6GKKHKKVs83SMicBF2JwAX6A1PuOLMqpD001w==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-arm64@0.19.8: + resolution: {integrity: sha512-rq6WzBGjSzihI9deW3fC2Gqiak68+b7qo5/3kmB6Gvbh/NYPA0sJhrnp7wgV4bNwjqM+R2AApXGxMO7ZoGhIJg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-ia32@0.19.8: + resolution: {integrity: sha512-AIAbverbg5jMvJznYiGhrd3sumfwWs8572mIJL5NQjJa06P8KfCPWZQ0NwZbPQnbQi9OWSZhFVSUWjjIrn4hSw==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-x64@0.19.8: + resolution: {integrity: sha512-bfZ0cQ1uZs2PqpulNL5j/3w+GDhP36k1K5c38QdQg+Swy51jFZWWeIkteNsufkQxp986wnqRRsb/bHbY1WQ7TA==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@jest/schemas@29.6.3: + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@sinclair/typebox': 0.27.8 + dev: true + + /@jridgewell/sourcemap-codec@1.4.15: + resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + dev: true + + /@preact/signals-core@1.5.0: + resolution: {integrity: sha512-U2diO1Z4i1n2IoFgMYmRdHWGObNrcuTRxyNEn7deSq2cru0vj0583HYQZHsAqcs7FE+hQyX3mjIV7LAfHCvy8w==} + dev: false + + /@rollup/rollup-android-arm-eabi@4.6.0: + resolution: {integrity: sha512-keHkkWAe7OtdALGoutLY3utvthkGF+Y17ws9LYT8pxMBYXaCoH/8dXS2uzo6e8+sEhY7y/zi5RFo22Dy2lFpDw==} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-android-arm64@4.6.0: + resolution: {integrity: sha512-y3Kt+34smKQNWilicPbBz/MXEY7QwDzMFNgwEWeYiOhUt9MTWKjHqe3EVkXwT2fR7izOvHpDWZ0o2IyD9SWX7A==} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-darwin-arm64@4.6.0: + resolution: {integrity: sha512-oLzzxcUIHltHxOCmaXl+pkIlU+uhSxef5HfntW7RsLh1eHm+vJzjD9Oo4oUKso4YuP4PpbFJNlZjJuOrxo8dPg==} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-darwin-x64@4.6.0: + resolution: {integrity: sha512-+ANnmjkcOBaV25n0+M0Bere3roeVAnwlKW65qagtuAfIxXF9YxUneRyAn/RDcIdRa7QrjRNJL3jR7T43ObGe8Q==} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-arm-gnueabihf@4.6.0: + resolution: {integrity: sha512-tBTSIkjSVUyrekddpkAqKOosnj1Fc0ZY0rJL2bIEWPKqlEQk0paORL9pUIlt7lcGJi3LzMIlUGXvtNi1Z6MOCQ==} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-arm64-gnu@4.6.0: + resolution: {integrity: sha512-Ed8uJI3kM11de9S0j67wAV07JUNhbAqIrDYhQBrQW42jGopgheyk/cdcshgGO4fW5Wjq97COCY/BHogdGvKVNQ==} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-arm64-musl@4.6.0: + resolution: {integrity: sha512-mZoNQ/qK4D7SSY8v6kEsAAyDgznzLLuSFCA3aBHZTmf3HP/dW4tNLTtWh9+LfyO0Z1aUn+ecpT7IQ3WtIg3ViQ==} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-x64-gnu@4.6.0: + resolution: {integrity: sha512-rouezFHpwCqdEXsqAfNsTgSWO0FoZ5hKv5p+TGO5KFhyN/dvYXNMqMolOb8BkyKcPqjYRBeT+Z6V3aM26rPaYg==} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-x64-musl@4.6.0: + resolution: {integrity: sha512-Bbm+fyn3S6u51urfj3YnqBXg5vI2jQPncRRELaucmhBVyZkbWClQ1fEsRmdnCPpQOQfkpg9gZArvtMVkOMsh1w==} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-win32-arm64-msvc@4.6.0: + resolution: {integrity: sha512-+MRMcyx9L2kTrTUzYmR61+XVsliMG4odFb5UmqtiT8xOfEicfYAGEuF/D1Pww1+uZkYhBqAHpvju7VN+GnC3ng==} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-win32-ia32-msvc@4.6.0: + resolution: {integrity: sha512-rxfeE6K6s/Xl2HGeK6cO8SiQq3k/3BYpw7cfhW5Bk2euXNEpuzi2cc7llxx1si1QgwfjNtdRNTGqdBzGlFZGFw==} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-win32-x64-msvc@4.6.0: + resolution: {integrity: sha512-QqmCsydHS172Y0Kc13bkMXvipbJSvzeglBncJG3LsYJSiPlxYACz7MmJBs4A8l1oU+jfhYEIC/+AUSlvjmiX/g==} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@sinclair/typebox@0.27.8: + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + dev: true + + /@vitest/expect@1.0.2: + resolution: {integrity: sha512-mAIo/8uddSWkjQMLFcjqZP3WmkwvvN0OtlyZIu33jFnwme3vZds8m8EDMxtj+Uzni2DwtPfHNjJcTM8zTV1f4A==} + dependencies: + '@vitest/spy': 1.0.2 + '@vitest/utils': 1.0.2 + chai: 4.3.10 + dev: true + + /@vitest/runner@1.0.2: + resolution: {integrity: sha512-ZcHJXPT2kg/9Hc4fNkCbItlsgZSs3m4vQbxB8LCSdzpbG85bExCmSvu6K9lWpMNdoKfAr1Jn0BwS9SWUcGnbTQ==} + dependencies: + '@vitest/utils': 1.0.2 + p-limit: 5.0.0 + pathe: 1.1.1 + dev: true + + /@vitest/snapshot@1.0.2: + resolution: {integrity: sha512-9ClDz2/aV5TfWA4reV7XR9p+hE0e7bifhwxlURugj3Fw0YXeTFzHmKCNEHd6wOIFMfthbGGwhlq7TOJ2jDO4/g==} + dependencies: + magic-string: 0.30.5 + pathe: 1.1.1 + pretty-format: 29.7.0 + dev: true + + /@vitest/spy@1.0.2: + resolution: {integrity: sha512-YlnHmDntp+zNV3QoTVFI5EVHV0AXpiThd7+xnDEbWnD6fw0TH/J4/+3GFPClLimR39h6nA5m0W4Bjm5Edg4A/A==} + dependencies: + tinyspy: 2.2.0 + dev: true + + /@vitest/utils@1.0.2: + resolution: {integrity: sha512-GPQkGHAnFAP/+seSbB9pCsj339yRrMgILoI5H2sPevTLCYgBq0VRjF8QSllmnQyvf0EontF6KUIt2t5s2SmqoQ==} + dependencies: + diff-sequences: 29.6.3 + loupe: 2.3.7 + pretty-format: 29.7.0 + dev: true + + /acorn-walk@8.3.1: + resolution: {integrity: sha512-TgUZgYvqZprrl7YldZNoa9OciCAyZR+Ejm9eXzKCmjsF5IKp/wgQ7Z/ZpjpGTIUPwrHQIcYeI8qDh4PsEwxMbw==} + engines: {node: '>=0.4.0'} + dev: true + + /acorn@8.11.2: + resolution: {integrity: sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + + /ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + dev: true + + /assertion-error@1.1.0: + resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + dev: true + + /cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + dev: true + + /chai@4.3.10: + resolution: {integrity: sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==} + engines: {node: '>=4'} + dependencies: + assertion-error: 1.1.0 + check-error: 1.0.3 + deep-eql: 4.1.3 + get-func-name: 2.0.2 + loupe: 2.3.7 + pathval: 1.1.1 + type-detect: 4.0.8 + dev: true + + /check-error@1.0.3: + resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} + dependencies: + get-func-name: 2.0.2 + dev: true + + /cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + dev: true + + /debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + dev: true + + /deep-eql@4.1.3: + resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==} + engines: {node: '>=6'} + dependencies: + type-detect: 4.0.8 + dev: true + + /diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: true + + /esbuild@0.19.8: + resolution: {integrity: sha512-l7iffQpT2OrZfH2rXIp7/FkmaeZM0vxbxN9KfiCwGYuZqzMg/JdvX26R31Zxn/Pxvsrg3Y9N6XTcnknqDyyv4w==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/android-arm': 0.19.8 + '@esbuild/android-arm64': 0.19.8 + '@esbuild/android-x64': 0.19.8 + '@esbuild/darwin-arm64': 0.19.8 + '@esbuild/darwin-x64': 0.19.8 + '@esbuild/freebsd-arm64': 0.19.8 + '@esbuild/freebsd-x64': 0.19.8 + '@esbuild/linux-arm': 0.19.8 + '@esbuild/linux-arm64': 0.19.8 + '@esbuild/linux-ia32': 0.19.8 + '@esbuild/linux-loong64': 0.19.8 + '@esbuild/linux-mips64el': 0.19.8 + '@esbuild/linux-ppc64': 0.19.8 + '@esbuild/linux-riscv64': 0.19.8 + '@esbuild/linux-s390x': 0.19.8 + '@esbuild/linux-x64': 0.19.8 + '@esbuild/netbsd-x64': 0.19.8 + '@esbuild/openbsd-x64': 0.19.8 + '@esbuild/sunos-x64': 0.19.8 + '@esbuild/win32-arm64': 0.19.8 + '@esbuild/win32-ia32': 0.19.8 + '@esbuild/win32-x64': 0.19.8 + dev: true + + /execa@8.0.1: + resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} + engines: {node: '>=16.17'} + dependencies: + cross-spawn: 7.0.3 + get-stream: 8.0.1 + human-signals: 5.0.0 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.1.0 + onetime: 6.0.0 + signal-exit: 4.1.0 + strip-final-newline: 3.0.0 + dev: true + + /fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /get-func-name@2.0.2: + resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} + dev: true + + /get-stream@8.0.1: + resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} + engines: {node: '>=16'} + dev: true + + /human-signals@5.0.0: + resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} + engines: {node: '>=16.17.0'} + dev: true + + /is-stream@3.0.0: + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: true + + /isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + dev: true + + /jsonc-parser@3.2.0: + resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} + dev: true + + /local-pkg@0.5.0: + resolution: {integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==} + engines: {node: '>=14'} + dependencies: + mlly: 1.4.2 + pkg-types: 1.0.3 + dev: true + + /loupe@2.3.7: + resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} + dependencies: + get-func-name: 2.0.2 + dev: true + + /magic-string@0.30.5: + resolution: {integrity: sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + + /merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + dev: true + + /mimic-fn@4.0.0: + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} + dev: true + + /mlly@1.4.2: + resolution: {integrity: sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg==} + dependencies: + acorn: 8.11.2 + pathe: 1.1.1 + pkg-types: 1.0.3 + ufo: 1.3.2 + dev: true + + /ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + dev: true + + /nanoid@3.3.7: + resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + dev: true + + /npm-run-path@5.1.0: + resolution: {integrity: sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + path-key: 4.0.0 + dev: true + + /onetime@6.0.0: + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} + engines: {node: '>=12'} + dependencies: + mimic-fn: 4.0.0 + dev: true + + /p-limit@5.0.0: + resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==} + engines: {node: '>=18'} + dependencies: + yocto-queue: 1.0.0 + dev: true + + /path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + dev: true + + /path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + dev: true + + /pathe@1.1.1: + resolution: {integrity: sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==} + dev: true + + /pathval@1.1.1: + resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} + dev: true + + /picocolors@1.0.0: + resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + dev: true + + /pkg-types@1.0.3: + resolution: {integrity: sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==} + dependencies: + jsonc-parser: 3.2.0 + mlly: 1.4.2 + pathe: 1.1.1 + dev: true + + /postcss@8.4.38: + resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.7 + picocolors: 1.0.0 + source-map-js: 1.2.0 + dev: true + + /pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.6.3 + ansi-styles: 5.2.0 + react-is: 18.2.0 + dev: true + + /react-is@18.2.0: + resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} + dev: true + + /rollup@4.6.0: + resolution: {integrity: sha512-R8i5Her4oO1LiMQ3jKf7MUglYV/mhQ5g5OKeld5CnkmPdIGo79FDDQYqPhq/PCVuTQVuxsWgIbDy9F+zdHn80w==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.6.0 + '@rollup/rollup-android-arm64': 4.6.0 + '@rollup/rollup-darwin-arm64': 4.6.0 + '@rollup/rollup-darwin-x64': 4.6.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.6.0 + '@rollup/rollup-linux-arm64-gnu': 4.6.0 + '@rollup/rollup-linux-arm64-musl': 4.6.0 + '@rollup/rollup-linux-x64-gnu': 4.6.0 + '@rollup/rollup-linux-x64-musl': 4.6.0 + '@rollup/rollup-win32-arm64-msvc': 4.6.0 + '@rollup/rollup-win32-ia32-msvc': 4.6.0 + '@rollup/rollup-win32-x64-msvc': 4.6.0 + fsevents: 2.3.3 + dev: true + + /shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + dependencies: + shebang-regex: 3.0.0 + dev: true + + /shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + dev: true + + /siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + dev: true + + /signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + dev: true + + /source-map-js@1.2.0: + resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} + engines: {node: '>=0.10.0'} + dev: true + + /stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + dev: true + + /std-env@3.6.0: + resolution: {integrity: sha512-aFZ19IgVmhdB2uX599ve2kE6BIE3YMnQ6Gp6BURhW/oIzpXGKr878TQfAQZn1+i0Flcc/UKUy1gOlcfaUBCryg==} + dev: true + + /strip-final-newline@3.0.0: + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} + dev: true + + /strip-literal@1.3.0: + resolution: {integrity: sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==} + dependencies: + acorn: 8.11.2 + dev: true + + /tinybench@2.5.1: + resolution: {integrity: sha512-65NKvSuAVDP/n4CqH+a9w2kTlLReS9vhsAP06MWx+/89nMinJyB2icyl58RIcqCmIggpojIGeuJGhjU1aGMBSg==} + dev: true + + /tinypool@0.8.1: + resolution: {integrity: sha512-zBTCK0cCgRROxvs9c0CGK838sPkeokNGdQVUUwHAbynHFlmyJYj825f/oRs528HaIJ97lo0pLIlDUzwN+IorWg==} + engines: {node: '>=14.0.0'} + dev: true + + /tinyspy@2.2.0: + resolution: {integrity: sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==} + engines: {node: '>=14.0.0'} + dev: true + + /type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + dev: true + + /typescript@5.3.2: + resolution: {integrity: sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==} + engines: {node: '>=14.17'} + hasBin: true + dev: true + + /ufo@1.3.2: + resolution: {integrity: sha512-o+ORpgGwaYQXgqGDwd+hkS4PuZ3QnmqMMxRuajK/a38L6fTpcE5GPIfrf+L/KemFzfUpeUQc1rRS1iDBozvnFA==} + dev: true + + /vite-node@1.0.2: + resolution: {integrity: sha512-h7BbMJf46fLvFW/9Ygo3snkIBEHFh6fHpB4lge98H5quYrDhPFeI3S0LREz328uqPWSnii2yeJXktQ+Pmqk5BQ==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + dependencies: + cac: 6.7.14 + debug: 4.3.4 + pathe: 1.1.1 + picocolors: 1.0.0 + vite: 5.0.13 + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true + + /vite@5.0.13: + resolution: {integrity: sha512-/9ovhv2M2dGTuA+dY93B9trfyWMDRQw2jdVBhHNP6wr0oF34wG2i/N55801iZIpgUpnHDm4F/FabGQLyc+eOgg==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + dependencies: + esbuild: 0.19.8 + postcss: 8.4.38 + rollup: 4.6.0 + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /vitest@1.0.2: + resolution: {integrity: sha512-F3NVwwpXfRSDnJmyv+ALPwSRVt0zDkRRE18pwUHSUPXAlWQ47rY1dc99ziMW5bBHyqwK2ERjMisLNoef64qk9w==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': ^1.0.0 + '@vitest/ui': ^1.0.0 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + dependencies: + '@vitest/expect': 1.0.2 + '@vitest/runner': 1.0.2 + '@vitest/snapshot': 1.0.2 + '@vitest/spy': 1.0.2 + '@vitest/utils': 1.0.2 + acorn-walk: 8.3.1 + cac: 6.7.14 + chai: 4.3.10 + debug: 4.3.4 + execa: 8.0.1 + local-pkg: 0.5.0 + magic-string: 0.30.5 + pathe: 1.1.1 + picocolors: 1.0.0 + std-env: 3.6.0 + strip-literal: 1.3.0 + tinybench: 2.5.1 + tinypool: 0.8.1 + vite: 5.0.13 + vite-node: 1.0.2 + why-is-node-running: 2.2.2 + transitivePeerDependencies: + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true + + /which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + dependencies: + isexe: 2.0.0 + dev: true + + /why-is-node-running@2.2.2: + resolution: {integrity: sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==} + engines: {node: '>=8'} + hasBin: true + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + dev: true + + /yocto-queue@1.0.0: + resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} + engines: {node: '>=12.20'} + dev: true diff --git a/_examples/golang-sse/webapp/src/main.ts b/_examples/golang-sse/webapp/src/main.ts new file mode 100644 index 00000000..e3590c6a --- /dev/null +++ b/_examples/golang-sse/webapp/src/main.ts @@ -0,0 +1,189 @@ +import { effect, signal } from "@preact/signals-core"; +import { Chat, Message, SubscribeMessagesReturn, WebrpcError } from "./rpc.gen"; +import "./style.css"; + +// Create client +const api = new Chat("http://localhost:4848", fetch); + +// Create signal for messages and log +const messages = signal([]); +const connectionStatus = signal< + "connected" | "error" | "aborted" | "disconnected" +>("disconnected"); + +type Log = { type: "error" | "info" | "warn"; log: string }; +const log = signal([]); +const appendLog = (logValue: Log) => { + log.value = [...log.value, logValue]; +}; + +// Create message handlers +const onMessage = (message: SubscribeMessagesReturn) => { + console.log("onMessage()", message); + messages.value = [...messages.value, message.message]; +}; + +let retryCount = 0; +const maxDelay = 30; // seconds + +const onError = (error: WebrpcError, reconnect?: () => void) => { + connectionStatus.value = "error"; + console.error("onError()", error); + if (error.message == "AbortError") { + connectionStatus.value = "aborted"; + appendLog({ type: "warn", log: "Connection closed by abort signal" }); + // TODO: Reconnect + } else { + appendLog({ type: "error", log: "Lost connection" }); + appendLog({ type: "error", log: String(error) }); + if (reconnect) { + appendLog({ + type: "warn", + log: `Attempting reconnect ${retryCount + 1}`, + }); + retryCount++; + const backoffTime = Math.min(maxDelay, Math.pow(2, retryCount)) * 1000; + setTimeout(reconnect, backoffTime); + } + } +}; + +const onOpen = () => { + console.log("onOpen()"); + connectionStatus.value = "connected"; + appendLog({ type: "info", log: "Connected" }); +}; + +const onClose = () => { + console.log("onClose()"); + connectionStatus.value = "disconnected"; + appendLog({ type: "info", log: "Disconnected" }); +}; + +const username = randomUserName(); + +const controller = new AbortController(); +const abortSignal = controller.signal; + +const toggleConnectHandler = () => { + if (connectionStatus.value == "connected") { + controller.abort(); + connectionStatus.value = "aborted"; + } else if (connectionStatus.value == "aborted") { + // reconnect(); + } +}; + +// Subscribe to messages +api.subscribeMessages( + { username }, + { onMessage, onError, onOpen, onClose, signal: abortSignal } +); + +// Update chatbox +const chatbox = document.querySelector("#chat"); + +effect(() => { + if (chatbox) { + chatbox.innerHTML = ` + ${messages.value + .map( + (message) => ` +
  • +
    + ${message.username} +
    +
    + ${message.text} +
    +
    ${formatTime(message.createdAt)}
    +
  • ` + ) + .join("")} + `; + chatbox.scrollTo(0, chatbox.scrollHeight); + } +}); + +// Send new message on submit +const form = document.querySelector("form") as HTMLFormElement; + +function sendMessage(event: Event) { + const textField = form.elements.namedItem("text") as HTMLInputElement; + try { + api.sendMessage({ + username, + text: textField.value, + }); + } catch (e) { + console.error(e); + } + textField.value = ""; + event.preventDefault(); +} + +form.addEventListener("submit", sendMessage); + +// Update log +const logEl = document.querySelector("#log"); + +effect(() => { + if (logEl) { + logEl.innerHTML = ` + ${log.value + .map((log) => `
    ${log.log}
    `) + .join("")} + `; + logEl.scrollTo(0, logEl.scrollHeight); + } +}); + +// Abort when disconnect button is clicked +const toggleConnectButton = document.getElementById( + "toggle-connect" +) as HTMLButtonElement; +toggleConnectButton.addEventListener("click", toggleConnectHandler); + +effect(() => { + switch (connectionStatus.value) { + case "connected": + toggleConnectButton.innerText = "Disconnect"; + toggleConnectButton.disabled = false; + break; + case "disconnected": + case "aborted": + toggleConnectButton.innerText = "Connect"; + toggleConnectButton.disabled = false; + break; + case "error": + toggleConnectButton.innerText = "Connection error"; + toggleConnectButton.disabled = true; + break; + } +}); + +function randomUserName() { + const names = [ + "Chuck Norris", + "Mr. Bean", + "Bugs Bunny", + "Homer Simpson", + "SpongeBob", + "Patrick Star", + "Pikachu", + "Mario", + "Luigi", + "Yoda", + ]; + const randomIndex = Math.floor(Math.random() * names.length); + return names[randomIndex]; +} + +function formatTime(dateString: string) { + const date = new Date(dateString); + const hours = date.getHours(); + const minutes = date.getMinutes(); + const seconds = date.getSeconds(); + + return `${hours}:${minutes}:${seconds}`; +} diff --git a/_examples/golang-sse/webapp/src/rpc.gen.ts b/_examples/golang-sse/webapp/src/rpc.gen.ts new file mode 100644 index 00000000..6e1eb371 --- /dev/null +++ b/_examples/golang-sse/webapp/src/rpc.gen.ts @@ -0,0 +1,575 @@ +/* eslint-disable */ +// webrpc-sse-chat v1.0.0 21faef7a7920c42e730d4a5df1dae90fde9f2e5b +// -- +// Code generated by webrpc-gen with typescript generator. DO NOT EDIT. +// +// webrpc-gen -schema=proto/chat.ridl -target=typescript -client -out=webapp/src/rpc.gen.ts + +export const WebrpcHeader = "Webrpc" + +export const WebrpcHeaderValue = "webrpc;gen-typescript@v0.16.2;webrpc-sse-chat@v1.0.0" + +// WebRPC description and code-gen version +export const WebRPCVersion = "v1" + +// Schema version of your RIDL schema +export const WebRPCSchemaVersion = "v1.0.0" + +// Schema hash generated from your RIDL schema +export const WebRPCSchemaHash = "21faef7a7920c42e730d4a5df1dae90fde9f2e5b" + +type WebrpcGenVersions = { + webrpcGenVersion: string; + codeGenName: string; + codeGenVersion: string; + schemaName: string; + schemaVersion: string; +}; + +export function VersionFromHeader(headers: Headers): WebrpcGenVersions { + const headerValue = headers.get(WebrpcHeader); + if (!headerValue) { + return { + webrpcGenVersion: "", + codeGenName: "", + codeGenVersion: "", + schemaName: "", + schemaVersion: "", + }; + } + + return parseWebrpcGenVersions(headerValue); +} + +function parseWebrpcGenVersions(header: string): WebrpcGenVersions { + const versions = header.split(";"); + if (versions.length < 3) { + return { + webrpcGenVersion: "", + codeGenName: "", + codeGenVersion: "", + schemaName: "", + schemaVersion: "", + }; + } + + const [_, webrpcGenVersion] = versions[0].split("@"); + const [codeGenName, codeGenVersion] = versions[1].split("@"); + const [schemaName, schemaVersion] = versions[2].split("@"); + + return { + webrpcGenVersion, + codeGenName, + codeGenVersion, + schemaName, + schemaVersion, + }; +} + +// +// Types +// + + +export interface Message { + id: number + username: string + text: string + createdAt: string +} + +export interface Chat { + sendMessage(args: SendMessageArgs, headers?: object, signal?: AbortSignal): Promise + subscribeMessages(args: SubscribeMessagesArgs, options: WebrpcStreamOptions): WebrpcStreamController +} + +export interface SendMessageArgs { + username: string + text: string +} + +export interface SendMessageReturn { +} +export interface SubscribeMessagesArgs { + username: string +} + +export interface SubscribeMessagesReturn { + message: Message +} + + + +// +// Client +// +export class Chat implements Chat { + protected hostname: string + protected fetch: Fetch + protected path = '/rpc/Chat/' + + constructor(hostname: string, fetch: Fetch) { + this.hostname = hostname.replace(/\/*$/, '') + this.fetch = (input: RequestInfo, init?: RequestInit) => fetch(input, init) + } + + private url(name: string): string { + return this.hostname + this.path + name + } + + sendMessage = (args: SendMessageArgs, headers?: object, signal?: AbortSignal): Promise => { + return this.fetch( + this.url('SendMessage'), + createHTTPRequest(args, headers, signal)).then((res) => { + return buildResponse(res).then(_data => { + return {} + }) + }, (error) => { + throw WebrpcRequestFailedError.new({ cause: `fetch(): ${error.message || ''}` }) + }) + } + + subscribeMessages = (args: SubscribeMessagesArgs, options: WebrpcStreamOptions): WebrpcStreamController => { + const abortController = new AbortController(); + const abortSignal = abortController.signal + + if (options.signal) { + abortSignal.addEventListener("abort", () => abortController.abort(options.signal?.reason), { + signal: options.signal, + }); + } + + const _fetch = () => this.fetch(this.url('SubscribeMessages'),createHTTPRequest(args, options.headers, abortSignal) + ).then(async (res) => { + await sseResponse(res, options, _fetch); + }, (error) => { + options.onError(error, _fetch); + }); + + const resp = _fetch(); + return { + abort: abortController.abort.bind(abortController), + closed: resp + }; + } +} + +const sseResponse = async ( + res: Response, + options: WebrpcStreamOptions, + retryFetch: () => Promise +) => { + const {onMessage, onOpen, onClose, onError} = options; + + if (!res.ok) { + try { + await buildResponse(res); + } catch (error) { + // @ts-ignore + onError(error, retryFetch); + } + return; + } + + if (!res.body) { + onError( + WebrpcBadResponseError.new({ + status: res.status, + cause: "Invalid response, missing body", + }), + retryFetch + ); + return; + } + + onOpen && onOpen(); + + const reader = res.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ""; + let lastReadTime = Date.now(); + const timeout = (10 + 1) * 1000; + let timeoutError = false + const intervalId = setInterval(() => { + if (Date.now() - lastReadTime > timeout) { + timeoutError = true + clearInterval(intervalId) + reader.releaseLock() + } + }, timeout); + + while (true) { + let value; + let done; + try { + ({value, done} = await reader.read()); + if (timeoutError) throw new Error("Timeout, no data or heartbeat received") + lastReadTime = Date.now(); + buffer += decoder.decode(value, {stream: true}); + } catch (error) { + let message = ""; + if (error instanceof Error) { + message = error.message; + } + + if (error instanceof DOMException && error.name === "AbortError") { + onError( + WebrpcRequestFailedError.new({ + message: "AbortError", + cause: `AbortError: ${message}`, + }), + () => { + throw new Error("Abort signal cannot be used to reconnect"); + } + ); + } else { + onError( + WebrpcStreamLostError.new({ + cause: `reader.read(): ${message}`, + }), + retryFetch + ); + } + return; + } + + let lines = buffer.split("\n"); + for (let i = 0; i < lines.length - 1; i++) { + if (lines[i].length == 0) { + continue; + } + let data: any; + try { + data = JSON.parse(lines[i]); + if (data.hasOwnProperty("webrpcError")) { + const error = data.webrpcError; + const code: number = + typeof error.code === "number" ? error.code : 0; + onError( + (webrpcErrorByCode[code] || WebrpcError).new(error), + retryFetch + ); + return; + } + } catch (error) { + if ( + error instanceof Error && + error.message === "Abort signal cannot be used to reconnect" + ) { + throw error; + } + onError( + WebrpcBadResponseError.new({ + status: res.status, + // @ts-ignore + cause: `JSON.parse(): ${error.message}`, + }), + retryFetch + ); + } + onMessage(data); + } + + if (!done) { + buffer = lines[lines.length - 1]; + continue; + } + + onClose && onClose(); + return; + } +}; + + + + const createHTTPRequest = (body: object = {}, headers: object = {}, signal: AbortSignal | null = null): object => { + const reqHeaders: {[key: string]: string} = { ...headers, 'Content-Type': 'application/json' } + reqHeaders[WebrpcHeader] = WebrpcHeaderValue + + return { + method: 'POST', + headers: reqHeaders, + body: JSON.stringify(body || {}), + signal + } +} + +const buildResponse = (res: Response): Promise => { + return res.text().then(text => { + let data + try { + data = JSON.parse(text) + } catch(error) { + let message = '' + if (error instanceof Error) { + message = error.message + } + throw WebrpcBadResponseError.new({ + status: res.status, + cause: `JSON.parse(): ${message}: response text: ${text}`}, + ) + } + if (!res.ok) { + const code: number = (typeof data.code === 'number') ? data.code : 0 + throw (webrpcErrorByCode[code] || WebrpcError).new(data) + } + return data + }) +} + +// +// Errors +// + +export class WebrpcError extends Error { + name: string + code: number + message: string + status: number + cause?: string + + /** @deprecated Use message instead of msg. Deprecated in webrpc v0.11.0. */ + msg: string + + constructor(name: string, code: number, message: string, status: number, cause?: string) { + super(message) + this.name = name || 'WebrpcError' + this.code = typeof code === 'number' ? code : 0 + this.message = message || `endpoint error ${this.code}` + this.msg = this.message + this.status = typeof status === 'number' ? status : 0 + this.cause = cause + Object.setPrototypeOf(this, WebrpcError.prototype) + } + + static new(payload: any): WebrpcError { + return new this(payload.error, payload.code, payload.message || payload.msg, payload.status, payload.cause) + } +} + +// Webrpc errors + +export class WebrpcEndpointError extends WebrpcError { + constructor( + name: string = 'WebrpcEndpoint', + code: number = 0, + message: string = `endpoint error`, + status: number = 0, + cause?: string + ) { + super(name, code, message, status, cause) + Object.setPrototypeOf(this, WebrpcEndpointError.prototype) + } +} + +export class WebrpcRequestFailedError extends WebrpcError { + constructor( + name: string = 'WebrpcRequestFailed', + code: number = -1, + message: string = `request failed`, + status: number = 0, + cause?: string + ) { + super(name, code, message, status, cause) + Object.setPrototypeOf(this, WebrpcRequestFailedError.prototype) + } +} + +export class WebrpcBadRouteError extends WebrpcError { + constructor( + name: string = 'WebrpcBadRoute', + code: number = -2, + message: string = `bad route`, + status: number = 0, + cause?: string + ) { + super(name, code, message, status, cause) + Object.setPrototypeOf(this, WebrpcBadRouteError.prototype) + } +} + +export class WebrpcBadMethodError extends WebrpcError { + constructor( + name: string = 'WebrpcBadMethod', + code: number = -3, + message: string = `bad method`, + status: number = 0, + cause?: string + ) { + super(name, code, message, status, cause) + Object.setPrototypeOf(this, WebrpcBadMethodError.prototype) + } +} + +export class WebrpcBadRequestError extends WebrpcError { + constructor( + name: string = 'WebrpcBadRequest', + code: number = -4, + message: string = `bad request`, + status: number = 0, + cause?: string + ) { + super(name, code, message, status, cause) + Object.setPrototypeOf(this, WebrpcBadRequestError.prototype) + } +} + +export class WebrpcBadResponseError extends WebrpcError { + constructor( + name: string = 'WebrpcBadResponse', + code: number = -5, + message: string = `bad response`, + status: number = 0, + cause?: string + ) { + super(name, code, message, status, cause) + Object.setPrototypeOf(this, WebrpcBadResponseError.prototype) + } +} + +export class WebrpcServerPanicError extends WebrpcError { + constructor( + name: string = 'WebrpcServerPanic', + code: number = -6, + message: string = `server panic`, + status: number = 0, + cause?: string + ) { + super(name, code, message, status, cause) + Object.setPrototypeOf(this, WebrpcServerPanicError.prototype) + } +} + +export class WebrpcInternalErrorError extends WebrpcError { + constructor( + name: string = 'WebrpcInternalError', + code: number = -7, + message: string = `internal error`, + status: number = 0, + cause?: string + ) { + super(name, code, message, status, cause) + Object.setPrototypeOf(this, WebrpcInternalErrorError.prototype) + } +} + +export class WebrpcClientDisconnectedError extends WebrpcError { + constructor( + name: string = 'WebrpcClientDisconnected', + code: number = -8, + message: string = `client disconnected`, + status: number = 0, + cause?: string + ) { + super(name, code, message, status, cause) + Object.setPrototypeOf(this, WebrpcClientDisconnectedError.prototype) + } +} + +export class WebrpcStreamLostError extends WebrpcError { + constructor( + name: string = 'WebrpcStreamLost', + code: number = -9, + message: string = `stream lost`, + status: number = 0, + cause?: string + ) { + super(name, code, message, status, cause) + Object.setPrototypeOf(this, WebrpcStreamLostError.prototype) + } +} + +export class WebrpcStreamFinishedError extends WebrpcError { + constructor( + name: string = 'WebrpcStreamFinished', + code: number = -10, + message: string = `stream finished`, + status: number = 0, + cause?: string + ) { + super(name, code, message, status, cause) + Object.setPrototypeOf(this, WebrpcStreamFinishedError.prototype) + } +} + + +// Schema errors + +export class EmptyUsernameError extends WebrpcError { + constructor( + name: string = 'EmptyUsername', + code: number = 100, + message: string = `Username must be provided.`, + status: number = 0, + cause?: string + ) { + super(name, code, message, status, cause) + Object.setPrototypeOf(this, EmptyUsernameError.prototype) + } +} + + +export enum errors { + WebrpcEndpoint = 'WebrpcEndpoint', + WebrpcRequestFailed = 'WebrpcRequestFailed', + WebrpcBadRoute = 'WebrpcBadRoute', + WebrpcBadMethod = 'WebrpcBadMethod', + WebrpcBadRequest = 'WebrpcBadRequest', + WebrpcBadResponse = 'WebrpcBadResponse', + WebrpcServerPanic = 'WebrpcServerPanic', + WebrpcInternalError = 'WebrpcInternalError', + WebrpcClientDisconnected = 'WebrpcClientDisconnected', + WebrpcStreamLost = 'WebrpcStreamLost', + WebrpcStreamFinished = 'WebrpcStreamFinished', + EmptyUsername = 'EmptyUsername', +} + +export enum WebrpcErrorCodes { + WebrpcEndpoint = 0, + WebrpcRequestFailed = -1, + WebrpcBadRoute = -2, + WebrpcBadMethod = -3, + WebrpcBadRequest = -4, + WebrpcBadResponse = -5, + WebrpcServerPanic = -6, + WebrpcInternalError = -7, + WebrpcClientDisconnected = -8, + WebrpcStreamLost = -9, + WebrpcStreamFinished = -10, + EmptyUsername = 100, +} + +export const webrpcErrorByCode: { [code: number]: any } = { + [0]: WebrpcEndpointError, + [-1]: WebrpcRequestFailedError, + [-2]: WebrpcBadRouteError, + [-3]: WebrpcBadMethodError, + [-4]: WebrpcBadRequestError, + [-5]: WebrpcBadResponseError, + [-6]: WebrpcServerPanicError, + [-7]: WebrpcInternalErrorError, + [-8]: WebrpcClientDisconnectedError, + [-9]: WebrpcStreamLostError, + [-10]: WebrpcStreamFinishedError, + [100]: EmptyUsernameError, +} + +export type Fetch = (input: RequestInfo, init?: RequestInit) => Promise + +export interface WebrpcStreamOptions extends WebrpcOptions { + onMessage: (message: T) => void; + onError: (error: WebrpcError, reconnect: () => void) => void; + onOpen?: () => void; + onClose?: () => void; +} + +export interface WebrpcOptions { + headers?: HeadersInit; + signal?: AbortSignal; +} + +export interface WebrpcStreamController { + abort: (reason?: any) => void; + closed: Promise; +} + diff --git a/_examples/golang-sse/webapp/src/style.css b/_examples/golang-sse/webapp/src/style.css new file mode 100644 index 00000000..af45dac6 --- /dev/null +++ b/_examples/golang-sse/webapp/src/style.css @@ -0,0 +1,138 @@ +* { + box-sizing: border-box; +} + +body { + margin: 0; + padding: 0; + font-family: "Open Sans", sans-serif; + color: #9fa2a7; +} + +.container { + width: min(95%, 780px); + min-width: 450px; + margin: 30px auto; + border-radius: 6px; + box-shadow: 0 0 6px rgba(0, 0, 0, 0.3); + height: 530px; + display: grid; + grid-template-columns: 1fr 200px; +} + +header { + padding: 10px; + border-bottom: 1px solid #cfdae1; + color: #5d7185; + font-weight: bold; + font-size: 20px; +} + +.chat-container { + position: relative; +} + +#chat { + height: 435px; + overflow: auto; +} + +.side-bar { + border-left: 1px solid #cfdae1; + background: #e4eaee; + position: relative; +} + +#log { + padding: 10px; + height: 435px; + overflow: auto; +} + +#toggle-connect { + margin-left: auto; +} + +ul { + list-style: none; + margin: 0; + padding: 0; + overflow-y: auto; +} + +li { + border-bottom: 1px solid #cfdae1; + overflow: hidden; + display: grid; + grid-template-columns: 150px 1fr 60px; +} +li.me { + background: #e4eaee; +} + +li > div { + padding: 10px; +} +.name { + color: #5d7185; + font-weight: bold; +} +.message { + border-left: 1px solid #cfdae1; + color: #333f4d; +} + +.msg-time { + color: #738ba3; + font-size: 9px; +} +footer { + position: absolute; + bottom: 0; + width: 100%; + background: #e4eaee; + padding: 6px; + display: flex; + border-top: 1px solid #cfdae1; +} +form { + display: flex; + gap: 10px; + flex: 1; +} +input { + border: 0; + margin: 0; + padding: 0 10px; + outline: none; + color: #5d7185; + background: #fff; + font-weight: bold; + border-radius: 4px; + height: 36px; + line-height: 30px; + flex: 1; +} +button { + background: #a0b4c0; + border: none; + color: #fff; + border-radius: 4px; + padding: 10px; + cursor: pointer; +} + +#log > div { + padding-block: 7px; + border-bottom: 1px solid #cfdae1; +} + +.error { + color: red; +} +.info { + color: green; +} +.warn { + color: orange; +} diff --git a/_examples/golang-sse/webapp/src/vite-env.d.ts b/_examples/golang-sse/webapp/src/vite-env.d.ts new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/_examples/golang-sse/webapp/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/_examples/golang-sse/webapp/tests/sse.test.ts b/_examples/golang-sse/webapp/tests/sse.test.ts new file mode 100644 index 00000000..4f578bb0 --- /dev/null +++ b/_examples/golang-sse/webapp/tests/sse.test.ts @@ -0,0 +1,185 @@ +import { beforeEach, test, expect, vi } from "vitest"; + +import { + WebrpcError, + Chat, + SubscribeMessagesReturn, + Message, + WebrpcServerPanicError, + WebrpcRequestFailedError, + WebrpcStreamLostError, +} from "../src/rpc"; + +const data = [ + { + id: 1, + text: "Hello", + authorName: "John", + createdAt: new Date().toDateString(), + }, + { + id: 2, + text: "Hi", + authorName: "Joe", + createdAt: new Date().toDateString(), + }, + { + id: 3, + text: "How are you?", + authorName: "John", + createdAt: new Date().toDateString(), + }, +] satisfies Message[]; + +function createMockFetch( + { + status, + body, + errorAfter, + closeStream, + }: { + status?: number; + body?: any; + errorAfter?: number; + closeStream?: boolean; + } = { + status: 200, + body: undefined, + errorAfter: undefined, + closeStream: true, + } +) { + return function mockFetch(input: RequestInfo | URL, init?: RequestInit) { + const stream = new ReadableStream({ + start(controller) { + data.forEach((item) => { + const buffer = new TextEncoder().encode( + JSON.stringify({ message: item }) + "\n" + ); + controller.enqueue(buffer); + }); + + if (errorAfter) { + setTimeout(() => { + controller.error(); + }, errorAfter); + } else if (closeStream) { + controller.close(); + } + }, + }); + + body = body || stream; + + return Promise.resolve( + new Response(body, { + status, + headers: { + "Content-Type": "application/x-ndjson", + }, + }) + ); + }; +} + +let onMessage = (msg: any) => {}; +let onError = (err: WebrpcError) => {}; + +beforeEach(() => { + onMessage = (msg: any) => {}; + onError = (err: WebrpcError) => {}; +}); + +test("call onOpen right before opening stream", async () => { + const mockFetch = createMockFetch(); + const api = new Chat("", mockFetch); + + const onOpen = vi.fn(); + await api.subscribeMessages( + { serverTimeoutSec: 10 }, + { onOpen, onMessage, onError } + ); + + expect(onOpen).toHaveBeenCalled(); +}); + +test("call onMessage with correct data", async () => { + const mockFetch = createMockFetch(); + const api = new Chat("", mockFetch); + + let messages: Message[] = []; + const onMessage = (msg: SubscribeMessagesReturn) => { + messages.push(msg.message); + }; + await api.subscribeMessages({ serverTimeoutSec: 10 }, { onMessage, onError }); + + expect(messages).toEqual(data); +}); + +test("call onClose when stream is done", async () => { + const mockFetch = createMockFetch(); + const api = new Chat("", mockFetch); + + const onClose = vi.fn(); + + await api.subscribeMessages( + { serverTimeoutSec: 10 }, + { onMessage, onError, onClose } + ); + + expect(onClose).toHaveBeenCalled(); +}); + +test("call onError with WebrpcServerPanicError on server panic", async () => { + const mockFetch = createMockFetch({ + status: 500, + body: JSON.stringify({ code: -6 }), + }); + + const api = new Chat("", mockFetch); + let error: WebrpcError | undefined; + + const onError = (err: WebrpcError) => { + error = err; + }; + + await api.subscribeMessages({ serverTimeoutSec: 10 }, { onMessage, onError }); + + expect(error).toEqual(new WebrpcServerPanicError()); +}); + +test("call onError with WebrpcStreamLostError on stream error", async () => { + const mockFetch = createMockFetch({ errorAfter: 100 }); + const api = new Chat("", mockFetch); + let error: WebrpcError | undefined; + + const onError = (err: WebrpcError) => { + error = err; + }; + + await api.subscribeMessages({ serverTimeoutSec: 10 }, { onMessage, onError }); + + expect(error).toEqual(new WebrpcStreamLostError()); +}); + +test( + "call onError with WebrpcStreamLostError on stream timeout", + async () => { + const mockFetch = createMockFetch({ closeStream: false }); + const api = new Chat("", mockFetch); + let error: WebrpcError | undefined; + + const onError = (err: WebrpcError) => { + console.log("onError()", err); + error = err; + }; + + await api.subscribeMessages( + { serverTimeoutSec: 10 }, + { onMessage, onError } + ); + + expect(error).toEqual(new WebrpcStreamLostError()); + }, + 20 * 1000 +); diff --git a/_examples/golang-sse/webapp/tsconfig.json b/_examples/golang-sse/webapp/tsconfig.json new file mode 100644 index 00000000..75abdef2 --- /dev/null +++ b/_examples/golang-sse/webapp/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/_examples/golang-sse/webapp/vite.config.js b/_examples/golang-sse/webapp/vite.config.js new file mode 100644 index 00000000..6f5ba284 --- /dev/null +++ b/_examples/golang-sse/webapp/vite.config.js @@ -0,0 +1,5 @@ +export default { + server: { + strictPort: true, + }, +}; diff --git a/_examples/hello-webrpc-ts/Makefile b/_examples/hello-webrpc-ts/Makefile index 0e64c26f..aa876602 100644 --- a/_examples/hello-webrpc-ts/Makefile +++ b/_examples/hello-webrpc-ts/Makefile @@ -1,5 +1,5 @@ all: - @echo "please read Makefile source or README to see available commands" + @awk -F'[ :]' '!/^all:/ && /^([A-z_-]+):/ {print "make " $$1}' Makefile tools: GO111MODULE=off go get -u github.com/goware/webify @@ -7,10 +7,10 @@ tools: generate: generate-server generate-client generate-server: - ../../bin/webrpc-gen -schema=hello-api.ridl -target=go -pkg=main -server -out=./server/hello_api.gen.go + webrpc-gen -schema=hello-api.ridl -target=golang -pkg=main -server -out=./server/hello_api.gen.go generate-client: - ../../bin/webrpc-gen -schema=hello-api.ridl -target=ts -client -out=./webapp/src/client.gen.ts + webrpc-gen -schema=hello-api.ridl -target=typescript -client -out=./webapp/src/client.gen.ts bootstrap: rm -rf webapp/node_modules diff --git a/_examples/hello-webrpc-ts/hello-api.ridl b/_examples/hello-webrpc-ts/hello-api.ridl index 22116e05..cc79b0b3 100644 --- a/_examples/hello-webrpc-ts/hello-api.ridl +++ b/_examples/hello-webrpc-ts/hello-api.ridl @@ -7,7 +7,7 @@ enum Kind: uint32 - USER = 1 - ADMIN = 2 -message User +struct User - ID: uint64 + json = id + go.tag.db = id @@ -24,10 +24,11 @@ message User + json = - - createdAt?: timestamp - + json = created_at,omitempty + + json = created_at + + go.tag.json = created_at,omitempty + go.tag.db = created_at -message Page +struct Page - num: uint32 service ExampleService diff --git a/_examples/hello-webrpc-ts/hello-api.webrpc.json b/_examples/hello-webrpc-ts/hello-api.webrpc.json index 14830ef9..067d54fb 100644 --- a/_examples/hello-webrpc-ts/hello-api.webrpc.json +++ b/_examples/hello-webrpc-ts/hello-api.webrpc.json @@ -1,111 +1,120 @@ { - "webrpc": "v1", - "name": "hello-api", - "version": "v0.0.1", - "messages": [ - { - "name": "Kind", - "type": "enum", - "fields": [ - { - "name": "USER", - "type": "uint32", - "value": "1" - }, - { - "name": "ADMIN", - "type": "uint32", - "value": "2" - } - ] - }, - { - "name": "Empty", - "type": "struct", - "fields": [ - ] - }, - { - "name": "GetUserRequest", - "type": "struct", - "fields": [ - { - "name": "userID", - "type": "uint64", - "optional": false - } - ] - }, - { - "name": "User", - "type": "struct", - "fields": [ - { - "name": "ID", - "type": "uint64", - "optional": false, - "meta": [ - { "json": "id" }, - { "go.tag.db": "id" } - ] - }, - { - "name": "username", - "type": "string", - "optional": false, - "meta": [ - { "json": "USERNAME" }, - { "go.tag.db": "username" } - ] - }, - { - "name": "role", - "type": "Kind", - "optional": false - }, - { - "name": "createdAt", - "type": "timestamp", - "optional": true, - "meta": [ - { "json": "created_at,omitempty" }, - { "go.tag.db": "created_at" } - ] - } - - ] - } - ], - "services": [ - { - "name": "ExampleService", - "methods": [ - { - "name": "Ping", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "bool" - } - ] - }, - { - "name": "GetUser", - "inputs": [ - { - "name": "req", - "type": "GetUserRequest" - } - ], - "outputs": [ - { - "name": "", - "type": "User" - } - ] - } - ] - } - ] -} + "webrpc": "v1", + "name": "hello-api", + "version": "v0.0.1", + "types": [ + { + "name": "Kind", + "kind": "enum", + "type": "uint32", + "fields": [ + { + "name": "USER", + "value": "1" + }, + { + "name": "ADMIN", + "value": "2" + } + ] + }, + { + "name": "Empty", + "kind": "struct", + "fields": [] + }, + { + "name": "GetUserRequest", + "kind": "struct", + "fields": [ + { + "name": "userID", + "type": "uint64", + "optional": false + } + ] + }, + { + "name": "User", + "kind": "struct", + "fields": [ + { + "name": "ID", + "type": "uint64", + "optional": false, + "meta": [ + { + "json": "id" + }, + { + "go.tag.db": "id" + } + ] + }, + { + "name": "username", + "type": "string", + "optional": false, + "meta": [ + { + "json": "USERNAME" + }, + { + "go.tag.db": "username" + } + ] + }, + { + "name": "role", + "type": "Kind", + "optional": false + }, + { + "name": "createdAt", + "type": "timestamp", + "optional": true, + "meta": [ + { + "json": "created_at,omitempty" + }, + { + "go.tag.db": "created_at" + } + ] + } + ] + } + ], + "services": [ + { + "name": "ExampleService", + "methods": [ + { + "name": "Ping", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bool" + } + ] + }, + { + "name": "GetUser", + "inputs": [ + { + "name": "req", + "type": "GetUserRequest" + } + ], + "outputs": [ + { + "name": "", + "type": "User" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/_examples/hello-webrpc-ts/server/hello_api.gen.go b/_examples/hello-webrpc-ts/server/hello_api.gen.go index a01bdb19..a9b48c29 100644 --- a/_examples/hello-webrpc-ts/server/hello_api.gen.go +++ b/_examples/hello-webrpc-ts/server/hello_api.gen.go @@ -1,20 +1,25 @@ -// hello-webrpc v1.0.0 87ce8159bce3ad056518dfb1f1877b1a1012b34d +// hello-webrpc v1.0.0 395d139e72bef4b65e618ab33277fdcb2be6eb9e // -- -// This file has been generated by https://github.com/webrpc/webrpc using gen/golang -// Do not edit by hand. Update your webrpc schema and re-generate. +// Code generated by webrpc-gen with golang generator. DO NOT EDIT. +// +// webrpc-gen -schema=hello-api.ridl -target=golang -pkg=main -server -out=./server/hello_api.gen.go package main import ( - "bytes" "context" "encoding/json" + "errors" "fmt" - "io/ioutil" + "io" "net/http" "strings" "time" ) +const WebrpcHeader = "Webrpc" + +const WebrpcHeaderValue = "webrpc;gen-golang@v0.17.0;hello-webrpc@v1.0.0" + // WebRPC description and code-gen version func WebRPCVersion() string { return "v1" @@ -27,11 +32,62 @@ func WebRPCSchemaVersion() string { // Schema hash generated from your RIDL schema func WebRPCSchemaHash() string { - return "87ce8159bce3ad056518dfb1f1877b1a1012b34d" + return "395d139e72bef4b65e618ab33277fdcb2be6eb9e" +} + +type WebrpcGenVersions struct { + WebrpcGenVersion string + CodeGenName string + CodeGenVersion string + SchemaName string + SchemaVersion string +} + +func VersionFromHeader(h http.Header) (*WebrpcGenVersions, error) { + if h.Get(WebrpcHeader) == "" { + return nil, fmt.Errorf("header is empty or missing") + } + + versions, err := parseWebrpcGenVersions(h.Get(WebrpcHeader)) + if err != nil { + return nil, fmt.Errorf("webrpc header is invalid: %w", err) + } + + return versions, nil +} + +func parseWebrpcGenVersions(header string) (*WebrpcGenVersions, error) { + versions := strings.Split(header, ";") + if len(versions) < 3 { + return nil, fmt.Errorf("expected at least 3 parts while parsing webrpc header: %v", header) + } + + _, webrpcGenVersion, ok := strings.Cut(versions[0], "@") + if !ok { + return nil, fmt.Errorf("webrpc gen version could not be parsed from: %s", versions[0]) + } + + tmplTarget, tmplVersion, ok := strings.Cut(versions[1], "@") + if !ok { + return nil, fmt.Errorf("tmplTarget and tmplVersion could not be parsed from: %s", versions[1]) + } + + schemaName, schemaVersion, ok := strings.Cut(versions[2], "@") + if !ok { + return nil, fmt.Errorf("schema name and schema version could not be parsed from: %s", versions[2]) + } + + return &WebrpcGenVersions{ + WebrpcGenVersion: webrpcGenVersion, + CodeGenName: tmplTarget, + CodeGenVersion: tmplVersion, + SchemaName: schemaName, + SchemaVersion: schemaVersion, + }, nil } // -// Types +// Common types // type Kind uint32 @@ -55,27 +111,31 @@ func (x Kind) String() string { return Kind_name[uint32(x)] } -func (x Kind) MarshalJSON() ([]byte, error) { - buf := bytes.NewBufferString(`"`) - buf.WriteString(Kind_name[uint32(x)]) - buf.WriteString(`"`) - return buf.Bytes(), nil +func (x Kind) MarshalText() ([]byte, error) { + return []byte(Kind_name[uint32(x)]), nil } -func (x *Kind) UnmarshalJSON(b []byte) error { - var j string - err := json.Unmarshal(b, &j) - if err != nil { - return err - } - *x = Kind(Kind_value[j]) +func (x *Kind) UnmarshalText(b []byte) error { + *x = Kind(Kind_value[string(b)]) return nil } +func (x *Kind) Is(values ...Kind) bool { + if x == nil { + return false + } + for _, v := range values { + if *x == v { + return true + } + } + return false +} + type User struct { ID uint64 `json:"id" db:"id"` Username string `json:"USERNAME" db:"username"` - Role *Kind `json:"role"` + Role Kind `json:"role"` Meta map[string]interface{} `json:"meta"` InternalID uint64 `json:"-"` CreatedAt *time.Time `json:"created_at,omitempty" db:"created_at"` @@ -85,10 +145,31 @@ type Page struct { Num uint32 `json:"num"` } -type ExampleService interface { - Ping(ctx context.Context) (bool, error) - GetUser(ctx context.Context, userID uint64) (*User, error) - FindUsers(ctx context.Context, q string) (*Page, []*User, error) +var methods = map[string]method{ + "/rpc/ExampleService/Ping": { + Name: "Ping", + Service: "ExampleService", + Annotations: map[string]string{}, + }, + "/rpc/ExampleService/GetUser": { + Name: "GetUser", + Service: "ExampleService", + Annotations: map[string]string{}, + }, + "/rpc/ExampleService/FindUsers": { + Name: "FindUsers", + Service: "ExampleService", + Annotations: map[string]string{}, + }, +} + +func WebrpcMethods() map[string]method { + res := make(map[string]method, len(methods)) + for k, v := range methods { + res[k] = v + } + + return res } var WebRPCServices = map[string][]string{ @@ -99,6 +180,26 @@ var WebRPCServices = map[string][]string{ }, } +// +// Server types +// + +type ExampleService interface { + Ping(ctx context.Context) (bool, error) + GetUser(ctx context.Context, userID uint64) (*User, error) + FindUsers(ctx context.Context, q string) (*Page, []*User, error) +} + +// +// Client types +// + +type ExampleServiceClient interface { + Ping(ctx context.Context) (bool, error) + GetUser(ctx context.Context, userID uint64) (*User, error) + FindUsers(ctx context.Context, q string) (*Page, []*User, error) +} + // // Server // @@ -109,87 +210,101 @@ type WebRPCServer interface { type exampleServiceServer struct { ExampleService + OnError func(r *http.Request, rpcErr *WebRPCError) + OnRequest func(w http.ResponseWriter, r *http.Request) error } -func NewExampleServiceServer(svc ExampleService) WebRPCServer { +func NewExampleServiceServer(svc ExampleService) *exampleServiceServer { return &exampleServiceServer{ ExampleService: svc, } } func (s *exampleServiceServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { + defer func() { + // In case of a panic, serve a HTTP 500 error and then panic. + if rr := recover(); rr != nil { + s.sendErrorJSON(w, r, ErrWebrpcServerPanic.WithCausef("%v", rr)) + panic(rr) + } + }() + + w.Header().Set(WebrpcHeader, WebrpcHeaderValue) + ctx := r.Context() ctx = context.WithValue(ctx, HTTPResponseWriterCtxKey, w) ctx = context.WithValue(ctx, HTTPRequestCtxKey, r) ctx = context.WithValue(ctx, ServiceNameCtxKey, "ExampleService") - if r.Method != "POST" { - err := Errorf(ErrBadRoute, "unsupported method %q (only POST is allowed)", r.Method) - RespondWithError(w, err) - return - } + r = r.WithContext(ctx) + var handler func(ctx context.Context, w http.ResponseWriter, r *http.Request) switch r.URL.Path { case "/rpc/ExampleService/Ping": - s.servePing(ctx, w, r) - return + handler = s.servePingJSON case "/rpc/ExampleService/GetUser": - s.serveGetUser(ctx, w, r) - return + handler = s.serveGetUserJSON case "/rpc/ExampleService/FindUsers": - s.serveFindUsers(ctx, w, r) - return + handler = s.serveFindUsersJSON default: - err := Errorf(ErrBadRoute, "no handler for path %q", r.URL.Path) - RespondWithError(w, err) + err := ErrWebrpcBadRoute.WithCausef("no webrpc method defined for path %v", r.URL.Path) + s.sendErrorJSON(w, r, err) + return + } + + if r.Method != "POST" { + w.Header().Add("Allow", "POST") // RFC 9110. + err := ErrWebrpcBadMethod.WithCausef("unsupported HTTP method %v (only POST is allowed)", r.Method) + s.sendErrorJSON(w, r, err) return } -} -func (s *exampleServiceServer) servePing(ctx context.Context, w http.ResponseWriter, r *http.Request) { - header := r.Header.Get("Content-Type") - i := strings.Index(header, ";") - if i == -1 { - i = len(header) + contentType := r.Header.Get("Content-Type") + if i := strings.Index(contentType, ";"); i >= 0 { + contentType = contentType[:i] } + contentType = strings.TrimSpace(strings.ToLower(contentType)) - switch strings.TrimSpace(strings.ToLower(header[:i])) { + switch contentType { case "application/json": - s.servePingJSON(ctx, w, r) + if s.OnRequest != nil { + if err := s.OnRequest(w, r); err != nil { + rpcErr, ok := err.(WebRPCError) + if !ok { + rpcErr = ErrWebrpcEndpoint.WithCause(err) + } + s.sendErrorJSON(w, r, rpcErr) + return + } + } + + handler(ctx, w, r) default: - err := Errorf(ErrBadRoute, "unexpected Content-Type: %q", r.Header.Get("Content-Type")) - RespondWithError(w, err) + err := ErrWebrpcBadRequest.WithCausef("unsupported Content-Type %q (only application/json is allowed)", r.Header.Get("Content-Type")) + s.sendErrorJSON(w, r, err) } } func (s *exampleServiceServer) servePingJSON(ctx context.Context, w http.ResponseWriter, r *http.Request) { - var err error ctx = context.WithValue(ctx, MethodNameCtxKey, "Ping") - // Call service method - var ret0 bool - func() { - defer func() { - // In case of a panic, serve a 500 error and then panic. - if rr := recover(); rr != nil { - RespondWithError(w, ErrorInternal("internal service panic")) - panic(rr) - } - }() - ret0, err = s.ExampleService.Ping(ctx) - }() - respContent := struct { - Ret0 bool `json:"status"` - }{ret0} - + // Call service method implementation. + ret0, err := s.ExampleService.Ping(ctx) if err != nil { - RespondWithError(w, err) + rpcErr, ok := err.(WebRPCError) + if !ok { + rpcErr = ErrWebrpcEndpoint.WithCause(err) + } + s.sendErrorJSON(w, r, rpcErr) return } - respBody, err := json.Marshal(respContent) + + respPayload := struct { + Ret0 bool `json:"status"` + }{ret0} + respBody, err := json.Marshal(respPayload) if err != nil { - err = WrapError(ErrInternal, err, "failed to marshal json response") - RespondWithError(w, err) + s.sendErrorJSON(w, r, ErrWebrpcBadResponse.WithCausef("failed to marshal json response: %w", err)) return } @@ -198,68 +313,41 @@ func (s *exampleServiceServer) servePingJSON(ctx context.Context, w http.Respons w.Write(respBody) } -func (s *exampleServiceServer) serveGetUser(ctx context.Context, w http.ResponseWriter, r *http.Request) { - header := r.Header.Get("Content-Type") - i := strings.Index(header, ";") - if i == -1 { - i = len(header) - } - - switch strings.TrimSpace(strings.ToLower(header[:i])) { - case "application/json": - s.serveGetUserJSON(ctx, w, r) - default: - err := Errorf(ErrBadRoute, "unexpected Content-Type: %q", r.Header.Get("Content-Type")) - RespondWithError(w, err) - } -} - func (s *exampleServiceServer) serveGetUserJSON(ctx context.Context, w http.ResponseWriter, r *http.Request) { - var err error ctx = context.WithValue(ctx, MethodNameCtxKey, "GetUser") - reqContent := struct { - Arg0 uint64 `json:"userID"` - }{} - reqBody, err := ioutil.ReadAll(r.Body) + reqBody, err := io.ReadAll(r.Body) if err != nil { - err = WrapError(ErrInternal, err, "failed to read request data") - RespondWithError(w, err) + s.sendErrorJSON(w, r, ErrWebrpcBadRequest.WithCausef("failed to read request data: %w", err)) return } defer r.Body.Close() - err = json.Unmarshal(reqBody, &reqContent) - if err != nil { - err = WrapError(ErrInvalidArgument, err, "failed to unmarshal request data") - RespondWithError(w, err) + reqPayload := struct { + Arg0 uint64 `json:"userID"` + }{} + if err := json.Unmarshal(reqBody, &reqPayload); err != nil { + s.sendErrorJSON(w, r, ErrWebrpcBadRequest.WithCausef("failed to unmarshal request data: %w", err)) return } - // Call service method - var ret0 *User - func() { - defer func() { - // In case of a panic, serve a 500 error and then panic. - if rr := recover(); rr != nil { - RespondWithError(w, ErrorInternal("internal service panic")) - panic(rr) - } - }() - ret0, err = s.ExampleService.GetUser(ctx, reqContent.Arg0) - }() - respContent := struct { - Ret0 *User `json:"user"` - }{ret0} - + // Call service method implementation. + ret0, err := s.ExampleService.GetUser(ctx, reqPayload.Arg0) if err != nil { - RespondWithError(w, err) + rpcErr, ok := err.(WebRPCError) + if !ok { + rpcErr = ErrWebrpcEndpoint.WithCause(err) + } + s.sendErrorJSON(w, r, rpcErr) return } - respBody, err := json.Marshal(respContent) + + respPayload := struct { + Ret0 *User `json:"user"` + }{ret0} + respBody, err := json.Marshal(respPayload) if err != nil { - err = WrapError(ErrInternal, err, "failed to marshal json response") - RespondWithError(w, err) + s.sendErrorJSON(w, r, ErrWebrpcBadResponse.WithCausef("failed to marshal json response: %w", err)) return } @@ -268,70 +356,42 @@ func (s *exampleServiceServer) serveGetUserJSON(ctx context.Context, w http.Resp w.Write(respBody) } -func (s *exampleServiceServer) serveFindUsers(ctx context.Context, w http.ResponseWriter, r *http.Request) { - header := r.Header.Get("Content-Type") - i := strings.Index(header, ";") - if i == -1 { - i = len(header) - } - - switch strings.TrimSpace(strings.ToLower(header[:i])) { - case "application/json": - s.serveFindUsersJSON(ctx, w, r) - default: - err := Errorf(ErrBadRoute, "unexpected Content-Type: %q", r.Header.Get("Content-Type")) - RespondWithError(w, err) - } -} - func (s *exampleServiceServer) serveFindUsersJSON(ctx context.Context, w http.ResponseWriter, r *http.Request) { - var err error ctx = context.WithValue(ctx, MethodNameCtxKey, "FindUsers") - reqContent := struct { - Arg0 string `json:"q"` - }{} - reqBody, err := ioutil.ReadAll(r.Body) + reqBody, err := io.ReadAll(r.Body) if err != nil { - err = WrapError(ErrInternal, err, "failed to read request data") - RespondWithError(w, err) + s.sendErrorJSON(w, r, ErrWebrpcBadRequest.WithCausef("failed to read request data: %w", err)) return } defer r.Body.Close() - err = json.Unmarshal(reqBody, &reqContent) + reqPayload := struct { + Arg0 string `json:"q"` + }{} + if err := json.Unmarshal(reqBody, &reqPayload); err != nil { + s.sendErrorJSON(w, r, ErrWebrpcBadRequest.WithCausef("failed to unmarshal request data: %w", err)) + return + } + + // Call service method implementation. + ret0, ret1, err := s.ExampleService.FindUsers(ctx, reqPayload.Arg0) if err != nil { - err = WrapError(ErrInvalidArgument, err, "failed to unmarshal request data") - RespondWithError(w, err) + rpcErr, ok := err.(WebRPCError) + if !ok { + rpcErr = ErrWebrpcEndpoint.WithCause(err) + } + s.sendErrorJSON(w, r, rpcErr) return } - // Call service method - var ret0 *Page - var ret1 []*User - func() { - defer func() { - // In case of a panic, serve a 500 error and then panic. - if rr := recover(); rr != nil { - RespondWithError(w, ErrorInternal("internal service panic")) - panic(rr) - } - }() - ret0, ret1, err = s.ExampleService.FindUsers(ctx, reqContent.Arg0) - }() - respContent := struct { + respPayload := struct { Ret0 *Page `json:"page"` Ret1 []*User `json:"users"` }{ret0, ret1} - - if err != nil { - RespondWithError(w, err) - return - } - respBody, err := json.Marshal(respContent) + respBody, err := json.Marshal(respPayload) if err != nil { - err = WrapError(ErrInternal, err, "failed to marshal json response") - RespondWithError(w, err) + s.sendErrorJSON(w, r, ErrWebrpcBadResponse.WithCausef("failed to marshal json response: %w", err)) return } @@ -340,18 +400,28 @@ func (s *exampleServiceServer) serveFindUsersJSON(ctx context.Context, w http.Re w.Write(respBody) } +func (s *exampleServiceServer) sendErrorJSON(w http.ResponseWriter, r *http.Request, rpcErr WebRPCError) { + if s.OnError != nil { + s.OnError(r, &rpcErr) + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(rpcErr.HTTPStatus) + + respBody, _ := json.Marshal(rpcErr) + w.Write(respBody) +} + func RespondWithError(w http.ResponseWriter, err error) { - rpcErr, ok := err.(Error) + rpcErr, ok := err.(WebRPCError) if !ok { - rpcErr = WrapError(ErrInternal, err, "webrpc error") + rpcErr = ErrWebrpcEndpoint.WithCause(err) } - statusCode := HTTPStatusFromErrorCode(rpcErr.Code()) - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(statusCode) + w.WriteHeader(rpcErr.HTTPStatus) - respBody, _ := json.Marshal(rpcErr.Payload()) + respBody, _ := json.Marshal(rpcErr) w.Write(respBody) } @@ -359,274 +429,131 @@ func RespondWithError(w http.ResponseWriter, err error) { // Helpers // -type ErrorPayload struct { - Status int `json:"status"` - Code string `json:"code"` - Cause string `json:"cause,omitempty"` - Msg string `json:"msg"` - Error string `json:"error"` +type method struct { + Name string + Service string + Annotations map[string]string } -type Error interface { - // Code is of the valid error codes - Code() ErrorCode - - // Msg returns a human-readable, unstructured messages describing the error - Msg() string - - // Cause is reason for the error - Cause() error - - // Error returns a string of the form "webrpc error : " - Error() string - - // Error response payload - Payload() ErrorPayload +type contextKey struct { + name string } -func Errorf(code ErrorCode, msgf string, args ...interface{}) Error { - msg := fmt.Sprintf(msgf, args...) - if IsValidErrorCode(code) { - return &rpcErr{code: code, msg: msg} - } - return &rpcErr{code: ErrInternal, msg: "invalid error type " + string(code)} +func (k *contextKey) String() string { + return "webrpc context value " + k.name } -func WrapError(code ErrorCode, cause error, format string, args ...interface{}) Error { - msg := fmt.Sprintf(format, args...) - if IsValidErrorCode(code) { - return &rpcErr{code: code, msg: msg, cause: cause} - } - return &rpcErr{code: ErrInternal, msg: "invalid error type " + string(code), cause: cause} -} +var ( + HTTPResponseWriterCtxKey = &contextKey{"HTTPResponseWriter"} -func ErrorNotFound(format string, args ...interface{}) Error { - return Errorf(ErrNotFound, format, args...) -} + HTTPRequestCtxKey = &contextKey{"HTTPRequest"} -func ErrorInvalidArgument(argument string, validationMsg string) Error { - return Errorf(ErrInvalidArgument, argument+" "+validationMsg) -} + ServiceNameCtxKey = &contextKey{"ServiceName"} -func ErrorRequiredArgument(argument string) Error { - return ErrorInvalidArgument(argument, "is required") -} + MethodNameCtxKey = &contextKey{"MethodName"} +) -func ErrorInternal(format string, args ...interface{}) Error { - return Errorf(ErrInternal, format, args...) +func ServiceNameFromContext(ctx context.Context) string { + service, _ := ctx.Value(ServiceNameCtxKey).(string) + return service } -type ErrorCode string +func MethodNameFromContext(ctx context.Context) string { + method, _ := ctx.Value(MethodNameCtxKey).(string) + return method +} -const ( - // Canceled indicates the operation was cancelled (typically by the caller). - ErrCanceled ErrorCode = "canceled" - - // Unknown error. For example when handling errors raised by APIs that do not - // return enough error information. - ErrUnknown ErrorCode = "unknown" - - // InvalidArgument indicates client specified an invalid argument. It - // indicates arguments that are problematic regardless of the state of the - // system (i.e. a malformed file name, required argument, number out of range, - // etc.). - ErrInvalidArgument ErrorCode = "invalid argument" - - // DeadlineExceeded means operation expired before completion. For operations - // that change the state of the system, this error may be returned even if the - // operation has completed successfully (timeout). - ErrDeadlineExceeded ErrorCode = "deadline exceeded" - - // NotFound means some requested entity was not found. - ErrNotFound ErrorCode = "not found" - - // BadRoute means that the requested URL path wasn't routable to a webrpc - // service and method. This is returned by the generated server, and usually - // shouldn't be returned by applications. Instead, applications should use - // NotFound or Unimplemented. - ErrBadRoute ErrorCode = "bad route" - - // AlreadyExists means an attempt to create an entity failed because one - // already exists. - ErrAlreadyExists ErrorCode = "already exists" - - // PermissionDenied indicates the caller does not have permission to execute - // the specified operation. It must not be used if the caller cannot be - // identified (Unauthenticated). - ErrPermissionDenied ErrorCode = "permission denied" - - // Unauthenticated indicates the request does not have valid authentication - // credentials for the operation. - ErrUnauthenticated ErrorCode = "unauthenticated" - - // ResourceExhausted indicates some resource has been exhausted, perhaps a - // per-user quota, or perhaps the entire file system is out of space. - ErrResourceExhausted ErrorCode = "resource exhausted" - - // FailedPrecondition indicates operation was rejected because the system is - // not in a state required for the operation's execution. For example, doing - // an rmdir operation on a directory that is non-empty, or on a non-directory - // object, or when having conflicting read-modify-write on the same resource. - ErrFailedPrecondition ErrorCode = "failed precondition" - - // Aborted indicates the operation was aborted, typically due to a concurrency - // issue like sequencer check failures, transaction aborts, etc. - ErrAborted ErrorCode = "aborted" - - // OutOfRange means operation was attempted past the valid range. For example, - // seeking or reading past end of a paginated collection. - // - // Unlike InvalidArgument, this error indicates a problem that may be fixed if - // the system state changes (i.e. adding more items to the collection). - // - // There is a fair bit of overlap between FailedPrecondition and OutOfRange. - // We recommend using OutOfRange (the more specific error) when it applies so - // that callers who are iterating through a space can easily look for an - // OutOfRange error to detect when they are done. - ErrOutOfRange ErrorCode = "out of range" - - // Unimplemented indicates operation is not implemented or not - // supported/enabled in this service. - ErrUnimplemented ErrorCode = "unimplemented" - - // Internal errors. When some invariants expected by the underlying system - // have been broken. In other words, something bad happened in the library or - // backend service. Do not confuse with HTTP Internal Server Error; an - // Internal error could also happen on the client code, i.e. when parsing a - // server response. - ErrInternal ErrorCode = "internal" - - // Unavailable indicates the service is currently unavailable. This is a most - // likely a transient condition and may be corrected by retrying with a - // backoff. - ErrUnavailable ErrorCode = "unavailable" - - // DataLoss indicates unrecoverable data loss or corruption. - ErrDataLoss ErrorCode = "data loss" - - // ErrNone is the zero-value, is considered an empty error and should not be - // used. - ErrNone ErrorCode = "" -) +func RequestFromContext(ctx context.Context) *http.Request { + r, _ := ctx.Value(HTTPRequestCtxKey).(*http.Request) + return r +} -func HTTPStatusFromErrorCode(code ErrorCode) int { - switch code { - case ErrCanceled: - return 408 // RequestTimeout - case ErrUnknown: - return 500 // Internal Server Error - case ErrInvalidArgument: - return 400 // BadRequest - case ErrDeadlineExceeded: - return 408 // RequestTimeout - case ErrNotFound: - return 404 // Not Found - case ErrBadRoute: - return 404 // Not Found - case ErrAlreadyExists: - return 409 // Conflict - case ErrPermissionDenied: - return 403 // Forbidden - case ErrUnauthenticated: - return 401 // Unauthorized - case ErrResourceExhausted: - return 403 // Forbidden - case ErrFailedPrecondition: - return 412 // Precondition Failed - case ErrAborted: - return 409 // Conflict - case ErrOutOfRange: - return 400 // Bad Request - case ErrUnimplemented: - return 501 // Not Implemented - case ErrInternal: - return 500 // Internal Server Error - case ErrUnavailable: - return 503 // Service Unavailable - case ErrDataLoss: - return 500 // Internal Server Error - case ErrNone: - return 200 // OK - default: - return 0 // Invalid! +func MethodCtx(ctx context.Context) (method, bool) { + req := RequestFromContext(ctx) + if req == nil { + return method{}, false } -} -func IsErrorCode(err error, code ErrorCode) bool { - if rpcErr, ok := err.(Error); ok { - if rpcErr.Code() == code { - return true - } + m, ok := methods[req.URL.Path] + if !ok { + return method{}, false } - return false -} -func IsValidErrorCode(code ErrorCode) bool { - return HTTPStatusFromErrorCode(code) != 0 + return m, true } -type rpcErr struct { - code ErrorCode - msg string - cause error +func ResponseWriterFromContext(ctx context.Context) http.ResponseWriter { + w, _ := ctx.Value(HTTPResponseWriterCtxKey).(http.ResponseWriter) + return w } -func (e *rpcErr) Code() ErrorCode { - return e.code -} +// +// Errors +// -func (e *rpcErr) Msg() string { - return e.msg +type WebRPCError struct { + Name string `json:"error"` + Code int `json:"code"` + Message string `json:"msg"` + Cause string `json:"cause,omitempty"` + HTTPStatus int `json:"status"` + cause error } -func (e *rpcErr) Cause() error { - return e.cause -} +var _ error = WebRPCError{} -func (e *rpcErr) Error() string { - if e.cause != nil && e.cause.Error() != "" { - if e.msg != "" { - return fmt.Sprintf("webrpc %s error: %s -- %s", e.code, e.cause.Error(), e.msg) - } else { - return fmt.Sprintf("webrpc %s error: %s", e.code, e.cause.Error()) - } - } else { - return fmt.Sprintf("webrpc %s error: %s", e.code, e.msg) +func (e WebRPCError) Error() string { + if e.cause != nil { + return fmt.Sprintf("%s %d: %s: %v", e.Name, e.Code, e.Message, e.cause) } + return fmt.Sprintf("%s %d: %s", e.Name, e.Code, e.Message) } -func (e *rpcErr) Payload() ErrorPayload { - statusCode := HTTPStatusFromErrorCode(e.Code()) - errPayload := ErrorPayload{ - Status: statusCode, - Code: string(e.Code()), - Msg: e.Msg(), - Error: e.Error(), +func (e WebRPCError) Is(target error) bool { + if target == nil { + return false } - if e.Cause() != nil { - errPayload.Cause = e.Cause().Error() + if rpcErr, ok := target.(WebRPCError); ok { + return rpcErr.Code == e.Code } - return errPayload + return errors.Is(e.cause, target) } -type contextKey struct { - name string +func (e WebRPCError) Unwrap() error { + return e.cause } -func (k *contextKey) String() string { - return "webrpc context value " + k.name +func (e WebRPCError) WithCause(cause error) WebRPCError { + err := e + err.cause = cause + err.Cause = cause.Error() + return err } -var ( - // For Client - HTTPClientRequestHeadersCtxKey = &contextKey{"HTTPClientRequestHeaders"} - - // For Server - HTTPResponseWriterCtxKey = &contextKey{"HTTPResponseWriter"} - - HTTPRequestCtxKey = &contextKey{"HTTPRequest"} +func (e WebRPCError) WithCausef(format string, args ...interface{}) WebRPCError { + cause := fmt.Errorf(format, args...) + err := e + err.cause = cause + err.Cause = cause.Error() + return err +} - ServiceNameCtxKey = &contextKey{"ServiceName"} +// Deprecated: Use .WithCause() method on WebRPCError. +func ErrorWithCause(rpcErr WebRPCError, cause error) WebRPCError { + return rpcErr.WithCause(cause) +} - MethodNameCtxKey = &contextKey{"MethodName"} +// Webrpc errors +var ( + ErrWebrpcEndpoint = WebRPCError{Code: 0, Name: "WebrpcEndpoint", Message: "endpoint error", HTTPStatus: 400} + ErrWebrpcRequestFailed = WebRPCError{Code: -1, Name: "WebrpcRequestFailed", Message: "request failed", HTTPStatus: 400} + ErrWebrpcBadRoute = WebRPCError{Code: -2, Name: "WebrpcBadRoute", Message: "bad route", HTTPStatus: 404} + ErrWebrpcBadMethod = WebRPCError{Code: -3, Name: "WebrpcBadMethod", Message: "bad method", HTTPStatus: 405} + ErrWebrpcBadRequest = WebRPCError{Code: -4, Name: "WebrpcBadRequest", Message: "bad request", HTTPStatus: 400} + ErrWebrpcBadResponse = WebRPCError{Code: -5, Name: "WebrpcBadResponse", Message: "bad response", HTTPStatus: 500} + ErrWebrpcServerPanic = WebRPCError{Code: -6, Name: "WebrpcServerPanic", Message: "server panic", HTTPStatus: 500} + ErrWebrpcInternalError = WebRPCError{Code: -7, Name: "WebrpcInternalError", Message: "internal error", HTTPStatus: 500} + ErrWebrpcClientDisconnected = WebRPCError{Code: -8, Name: "WebrpcClientDisconnected", Message: "client disconnected", HTTPStatus: 400} + ErrWebrpcStreamLost = WebRPCError{Code: -9, Name: "WebrpcStreamLost", Message: "stream lost", HTTPStatus: 400} + ErrWebrpcStreamFinished = WebRPCError{Code: -10, Name: "WebrpcStreamFinished", Message: "stream finished", HTTPStatus: 200} ) diff --git a/_examples/hello-webrpc-ts/server/main.go b/_examples/hello-webrpc-ts/server/main.go index f8e5e8d7..096fbcbd 100644 --- a/_examples/hello-webrpc-ts/server/main.go +++ b/_examples/hello-webrpc-ts/server/main.go @@ -28,8 +28,8 @@ func startServer() error { AllowedOrigins: []string{"*"}, // AllowOriginFunc: func(r *http.Request, origin string) bool { return true }, AllowedMethods: []string{"POST", "OPTIONS"}, - AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"}, - ExposedHeaders: []string{"Link"}, + AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token", "Webrpc"}, + ExposedHeaders: []string{"Link", "Webrpc"}, AllowCredentials: true, MaxAge: 300, // Maximum value not ignored by any of major browsers }) diff --git a/_examples/hello-webrpc-ts/webapp/src/client.gen.ts b/_examples/hello-webrpc-ts/webapp/src/client.gen.ts index d623d5d7..a7493d49 100644 --- a/_examples/hello-webrpc-ts/webapp/src/client.gen.ts +++ b/_examples/hello-webrpc-ts/webapp/src/client.gen.ts @@ -1,8 +1,13 @@ -/* tslint:disable */ -// hello-webrpc v1.0.0 87ce8159bce3ad056518dfb1f1877b1a1012b34d +/* eslint-disable */ +// hello-webrpc v1.0.0 395d139e72bef4b65e618ab33277fdcb2be6eb9e // -- -// This file has been generated by https://github.com/webrpc/webrpc using gen/typescript -// Do not edit by hand. Update your webrpc schema and re-generate. +// Code generated by webrpc-gen with typescript generator. DO NOT EDIT. +// +// webrpc-gen -schema=hello-api.ridl -target=typescript -client -out=./webapp/src/client.gen.ts + +export const WebrpcHeader = "Webrpc" + +export const WebrpcHeaderValue = "webrpc;gen-typescript@v0.16.2;hello-webrpc@v1.0.0" // WebRPC description and code-gen version export const WebRPCVersion = "v1" @@ -11,12 +16,61 @@ export const WebRPCVersion = "v1" export const WebRPCSchemaVersion = "v1.0.0" // Schema hash generated from your RIDL schema -export const WebRPCSchemaHash = "87ce8159bce3ad056518dfb1f1877b1a1012b34d" +export const WebRPCSchemaHash = "395d139e72bef4b65e618ab33277fdcb2be6eb9e" + +type WebrpcGenVersions = { + webrpcGenVersion: string; + codeGenName: string; + codeGenVersion: string; + schemaName: string; + schemaVersion: string; +}; + +export function VersionFromHeader(headers: Headers): WebrpcGenVersions { + const headerValue = headers.get(WebrpcHeader); + if (!headerValue) { + return { + webrpcGenVersion: "", + codeGenName: "", + codeGenVersion: "", + schemaName: "", + schemaVersion: "", + }; + } + + return parseWebrpcGenVersions(headerValue); +} + +function parseWebrpcGenVersions(header: string): WebrpcGenVersions { + const versions = header.split(";"); + if (versions.length < 3) { + return { + webrpcGenVersion: "", + codeGenName: "", + codeGenVersion: "", + schemaName: "", + schemaVersion: "", + }; + } + const [_, webrpcGenVersion] = versions[0].split("@"); + const [codeGenName, codeGenVersion] = versions[1].split("@"); + const [schemaName, schemaVersion] = versions[2].split("@"); + + return { + webrpcGenVersion, + codeGenName, + codeGenVersion, + schemaName, + schemaVersion, + }; +} // // Types // + + export enum Kind { USER = 'USER', ADMIN = 'ADMIN' @@ -27,7 +81,6 @@ export interface User { USERNAME: string role: Kind meta: {[key: string]: any} - created_at?: string } @@ -36,9 +89,9 @@ export interface Page { } export interface ExampleService { - ping(headers?: object): Promise - getUser(args: GetUserArgs, headers?: object): Promise - findUsers(args: FindUsersArgs, headers?: object): Promise + ping(headers?: object, signal?: AbortSignal): Promise + getUser(args: GetUserArgs, headers?: object, signal?: AbortSignal): Promise + findUsers(args: FindUsersArgs, headers?: object, signal?: AbortSignal): Promise } export interface PingArgs { @@ -69,71 +122,74 @@ export interface FindUsersReturn { // Client // export class ExampleService implements ExampleService { - private hostname: string - private fetch: Fetch - private path = '/rpc/ExampleService/' + protected hostname: string + protected fetch: Fetch + protected path = '/rpc/ExampleService/' constructor(hostname: string, fetch: Fetch) { - this.hostname = hostname - this.fetch = fetch + this.hostname = hostname.replace(/\/*$/, '') + this.fetch = (input: RequestInfo, init?: RequestInit) => fetch(input, init) } private url(name: string): string { return this.hostname + this.path + name } - ping = (headers?: object): Promise => { + ping = (headers?: object, signal?: AbortSignal): Promise => { return this.fetch( this.url('Ping'), - createHTTPRequest({}, headers) + createHTTPRequest({}, headers, signal) ).then((res) => { return buildResponse(res).then(_data => { return { - status: (_data.status) + status: (_data.status), } }) + }, (error) => { + throw WebrpcRequestFailedError.new({ cause: `fetch(): ${error.message || ''}` }) }) } - getUser = (args: GetUserArgs, headers?: object): Promise => { + getUser = (args: GetUserArgs, headers?: object, signal?: AbortSignal): Promise => { return this.fetch( this.url('GetUser'), - createHTTPRequest(args, headers)).then((res) => { + createHTTPRequest(args, headers, signal)).then((res) => { return buildResponse(res).then(_data => { return { - user: (_data.user) + user: (_data.user), } }) + }, (error) => { + throw WebrpcRequestFailedError.new({ cause: `fetch(): ${error.message || ''}` }) }) } - findUsers = (args: FindUsersArgs, headers?: object): Promise => { + findUsers = (args: FindUsersArgs, headers?: object, signal?: AbortSignal): Promise => { return this.fetch( this.url('FindUsers'), - createHTTPRequest(args, headers)).then((res) => { + createHTTPRequest(args, headers, signal)).then((res) => { return buildResponse(res).then(_data => { return { - page: (_data.page), - users: >(_data.users) + page: (_data.page), + users: >(_data.users), } }) + }, (error) => { + throw WebrpcRequestFailedError.new({ cause: `fetch(): ${error.message || ''}` }) }) } } - -export interface WebRPCError extends Error { - code: string - msg: string - status: number -} + const createHTTPRequest = (body: object = {}, headers: object = {}, signal: AbortSignal | null = null): object => { + const reqHeaders: {[key: string]: string} = { ...headers, 'Content-Type': 'application/json' } + reqHeaders[WebrpcHeader] = WebrpcHeaderValue -const createHTTPRequest = (body: object = {}, headers: object = {}): object => { return { method: 'POST', - headers: { ...headers, 'Content-Type': 'application/json' }, - body: JSON.stringify(body || {}) + headers: reqHeaders, + body: JSON.stringify(body || {}), + signal } } @@ -142,14 +198,244 @@ const buildResponse = (res: Response): Promise => { let data try { data = JSON.parse(text) - } catch(err) { - throw { code: 'unknown', msg: `expecting JSON, got: ${text}`, status: res.status } as WebRPCError + } catch(error) { + let message = '' + if (error instanceof Error) { + message = error.message + } + throw WebrpcBadResponseError.new({ + status: res.status, + cause: `JSON.parse(): ${message}: response text: ${text}`}, + ) } if (!res.ok) { - throw data // webrpc error response + const code: number = (typeof data.code === 'number') ? data.code : 0 + throw (webrpcErrorByCode[code] || WebrpcError).new(data) } return data }) } +// +// Errors +// + +export class WebrpcError extends Error { + name: string + code: number + message: string + status: number + cause?: string + + /** @deprecated Use message instead of msg. Deprecated in webrpc v0.11.0. */ + msg: string + + constructor(name: string, code: number, message: string, status: number, cause?: string) { + super(message) + this.name = name || 'WebrpcError' + this.code = typeof code === 'number' ? code : 0 + this.message = message || `endpoint error ${this.code}` + this.msg = this.message + this.status = typeof status === 'number' ? status : 0 + this.cause = cause + Object.setPrototypeOf(this, WebrpcError.prototype) + } + + static new(payload: any): WebrpcError { + return new this(payload.error, payload.code, payload.message || payload.msg, payload.status, payload.cause) + } +} + +// Webrpc errors + +export class WebrpcEndpointError extends WebrpcError { + constructor( + name: string = 'WebrpcEndpoint', + code: number = 0, + message: string = `endpoint error`, + status: number = 0, + cause?: string + ) { + super(name, code, message, status, cause) + Object.setPrototypeOf(this, WebrpcEndpointError.prototype) + } +} + +export class WebrpcRequestFailedError extends WebrpcError { + constructor( + name: string = 'WebrpcRequestFailed', + code: number = -1, + message: string = `request failed`, + status: number = 0, + cause?: string + ) { + super(name, code, message, status, cause) + Object.setPrototypeOf(this, WebrpcRequestFailedError.prototype) + } +} + +export class WebrpcBadRouteError extends WebrpcError { + constructor( + name: string = 'WebrpcBadRoute', + code: number = -2, + message: string = `bad route`, + status: number = 0, + cause?: string + ) { + super(name, code, message, status, cause) + Object.setPrototypeOf(this, WebrpcBadRouteError.prototype) + } +} + +export class WebrpcBadMethodError extends WebrpcError { + constructor( + name: string = 'WebrpcBadMethod', + code: number = -3, + message: string = `bad method`, + status: number = 0, + cause?: string + ) { + super(name, code, message, status, cause) + Object.setPrototypeOf(this, WebrpcBadMethodError.prototype) + } +} + +export class WebrpcBadRequestError extends WebrpcError { + constructor( + name: string = 'WebrpcBadRequest', + code: number = -4, + message: string = `bad request`, + status: number = 0, + cause?: string + ) { + super(name, code, message, status, cause) + Object.setPrototypeOf(this, WebrpcBadRequestError.prototype) + } +} + +export class WebrpcBadResponseError extends WebrpcError { + constructor( + name: string = 'WebrpcBadResponse', + code: number = -5, + message: string = `bad response`, + status: number = 0, + cause?: string + ) { + super(name, code, message, status, cause) + Object.setPrototypeOf(this, WebrpcBadResponseError.prototype) + } +} + +export class WebrpcServerPanicError extends WebrpcError { + constructor( + name: string = 'WebrpcServerPanic', + code: number = -6, + message: string = `server panic`, + status: number = 0, + cause?: string + ) { + super(name, code, message, status, cause) + Object.setPrototypeOf(this, WebrpcServerPanicError.prototype) + } +} + +export class WebrpcInternalErrorError extends WebrpcError { + constructor( + name: string = 'WebrpcInternalError', + code: number = -7, + message: string = `internal error`, + status: number = 0, + cause?: string + ) { + super(name, code, message, status, cause) + Object.setPrototypeOf(this, WebrpcInternalErrorError.prototype) + } +} + +export class WebrpcClientDisconnectedError extends WebrpcError { + constructor( + name: string = 'WebrpcClientDisconnected', + code: number = -8, + message: string = `client disconnected`, + status: number = 0, + cause?: string + ) { + super(name, code, message, status, cause) + Object.setPrototypeOf(this, WebrpcClientDisconnectedError.prototype) + } +} + +export class WebrpcStreamLostError extends WebrpcError { + constructor( + name: string = 'WebrpcStreamLost', + code: number = -9, + message: string = `stream lost`, + status: number = 0, + cause?: string + ) { + super(name, code, message, status, cause) + Object.setPrototypeOf(this, WebrpcStreamLostError.prototype) + } +} + +export class WebrpcStreamFinishedError extends WebrpcError { + constructor( + name: string = 'WebrpcStreamFinished', + code: number = -10, + message: string = `stream finished`, + status: number = 0, + cause?: string + ) { + super(name, code, message, status, cause) + Object.setPrototypeOf(this, WebrpcStreamFinishedError.prototype) + } +} + + +// Schema errors + + +export enum errors { + WebrpcEndpoint = 'WebrpcEndpoint', + WebrpcRequestFailed = 'WebrpcRequestFailed', + WebrpcBadRoute = 'WebrpcBadRoute', + WebrpcBadMethod = 'WebrpcBadMethod', + WebrpcBadRequest = 'WebrpcBadRequest', + WebrpcBadResponse = 'WebrpcBadResponse', + WebrpcServerPanic = 'WebrpcServerPanic', + WebrpcInternalError = 'WebrpcInternalError', + WebrpcClientDisconnected = 'WebrpcClientDisconnected', + WebrpcStreamLost = 'WebrpcStreamLost', + WebrpcStreamFinished = 'WebrpcStreamFinished', +} + +export enum WebrpcErrorCodes { + WebrpcEndpoint = 0, + WebrpcRequestFailed = -1, + WebrpcBadRoute = -2, + WebrpcBadMethod = -3, + WebrpcBadRequest = -4, + WebrpcBadResponse = -5, + WebrpcServerPanic = -6, + WebrpcInternalError = -7, + WebrpcClientDisconnected = -8, + WebrpcStreamLost = -9, + WebrpcStreamFinished = -10, +} + +export const webrpcErrorByCode: { [code: number]: any } = { + [0]: WebrpcEndpointError, + [-1]: WebrpcRequestFailedError, + [-2]: WebrpcBadRouteError, + [-3]: WebrpcBadMethodError, + [-4]: WebrpcBadRequestError, + [-5]: WebrpcBadResponseError, + [-6]: WebrpcServerPanicError, + [-7]: WebrpcInternalErrorError, + [-8]: WebrpcClientDisconnectedError, + [-9]: WebrpcStreamLostError, + [-10]: WebrpcStreamFinishedError, +} + export type Fetch = (input: RequestInfo, init?: RequestInit) => Promise + diff --git a/_examples/hello-webrpc/Makefile b/_examples/hello-webrpc/Makefile index 26a63fda..5db31466 100644 --- a/_examples/hello-webrpc/Makefile +++ b/_examples/hello-webrpc/Makefile @@ -1,5 +1,5 @@ all: - @echo "please read Makefile source or README to see available commands" + @awk -F'[ :]' '!/^all:/ && /^([A-z_-]+):/ {print "make " $$1}' Makefile tools: GO111MODULE=off go get -u github.com/goware/webify @@ -7,14 +7,14 @@ tools: generate: generate-server generate-client generate-server: - ../../bin/webrpc-gen -schema=hello-api.ridl -target=go -pkg=main -server -out=./server/hello_api.gen.go + webrpc-gen -schema=hello-api.ridl -target=golang -pkg=main -server -out=./server/hello_api.gen.go generate-client: - ../../bin/webrpc-gen -schema=hello-api.ridl -target=js -extra=noexports -client -out=./webapp/client.gen.js + webrpc-gen -schema=hello-api.ridl -target=javascript -exports=false -client -out=./webapp/client.gen.js run-server: go run ./server run-client: - webify -port=4444 -dir=./webapp + go run github.com/goware/webify -host=localhost -port=4444 -dir=./webapp diff --git a/_examples/hello-webrpc/go.mod b/_examples/hello-webrpc/go.mod new file mode 100644 index 00000000..789c7c4d --- /dev/null +++ b/_examples/hello-webrpc/go.mod @@ -0,0 +1,16 @@ +module github.com/webrpc/webrpc/_examples/hellow-webrpc + +go 1.22.3 + +require ( + github.com/go-chi/chi/v5 v5.0.13 + github.com/go-chi/cors v1.2.1 +) + +require ( + github.com/go-chi/httplog v0.2.1 // indirect + github.com/go-chi/transport v0.2.0 // indirect + github.com/goware/webify v1.3.0 // indirect + github.com/rs/zerolog v1.26.1 // indirect + moul.io/http2curl/v2 v2.3.0 // indirect +) diff --git a/_examples/hello-webrpc/go.sum b/_examples/hello-webrpc/go.sum new file mode 100644 index 00000000..8aa7690d --- /dev/null +++ b/_examples/hello-webrpc/go.sum @@ -0,0 +1,70 @@ +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs= +github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/chi/v5 v5.0.13 h1:JlH2F2M8qnwl0N1+JFFzlX9TlKJYas3aPXdiuTmJL+w= +github.com/go-chi/chi/v5 v5.0.13/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/cors v1.2.0/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= +github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= +github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= +github.com/go-chi/httplog v0.2.1 h1:KgCtIUkYNlfIsUPzE3utxd1KDKOvCrnAKaqdo0rmrh0= +github.com/go-chi/httplog v0.2.1/go.mod h1:JyHOFO9twSfGoTin/RoP25Lx2a9Btq10ug+sgxe0+bo= +github.com/go-chi/transport v0.2.0 h1:PMQr82GGAzTIouFwQDnMpadPgQpgvtjTeZhcQvPtc5I= +github.com/go-chi/transport v0.2.0/go.mod h1:/6vqZkTndiNlc6pN3uPZgyt4diOlrzgM2Y4bXCaRzpM= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/goware/webify v1.3.0 h1:xaoJKiCtlK2U88BvSQ6DCQF2hPajt4v0BdnRZL5XP0Y= +github.com/goware/webify v1.3.0/go.mod h1:5kP6SMsqT6v4iEVVee9WtPVPuLBAjk4YyH1ryJQBVrM= +github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7/go.mod h1:zO8QMzTeZd5cpnIkz/Gn6iK0jDfGicM1nynOkkPIl28= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.18.1-0.20200514152719-663cbb4c8469/go.mod h1:IzD0RJ65iWH0w97OQQebJEvTZYvsCUm9WVLWBQrJRjo= +github.com/rs/zerolog v1.26.1 h1:/ihwxqH+4z8UxyI70wM1z9yCvkWcfz/a3mj48k/Zngc= +github.com/rs/zerolog v1.26.1/go.mod h1:/wSSJWX7lVrsOwlbyTRSOJvqRlc+WjWlfes+CiJ+tmc= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs= +moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHcE= diff --git a/_examples/hello-webrpc/hello-api.ridl b/_examples/hello-webrpc/hello-api.ridl index 5f19257e..6ddb72ec 100644 --- a/_examples/hello-webrpc/hello-api.ridl +++ b/_examples/hello-webrpc/hello-api.ridl @@ -7,9 +7,9 @@ enum Kind: uint32 - USER = 1 - ADMIN = 2 -message Empty +struct Empty -message User +struct User - ID: uint64 + json = id + go.tag.db = id @@ -19,10 +19,18 @@ message User + go.tag.db = username - createdAt?: timestamp - + json = created_at,omitempty + + json = created_at + + go.tag.json = created_at,omitempty + go.tag.db = created_at + - updatedAt?: timestamp + + go.tag.json = ,omitempty + + - deletedAt?: timestamp + +error 1000 UserNotFound "User not found" HTTP 400 service ExampleService - Ping() => (status: bool) - GetUser(userID: uint64) => (user: User) + diff --git a/_examples/hello-webrpc/hello-api.webrpc.json b/_examples/hello-webrpc/hello-api.webrpc.json index 703496b0..64ea5ff0 100644 --- a/_examples/hello-webrpc/hello-api.webrpc.json +++ b/_examples/hello-webrpc/hello-api.webrpc.json @@ -1,106 +1,115 @@ { - "webrpc": "v1", - "name": "hello-api", - "version": "v0.0.1", - "messages": [ - { - "name": "Kind", - "type": "enum", - "fields": [ - { - "name": "USER", - "type": "uint32", - "value": "1" - }, - { - "name": "ADMIN", - "type": "uint32", - "value": "2" - } - ] - }, - { - "name": "Empty", - "type": "struct", - "fields": [ - ] - }, - { - "name": "GetUserRequest", - "type": "struct", - "fields": [ - { - "name": "userID", - "type": "uint64", - "optional": false - } - ] - }, - { - "name": "User", - "type": "struct", - "fields": [ - { - "name": "ID", - "type": "uint64", - "optional": false, - "meta": [ - { "json": "id" }, - { "go.tag.db": "id" } - ] - }, - { - "name": "username", - "type": "string", - "optional": false, - "meta": [ - { "json": "USERNAME" }, - { "go.tag.db": "username" } - ] - }, - { - "name": "createdAt", - "type": "timestamp", - "optional": true, - "meta": [ - { "json": "created_at,omitempty" }, - { "go.tag.db": "created_at" } - ] - } - - ] - } - ], - "services": [ - { - "name": "ExampleService", - "methods": [ - { - "name": "Ping", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "bool" - } - ] - }, - { - "name": "GetUser", - "inputs": [ - { - "name": "req", - "type": "GetUserRequest" - } - ], - "outputs": [ - { - "name": "", - "type": "User" - } - ] - } - ] - } - ] -} + "webrpc": "v1", + "name": "hello-api", + "version": "v0.0.1", + "types": [ + { + "name": "Kind", + "kind": "enum", + "type": "uint32", + "fields": [ + { + "name": "USER", + "value": "1" + }, + { + "name": "ADMIN", + "value": "2" + } + ] + }, + { + "name": "Empty", + "kind": "struct", + "fields": [] + }, + { + "name": "GetUserRequest", + "kind": "struct", + "fields": [ + { + "name": "userID", + "type": "uint64", + "optional": false + } + ] + }, + { + "name": "User", + "kind": "struct", + "fields": [ + { + "name": "ID", + "type": "uint64", + "optional": false, + "meta": [ + { + "json": "id" + }, + { + "go.tag.db": "id" + } + ] + }, + { + "name": "username", + "type": "string", + "optional": false, + "meta": [ + { + "json": "USERNAME" + }, + { + "go.tag.db": "username" + } + ] + }, + { + "name": "createdAt", + "type": "timestamp", + "optional": true, + "meta": [ + { + "json": "created_at,omitempty" + }, + { + "go.tag.db": "created_at" + } + ] + } + ] + } + ], + "services": [ + { + "name": "ExampleService", + "methods": [ + { + "name": "Ping", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bool" + } + ] + }, + { + "name": "GetUser", + "inputs": [ + { + "name": "req", + "type": "GetUserRequest" + } + ], + "outputs": [ + { + "name": "", + "type": "User" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/_examples/hello-webrpc/server/hello_api.gen.go b/_examples/hello-webrpc/server/hello_api.gen.go index bff576f7..879f3473 100644 --- a/_examples/hello-webrpc/server/hello_api.gen.go +++ b/_examples/hello-webrpc/server/hello_api.gen.go @@ -1,20 +1,25 @@ -// hello-webrpc v1.0.0 c929128d878e94653f3a856f80c4671008e22a45 +// hello-webrpc v1.0.0 1769ce5a249c8ed4e4dab8320a7d67779eae0664 // -- -// This file has been generated by https://github.com/webrpc/webrpc using gen/golang -// Do not edit by hand. Update your webrpc schema and re-generate. +// Code generated by webrpc-gen with golang generator. DO NOT EDIT. +// +// webrpc-gen -schema=hello-api.ridl -target=golang -pkg=main -server -out=./server/hello_api.gen.go package main import ( - "bytes" "context" "encoding/json" + "errors" "fmt" - "io/ioutil" + "io" "net/http" "strings" "time" ) +const WebrpcHeader = "Webrpc" + +const WebrpcHeaderValue = "webrpc;gen-golang@v0.17.0;hello-webrpc@v1.0.0" + // WebRPC description and code-gen version func WebRPCVersion() string { return "v1" @@ -27,11 +32,62 @@ func WebRPCSchemaVersion() string { // Schema hash generated from your RIDL schema func WebRPCSchemaHash() string { - return "c929128d878e94653f3a856f80c4671008e22a45" + return "1769ce5a249c8ed4e4dab8320a7d67779eae0664" +} + +type WebrpcGenVersions struct { + WebrpcGenVersion string + CodeGenName string + CodeGenVersion string + SchemaName string + SchemaVersion string +} + +func VersionFromHeader(h http.Header) (*WebrpcGenVersions, error) { + if h.Get(WebrpcHeader) == "" { + return nil, fmt.Errorf("header is empty or missing") + } + + versions, err := parseWebrpcGenVersions(h.Get(WebrpcHeader)) + if err != nil { + return nil, fmt.Errorf("webrpc header is invalid: %w", err) + } + + return versions, nil +} + +func parseWebrpcGenVersions(header string) (*WebrpcGenVersions, error) { + versions := strings.Split(header, ";") + if len(versions) < 3 { + return nil, fmt.Errorf("expected at least 3 parts while parsing webrpc header: %v", header) + } + + _, webrpcGenVersion, ok := strings.Cut(versions[0], "@") + if !ok { + return nil, fmt.Errorf("webrpc gen version could not be parsed from: %s", versions[0]) + } + + tmplTarget, tmplVersion, ok := strings.Cut(versions[1], "@") + if !ok { + return nil, fmt.Errorf("tmplTarget and tmplVersion could not be parsed from: %s", versions[1]) + } + + schemaName, schemaVersion, ok := strings.Cut(versions[2], "@") + if !ok { + return nil, fmt.Errorf("schema name and schema version could not be parsed from: %s", versions[2]) + } + + return &WebrpcGenVersions{ + WebrpcGenVersion: webrpcGenVersion, + CodeGenName: tmplTarget, + CodeGenVersion: tmplVersion, + SchemaName: schemaName, + SchemaVersion: schemaVersion, + }, nil } // -// Types +// Common types // type Kind uint32 @@ -55,23 +111,27 @@ func (x Kind) String() string { return Kind_name[uint32(x)] } -func (x Kind) MarshalJSON() ([]byte, error) { - buf := bytes.NewBufferString(`"`) - buf.WriteString(Kind_name[uint32(x)]) - buf.WriteString(`"`) - return buf.Bytes(), nil +func (x Kind) MarshalText() ([]byte, error) { + return []byte(Kind_name[uint32(x)]), nil } -func (x *Kind) UnmarshalJSON(b []byte) error { - var j string - err := json.Unmarshal(b, &j) - if err != nil { - return err - } - *x = Kind(Kind_value[j]) +func (x *Kind) UnmarshalText(b []byte) error { + *x = Kind(Kind_value[string(b)]) return nil } +func (x *Kind) Is(values ...Kind) bool { + if x == nil { + return false + } + for _, v := range values { + if *x == v { + return true + } + } + return false +} + type Empty struct { } @@ -79,11 +139,30 @@ type User struct { ID uint64 `json:"id" db:"id"` Username string `json:"USERNAME" db:"username"` CreatedAt *time.Time `json:"created_at,omitempty" db:"created_at"` + UpdatedAt *time.Time `json:",omitempty"` + DeletedAt *time.Time `json:"deletedAt"` } -type ExampleService interface { - Ping(ctx context.Context) (bool, error) - GetUser(ctx context.Context, userID uint64) (*User, error) +var methods = map[string]method{ + "/rpc/ExampleService/Ping": { + Name: "Ping", + Service: "ExampleService", + Annotations: map[string]string{}, + }, + "/rpc/ExampleService/GetUser": { + Name: "GetUser", + Service: "ExampleService", + Annotations: map[string]string{}, + }, +} + +func WebrpcMethods() map[string]method { + res := make(map[string]method, len(methods)) + for k, v := range methods { + res[k] = v + } + + return res } var WebRPCServices = map[string][]string{ @@ -93,6 +172,24 @@ var WebRPCServices = map[string][]string{ }, } +// +// Server types +// + +type ExampleService interface { + Ping(ctx context.Context) (bool, error) + GetUser(ctx context.Context, userID uint64) (*User, error) +} + +// +// Client types +// + +type ExampleServiceClient interface { + Ping(ctx context.Context) (bool, error) + GetUser(ctx context.Context, userID uint64) (*User, error) +} + // // Server // @@ -103,84 +200,99 @@ type WebRPCServer interface { type exampleServiceServer struct { ExampleService + OnError func(r *http.Request, rpcErr *WebRPCError) + OnRequest func(w http.ResponseWriter, r *http.Request) error } -func NewExampleServiceServer(svc ExampleService) WebRPCServer { +func NewExampleServiceServer(svc ExampleService) *exampleServiceServer { return &exampleServiceServer{ ExampleService: svc, } } func (s *exampleServiceServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { + defer func() { + // In case of a panic, serve a HTTP 500 error and then panic. + if rr := recover(); rr != nil { + s.sendErrorJSON(w, r, ErrWebrpcServerPanic.WithCausef("%v", rr)) + panic(rr) + } + }() + + w.Header().Set(WebrpcHeader, WebrpcHeaderValue) + ctx := r.Context() ctx = context.WithValue(ctx, HTTPResponseWriterCtxKey, w) ctx = context.WithValue(ctx, HTTPRequestCtxKey, r) ctx = context.WithValue(ctx, ServiceNameCtxKey, "ExampleService") - if r.Method != "POST" { - err := Errorf(ErrBadRoute, "unsupported method %q (only POST is allowed)", r.Method) - RespondWithError(w, err) - return - } + r = r.WithContext(ctx) + var handler func(ctx context.Context, w http.ResponseWriter, r *http.Request) switch r.URL.Path { case "/rpc/ExampleService/Ping": - s.servePing(ctx, w, r) - return + handler = s.servePingJSON case "/rpc/ExampleService/GetUser": - s.serveGetUser(ctx, w, r) - return + handler = s.serveGetUserJSON default: - err := Errorf(ErrBadRoute, "no handler for path %q", r.URL.Path) - RespondWithError(w, err) + err := ErrWebrpcBadRoute.WithCausef("no webrpc method defined for path %v", r.URL.Path) + s.sendErrorJSON(w, r, err) return } -} -func (s *exampleServiceServer) servePing(ctx context.Context, w http.ResponseWriter, r *http.Request) { - header := r.Header.Get("Content-Type") - i := strings.Index(header, ";") - if i == -1 { - i = len(header) + if r.Method != "POST" { + w.Header().Add("Allow", "POST") // RFC 9110. + err := ErrWebrpcBadMethod.WithCausef("unsupported HTTP method %v (only POST is allowed)", r.Method) + s.sendErrorJSON(w, r, err) + return } - switch strings.TrimSpace(strings.ToLower(header[:i])) { + contentType := r.Header.Get("Content-Type") + if i := strings.Index(contentType, ";"); i >= 0 { + contentType = contentType[:i] + } + contentType = strings.TrimSpace(strings.ToLower(contentType)) + + switch contentType { case "application/json": - s.servePingJSON(ctx, w, r) + if s.OnRequest != nil { + if err := s.OnRequest(w, r); err != nil { + rpcErr, ok := err.(WebRPCError) + if !ok { + rpcErr = ErrWebrpcEndpoint.WithCause(err) + } + s.sendErrorJSON(w, r, rpcErr) + return + } + } + + handler(ctx, w, r) default: - err := Errorf(ErrBadRoute, "unexpected Content-Type: %q", r.Header.Get("Content-Type")) - RespondWithError(w, err) + err := ErrWebrpcBadRequest.WithCausef("unsupported Content-Type %q (only application/json is allowed)", r.Header.Get("Content-Type")) + s.sendErrorJSON(w, r, err) } } func (s *exampleServiceServer) servePingJSON(ctx context.Context, w http.ResponseWriter, r *http.Request) { - var err error ctx = context.WithValue(ctx, MethodNameCtxKey, "Ping") - // Call service method - var ret0 bool - func() { - defer func() { - // In case of a panic, serve a 500 error and then panic. - if rr := recover(); rr != nil { - RespondWithError(w, ErrorInternal("internal service panic")) - panic(rr) - } - }() - ret0, err = s.ExampleService.Ping(ctx) - }() - respContent := struct { - Ret0 bool `json:"status"` - }{ret0} - + // Call service method implementation. + ret0, err := s.ExampleService.Ping(ctx) if err != nil { - RespondWithError(w, err) + rpcErr, ok := err.(WebRPCError) + if !ok { + rpcErr = ErrWebrpcEndpoint.WithCause(err) + } + s.sendErrorJSON(w, r, rpcErr) return } - respBody, err := json.Marshal(respContent) + + respPayload := struct { + Ret0 bool `json:"status"` + }{ret0} + respBody, err := json.Marshal(respPayload) if err != nil { - err = WrapError(ErrInternal, err, "failed to marshal json response") - RespondWithError(w, err) + s.sendErrorJSON(w, r, ErrWebrpcBadResponse.WithCausef("failed to marshal json response: %w", err)) return } @@ -189,68 +301,41 @@ func (s *exampleServiceServer) servePingJSON(ctx context.Context, w http.Respons w.Write(respBody) } -func (s *exampleServiceServer) serveGetUser(ctx context.Context, w http.ResponseWriter, r *http.Request) { - header := r.Header.Get("Content-Type") - i := strings.Index(header, ";") - if i == -1 { - i = len(header) - } - - switch strings.TrimSpace(strings.ToLower(header[:i])) { - case "application/json": - s.serveGetUserJSON(ctx, w, r) - default: - err := Errorf(ErrBadRoute, "unexpected Content-Type: %q", r.Header.Get("Content-Type")) - RespondWithError(w, err) - } -} - func (s *exampleServiceServer) serveGetUserJSON(ctx context.Context, w http.ResponseWriter, r *http.Request) { - var err error ctx = context.WithValue(ctx, MethodNameCtxKey, "GetUser") - reqContent := struct { - Arg0 uint64 `json:"userID"` - }{} - reqBody, err := ioutil.ReadAll(r.Body) + reqBody, err := io.ReadAll(r.Body) if err != nil { - err = WrapError(ErrInternal, err, "failed to read request data") - RespondWithError(w, err) + s.sendErrorJSON(w, r, ErrWebrpcBadRequest.WithCausef("failed to read request data: %w", err)) return } defer r.Body.Close() - err = json.Unmarshal(reqBody, &reqContent) - if err != nil { - err = WrapError(ErrInvalidArgument, err, "failed to unmarshal request data") - RespondWithError(w, err) + reqPayload := struct { + Arg0 uint64 `json:"userID"` + }{} + if err := json.Unmarshal(reqBody, &reqPayload); err != nil { + s.sendErrorJSON(w, r, ErrWebrpcBadRequest.WithCausef("failed to unmarshal request data: %w", err)) return } - // Call service method - var ret0 *User - func() { - defer func() { - // In case of a panic, serve a 500 error and then panic. - if rr := recover(); rr != nil { - RespondWithError(w, ErrorInternal("internal service panic")) - panic(rr) - } - }() - ret0, err = s.ExampleService.GetUser(ctx, reqContent.Arg0) - }() - respContent := struct { - Ret0 *User `json:"user"` - }{ret0} - + // Call service method implementation. + ret0, err := s.ExampleService.GetUser(ctx, reqPayload.Arg0) if err != nil { - RespondWithError(w, err) + rpcErr, ok := err.(WebRPCError) + if !ok { + rpcErr = ErrWebrpcEndpoint.WithCause(err) + } + s.sendErrorJSON(w, r, rpcErr) return } - respBody, err := json.Marshal(respContent) + + respPayload := struct { + Ret0 *User `json:"user"` + }{ret0} + respBody, err := json.Marshal(respPayload) if err != nil { - err = WrapError(ErrInternal, err, "failed to marshal json response") - RespondWithError(w, err) + s.sendErrorJSON(w, r, ErrWebrpcBadResponse.WithCausef("failed to marshal json response: %w", err)) return } @@ -259,18 +344,28 @@ func (s *exampleServiceServer) serveGetUserJSON(ctx context.Context, w http.Resp w.Write(respBody) } +func (s *exampleServiceServer) sendErrorJSON(w http.ResponseWriter, r *http.Request, rpcErr WebRPCError) { + if s.OnError != nil { + s.OnError(r, &rpcErr) + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(rpcErr.HTTPStatus) + + respBody, _ := json.Marshal(rpcErr) + w.Write(respBody) +} + func RespondWithError(w http.ResponseWriter, err error) { - rpcErr, ok := err.(Error) + rpcErr, ok := err.(WebRPCError) if !ok { - rpcErr = WrapError(ErrInternal, err, "webrpc error") + rpcErr = ErrWebrpcEndpoint.WithCause(err) } - statusCode := HTTPStatusFromErrorCode(rpcErr.Code()) - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(statusCode) + w.WriteHeader(rpcErr.HTTPStatus) - respBody, _ := json.Marshal(rpcErr.Payload()) + respBody, _ := json.Marshal(rpcErr) w.Write(respBody) } @@ -278,274 +373,136 @@ func RespondWithError(w http.ResponseWriter, err error) { // Helpers // -type ErrorPayload struct { - Status int `json:"status"` - Code string `json:"code"` - Cause string `json:"cause,omitempty"` - Msg string `json:"msg"` - Error string `json:"error"` +type method struct { + Name string + Service string + Annotations map[string]string } -type Error interface { - // Code is of the valid error codes - Code() ErrorCode - - // Msg returns a human-readable, unstructured messages describing the error - Msg() string - - // Cause is reason for the error - Cause() error - - // Error returns a string of the form "webrpc error : " - Error() string - - // Error response payload - Payload() ErrorPayload +type contextKey struct { + name string } -func Errorf(code ErrorCode, msgf string, args ...interface{}) Error { - msg := fmt.Sprintf(msgf, args...) - if IsValidErrorCode(code) { - return &rpcErr{code: code, msg: msg} - } - return &rpcErr{code: ErrInternal, msg: "invalid error type " + string(code)} +func (k *contextKey) String() string { + return "webrpc context value " + k.name } -func WrapError(code ErrorCode, cause error, format string, args ...interface{}) Error { - msg := fmt.Sprintf(format, args...) - if IsValidErrorCode(code) { - return &rpcErr{code: code, msg: msg, cause: cause} - } - return &rpcErr{code: ErrInternal, msg: "invalid error type " + string(code), cause: cause} -} +var ( + HTTPResponseWriterCtxKey = &contextKey{"HTTPResponseWriter"} -func ErrorNotFound(format string, args ...interface{}) Error { - return Errorf(ErrNotFound, format, args...) -} + HTTPRequestCtxKey = &contextKey{"HTTPRequest"} -func ErrorInvalidArgument(argument string, validationMsg string) Error { - return Errorf(ErrInvalidArgument, argument+" "+validationMsg) -} + ServiceNameCtxKey = &contextKey{"ServiceName"} -func ErrorRequiredArgument(argument string) Error { - return ErrorInvalidArgument(argument, "is required") -} + MethodNameCtxKey = &contextKey{"MethodName"} +) -func ErrorInternal(format string, args ...interface{}) Error { - return Errorf(ErrInternal, format, args...) +func ServiceNameFromContext(ctx context.Context) string { + service, _ := ctx.Value(ServiceNameCtxKey).(string) + return service } -type ErrorCode string +func MethodNameFromContext(ctx context.Context) string { + method, _ := ctx.Value(MethodNameCtxKey).(string) + return method +} -const ( - // Canceled indicates the operation was cancelled (typically by the caller). - ErrCanceled ErrorCode = "canceled" - - // Unknown error. For example when handling errors raised by APIs that do not - // return enough error information. - ErrUnknown ErrorCode = "unknown" - - // InvalidArgument indicates client specified an invalid argument. It - // indicates arguments that are problematic regardless of the state of the - // system (i.e. a malformed file name, required argument, number out of range, - // etc.). - ErrInvalidArgument ErrorCode = "invalid argument" - - // DeadlineExceeded means operation expired before completion. For operations - // that change the state of the system, this error may be returned even if the - // operation has completed successfully (timeout). - ErrDeadlineExceeded ErrorCode = "deadline exceeded" - - // NotFound means some requested entity was not found. - ErrNotFound ErrorCode = "not found" - - // BadRoute means that the requested URL path wasn't routable to a webrpc - // service and method. This is returned by the generated server, and usually - // shouldn't be returned by applications. Instead, applications should use - // NotFound or Unimplemented. - ErrBadRoute ErrorCode = "bad route" - - // AlreadyExists means an attempt to create an entity failed because one - // already exists. - ErrAlreadyExists ErrorCode = "already exists" - - // PermissionDenied indicates the caller does not have permission to execute - // the specified operation. It must not be used if the caller cannot be - // identified (Unauthenticated). - ErrPermissionDenied ErrorCode = "permission denied" - - // Unauthenticated indicates the request does not have valid authentication - // credentials for the operation. - ErrUnauthenticated ErrorCode = "unauthenticated" - - // ResourceExhausted indicates some resource has been exhausted, perhaps a - // per-user quota, or perhaps the entire file system is out of space. - ErrResourceExhausted ErrorCode = "resource exhausted" - - // FailedPrecondition indicates operation was rejected because the system is - // not in a state required for the operation's execution. For example, doing - // an rmdir operation on a directory that is non-empty, or on a non-directory - // object, or when having conflicting read-modify-write on the same resource. - ErrFailedPrecondition ErrorCode = "failed precondition" - - // Aborted indicates the operation was aborted, typically due to a concurrency - // issue like sequencer check failures, transaction aborts, etc. - ErrAborted ErrorCode = "aborted" - - // OutOfRange means operation was attempted past the valid range. For example, - // seeking or reading past end of a paginated collection. - // - // Unlike InvalidArgument, this error indicates a problem that may be fixed if - // the system state changes (i.e. adding more items to the collection). - // - // There is a fair bit of overlap between FailedPrecondition and OutOfRange. - // We recommend using OutOfRange (the more specific error) when it applies so - // that callers who are iterating through a space can easily look for an - // OutOfRange error to detect when they are done. - ErrOutOfRange ErrorCode = "out of range" - - // Unimplemented indicates operation is not implemented or not - // supported/enabled in this service. - ErrUnimplemented ErrorCode = "unimplemented" - - // Internal errors. When some invariants expected by the underlying system - // have been broken. In other words, something bad happened in the library or - // backend service. Do not confuse with HTTP Internal Server Error; an - // Internal error could also happen on the client code, i.e. when parsing a - // server response. - ErrInternal ErrorCode = "internal" - - // Unavailable indicates the service is currently unavailable. This is a most - // likely a transient condition and may be corrected by retrying with a - // backoff. - ErrUnavailable ErrorCode = "unavailable" - - // DataLoss indicates unrecoverable data loss or corruption. - ErrDataLoss ErrorCode = "data loss" - - // ErrNone is the zero-value, is considered an empty error and should not be - // used. - ErrNone ErrorCode = "" -) +func RequestFromContext(ctx context.Context) *http.Request { + r, _ := ctx.Value(HTTPRequestCtxKey).(*http.Request) + return r +} -func HTTPStatusFromErrorCode(code ErrorCode) int { - switch code { - case ErrCanceled: - return 408 // RequestTimeout - case ErrUnknown: - return 500 // Internal Server Error - case ErrInvalidArgument: - return 400 // BadRequest - case ErrDeadlineExceeded: - return 408 // RequestTimeout - case ErrNotFound: - return 404 // Not Found - case ErrBadRoute: - return 404 // Not Found - case ErrAlreadyExists: - return 409 // Conflict - case ErrPermissionDenied: - return 403 // Forbidden - case ErrUnauthenticated: - return 401 // Unauthorized - case ErrResourceExhausted: - return 403 // Forbidden - case ErrFailedPrecondition: - return 412 // Precondition Failed - case ErrAborted: - return 409 // Conflict - case ErrOutOfRange: - return 400 // Bad Request - case ErrUnimplemented: - return 501 // Not Implemented - case ErrInternal: - return 500 // Internal Server Error - case ErrUnavailable: - return 503 // Service Unavailable - case ErrDataLoss: - return 500 // Internal Server Error - case ErrNone: - return 200 // OK - default: - return 0 // Invalid! +func MethodCtx(ctx context.Context) (method, bool) { + req := RequestFromContext(ctx) + if req == nil { + return method{}, false } -} -func IsErrorCode(err error, code ErrorCode) bool { - if rpcErr, ok := err.(Error); ok { - if rpcErr.Code() == code { - return true - } + m, ok := methods[req.URL.Path] + if !ok { + return method{}, false } - return false -} -func IsValidErrorCode(code ErrorCode) bool { - return HTTPStatusFromErrorCode(code) != 0 + return m, true } -type rpcErr struct { - code ErrorCode - msg string - cause error +func ResponseWriterFromContext(ctx context.Context) http.ResponseWriter { + w, _ := ctx.Value(HTTPResponseWriterCtxKey).(http.ResponseWriter) + return w } -func (e *rpcErr) Code() ErrorCode { - return e.code -} +// +// Errors +// -func (e *rpcErr) Msg() string { - return e.msg +type WebRPCError struct { + Name string `json:"error"` + Code int `json:"code"` + Message string `json:"msg"` + Cause string `json:"cause,omitempty"` + HTTPStatus int `json:"status"` + cause error } -func (e *rpcErr) Cause() error { - return e.cause -} +var _ error = WebRPCError{} -func (e *rpcErr) Error() string { - if e.cause != nil && e.cause.Error() != "" { - if e.msg != "" { - return fmt.Sprintf("webrpc %s error: %s -- %s", e.code, e.cause.Error(), e.msg) - } else { - return fmt.Sprintf("webrpc %s error: %s", e.code, e.cause.Error()) - } - } else { - return fmt.Sprintf("webrpc %s error: %s", e.code, e.msg) +func (e WebRPCError) Error() string { + if e.cause != nil { + return fmt.Sprintf("%s %d: %s: %v", e.Name, e.Code, e.Message, e.cause) } + return fmt.Sprintf("%s %d: %s", e.Name, e.Code, e.Message) } -func (e *rpcErr) Payload() ErrorPayload { - statusCode := HTTPStatusFromErrorCode(e.Code()) - errPayload := ErrorPayload{ - Status: statusCode, - Code: string(e.Code()), - Msg: e.Msg(), - Error: e.Error(), +func (e WebRPCError) Is(target error) bool { + if target == nil { + return false } - if e.Cause() != nil { - errPayload.Cause = e.Cause().Error() + if rpcErr, ok := target.(WebRPCError); ok { + return rpcErr.Code == e.Code } - return errPayload + return errors.Is(e.cause, target) } -type contextKey struct { - name string +func (e WebRPCError) Unwrap() error { + return e.cause } -func (k *contextKey) String() string { - return "webrpc context value " + k.name +func (e WebRPCError) WithCause(cause error) WebRPCError { + err := e + err.cause = cause + err.Cause = cause.Error() + return err } -var ( - // For Client - HTTPClientRequestHeadersCtxKey = &contextKey{"HTTPClientRequestHeaders"} - - // For Server - HTTPResponseWriterCtxKey = &contextKey{"HTTPResponseWriter"} +func (e WebRPCError) WithCausef(format string, args ...interface{}) WebRPCError { + cause := fmt.Errorf(format, args...) + err := e + err.cause = cause + err.Cause = cause.Error() + return err +} - HTTPRequestCtxKey = &contextKey{"HTTPRequest"} +// Deprecated: Use .WithCause() method on WebRPCError. +func ErrorWithCause(rpcErr WebRPCError, cause error) WebRPCError { + return rpcErr.WithCause(cause) +} - ServiceNameCtxKey = &contextKey{"ServiceName"} +// Webrpc errors +var ( + ErrWebrpcEndpoint = WebRPCError{Code: 0, Name: "WebrpcEndpoint", Message: "endpoint error", HTTPStatus: 400} + ErrWebrpcRequestFailed = WebRPCError{Code: -1, Name: "WebrpcRequestFailed", Message: "request failed", HTTPStatus: 400} + ErrWebrpcBadRoute = WebRPCError{Code: -2, Name: "WebrpcBadRoute", Message: "bad route", HTTPStatus: 404} + ErrWebrpcBadMethod = WebRPCError{Code: -3, Name: "WebrpcBadMethod", Message: "bad method", HTTPStatus: 405} + ErrWebrpcBadRequest = WebRPCError{Code: -4, Name: "WebrpcBadRequest", Message: "bad request", HTTPStatus: 400} + ErrWebrpcBadResponse = WebRPCError{Code: -5, Name: "WebrpcBadResponse", Message: "bad response", HTTPStatus: 500} + ErrWebrpcServerPanic = WebRPCError{Code: -6, Name: "WebrpcServerPanic", Message: "server panic", HTTPStatus: 500} + ErrWebrpcInternalError = WebRPCError{Code: -7, Name: "WebrpcInternalError", Message: "internal error", HTTPStatus: 500} + ErrWebrpcClientDisconnected = WebRPCError{Code: -8, Name: "WebrpcClientDisconnected", Message: "client disconnected", HTTPStatus: 400} + ErrWebrpcStreamLost = WebRPCError{Code: -9, Name: "WebrpcStreamLost", Message: "stream lost", HTTPStatus: 400} + ErrWebrpcStreamFinished = WebRPCError{Code: -10, Name: "WebrpcStreamFinished", Message: "stream finished", HTTPStatus: 200} +) - MethodNameCtxKey = &contextKey{"MethodName"} +// Schema errors +var ( + ErrUserNotFound = WebRPCError{Code: 1000, Name: "UserNotFound", Message: "User not found", HTTPStatus: 400} ) diff --git a/_examples/hello-webrpc/server/main.go b/_examples/hello-webrpc/server/main.go index 491d0863..f56b3fc1 100644 --- a/_examples/hello-webrpc/server/main.go +++ b/_examples/hello-webrpc/server/main.go @@ -5,8 +5,8 @@ import ( "log" "net/http" - "github.com/go-chi/chi" - "github.com/go-chi/chi/middleware" + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" "github.com/go-chi/cors" ) @@ -24,12 +24,11 @@ func startServer() error { r.Use(middleware.Recoverer) cors := cors.New(cors.Options{ - // AllowedOrigins: []string{"https://foo.com"}, // Use this to allow specific origin hosts - AllowedOrigins: []string{"*"}, - // AllowOriginFunc: func(r *http.Request, origin string) bool { return true }, + AllowedOrigins: []string{"http://localhost:4444"}, + //AllowOriginFunc: func(r *http.Request, origin string) bool { return true }, AllowedMethods: []string{"POST", "OPTIONS"}, - AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"}, - ExposedHeaders: []string{"Link"}, + AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token", "Webrpc"}, + ExposedHeaders: []string{"Link", "Webrpc"}, AllowCredentials: true, MaxAge: 300, // Maximum value not ignored by any of major browsers }) @@ -54,9 +53,7 @@ func (s *ExampleServiceRPC) Ping(ctx context.Context) (bool, error) { func (s *ExampleServiceRPC) GetUser(ctx context.Context, userID uint64) (*User, error) { if userID == 911 { - return nil, ErrorNotFound("unknown userID %d", 911) - // return nil, webrpc.Errorf(webrpc.ErrNotFound, "unknown userID %d", 911) - // return nil, webrpc.WrapError(webrpc.ErrNotFound, err, "unknown userID %d", 911) + return nil, ErrUserNotFound.WithCausef("unknown userID %d", 911) } return &User{ diff --git a/_examples/hello-webrpc/webapp/client.gen.js b/_examples/hello-webrpc/webapp/client.gen.js index 96401190..c1adfca9 100644 --- a/_examples/hello-webrpc/webapp/client.gen.js +++ b/_examples/hello-webrpc/webapp/client.gen.js @@ -1,17 +1,17 @@ -// hello-webrpc v1.0.0 c929128d878e94653f3a856f80c4671008e22a45 +// hello-webrpc v1.0.0 1769ce5a249c8ed4e4dab8320a7d67779eae0664 // -- -// This file has been generated by https://github.com/webrpc/webrpc using gen/javascript -// Do not edit by hand. Update your webrpc schema and re-generate. +// Code generated by webrpc-gen with javascript generator. DO NOT EDIT. +// +// webrpc-gen -schema=hello-api.ridl -target=javascript -exports=false -client -out=./webapp/client.gen.js // WebRPC description and code-gen version -export const WebRPCVersion = "v1" +const WebRPCVersion = "v1" // Schema version of your RIDL schema -export const WebRPCSchemaVersion = "v1.0.0" +const WebRPCSchemaVersion = "v1.0.0" // Schema hash generated from your RIDL schema -export const WebRPCSchemaHash = "c929128d878e94653f3a856f80c4671008e22a45" - +const WebRPCSchemaHash = "1769ce5a249c8ed4e4dab8320a7d67779eae0664" // // Types @@ -27,7 +27,6 @@ class Empty { constructor(_data) { this._data = {} if (_data) { - } } @@ -43,21 +42,25 @@ class User { this._data['id'] = _data['id'] this._data['USERNAME'] = _data['USERNAME'] this._data['created_at'] = _data['created_at'] - + this._data['updatedAt'] = _data['updatedAt'] + this._data['deletedAt'] = _data['deletedAt'] } } + get id() { return this._data['id'] } set id(value) { this._data['id'] = value } + get USERNAME() { return this._data['USERNAME'] } set USERNAME(value) { this._data['USERNAME'] = value } + get created_at() { return this._data['created_at'] } @@ -65,6 +68,20 @@ class User { this._data['created_at'] = value } + get updatedAt() { + return this._data['updatedAt'] + } + set updatedAt(value) { + this._data['updatedAt'] = value + } + + get deletedAt() { + return this._data['deletedAt'] + } + set deletedAt(value) { + this._data['deletedAt'] = value + } + toJSON() { return this._data } @@ -79,7 +96,7 @@ class ExampleService { constructor(hostname, fetch) { this.path = '/rpc/ExampleService/' this.hostname = hostname - this.fetch = fetch + this.fetch = (input, init) => fetch(input, init) } url(name) { diff --git a/_examples/node-ts/.prettierrc b/_examples/node-ts/.prettierrc new file mode 100644 index 00000000..4103b482 --- /dev/null +++ b/_examples/node-ts/.prettierrc @@ -0,0 +1,7 @@ +{ + "trailingComma": "es5", + "tabWidth": 2, + "semi": false, + "singleQuote": true, + "useTabs": true +} diff --git a/_examples/node-ts/Makefile b/_examples/node-ts/Makefile index 05de5c73..22c65577 100644 --- a/_examples/node-ts/Makefile +++ b/_examples/node-ts/Makefile @@ -1,5 +1,5 @@ all: - @echo "please read Makefile source or README to see available commands" + @awk -F'[ :]' '!/^all:/ && /^([A-z_-]+):/ {print "make " $$1}' Makefile bootstrap: rm -rf server/node_modules && rm -rf webapp/node_modules @@ -7,8 +7,8 @@ bootstrap: cd webapp && yarn generate: - ../../bin/webrpc-gen -schema=service.ridl -target=ts -server -out=./server/server.gen.ts - ../../bin/webrpc-gen -schema=service.ridl -target=ts -client -out=./webapp/client.gen.ts + webrpc-gen -schema=service.ridl -target=typescript -server -out=./server/server.gen.ts + webrpc-gen -schema=service.ridl -target=typescript -client -out=./webapp/client.gen.ts run-server: yarn --cwd ./server start diff --git a/_examples/node-ts/README.md b/_examples/node-ts/README.md index d9389bae..89da71c4 100644 --- a/_examples/node-ts/README.md +++ b/_examples/node-ts/README.md @@ -21,7 +21,7 @@ Visit http://localhost:4444 ! The cool thing about webrpc and other schema-driven rpc libraries (like grpc), is that you can generate a Go client for this node server just by running: -`webrpc-gen -schema=service.ridl -target=go -pkg=proto -client -out=./proto/client.gen.go` +`webrpc-gen -schema=service.ridl -target=golang -pkg=proto -client -out=./proto/client.gen.go` and tada, your Go programs now have full type definitions and network communication to the node server! diff --git a/_examples/node-ts/server/index.ts b/_examples/node-ts/server/index.ts index e7ae0b9b..5c083339 100644 --- a/_examples/node-ts/server/index.ts +++ b/_examples/node-ts/server/index.ts @@ -1,42 +1,40 @@ import express from 'express' -import { createExampleServiceApp } from './server.gen' import * as proto from './server.gen' +import { createExampleServiceApp } from './server.gen' const app = express() app.use((req, res, next) => { - res.setHeader('Access-Control-Allow-Origin', '*') - res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS') - res.setHeader('Access-Control-Allow-Headers', 'Content-Type') + res.setHeader('Access-Control-Allow-Origin', '*') + res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS') + res.setHeader('Access-Control-Allow-Headers', 'Content-Type') - if (req.method === 'OPTIONS') { - res.status(200).end() - return - } + if (req.method === 'OPTIONS') { + res.status(200).end() + return + } - next() -}); + next() +}) const exampleServiceApp = createExampleServiceApp({ - Ping: () => { - return { - status: false - } - }, - - GetUser: () => ({ - code: 1, - user: { - id: 1, - USERNAME: 'webrpcfan', - role: proto.Kind.ADMIN, - meta: {} - } - }) + Ping: () => { + return {} + }, + + GetUser: () => ({ + code: 1, + user: { + id: 1, + USERNAME: 'webrpcfan', + role: proto.Kind.ADMIN, + meta: {}, + }, + }), }) app.use(exampleServiceApp) app.listen(3000, () => { - console.log('> Listening on port 3000'); + console.log('> Listening on port 3000') }) diff --git a/_examples/node-ts/server/package.json b/_examples/node-ts/server/package.json index 9b3801b3..8f1aaa65 100644 --- a/_examples/node-ts/server/package.json +++ b/_examples/node-ts/server/package.json @@ -4,7 +4,7 @@ "main": "index.js", "license": "MIT", "dependencies": { - "express": "^4.16.4" + "express": "^4.19.2" }, "devDependencies": { "@types/express": "^4.16.1", diff --git a/_examples/node-ts/server/server.gen.ts b/_examples/node-ts/server/server.gen.ts index 6c5075d9..c3488d81 100644 --- a/_examples/node-ts/server/server.gen.ts +++ b/_examples/node-ts/server/server.gen.ts @@ -1,8 +1,13 @@ -/* tslint:disable */ -// node-ts v1.0.0 4d2858fa129683e5775e9b863ceceb740e7e09b1 +/* eslint-disable */ +// node-ts v1.0.0 6713366104e62b8479d628a193e2a7ca03f37edc // -- -// This file has been generated by https://github.com/webrpc/webrpc using gen/typescript -// Do not edit by hand. Update your webrpc schema and re-generate. +// Code generated by webrpc-gen with typescript generator. DO NOT EDIT. +// +// webrpc-gen -schema=service.ridl -target=typescript -server -out=./server/server.gen.ts + +export const WebrpcHeader = "Webrpc" + +export const WebrpcHeaderValue = "webrpc;gen-typescript@v0.16.2;node-ts@v1.0.0" // WebRPC description and code-gen version export const WebRPCVersion = "v1" @@ -11,12 +16,61 @@ export const WebRPCVersion = "v1" export const WebRPCSchemaVersion = "v1.0.0" // Schema hash generated from your RIDL schema -export const WebRPCSchemaHash = "4d2858fa129683e5775e9b863ceceb740e7e09b1" +export const WebRPCSchemaHash = "6713366104e62b8479d628a193e2a7ca03f37edc" + +type WebrpcGenVersions = { + webrpcGenVersion: string; + codeGenName: string; + codeGenVersion: string; + schemaName: string; + schemaVersion: string; +}; + +export function VersionFromHeader(headers: Headers): WebrpcGenVersions { + const headerValue = headers.get(WebrpcHeader); + if (!headerValue) { + return { + webrpcGenVersion: "", + codeGenName: "", + codeGenVersion: "", + schemaName: "", + schemaVersion: "", + }; + } + + return parseWebrpcGenVersions(headerValue); +} +function parseWebrpcGenVersions(header: string): WebrpcGenVersions { + const versions = header.split(";"); + if (versions.length < 3) { + return { + webrpcGenVersion: "", + codeGenName: "", + codeGenVersion: "", + schemaName: "", + schemaVersion: "", + }; + } + + const [_, webrpcGenVersion] = versions[0].split("@"); + const [codeGenName, codeGenVersion] = versions[1].split("@"); + const [schemaName, schemaVersion] = versions[2].split("@"); + + return { + webrpcGenVersion, + codeGenName, + codeGenVersion, + schemaName, + schemaVersion, + }; +} // // Types // + + export enum Kind { USER = 'USER', ADMIN = 'ADMIN' @@ -27,7 +81,6 @@ export interface User { USERNAME: string role: Kind meta: {[key: string]: any} - createdAt?: string } @@ -36,15 +89,14 @@ export interface Page { } export interface ExampleService { - ping(headers?: object): Promise - getUser(args: GetUserArgs, headers?: object): Promise + ping(headers?: object, signal?: AbortSignal): Promise + getUser(args: GetUserArgs, headers?: object, signal?: AbortSignal): Promise } export interface PingArgs { } -export interface PingReturn { - status: boolean +export interface PingReturn { } export interface GetUserArgs { userID: number @@ -60,6 +112,7 @@ export interface GetUserReturn { // // Server // + export class WebRPCError extends Error { statusCode?: number @@ -73,8 +126,6 @@ export class WebRPCError extends Error { } import express from 'express' - - export type ExampleServiceService = { @@ -92,6 +143,8 @@ import express from 'express' app.post('/*', async (req, res) => { const requestPath = req.baseUrl + req.path + res.header(WebrpcHeader, WebrpcHeaderValue); + if (!req.body) { res.status(400).send("webrpc error: missing body"); @@ -108,10 +161,6 @@ import express from 'express' const response = await serviceImplementation["Ping"](req.body); - if (!("status" in response)) { - throw new WebRPCError("internal", 500); - } - res.status(200).json(response); } catch (err) { diff --git a/_examples/node-ts/server/yarn.lock b/_examples/node-ts/server/yarn.lock index 6a60170e..9e8f67cc 100644 --- a/_examples/node-ts/server/yarn.lock +++ b/_examples/node-ts/server/yarn.lock @@ -57,13 +57,13 @@ "@types/express-serve-static-core" "*" "@types/mime" "*" -accepts@~1.3.5: - version "1.3.7" - resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" - integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== +accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== dependencies: - mime-types "~2.1.24" - negotiator "0.6.2" + mime-types "~2.1.34" + negotiator "0.6.3" arg@^4.1.0: version "4.1.0" @@ -75,51 +75,66 @@ array-flatten@1.1.1: resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= -body-parser@1.18.3: - version "1.18.3" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.3.tgz#5b292198ffdd553b3a0f20ded0592b956955c8b4" - integrity sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ= +body-parser@1.20.2: + version "1.20.2" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" + integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== dependencies: - bytes "3.0.0" - content-type "~1.0.4" + bytes "3.1.2" + content-type "~1.0.5" debug "2.6.9" - depd "~1.1.2" - http-errors "~1.6.3" - iconv-lite "0.4.23" - on-finished "~2.3.0" - qs "6.5.2" - raw-body "2.3.3" - type-is "~1.6.16" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.11.0" + raw-body "2.5.2" + type-is "~1.6.18" + unpipe "1.0.0" buffer-from@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== -bytes@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" - integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg= +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== -content-disposition@0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" - integrity sha1-DPaLud318r55YcOoUXjLhdunjLQ= +call-bind@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" + +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" -content-type@~1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" - integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== +content-type@~1.0.4, content-type@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== cookie-signature@1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= -cookie@0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" - integrity sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s= +cookie@0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051" + integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== debug@2.6.9: version "2.6.9" @@ -128,15 +143,24 @@ debug@2.6.9: dependencies: ms "2.0.0" -depd@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" - integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= +define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== -destroy@~1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" - integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== diff@^3.1.0: version "3.5.0" @@ -153,6 +177,18 @@ encodeurl@~1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" @@ -163,91 +199,140 @@ etag@~1.8.1: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= -express@^4.16.4: - version "4.16.4" - resolved "https://registry.yarnpkg.com/express/-/express-4.16.4.tgz#fddef61926109e24c515ea97fd2f1bdbf62df12e" - integrity sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg== +express@^4.19.2: + version "4.19.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465" + integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q== dependencies: - accepts "~1.3.5" + accepts "~1.3.8" array-flatten "1.1.1" - body-parser "1.18.3" - content-disposition "0.5.2" + body-parser "1.20.2" + content-disposition "0.5.4" content-type "~1.0.4" - cookie "0.3.1" + cookie "0.6.0" cookie-signature "1.0.6" debug "2.6.9" - depd "~1.1.2" + depd "2.0.0" encodeurl "~1.0.2" escape-html "~1.0.3" etag "~1.8.1" - finalhandler "1.1.1" + finalhandler "1.2.0" fresh "0.5.2" + http-errors "2.0.0" merge-descriptors "1.0.1" methods "~1.1.2" - on-finished "~2.3.0" - parseurl "~1.3.2" + on-finished "2.4.1" + parseurl "~1.3.3" path-to-regexp "0.1.7" - proxy-addr "~2.0.4" - qs "6.5.2" - range-parser "~1.2.0" - safe-buffer "5.1.2" - send "0.16.2" - serve-static "1.13.2" - setprototypeof "1.1.0" - statuses "~1.4.0" - type-is "~1.6.16" + proxy-addr "~2.0.7" + qs "6.11.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.18.0" + serve-static "1.15.0" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" utils-merge "1.0.1" vary "~1.1.2" -finalhandler@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.1.tgz#eebf4ed840079c83f4249038c9d703008301b105" - integrity sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg== +finalhandler@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" + integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== dependencies: debug "2.6.9" encodeurl "~1.0.2" escape-html "~1.0.3" - on-finished "~2.3.0" - parseurl "~1.3.2" - statuses "~1.4.0" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" unpipe "~1.0.0" -forwarded@~0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" - integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== fresh@0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= -http-errors@1.6.3, http-errors@~1.6.2, http-errors@~1.6.3: - version "1.6.3" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" - integrity sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0= +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +get-intrinsic@^1.1.3, get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== dependencies: - depd "~1.1.2" - inherits "2.0.3" - setprototypeof "1.1.0" - statuses ">= 1.4.0 < 2" + es-errors "^1.3.0" + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" -iconv-lite@0.4.23: - version "0.4.23" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63" - integrity sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA== +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + +has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + +has-proto@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" + integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== + +has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +hasown@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== dependencies: safer-buffer ">= 2.1.2 < 3" -inherits@2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= +inherits@2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -ipaddr.js@1.9.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.0.tgz#37df74e430a0e47550fe54a2defe30d8acd95f65" - integrity sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA== +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== make-error@^1.1.1: version "1.3.5" @@ -274,6 +359,11 @@ mime-db@1.40.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.40.0.tgz#a65057e998db090f732a68f6c276d387d4126c32" integrity sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA== +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + mime-types@~2.1.24: version "2.1.24" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.24.tgz#b6f8d0b3e951efb77dedeca194cff6d16f676f81" @@ -281,29 +371,46 @@ mime-types@~2.1.24: dependencies: mime-db "1.40.0" -mime@1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" - integrity sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ== +mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= -negotiator@0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" - integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== - -on-finished@~2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" - integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= +ms@2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +object-inspect@^1.13.1: + version "1.13.1" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2" + integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ== + +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== dependencies: ee-first "1.1.1" -parseurl@~1.3.2: +parseurl@~1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== @@ -313,77 +420,101 @@ path-to-regexp@0.1.7: resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= -proxy-addr@~2.0.4: - version "2.0.5" - resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.5.tgz#34cbd64a2d81f4b1fd21e76f9f06c8a45299ee34" - integrity sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ== +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== dependencies: - forwarded "~0.1.2" - ipaddr.js "1.9.0" + forwarded "0.2.0" + ipaddr.js "1.9.1" -qs@6.5.2: - version "6.5.2" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" - integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== +qs@6.11.0: + version "6.11.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== + dependencies: + side-channel "^1.0.4" -range-parser@~1.2.0: +range-parser@~1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== -raw-body@2.3.3: - version "2.3.3" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.3.tgz#1b324ece6b5706e153855bc1148c65bb7f6ea0c3" - integrity sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw== +raw-body@2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== dependencies: - bytes "3.0.0" - http-errors "1.6.3" - iconv-lite "0.4.23" + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" unpipe "1.0.0" -safe-buffer@5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== +safe-buffer@5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== "safer-buffer@>= 2.1.2 < 3": version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -send@0.16.2: - version "0.16.2" - resolved "https://registry.yarnpkg.com/send/-/send-0.16.2.tgz#6ecca1e0f8c156d141597559848df64730a6bbc1" - integrity sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw== +send@0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" + integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== dependencies: debug "2.6.9" - depd "~1.1.2" - destroy "~1.0.4" + depd "2.0.0" + destroy "1.2.0" encodeurl "~1.0.2" escape-html "~1.0.3" etag "~1.8.1" fresh "0.5.2" - http-errors "~1.6.2" - mime "1.4.1" - ms "2.0.0" - on-finished "~2.3.0" - range-parser "~1.2.0" - statuses "~1.4.0" - -serve-static@1.13.2: - version "1.13.2" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.13.2.tgz#095e8472fd5b46237db50ce486a43f4b86c6cec1" - integrity sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw== + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +serve-static@1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" + integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== dependencies: encodeurl "~1.0.2" escape-html "~1.0.3" - parseurl "~1.3.2" - send "0.16.2" + parseurl "~1.3.3" + send "0.18.0" -setprototypeof@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" - integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== +set-function-length@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +side-channel@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" + integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" source-map-support@^0.5.6: version "0.5.12" @@ -398,15 +529,15 @@ source-map@^0.6.0: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== -"statuses@>= 1.4.0 < 2": - version "1.5.0" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" - integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== -statuses@~1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087" - integrity sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew== +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== ts-node@^8.1.0: version "8.1.0" @@ -419,7 +550,7 @@ ts-node@^8.1.0: source-map-support "^0.5.6" yn "^3.0.0" -type-is@~1.6.16: +type-is@~1.6.18: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== diff --git a/_examples/node-ts/service.ridl b/_examples/node-ts/service.ridl index 44a883eb..a1214811 100644 --- a/_examples/node-ts/service.ridl +++ b/_examples/node-ts/service.ridl @@ -9,7 +9,7 @@ enum Kind: uint32 - ADMIN -message User +struct User - ID: uint64 + json = id @@ -26,10 +26,10 @@ message User - createdAt?: timestamp -message Page +struct Page - num: uint32 service ExampleService - - Ping() => (status: bool) + - Ping() => () - GetUser(userID: uint64) => (code: uint32, user: User) diff --git a/_examples/node-ts/webapp/client.gen.ts b/_examples/node-ts/webapp/client.gen.ts index f13bd10d..4f381c50 100644 --- a/_examples/node-ts/webapp/client.gen.ts +++ b/_examples/node-ts/webapp/client.gen.ts @@ -1,8 +1,13 @@ -/* tslint:disable */ -// node-ts v1.0.0 4d2858fa129683e5775e9b863ceceb740e7e09b1 +/* eslint-disable */ +// node-ts v1.0.0 6713366104e62b8479d628a193e2a7ca03f37edc // -- -// This file has been generated by https://github.com/webrpc/webrpc using gen/typescript -// Do not edit by hand. Update your webrpc schema and re-generate. +// Code generated by webrpc-gen with typescript generator. DO NOT EDIT. +// +// webrpc-gen -schema=service.ridl -target=typescript -client -out=./webapp/client.gen.ts + +export const WebrpcHeader = "Webrpc" + +export const WebrpcHeaderValue = "webrpc;gen-typescript@v0.16.2;node-ts@v1.0.0" // WebRPC description and code-gen version export const WebRPCVersion = "v1" @@ -11,12 +16,61 @@ export const WebRPCVersion = "v1" export const WebRPCSchemaVersion = "v1.0.0" // Schema hash generated from your RIDL schema -export const WebRPCSchemaHash = "4d2858fa129683e5775e9b863ceceb740e7e09b1" +export const WebRPCSchemaHash = "6713366104e62b8479d628a193e2a7ca03f37edc" + +type WebrpcGenVersions = { + webrpcGenVersion: string; + codeGenName: string; + codeGenVersion: string; + schemaName: string; + schemaVersion: string; +}; + +export function VersionFromHeader(headers: Headers): WebrpcGenVersions { + const headerValue = headers.get(WebrpcHeader); + if (!headerValue) { + return { + webrpcGenVersion: "", + codeGenName: "", + codeGenVersion: "", + schemaName: "", + schemaVersion: "", + }; + } + + return parseWebrpcGenVersions(headerValue); +} + +function parseWebrpcGenVersions(header: string): WebrpcGenVersions { + const versions = header.split(";"); + if (versions.length < 3) { + return { + webrpcGenVersion: "", + codeGenName: "", + codeGenVersion: "", + schemaName: "", + schemaVersion: "", + }; + } + const [_, webrpcGenVersion] = versions[0].split("@"); + const [codeGenName, codeGenVersion] = versions[1].split("@"); + const [schemaName, schemaVersion] = versions[2].split("@"); + + return { + webrpcGenVersion, + codeGenName, + codeGenVersion, + schemaName, + schemaVersion, + }; +} // // Types // + + export enum Kind { USER = 'USER', ADMIN = 'ADMIN' @@ -27,7 +81,6 @@ export interface User { USERNAME: string role: Kind meta: {[key: string]: any} - createdAt?: string } @@ -36,15 +89,14 @@ export interface Page { } export interface ExampleService { - ping(headers?: object): Promise - getUser(args: GetUserArgs, headers?: object): Promise + ping(headers?: object, signal?: AbortSignal): Promise + getUser(args: GetUserArgs, headers?: object, signal?: AbortSignal): Promise } export interface PingArgs { } -export interface PingReturn { - status: boolean +export interface PingReturn { } export interface GetUserArgs { userID: number @@ -61,59 +113,58 @@ export interface GetUserReturn { // Client // export class ExampleService implements ExampleService { - private hostname: string - private fetch: Fetch - private path = '/rpc/ExampleService/' + protected hostname: string + protected fetch: Fetch + protected path = '/rpc/ExampleService/' constructor(hostname: string, fetch: Fetch) { - this.hostname = hostname - this.fetch = fetch + this.hostname = hostname.replace(/\/*$/, '') + this.fetch = (input: RequestInfo, init?: RequestInit) => fetch(input, init) } private url(name: string): string { return this.hostname + this.path + name } - ping = (headers?: object): Promise => { + ping = (headers?: object, signal?: AbortSignal): Promise => { return this.fetch( this.url('Ping'), - createHTTPRequest({}, headers) + createHTTPRequest({}, headers, signal) ).then((res) => { return buildResponse(res).then(_data => { - return { - status: (_data.status) - } + return {} }) + }, (error) => { + throw WebrpcRequestFailedError.new({ cause: `fetch(): ${error.message || ''}` }) }) } - getUser = (args: GetUserArgs, headers?: object): Promise => { + getUser = (args: GetUserArgs, headers?: object, signal?: AbortSignal): Promise => { return this.fetch( this.url('GetUser'), - createHTTPRequest(args, headers)).then((res) => { + createHTTPRequest(args, headers, signal)).then((res) => { return buildResponse(res).then(_data => { return { - code: (_data.code), - user: (_data.user) + code: (_data.code), + user: (_data.user), } }) + }, (error) => { + throw WebrpcRequestFailedError.new({ cause: `fetch(): ${error.message || ''}` }) }) } } - -export interface WebRPCError extends Error { - code: string - msg: string - status: number -} + const createHTTPRequest = (body: object = {}, headers: object = {}, signal: AbortSignal | null = null): object => { + const reqHeaders: {[key: string]: string} = { ...headers, 'Content-Type': 'application/json' } + reqHeaders[WebrpcHeader] = WebrpcHeaderValue -const createHTTPRequest = (body: object = {}, headers: object = {}): object => { return { method: 'POST', - headers: { ...headers, 'Content-Type': 'application/json' }, - body: JSON.stringify(body || {}) + headers: reqHeaders, + body: JSON.stringify(body || {}), + signal } } @@ -122,14 +173,244 @@ const buildResponse = (res: Response): Promise => { let data try { data = JSON.parse(text) - } catch(err) { - throw { code: 'unknown', msg: `expecting JSON, got: ${text}`, status: res.status } as WebRPCError + } catch(error) { + let message = '' + if (error instanceof Error) { + message = error.message + } + throw WebrpcBadResponseError.new({ + status: res.status, + cause: `JSON.parse(): ${message}: response text: ${text}`}, + ) } if (!res.ok) { - throw data // webrpc error response + const code: number = (typeof data.code === 'number') ? data.code : 0 + throw (webrpcErrorByCode[code] || WebrpcError).new(data) } return data }) } +// +// Errors +// + +export class WebrpcError extends Error { + name: string + code: number + message: string + status: number + cause?: string + + /** @deprecated Use message instead of msg. Deprecated in webrpc v0.11.0. */ + msg: string + + constructor(name: string, code: number, message: string, status: number, cause?: string) { + super(message) + this.name = name || 'WebrpcError' + this.code = typeof code === 'number' ? code : 0 + this.message = message || `endpoint error ${this.code}` + this.msg = this.message + this.status = typeof status === 'number' ? status : 0 + this.cause = cause + Object.setPrototypeOf(this, WebrpcError.prototype) + } + + static new(payload: any): WebrpcError { + return new this(payload.error, payload.code, payload.message || payload.msg, payload.status, payload.cause) + } +} + +// Webrpc errors + +export class WebrpcEndpointError extends WebrpcError { + constructor( + name: string = 'WebrpcEndpoint', + code: number = 0, + message: string = `endpoint error`, + status: number = 0, + cause?: string + ) { + super(name, code, message, status, cause) + Object.setPrototypeOf(this, WebrpcEndpointError.prototype) + } +} + +export class WebrpcRequestFailedError extends WebrpcError { + constructor( + name: string = 'WebrpcRequestFailed', + code: number = -1, + message: string = `request failed`, + status: number = 0, + cause?: string + ) { + super(name, code, message, status, cause) + Object.setPrototypeOf(this, WebrpcRequestFailedError.prototype) + } +} + +export class WebrpcBadRouteError extends WebrpcError { + constructor( + name: string = 'WebrpcBadRoute', + code: number = -2, + message: string = `bad route`, + status: number = 0, + cause?: string + ) { + super(name, code, message, status, cause) + Object.setPrototypeOf(this, WebrpcBadRouteError.prototype) + } +} + +export class WebrpcBadMethodError extends WebrpcError { + constructor( + name: string = 'WebrpcBadMethod', + code: number = -3, + message: string = `bad method`, + status: number = 0, + cause?: string + ) { + super(name, code, message, status, cause) + Object.setPrototypeOf(this, WebrpcBadMethodError.prototype) + } +} + +export class WebrpcBadRequestError extends WebrpcError { + constructor( + name: string = 'WebrpcBadRequest', + code: number = -4, + message: string = `bad request`, + status: number = 0, + cause?: string + ) { + super(name, code, message, status, cause) + Object.setPrototypeOf(this, WebrpcBadRequestError.prototype) + } +} + +export class WebrpcBadResponseError extends WebrpcError { + constructor( + name: string = 'WebrpcBadResponse', + code: number = -5, + message: string = `bad response`, + status: number = 0, + cause?: string + ) { + super(name, code, message, status, cause) + Object.setPrototypeOf(this, WebrpcBadResponseError.prototype) + } +} + +export class WebrpcServerPanicError extends WebrpcError { + constructor( + name: string = 'WebrpcServerPanic', + code: number = -6, + message: string = `server panic`, + status: number = 0, + cause?: string + ) { + super(name, code, message, status, cause) + Object.setPrototypeOf(this, WebrpcServerPanicError.prototype) + } +} + +export class WebrpcInternalErrorError extends WebrpcError { + constructor( + name: string = 'WebrpcInternalError', + code: number = -7, + message: string = `internal error`, + status: number = 0, + cause?: string + ) { + super(name, code, message, status, cause) + Object.setPrototypeOf(this, WebrpcInternalErrorError.prototype) + } +} + +export class WebrpcClientDisconnectedError extends WebrpcError { + constructor( + name: string = 'WebrpcClientDisconnected', + code: number = -8, + message: string = `client disconnected`, + status: number = 0, + cause?: string + ) { + super(name, code, message, status, cause) + Object.setPrototypeOf(this, WebrpcClientDisconnectedError.prototype) + } +} + +export class WebrpcStreamLostError extends WebrpcError { + constructor( + name: string = 'WebrpcStreamLost', + code: number = -9, + message: string = `stream lost`, + status: number = 0, + cause?: string + ) { + super(name, code, message, status, cause) + Object.setPrototypeOf(this, WebrpcStreamLostError.prototype) + } +} + +export class WebrpcStreamFinishedError extends WebrpcError { + constructor( + name: string = 'WebrpcStreamFinished', + code: number = -10, + message: string = `stream finished`, + status: number = 0, + cause?: string + ) { + super(name, code, message, status, cause) + Object.setPrototypeOf(this, WebrpcStreamFinishedError.prototype) + } +} + + +// Schema errors + + +export enum errors { + WebrpcEndpoint = 'WebrpcEndpoint', + WebrpcRequestFailed = 'WebrpcRequestFailed', + WebrpcBadRoute = 'WebrpcBadRoute', + WebrpcBadMethod = 'WebrpcBadMethod', + WebrpcBadRequest = 'WebrpcBadRequest', + WebrpcBadResponse = 'WebrpcBadResponse', + WebrpcServerPanic = 'WebrpcServerPanic', + WebrpcInternalError = 'WebrpcInternalError', + WebrpcClientDisconnected = 'WebrpcClientDisconnected', + WebrpcStreamLost = 'WebrpcStreamLost', + WebrpcStreamFinished = 'WebrpcStreamFinished', +} + +export enum WebrpcErrorCodes { + WebrpcEndpoint = 0, + WebrpcRequestFailed = -1, + WebrpcBadRoute = -2, + WebrpcBadMethod = -3, + WebrpcBadRequest = -4, + WebrpcBadResponse = -5, + WebrpcServerPanic = -6, + WebrpcInternalError = -7, + WebrpcClientDisconnected = -8, + WebrpcStreamLost = -9, + WebrpcStreamFinished = -10, +} + +export const webrpcErrorByCode: { [code: number]: any } = { + [0]: WebrpcEndpointError, + [-1]: WebrpcRequestFailedError, + [-2]: WebrpcBadRouteError, + [-3]: WebrpcBadMethodError, + [-4]: WebrpcBadRequestError, + [-5]: WebrpcBadResponseError, + [-6]: WebrpcServerPanicError, + [-7]: WebrpcInternalErrorError, + [-8]: WebrpcClientDisconnectedError, + [-9]: WebrpcStreamLostError, + [-10]: WebrpcStreamFinishedError, +} + export type Fetch = (input: RequestInfo, init?: RequestInit) => Promise + diff --git a/_examples/node-ts/webapp/index.html b/_examples/node-ts/webapp/index.html index e9b9744e..95c25813 100644 --- a/_examples/node-ts/webapp/index.html +++ b/_examples/node-ts/webapp/index.html @@ -5,19 +5,22 @@ display: none; } - .user.loaded { - display: block; + div { + padding-bottom: 10px; } -
    -

    Username:

    -

    +
    + +
    - +
    + + +
    diff --git a/_examples/node-ts/webapp/index.ts b/_examples/node-ts/webapp/index.ts index 2c40f596..93a38992 100644 --- a/_examples/node-ts/webapp/index.ts +++ b/_examples/node-ts/webapp/index.ts @@ -1,26 +1,54 @@ import { ExampleService } from './client.gen' const exampleService = new ExampleService( - 'http://localhost:3000', - (input, init) => fetch(input, init) + 'http://localhost:3000', + (input, init) => fetch(input, init) ) document.addEventListener('DOMContentLoaded', () => { - const userContainer = document.getElementsByClassName('js-user')[0] - const loadUserButton = document.getElementsByClassName('js-load-user-btn')[0] - const userNameText = document.getElementsByClassName('js-username')[0] + const pingButton = document.getElementById('js-ping-btn') + const pingText = document.getElementById('js-ping-text') - loadUserButton.addEventListener('click', () => { - exampleService - .getUser({ - userID: 1 - }) - .then(({ user }) => { - console.log('getUser() responded with:', {user}) + if (!pingButton || !pingText) { + console.log('error getting ping HTML elements') + return + } - userContainer.classList.add('loaded') + pingButton.addEventListener('click', () => { + exampleService + .ping({}) + .then(({}) => { + console.log('ping() responded:', {}) + pingText.textContent = 'PONG' + }) + .catch((e) => { + console.log('ping() failed:', e) + pingText.textContent = 'ping() failed: ' + e.message + }) + }) +}) + +document.addEventListener('DOMContentLoaded', () => { + const getUserButton = document.getElementById('js-get-user-btn') + const usernameText = document.getElementById('js-username-text') + + if (!getUserButton || !usernameText) { + console.log('error getting username HTML elements') + return + } - userNameText.textContent = user.USERNAME - }) - }) + getUserButton.addEventListener('click', () => { + exampleService + .getUser({ + userID: 1, + }) + .then(({ user }) => { + console.log('getUser() responded with:', { user }) + usernameText.textContent = user.USERNAME + }) + .catch((e) => { + console.log('getUser() failed:', e) + usernameText.textContent = 'getUser() failed: ' + e.message + }) + }) }) diff --git a/_examples/node-ts/webapp/yarn.lock b/_examples/node-ts/webapp/yarn.lock index ec13356d..0b90dff0 100644 --- a/_examples/node-ts/webapp/yarn.lock +++ b/_examples/node-ts/webapp/yarn.lock @@ -981,9 +981,9 @@ babylon-walk@^1.0.2: lodash.clone "^4.5.0" balanced-match@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" - integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== base64-js@^1.0.2: version "1.3.0" @@ -1399,7 +1399,7 @@ component-emitter@^1.2.1: concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== concat-stream@~1.6.0: version "1.6.2" @@ -1748,9 +1748,9 @@ debug@^4.1.0: ms "^2.1.1" decode-uri-component@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" - integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= + version "0.2.2" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9" + integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ== deep-extend@^0.6.0: version "0.6.0" @@ -2905,9 +2905,9 @@ json-stringify-safe@~5.0.1: integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= json5@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" - integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== + version "1.0.2" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" + integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== dependencies: minimist "^1.2.0" @@ -3117,9 +3117,9 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= minimatch@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== dependencies: brace-expansion "^1.1.7" @@ -3129,9 +3129,9 @@ minimist@0.0.8: integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= minimist@^1.1.3, minimist@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" - integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= + version "1.2.7" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" + integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== minipass@^2.2.1, minipass@^2.3.4: version "2.3.5" @@ -4069,9 +4069,9 @@ q@^1.1.2: integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= qs@~6.5.2: - version "6.5.2" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" - integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== + version "6.5.3" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad" + integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA== querystring-es3@^0.2.0: version "0.2.1" @@ -4382,9 +4382,9 @@ sax@^1.2.4, sax@~1.2.4: integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.6.0: - version "5.7.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.0.tgz#790a7cf6fea5459bac96110b29b60412dc8ff96b" - integrity sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA== + version "5.7.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" + integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== send@0.16.2: version "0.16.2" @@ -5170,9 +5170,9 @@ wrappy@1: integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= ws@^5.1.1, ws@^5.2.0: - version "5.2.2" - resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.2.tgz#dffef14866b8e8dc9133582514d1befaf96e980f" - integrity sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA== + version "5.2.4" + resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.4.tgz#c7bea9f1cfb5f410de50e70e82662e562113f9a7" + integrity sha512-fFCejsuC8f9kOSu9FYaOw8CdO68O3h5v0lg4p74o8JqWpwTf9tniOD+nOB78aWoVSS6WptVUmDrp/KPsMVBWFQ== dependencies: async-limiter "~1.0.0" diff --git a/cmd/webrpc-gen/main.go b/cmd/webrpc-gen/main.go index b4a6c42a..bac79ae8 100644 --- a/cmd/webrpc-gen/main.go +++ b/cmd/webrpc-gen/main.go @@ -3,125 +3,262 @@ package main import ( "flag" "fmt" - "io/ioutil" "os" "path/filepath" + "strings" + "time" "github.com/webrpc/webrpc" "github.com/webrpc/webrpc/gen" - _ "github.com/webrpc/webrpc/gen/golang" - _ "github.com/webrpc/webrpc/gen/javascript" - _ "github.com/webrpc/webrpc/gen/typescript" + "github.com/webrpc/webrpc/schema" ) -var flags = flag.NewFlagSet("webrpc-gen", flag.ExitOnError) +var ( + flags = flag.NewFlagSet("webrpc-gen", flag.ExitOnError) + versionFlag = flags.Bool("version", false, "print version and exit") + schemaFlag = flags.String("schema", "", "webrpc input schema file, ie. proto.ridl or proto.json (required)") + targetFlag = flags.String("target", "", targetUsage()) + outFlag = flags.String("out", "", "generated output file (default stdout)") + fmtFlag = flags.Bool("fmt", true, "format generated code") + refreshCacheFlag = flags.Bool("refreshCache", false, "refresh webrpc cache") + silentFlag = flags.Bool("silent", false, "silence gen summary") + serviceFlag = flags.String("service", "", "generate passed service only separated by comma, empty string means all services") + ignoreFlag = flags.String("ignore", "", "ignore service methods with specific annotations separated by commas") + matchFlag = flags.String("match", "", "match service methods with specific annotations separated by commas") +) func main() { - versionFlag := flags.Bool("version", false, "print webrpc version and exit") - schemaFlag := flags.String("schema", "", "webrpc schema file (required)") - pkgFlag := flags.String("pkg", "proto", "generated package name for target language, default: proto") - outFlag := flags.String("out", "", "generated output file, default: stdout") - testFlag := flags.Bool("test", false, "test schema parsing (skips code-gen)") - clientFlag := flags.Bool("client", false, "enable webrpc client library generation, default: off") - serverFlag := flags.Bool("server", false, "enable webrpc server library generation, default: off") - - // registered/available target languages - targets := []string{} - for k, _ := range gen.Generators { - targets = append(targets, k) + // Collect CLI -flags and custom template -options. + cliFlags, templateOpts, err := collectCliArgs(flags, os.Args[1:]) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to parse CLI flags: %v\n", err) + os.Exit(1) } - targetFlag := flags.String("target", "", fmt.Sprintf("target language for webrpc library generation, %s (required)", targets)) - targetExtra := flags.String("extra", "", "target language extra/custom options") - flags.Parse(os.Args[1:]) + setTargetFlagsUsage(templateOpts) + flags.Parse(cliFlags) if *versionFlag { - fmt.Printf("webrpc %s\n", webrpc.VERSION) + fmt.Println("webrpc-gen", webrpc.VERSION) os.Exit(0) } if *schemaFlag == "" { - fmt.Println("oops, you must pass a -schema flag, see -h for help/usage") + fmt.Fprintf(os.Stderr, "-schema flag is required\n\n") + flags.Usage() os.Exit(1) } // Parse+validate the webrpc schema file - schema, err := webrpc.ParseSchemaFile(*schemaFlag) + s, err := webrpc.ParseSchemaFile(*schemaFlag) if err != nil { - fmt.Println(err.Error()) + fmt.Fprintf(os.Stderr, "failed to parse %s:%v\n", *schemaFlag, err) + os.Exit(1) + } + + // Code-gen targets + if *targetFlag == "" { + fmt.Fprintf(os.Stderr, "-target flag is required\n\n") + flags.Usage() + os.Exit(1) + } + + if *serviceFlag != "" { + s = schema.MatchServices(s, strings.Split(*serviceFlag, ",")) + } + + if *ignoreFlag != "" && *matchFlag != "" { + fmt.Fprintf(os.Stderr, "-ignore and -match flags are mutually exclusive\n\n") os.Exit(1) } - // Test the schema file (useful for ridl files) - if *testFlag { - jout, err := schema.ToJSON(true) + if *ignoreFlag != "" { + ignoreAnnotations, err := makeAnnotationsMap(*ignoreFlag) if err != nil { - fmt.Println(err.Error()) + fmt.Fprintf(os.Stderr, "parse ignore annotations %s:%v\n", *ignoreFlag, err) os.Exit(1) } - fmt.Println(jout) - os.Exit(0) + + s = schema.IgnoreMethodsWithAnnotations(s, ignoreAnnotations) } - // Code-gen targets - if *targetFlag == "" { - fmt.Println("oops, you must pass a -target flag, see -h for help/usage") - os.Exit(1) + if *matchFlag != "" { + matchAnnotations, err := makeAnnotationsMap(*matchFlag) + if err != nil { + fmt.Fprintf(os.Stderr, "parse match annotations %s:%v\n", *matchFlag, err) + os.Exit(1) + } + + s = schema.MatchMethodsWithAnnotations(s, matchAnnotations) } - targetLang := *targetFlag - if _, ok := gen.Generators[targetLang]; !ok { - fmt.Printf("oops, you passed an invalid -target flag, try one of registered generators: %s\n", targets) - os.Exit(1) + config := &gen.Config{ + RefreshCache: *refreshCacheFlag, + Format: *fmtFlag, + TemplateOptions: templateOpts, } - // Call our target code-generator - generator := gen.GetGenerator(*targetFlag) - if generator == nil { - fmt.Printf("error! unable to find generator for target '%s'\n", *targetFlag) + genOutput, err := gen.Generate(s, *targetFlag, config) + if err != nil { + fmt.Fprintln(os.Stderr, err.Error()) os.Exit(1) } - targetOpts := gen.TargetOptions{ - PkgName: *pkgFlag, - Client: *clientFlag, - Server: *serverFlag, - Extra: *targetExtra, + // Write output to stdout + if *outFlag == "" || *outFlag == "stdout" { + fmt.Println(genOutput.Code) + os.Exit(0) } - protoGen, err := generator.Gen(schema, targetOpts) + // Write output to a file + err = writeOutfile(*outFlag, []byte(genOutput.Code)) if err != nil { - fmt.Println(err.Error()) + fmt.Fprintln(os.Stderr, err.Error()) os.Exit(1) } - // Write output - if *outFlag != "" && *outFlag != "stdout" { - outfile := *outFlag - cwd, err := os.Getwd() - if err != nil { - fmt.Println(err.Error()) - os.Exit(1) + // Print gen report + if *silentFlag { + os.Exit(0) + } + + fmt.Println("=======================================") + fmt.Println("| webrpc generated summary |") + fmt.Println("=======================================") + fmt.Println(" webrpc-gen version :", webrpc.VERSION) + fmt.Println(" target :", genOutput.TmplVersion) + if !genOutput.IsLocal { + fmt.Println(" target cache :", genOutput.TmplDir) + cacheAge := "now (refreshed)" + if genOutput.CacheAge > 0 { + cacheAge = fmt.Sprintf("%v", genOutput.CacheAge.Truncate(time.Second)) + if genOutput.CacheRefreshErr != nil { + cacheAge += fmt.Sprintf(" (failed to refresh: %v)", genOutput.CacheRefreshErr) + } } - if outfile[0:1] != "/" { - outfile = filepath.Join(cwd, outfile) + fmt.Println(" target cache age :", cacheAge) + } + fmt.Println(" schema file :", *schemaFlag) + fmt.Println(" output file :", *outFlag) + if genOutput.FormatErr != nil { + fmt.Println(" format error :", genOutput.FormatErr) + os.Exit(1) + } +} + +func makeAnnotationsMap(annotations string) (map[string]string, error) { + annotationsMap := map[string]string{} + + for _, annonation := range strings.Split(annotations, ",") { + _, fullAnnotation, ok := strings.Cut(annonation, "@") + if !ok { + return annotationsMap, fmt.Errorf("invalid annotation format: %s", annonation) } - outdir := filepath.Dir(outfile) - if _, err := os.Stat(outdir); os.IsNotExist(err) { - err := os.MkdirAll(outdir, 0755) - if err != nil { - fmt.Println(err.Error()) - os.Exit(1) - } + annotationName, annotationValue, _ := strings.Cut(fullAnnotation, ":") + + annotationsMap[annotationName] = annotationValue + } + + return annotationsMap, nil +} + +func collectCliArgs(flags *flag.FlagSet, args []string) (cliFlags []string, templateOpts map[string]interface{}, err error) { + templateOpts = map[string]interface{}{} + + for _, arg := range args { + //name, value, _ := strings.Cut(arg, "=") // Added in Go 1.18. + name, value := arg, "" + if i := strings.Index(arg, "="); i >= 0 { + name = arg[:i] + value = arg[i+1:] + } + + if !strings.HasPrefix(name, "-") { + return nil, nil, fmt.Errorf("option %q is invalid (expected -name=value)", arg) + } + name = strings.TrimLeft(name, "-") + if len(name) == 0 { + return nil, nil, fmt.Errorf("option %q is invalid (expected -name=value)", arg) } - err = ioutil.WriteFile(outfile, []byte(protoGen), 0644) + if flags.Lookup(name) != nil { + cliFlags = append(cliFlags, arg) + } else if name == "h" || name == "help" { + cliFlags = append(cliFlags, arg) + templateOpts["help"] = "" + } else { + templateOpts[name] = value + } + + // Support webrpc-gen v0.6.0 -target=js -extra=noexports flag. + if name == "extra" && value == "noexports" { + templateOpts["export"] = "false" + } + } + + return +} + +func writeOutfile(outfile string, protoGen []byte) error { + cwd, err := os.Getwd() + if err != nil { + return err + } + if outfile[0:1] != "/" { + outfile = filepath.Join(cwd, outfile) + } + + outdir := filepath.Dir(outfile) + if _, err := os.Stat(outdir); os.IsNotExist(err) { + err := os.MkdirAll(outdir, 0755) if err != nil { - fmt.Println(err.Error()) - os.Exit(1) + return err + } + } + + err = os.WriteFile(outfile, []byte(protoGen), 0644) + if err != nil { + return err + } + + return nil +} + +func targetUsage() string { + var b strings.Builder + fmt.Fprintln(&b, "target code generator (required)") + fmt.Fprintln(&b, "built-in targets:") + for _, target := range gen.EmbeddedTargetNames { + fmt.Fprintf(&b, " -target=%s\n", target) + } + fmt.Fprintln(&b, " -target=json (prints schema in JSON)") + fmt.Fprintln(&b, " -target=debug (prints schema and template variables incl. Go type information)") + fmt.Fprintln(&b, "remote git repo:") + for _, target := range gen.EmbeddedTargetNames { + fmt.Fprintf(&b, " -target=%s\n", gen.EmbeddedTargets[target].ImportTag) + } + fmt.Fprintln(&b, "local folder:") + fmt.Fprintln(&b, " -target=../local-go-templates-on-disk") + fmt.Fprintln(&b, " (see https://github.com/webrpc/webrpc/tree/master/gen)") + + return b.String() +} + +func setTargetFlagsUsage(templateOpts map[string]interface{}) { + flags.Usage = func() { + fmt.Fprintf(os.Stderr, "Usage: %s -schema= -target= -out= [...targetOpts]\n", flags.Name()) + flags.PrintDefaults() + fmt.Fprintf(os.Stderr, "See https://github.com/webrpc/webrpc for more info.\n") + + if *targetFlag != "" { + fmt.Fprintf(os.Stderr, "\nTarget generator usage:\n") + templateHelp, err := gen.Generate(&schema.WebRPCSchema{}, *targetFlag, &gen.Config{TemplateOptions: templateOpts}) + if err != nil { + fmt.Fprintln(os.Stderr, templateHelp.Code) + } else { + fmt.Fprintf(os.Stderr, "failed to render -help: %v\n", err) + } } - } else { - fmt.Println(protoGen) } } diff --git a/cmd/webrpc-test/main.go b/cmd/webrpc-test/main.go new file mode 100644 index 00000000..e0c658db --- /dev/null +++ b/cmd/webrpc-test/main.go @@ -0,0 +1,105 @@ +package main + +import ( + "context" + _ "embed" + "flag" + "fmt" + "os" + "time" + + "github.com/webrpc/webrpc" + "github.com/webrpc/webrpc/tests" + "github.com/webrpc/webrpc/tests/client" + "github.com/webrpc/webrpc/tests/server" +) + +var ( + flags = flag.NewFlagSet("webrpc-test", flag.ContinueOnError) + clientFlag = flags.Bool("client", false, "run client tests") + serverFlag = flags.Bool("server", false, "run test server") + waitFlag = flags.Bool("waitForServer", false, "wait for server to be ready") + versionFlag = flags.Bool("version", false, "print version and exit") + printSchema = flags.Bool("print-schema", false, "obsolete flag (use -printRIDL)") // Obsolete. + printRIDL = flags.Bool("printRIDL", false, "print schema in RIDL") + printJSON = flags.Bool("printJSON", false, "print schema in JSON") + + // webrpc-test -client -url=http://localhost:9988 + clientFlags = flag.NewFlagSet("webrpc-test -client", flag.ExitOnError) + clientUrlFlag = clientFlags.String("url", "http://localhost:9988", "run client against given server URL") + + // webrpc-test -server -port=9988 -timeout=1m + serverFlags = flag.NewFlagSet("webrpc-test -server", flag.ExitOnError) + serverPortFlag = serverFlags.Int("port", 9988, "run server at given port") + serverTimeoutFlag = serverFlags.Duration("timeout", time.Minute, "exit after given timeout") + + // webrpc-test -waitForServer -url=http://localhost:9988 -timeout=1m + waitFlags = flag.NewFlagSet("webrpc-test -waitForServer", flag.ExitOnError) + waitUrlFlag = waitFlags.String("url", "http://localhost:9988", "run client against given server URL") + waitTimeoutFlag = waitFlags.Duration("timeout", time.Minute, "exit after given timeout") +) + +func main() { + if len(os.Args) >= 2 { + if err := flags.Parse(os.Args[1:2]); err != nil { + os.Exit(1) + } + } + + switch { + case *versionFlag: + fmt.Println("webrpc-test", webrpc.VERSION) + + case *printRIDL, *printSchema: + fmt.Println(tests.GetRIDLSchema()) + + case *printJSON: + fmt.Println(tests.GetJSONSchema()) + + case *clientFlag: + if err := clientFlags.Parse(os.Args[2:]); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + + if err := client.RunTests(context.Background(), *clientUrlFlag); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + + case *serverFlag: + if err := serverFlags.Parse(os.Args[2:]); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + + server, err := server.RunTestServer(fmt.Sprintf("0.0.0.0:%v", *serverPortFlag), *serverTimeoutFlag) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + + if err := server.Wait(); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + + case *waitFlag: + if err := waitFlags.Parse(os.Args[2:]); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + + start := time.Now() + if err := client.Wait(*waitUrlFlag, *waitTimeoutFlag); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + + fmt.Fprintf(os.Stdout, "wait: test server ready in %v\n", time.Since(start).Round(time.Millisecond)) + + default: + flags.Usage() + os.Exit(1) + } +} diff --git a/gen/README.md b/gen/README.md index 62c5d8da..2afd8150 100644 --- a/gen/README.md +++ b/gen/README.md @@ -1,17 +1,329 @@ -# `gen` - webrpc code-generation for multiple language targets +# webrpc generators -`gen` uses a basic templating language, along with the webrpc schema AST (abtract-syntax-tree) -to generate source code of the Web service's type system, client library and server handlers. +`webrpc-gen` uses Go [text/template](https://pkg.go.dev/text/template) language, along with the webrpc schema AST (abtract-syntax-tree) to generate source code of the target's type system, client library and server handlers. -## Supported targets +The Go templates are used in many popular projects including [Hugo](https://gohugo.io/) and [Helm](https://helm.sh). Hugo has a [nice introduction to Go templates](https://gohugo.io/templates/introduction/). -* `go` - [gen/golang](./golang) -* `ts` - [gen/typescript](./typescript) -* `js` - [gen/javascript](./javascript) +- [Developing a new generator](#developing-a-new-generator) +- [Interoperability tests](#interoperability-tests) +- [Template structure](#template-structure) + - [Create "main" template](#create-main-template) + - [Require specific webrpc protocol version](#require-specific-webrpc-protocol-version) + - [Require specific webrpc-gen version](#require-specific-webrpc-gen-version) + - [Print help on -help flag](#print-help-on--help-flag) + - [Set default values for your custom generator options](#set-default-values-for-your-custom-generator-options) + - [Map webrpc types to your type system](#map-webrpc-types-to-your-type-system) + - [Split your template into sub-templates](#split-your-template-into-sub-templates) + - [Create a recursive "type" template](#create-a-recursive-type-template) +- [Template variables](#template-variables) + - [Default CLI variables](#default-cli-variables) + - [Custom CLI variables](#custom-cli-variables) + - [Schema variables](#schema-variables) +- [Template functions](#template-functions) + - [Go text/template functions](#go-texttemplate-functions) + - [webrpc-gen functions](#webrpc-gen-functions) -## Adding a new target +# Developing a new generator -Adding a new target is easy, just add a folder under `gen/` and copy one of the existing -targets and start adapting it for your language. Run `make build` to re-generate all templates -and execute the code-generator via the `go:generate` in the target package you make. +`webrpc-gen` can be invoked against templates located in a local directory: +``` +webrpc-gen -schema=api.ridl -target=./local/directory +``` + +# Interoperability tests + +All webrpc generators are expected to implement reference [TestApi schema](../tests/schema/test.ridl) and run client/server interoperability tests against the official [webrpc-test binaries](https://github.com/webrpc/webrpc/releases). + +For more info, see [typescript](https://github.com/webrpc/gen-typescript/tree/master/tests) or [golang](https://github.com/webrpc/gen-golang/tree/master/tests) tests. + +# Template structure + +## Create "main" template + +`webrpc-gen` expects at least one `*.go.tmpl` file with the entrypoint template called `"main"`. + +```go +{{- define "main" -}} + +{{/* Your generator code */}} + +{{- end -}} +``` + +## Require specific webrpc protocol version + +```go +{{- if ne .WebrpcVersion "v1" -}} + {{- stderrPrintf "%s generator error: unsupported webrpc protocol version %s\n" .WebrpcTarget .WebrpcVersion -}} + {{- exit 1 -}} +{{- end -}} +``` + +## Require specific webrpc-gen version + +Require specific `webrpc-gen` version to ensure the API of the template functions. + +```go +{{- if not (minVersion .WebrpcGenVersion "v0.7.0") -}} + {{- stderrPrintf "%s generator error: unsupported webrpc-gen version %s, please update\n" .WebrpcTarget .WebrpcGenVersion -}} + {{- exit 1 -}} +{{- end -}} +``` + +## Print help on -help flag + +`webrpc-gen -schema=proto.ridl -target=golang -h` + +```go +{{- if exists .Opts "help" -}} + {{- template "help" $opts -}} + {{- exit 0 -}} +{{- end -}} +``` + +## Set default values for your custom generator options + +```go +{{- $opts := dict -}} +{{- set $opts "pkg" (default .Opts.pkg "proto") -}} +{{- set $opts "client" (ternary (in .Opts.client "" "true") true false) -}} +{{- set $opts "server" (ternary (in .Opts.server "" "true") true false) -}} + +{{- /* Print help on unsupported option. */ -}} +{{- range $k, $v := .Opts }} + {{- if not (exists $opts $k) -}} + {{- stderrPrintf "-%v=%q is not supported target option\n\nUsage:\n" $k $v -}} + {{- template "help" $opts -}} + {{- exit 1 -}} + {{- end -}} +{{- end -}} +``` + +## Map webrpc types to your type system +```go +{{- /* Type mapping. */ -}} +{{- $typeMap := dict }} +{{- set $typeMap "null" "null" -}} +{{- set $typeMap "any" "object" -}} +{{- set $typeMap "byte" "string" -}} +{{- set $typeMap "bool" "boolean" -}} +{{- set $typeMap "uint" "number" -}} +{{- set $typeMap "uint8" "number" -}} +{{- set $typeMap "uint16" "number" -}} +{{- set $typeMap "uint32" "number" -}} +{{- set $typeMap "uint64" "number" -}} +{{- set $typeMap "int" "number" -}} +{{- set $typeMap "int8" "number" -}} +{{- set $typeMap "int16" "number" -}} +{{- set $typeMap "int32" "number" -}} +{{- set $typeMap "int64" "number" -}} +{{- set $typeMap "float32" "number" -}} +{{- set $typeMap "float64" "number" -}} +{{- set $typeMap "string" "string" -}} +{{- set $typeMap "timestamp" "string" -}} +{{- set $typeMap "map" "object" -}} +{{- set $typeMap "[]" "array" -}} +``` + +Timestamps must be serialized in JSON to [ECMA Script ISO 8601 format](https://tc39.es/ecma262/multipage/numbers-and-dates.html#sec-date-time-string-format): `YYYY-MM-DDTHH:mm:ss.sssZ` + +Call `{{ get $typeMap .Type }}` to print your type. + +## Split your template into sub-templates + +Import a sub-template. + +```go +{{ template "sub-template" }} +``` + +Use `dict` function to pass multiple variables into the sub-template: + +```go +{{ template "sub-template" dict "Type" .Type "TypeMap" $typeMap }} +``` + +## Create a recursive "type" template + +Base webrpc types can be nested (ie. `map>`), so you will need to render them recursively. + +```go +{{- define "type" -}} +{{- $typeMap := .TypeMap -}} + +{{- if isMapType .Type -}} + map[{{mapKeyType .Type}}]{{template "type" dict "Type" (mapValueType .Type) "TypeMap" $typeMap}} +{{- else if isArrayType .Type -}} + []{{template "type" dict "Type" (arrayItemType .Type) "TypeMap" $typeMap}} +{{- else if isCoreType .Type -}} + {{ get $typeMap .Type }} +{{- else -}} + *{{.Type}} +{{- end -}} + +{{- end -}} +``` + +# Template variables + +## Default CLI variables + +| Variable | Description | Example value | +|------------------------------|-------------------------|-----------------------------------------| +| `{{.WebrpcVersion}}` | webrpc protocol version | `"v1"` | +| `{{.WebrpcGenVersion}}` | webrpc-gen CLI version | `"v0.7.0"` | +| `{{.WebrpcGenCmd}}` | webrpc-gen command | `"webrpc-gen ..."` | +| `{{.WebrpcTarget}}` | webrpc-gen target | `"github.com/webrpc/gen-golang@v0.7.0"` | + +## Custom CLI variables + +You can let users pass custom variables into your template by adding custom `-options` to `webrpc-gen` CLI. + +| webrpc-gen -option | Template variable | +|-----------------------------|---------------------------| +| `-name=HelloService` | `{{.Opts.name}}` | +| `-description="some value"` | `{{.Opts.description}}` | +| `-enableFeature` | `{{.Opts.someFeature}}` | + +Example: + +`webrpc-gen -schema=proto.ridl -target=./custom-template -name=Hello -description="some value" -enableFeature` + +will pass `{{.Opts.name}}`, `{{.Opts.description}}` and `{{.Opts.enableFeature}}` variables into your template. + +## Schema variables + +| Variable | Description | Example value | +|------------------------------------------------|--------------------------------|-----------------------------| +| `{{.SchemaName}}` | schema name | `"example schema"` | +| `{{.SchemaVersion}}` | schema version | `"v0.0.1"` | +| `{{.SchemaHash}}` | `sha1` schema hash | `"483889fb084764e3a256"` | +| `{{.WebrpcErrors}}` | [webrpc errors](./errors.go) | array of built-in errors | +| `{{.WebrpcErrors[0].Code}}` | unique error code | `-4` (0 or negative number) | +| `{{.WebrpcErrors[0].Name}}` | unique error name | `"WebrpcBadRequest"` | +| `{{.WebrpcErrors[0].Message}}` | error description | `"bad request"` | +| `{{.WebrpcErrors[0].HTTPStatus}}` | HTTP response status code | `400` (number `400`-`599`) | +| `{{.Errors}}` | schema errors | array of schema errors | +| `{{.Errors[0].Code}}` | unique error code | `1001` (positive number) | +| `{{.Errors[0].Name}}` | unique error name | `"RateLimited"` | +| `{{.Errors[0].Message}}` | error description | `"rate limited, slow down"` | +| `{{.Errors[0].HTTPStatus}}` | HTTP response status code | `429` (number `100`-`599`) | +| `{{.Types}}` | types | array of types | +| `{{.Types[0].Name}}` | type name | `"User"` | +| `{{.Types[0].Type}}` | type | `"struct"` | +| `{{.Types[0].Fields}}` | type fields | array of fields | +| `{{.Types[0].Fields[0].Name}}` | field name | `"ID"` | +| `{{.Types[0].Fields[0].Type}}` | field type | `"int"` | +| `{{.Types[0].Fields[0].Optional}}` | field optional? | `false` | +| `{{.Types[0].Fields[0].Meta}}` | field metadata | array of `{"key": "value"}` | +| `{{.Services}}` | schema services | array of services | +| `{{.Services[0].Name}}` | service name | `"ExampleService"` | +| `{{.Services[0].Methods}}` | service methods | array of methods | +| `{{.Services[0].Methods[0].Inputs}}` | method inputs | array of method inputs | +| `{{.Services[0].Methods[0].Outputs}}` | method outputs | array of method outputs | +| `{{.Services[0].Methods[0].Inputs[0].Name}}` | method input name | `"header"` | +| `{{.Services[0].Methods[0].Inputs[0].Type}}` | method input type | `"map"` | +| `{{.Services[0].Methods[0].Outputs[0].Name}}` | method output name | `"user"` | +| `{{.Services[0].Methods[0].Outputs[0].Type}}` | method output type | `"User"` | + +See the [example schema JSON file](https://github.com/webrpc/webrpc/blob/master/_examples/golang-basics/example.webrpc.json). + +For example, you can iterate over the schema methods and print their names: +```go +{{- range $_, $msg := .Services -}} + {{- range $_, $method := .Methods -}} + method {{.Name}}() + {{- end -}} +{{- end -}} +``` + +# Template functions + +## Go text/template functions + +| Function | Description | +|------------------------------------------------|--------------------------------| +| `and EXPR` | Returns the boolean AND of its arguments by returning the first empty argument or the last argument. That is, "and x y" behaves as "if x then y else x." Evaluation proceeds through the arguments left to right and returns when the result is determined. | +| `call FUNC ARGS...` | Returns the result of calling the first argument, which must be a function, with the remaining arguments as parameters. Thus "call .X.Y 1 2" is, in Go notation, dot.X.Y(1, 2) where Y is a func-valued field, map entry, or the like. The first argument must be the result of an evaluation that yields a value of function type (as distinct from a predefined function such as print). The function must return either one or two result values, the second of which is of type error. If the arguments don't match the function or the returned error value is non-nil, execution stops. | +| `html STRING` | Returns the escaped HTML equivalent of the textual representation of its arguments. This function is unavailable in html/template, with a few exceptions. | +| `index ARRAY 1` | Returns the result of indexing its first argument by the following arguments. Thus "index x 1 2 3" is, in Go syntax, x[1][2][3]. Each indexed item must be a map, slice, or array. | +| `slice ARRAY 1 2` | slice returns the result of slicing its first argument by the remaining arguments. Thus "slice x 1 2" is, in Go syntax, x[1:2], while "slice x" is x[:], "slice x 1" is x[1:], and "slice x 1 2 3" is x[1:2:3]. The first argument must be a string, slice, or array. | +| `js STRING` | Returns the escaped JavaScript equivalent of the textual representation of its arguments. | +| `len ARRAY\|MAP` | Returns the integer length of its argument. | +| `not EXPR` | Returns the boolean negation of its single argument. | +| `or EXPR` | Returns the boolean OR of its arguments by returning the first non-empty argument or the last argument, that is, "or x y" behaves as "if x then x else y". Evaluation proceeds through the arguments left to right and returns when the result is determined. | +| `print "format %v" ARGS...` | Print and format, see Go's [fmt.Sprint()](https://pkg.go.dev/fmt) | +| `printf "format %v" ARGS...` | Print and format, see Go's [fmt.Sprintf()](https://pkg.go.dev/fmt) | +| `println "format %v" ARGS...` | Print and format, see Go's [fmt.Sprintln()](https://pkg.go.dev/fmt) | +| `urlquery STRING` | Returns the escaped value of the textual representation of its arguments in a form suitable for embedding in a URL query. This function is unavailable in html/template, with a few exceptions. | + +See https://pkg.go.dev/text/template#hdr-Functions + +## sprig v3 functions + +You have access to all template functions in sprig v3 except for those overloaded below. + +See https://masterminds.github.io/sprig/ + +## webrpc-gen functions + +You have access to all template functions in [sprig v3](https://masterminds.github.io/sprig/) except for those overloaded below. + +| Template flow | Description | webrpc-gen | +|------------------------------------------------|-------------------------------------------------|-------------| +| `minVersion {{.WebrpcVersion}} v1.4` | Returns `boolean` if the given major/minor semver is at least v1.4 | v0.7.0 | +| `stderrPrint ARGS...` | `print` to `webrpc-gen` CLI stderr | v0.13.0 | +| `stderrPrintf "format %v" ARGS...` | `printf` to `webrpc-gen` CLI stderr | v0.7.0 | +| `exit INT` | Terminate template execution, useful for fatal errors | v0.7.0 | +| `dump VAR` | Dump variable | v0.13.0 | +| `hasField OBJ FIELD` | Check if object has a given field | v0.13.0 | + +| Schema type helpers | Description | webrpc-gen | +|------------------------------------------------|-------------------------------------------------|-------------| +| `isBasicType .Type` | Returns `true` if `.Type` is [core type](https://github.com/webrpc/webrpc/tree/master/schema#core-types) | v0.7.0 (deprecated) | +| `isCoreType .Type` | Returns `true` if `.Type` is [core type](https://github.com/webrpc/webrpc/tree/master/schema#core-types) | v0.9.0 | +| `isStructType .Type` | Returns `true` if `.Type` is [struct](https://github.com/webrpc/webrpc/tree/master/schema#struct) | v0.7.0 | +| `isEnumType .Type` | Returns `true` if `.Type` is [enum](https://github.com/webrpc/webrpc/tree/master/schema#enum) | v0.7.0 | +| `isMapType .Type` | Returns `true` if `.Type` is [map](https://github.com/webrpc/webrpc/tree/master/schema#map) (`map`) | v0.7.0 | +| `isListType .Type` | Returns `true` if `.Type` is [list](https://github.com/webrpc/webrpc/tree/master/schema#list-array) (`[]T`) | v0.7.0 | +| `mapKeyType .MapType` | Returns map's key type (ie. `T1` from `map`) | v0.7.0 | +| `mapValueType .MapType` | Returns map's value type (ie. `T2` from `map`) | v0.7.0 | +| `listElemType .ListType` | Returns list's element type (ie. `T` from `[]T`) | v0.7.0 | + +| Dictionary (`map[string]any`) | Description | webrpc-gen | +|------------------------------------------------|-------------------------------------------------|-------------| +| `dict [KEY VALUE]...` | Create a new dictionary (`map[string]any`) | v0.7.0 | +| `get $dict KEY` | Get value for the given KEY in dictionary | v0.7.0 | +| `set $dict KEY VALUE` | Set value for the given KEY in dictionary | v0.7.0 | +| `exists $dict KEY` | Returns `true` if the KEY exists in the given dictionary | v0.7.0 | + +| String utils | Description | webrpc-gen | +|------------------------------------------------|-------------------------------------------------|-------------| +| `join ARRAY SEPARATOR` | Concatenate array into a string with separator between elements (see [strings.Join()](https://pkg.go.dev/strings#Join)) | v0.7.0 | +| `split SEPARATOR STRING` | Split string by a separator into string array `[]string` | v0.7.0 | +| `hasPrefix STRING PREFIX` | Returns `true` if the given string starts with PREFIX | v0.8.0 | +| `hasSuffix STRING SUFFIX` | Returns `true` if the given string ends with SUFFIX | v0.8.0 | +| `trimPrefix STRING PREFIX` | Trim prefix from a given string | v0.8.0 | +| `trimSuffix STRING SUFFIX` | Trim suffix from a given string | v0.8.0 | +| `toLower STRING` | Converts input to `"lower case"` | v0.7.0 | +| `toUpper STRING` | Converts input to `"UPPER CASE"` | v0.7.0 | +| `firstLetterToLower STRING` | Converts first letter to lower case | v0.7.0 | +| `firstLetterToUpper STRING` | Converts first letter to UPPER CASE | v0.7.0 | +| `camelCase STRING` | Converts input to `"camelCase"` | v0.7.0 | +| `pascalCase STRING` | Converts input to `"PascalCase"` | v0.7.0 | +| `snakeCase STRING` | Converts input to `"snake_case"` | v0.7.0 | +| `kebabCase STRING` | Converts input to `"kebab-case"` | v0.7.0 | + +| Generic utils | Description | webrpc-gen | +|------------------------------------------------|-------------------------------------------------|-------------| +| `lastIndex ARRAY` | Return the index of the last element of the array | v0.18.0 | +| `array [ELEMENTS]...` | Create a new string array | v0.11.2 (string support v0.8.0) | +| `append ARRAY [ELEMENTS]...` | Append elements to existing string array | v0.11.2 (string support v0.8.0) | +| `first ARRAY` | Return first element from the given array | v0.11.2 (string support v0.7.0) | +| `last ARRAY` | Return last element from the given array | v0.11.2 (string support v0.7.0) | +| `sort ARRAY` | Return sorted copy of the given array (ascending order) | v0.8.0 | +| `coalesce VALUES...` | Returns first non-empty value | v0.7.0 | +| `default VALUE DEFAULT` | Returns `DEFAULT` value if given `VALUE` is empty | v0.7.0 | +| `in FIRST VALUES...` | Returns `true` if any of the given VALUES match the `first` value | v0.7.0 | +| `ternary BOOL FIRST SECOND` | Ternary if-else. Returns first value if `true`, second value if `false` | v0.7.0 | diff --git a/gen/embed.go b/gen/embed.go new file mode 100644 index 00000000..84e48e8a --- /dev/null +++ b/gen/embed.go @@ -0,0 +1,84 @@ +package gen + +import ( + "bufio" + "embed" + "fmt" + "os" + "strings" + + "github.com/webrpc/webrpc" + + dart "github.com/webrpc/gen-dart" + golang "github.com/webrpc/gen-golang" + javascript "github.com/webrpc/gen-javascript" + kotlin "github.com/webrpc/gen-kotlin" + openapi "github.com/webrpc/gen-openapi" + typescript "github.com/webrpc/gen-typescript" +) + +// Embedded templates officially supported by webrpc-gen tooling. +// Versioning is managed via Go modules (go.mod file). +// +// To propose a new officially supported github.com/webrpc/gen-* template, +// please submit an issue at https://github.com/webrpc/webrpc/issues/new. +var embeddedTargetFS = map[string]embed.FS{ + "golang": golang.FS, + "typescript": typescript.FS, + "javascript": javascript.FS, + "openapi": openapi.FS, + "kotlin": kotlin.FS, + "dart": dart.FS, +} + +// The values are computed in init() function based on go.mod file. +var ( + EmbeddedTargetNames = []string{} + EmbeddedTargets = map[string]EmbeddedTarget{} +) + +type EmbeddedTarget struct { + Name string + Version string + ImportTag string + FS embed.FS +} + +func init() { + // Parse target versions from go.mod file + scanner := bufio.NewScanner(strings.NewReader(webrpc.GoModFile)) + for scanner.Scan() { + // github.com/webrpc/gen-golang v0.14.2 // comment + line := scanner.Text() + prefix := "\tgithub.com/webrpc/gen-" + if !strings.HasPrefix(line, prefix) { + // NOTE: Use strings.CutPrefix() once we decide to bump go.mod to Go 1.20. + continue + } + parts := strings.Split(line[len(prefix):], " ") + if len(parts) < 2 { + continue + } + name, version := parts[0], parts[1] + fs, ok := embeddedTargetFS[name] + if !ok { + fmt.Fprintf(os.Stderr, "%s embed FS not found", name) + continue + } + EmbeddedTargetNames = append(EmbeddedTargetNames, name) + + for _, gen := range []string{ + name, // golang + fmt.Sprintf("%s@%s", name, version), // golang@v0.14.2 + fmt.Sprintf("github.com/webrpc/gen-%s", name), // github.com/webrpc/gen-golang + fmt.Sprintf("github.com/webrpc/gen-%s@%s", name, version), // github.com/webrpc/gen-golang@v0.14.2 + } { + EmbeddedTargets[gen] = EmbeddedTarget{ + Name: name, + ImportTag: fmt.Sprintf("github.com/webrpc/gen-%s@%s", name, version), + Version: version, + FS: fs, + } + } + } +} diff --git a/gen/errors.go b/gen/errors.go new file mode 100644 index 00000000..10942022 --- /dev/null +++ b/gen/errors.go @@ -0,0 +1,19 @@ +package gen + +import "github.com/webrpc/webrpc/schema" + +var WebrpcErrors = []*schema.Error{ + {Code: 0, Name: "WebrpcEndpoint", Message: "endpoint error", HTTPStatus: 400}, + {Code: -1, Name: "WebrpcRequestFailed", Message: "request failed", HTTPStatus: 400}, + {Code: -2, Name: "WebrpcBadRoute", Message: "bad route", HTTPStatus: 404}, + {Code: -3, Name: "WebrpcBadMethod", Message: "bad method", HTTPStatus: 405}, + {Code: -4, Name: "WebrpcBadRequest", Message: "bad request", HTTPStatus: 400}, + {Code: -5, Name: "WebrpcBadResponse", Message: "bad response", HTTPStatus: 500}, + {Code: -6, Name: "WebrpcServerPanic", Message: "server panic", HTTPStatus: 500}, + {Code: -7, Name: "WebrpcInternalError", Message: "internal error", HTTPStatus: 500}, + {Code: -8, Name: "WebrpcClientDisconnected", Message: "client disconnected", HTTPStatus: 400}, + {Code: -9, Name: "WebrpcStreamLost", Message: "stream lost", HTTPStatus: 400}, + {Code: -10, Name: "WebrpcStreamFinished", Message: "stream finished", HTTPStatus: 200}, + // Note: Do not change existing values. Append only. + // Keep the list short. Code and Name must be unique. +} diff --git a/gen/funcmap.go b/gen/funcmap.go new file mode 100644 index 00000000..ee506165 --- /dev/null +++ b/gen/funcmap.go @@ -0,0 +1,85 @@ +package gen + +import ( + "strings" + + "github.com/Masterminds/sprig/v3" + "github.com/davecgh/go-spew/spew" + "github.com/golang-cz/textcase" +) + +// Template functions are part of webrpc-gen API. Keep backward-compatible. +func templateFuncMap(opts map[string]interface{}) map[string]interface{} { + f := sprig.FuncMap() + extra := map[string]interface{}{ + // Template flow, errors, debugging. + "stderrPrint": stderrPrint, // v0.13.0 + "stderrPrintf": stderrPrintf, // v0.7.0 + "exit": exit, // v0.7.0 + "minVersion": minVersion, // v0.7.0 + "dump": spew.Sdump, // v0.13.0 + "hasField": hasField, // v0.13.0 + + // Schema type helpers. + "isBasicType": isCoreType, // v0.7.0 (deprecated in v0.9.0) + "isCoreType": isCoreType, // v0.9.0 + "isStructType": isStructType, // v0.7.0 + "isEnumType": isEnumType, // v0.7.0 + "isMapType": isMapType, // v0.7.0 + "isListType": isListType, // v0.7.0 + "mapKeyType": mapKeyType, // v0.7.0 + "mapValueType": mapValueType, // v0.7.0 + "listElemType": listElemType, // v0.7.0 + + // Dictionary (map[string]any). + "dict": dict, // v0.7.0 + "get": get, // v0.7.0 + "set": set, // v0.7.0 + "exists": exists, // v0.7.0 + + // Generic utils. + "lastIndex": lastIndex, // v0.18.0 + "array": array, // v0.11.2 (string support since v0.8.0) + "append": appendFn, // v0.11.2 (string support since v0.7.0) + "first": first, // v0.11.2 (string support since v0.7.0) + "last": last, // v0.11.2 (string support since v0.7.0) + "sort": sortFn, // v0.11.2 (string support since v0.8.0) + "coalesce": coalesce, // v0.7.0 + "default": defaultFn, // v0.7.0 + "in": in, // v0.7.0 + "ternary": ternary, // v0.7.0 + + // String utils. + "join": join, // v0.7.0 + "split": split, // v0.7.0 + "hasPrefix": strings.HasPrefix, // v0.7.0 + "hasSuffix": strings.HasSuffix, // v0.7.0 + "trimPrefix": strings.TrimPrefix, // v0.8.0 + "trimSuffix": strings.TrimSuffix, // v0.8.0 + "toLower": applyStringFunction("toLower", strings.ToLower), // v0.7.0 + "toUpper": applyStringFunction("toLower", strings.ToUpper), // v0.7.0 + "firstLetterToLower": applyStringFunction("firstLetterToLower", func(input string) string { // v0.7.0 + if input == "" { + return "" + } + return strings.ToLower(input[:1]) + input[1:] + }), + "firstLetterToUpper": applyStringFunction("firstLetterToUpper", func(input string) string { // v0.7.0 + if input == "" { + return "" + } + return strings.ToUpper(input[:1]) + input[1:] + }), + "camelCase": applyStringFunction("camelCase", textcase.CamelCase), // v0.7.0 + "pascalCase": applyStringFunction("pascalCase", textcase.PascalCase), // v0.7.0 + "snakeCase": applyStringFunction("snakeCase", textcase.SnakeCase), // v0.7.0 + "kebabCase": applyStringFunction("kebabCase", textcase.KebabCase), // v0.7.0 + "replaceAll": strings.ReplaceAll, + } + + for k, v := range extra { + f[k] = v + } + + return f +} diff --git a/gen/funcmap_dict.go b/gen/funcmap_dict.go new file mode 100644 index 00000000..099205c9 --- /dev/null +++ b/gen/funcmap_dict.go @@ -0,0 +1,45 @@ +package gen + +import ( + "fmt" +) + +// Create new dictionary. +func dict(pairs ...interface{}) map[string]interface{} { + if len(pairs)%2 == 1 { + panic("dict must be created with even number of parameters (key:value pairs)") + } + + m := map[string]interface{}{} + for i := 0; i < len(pairs); i += 2 { + key, ok := pairs[i].(string) + if !ok { + panic(fmt.Sprintf("dict argument(%v) must be string key", i)) + } + m[key] = pairs[i+1] + } + + return m +} + +func get(m map[string]interface{}, key interface{}) (interface{}, error) { + keyStr := toString(key) + + val, ok := m[keyStr] + if !ok { + return nil, fmt.Errorf("get(): key %q doesn't exist", keyStr) + } + + return val, nil +} + +func set(m map[string]interface{}, key string, value interface{}) string { + m[key] = value + return "" +} + +// TODO: Support slices too? +func exists(m map[string]interface{}, key string) bool { + _, ok := m[key] + return ok +} diff --git a/gen/funcmap_flow.go b/gen/funcmap_flow.go new file mode 100644 index 00000000..1e41f209 --- /dev/null +++ b/gen/funcmap_flow.go @@ -0,0 +1,84 @@ +package gen + +import ( + "fmt" + "os" + "reflect" + "strconv" + "strings" +) + +// Similar to "printf" but instead of writing into the generated +// output file, stderrPrintf writes to webrpc-gen CLI stderr. +// Useful for printing template errors / debugging. +func stderrPrintf(format string, a ...interface{}) string { + _, _ = fmt.Fprintf(os.Stderr, format, a...) + return "" +} + +// Similar to "print" but instead of writing into the generated +// output file, stderrPrint writes to webrpc-gen CLI stderr. +// Useful for printing template errors / debugging. +func stderrPrint(a ...interface{}) string { + _, _ = fmt.Fprint(os.Stderr, a...) + return "" +} + +// Terminate template execution with a status code. +// Useful for exiting early or for printing fatal errors from within templates. +func exit(code int) error { + os.Exit(code) + return nil +} + +func minVersion(version string, minVersion string) bool { + major, minor, err := parseMajorMinorVersion(version) + if err != nil { + panic(fmt.Sprintf("minVersion: unexpected version %q", version)) + } + + minMajor, minMinor, err := parseMajorMinorVersion(minVersion) + if err != nil { + panic(fmt.Sprintf("minVersion: unexpected min version %q", minVersion)) + } + + if minMajor > major { + return false + } + + if minMinor > minor { + return false + } + + return true +} + +func parseMajorMinorVersion(version string) (major int, minor int, err error) { + version = strings.TrimPrefix(version, "v") + parts := strings.Split(version, ".") + + major, err = strconv.Atoi(parts[0]) + if err != nil { + return + } + + if len(parts) > 1 { + minor, err = strconv.Atoi(parts[1]) + if err != nil { + return + } + } + + return +} + +func hasField(v interface{}, name string) bool { + rv := reflect.ValueOf(v) + if rv.Kind() == reflect.Ptr { + rv = rv.Elem() + } + if rv.Kind() != reflect.Struct { + return false + } + return rv.FieldByName(name).IsValid() +} diff --git a/gen/funcmap_generic.go b/gen/funcmap_generic.go new file mode 100644 index 00000000..c03a4d34 --- /dev/null +++ b/gen/funcmap_generic.go @@ -0,0 +1,133 @@ +package gen + +import ( + "fmt" + "reflect" + "sort" +) + +// Array creates new array from given elements. +func array(elems ...interface{}) []interface{} { + return append([]interface{}{}, elems...) +} + +// Appends new elements to the existing slice. +func appendFn(slice []interface{}, elems ...interface{}) []interface{} { + return append(slice, elems...) +} + +// Returns first element from given array. +func first(elems interface{}) (interface{}, error) { + switch v := elems.(type) { + case []string: + if len(v) == 0 { + return "", fmt.Errorf("first(): no elements in the array") + } + return v[0], nil + case []interface{}: + if len(v) == 0 { + return "", fmt.Errorf("first(): no elements in the array") + } + return v[0], nil + default: + panic(fmt.Sprintf("first(): unknown arg type %T", v)) + } +} + +// Returns last element from given array. +func last(elems interface{}) (interface{}, error) { + switch v := elems.(type) { + case []string: + if len(v) == 0 { + return "", fmt.Errorf("last(): no elements in the array") + } + return v[len(v)-1], nil + case []interface{}: + if len(v) == 0 { + return "", fmt.Errorf("last(): no elements in the array") + } + return v[len(v)-1], nil + default: + panic(fmt.Sprintf("last(): unknown arg type %T", v)) + } +} + +// Returns true if any of the given values match the first value. +func in(first interface{}, values ...interface{}) bool { + for _, value := range values { + if reflect.DeepEqual(first, value) { + return true + } + } + return false +} + +// Returns defaultValue, if given value is empty. +func defaultFn(value interface{}, defaultValue interface{}) interface{} { + val := reflect.ValueOf(value) + if !val.IsValid() || val.IsZero() { + return defaultValue + } + + return value +} + +// Returns first non-empty value. +func coalesce(v ...interface{}) interface{} { + for _, v := range v { + val := reflect.ValueOf(v) + if !val.IsValid() || val.IsZero() { + continue + } + return v + } + return "" +} + +// Sorts given array. +func sortFn(array []interface{}) []interface{} { + sorted := make([]interface{}, len(array)) + copy(sorted, array) + sort.Slice(sorted, func(i, j int) bool { + return toString(sorted[i]) < toString(sorted[j]) + }) + return sorted +} + +// Ternary if-else. Returns first value if true, second value if false. +func ternary(boolean interface{}, first interface{}, second interface{}) interface{} { + if toBool(boolean) { + return first + } + return second +} + +func toBool(in interface{}) bool { + switch v := in.(type) { + case bool: + return v + case string: + if in == "true" { + return true + } + if in == "false" { + return false + } + panic(fmt.Sprintf("toBool(): unexpected boolean %q", in)) + default: + panic(fmt.Sprintf("toBool(): unexpected boolean %v", v)) + } +} + +// Returns the index value of the last element of the array-like input. Panics +// if the argument is not an array-like object. +func lastIndex(array interface{}) int { + switch reflect.TypeOf(array).Kind() { + case reflect.Slice: + return reflect.ValueOf(array).Len() - 1 + case reflect.Array: + return reflect.ValueOf(array).Type().Len() - 1 + default: + panic("lastIndex(): non array-like") + } +} diff --git a/gen/funcmap_string.go b/gen/funcmap_string.go new file mode 100644 index 00000000..3e5d65a6 --- /dev/null +++ b/gen/funcmap_string.go @@ -0,0 +1,64 @@ +package gen + +import ( + "fmt" + "strings" + + "github.com/webrpc/webrpc/schema" +) + +func toString(v interface{}) string { + switch t := v.(type) { + case schema.VarType: + return t.String() + case *schema.VarType: + if t != nil { + return t.String() + } + panic(fmt.Sprintf("toString(): nil %T", v)) + case schema.Type: + return t.Kind + case *schema.Type: + return t.Kind + case string: + return t + case map[string]interface{}: + var b strings.Builder + for k, v := range t { + b.WriteString(fmt.Sprintf("%v=%v\n", k, v)) + } + return b.String() + default: + panic(fmt.Sprintf("toString(): unknown arg type %T", v)) + } +} + +func join(elems interface{}, sep string) string { + switch v := elems.(type) { + case []string: + return strings.Join(v, sep) + case []interface{}: + strElems := make([]string, len(v)) + for i, elem := range v { + strElems[i] = toString(elem) + } + return strings.Join(strElems, sep) + default: + panic(fmt.Sprintf("join(): unknown arg type %T", v)) + } +} + +func split(sep string, str string) []string { + return strings.Split(str, sep) +} + +func applyStringFunction(fnName string, fn func(string) string) func(v interface{}) string { + return func(v interface{}) string { + switch t := v.(type) { + case string: + return fn(t) + default: + panic(fmt.Errorf("%v(): unknown arg type %T", fnName, v)) + } + } +} diff --git a/gen/funcmap_test.go b/gen/funcmap_test.go new file mode 100644 index 00000000..e1f7f8a0 --- /dev/null +++ b/gen/funcmap_test.go @@ -0,0 +1,158 @@ +package gen + +import ( + "fmt" + "testing" +) + +func TestMinVersion(t *testing.T) { + tt := []struct { + Version string + MinVersion string + Result bool + }{ + {"v1.0.0", "v1", true}, + {"v1.0.0", "v1.0", true}, + {"v1.0.0", "v1.0.0", true}, + {"v2.5.8", "v1", true}, + {"v2.5.8", "v2.0", true}, + {"v2.5.8", "v1.1", true}, + {"v2.5.8", "v1.5.8", true}, + {"v2.5.8", "v2", true}, + {"v2.5.8", "v2.5", true}, + {"v2.5.8", "v2.5.5", true}, + + {"v1.0.0", "v2", false}, + {"v1.0.0", "v2.0", false}, + {"v1.0.0", "v1.1", false}, + {"v1.0.0", "v1.5.8", false}, + {"v1.0.0", "v2", false}, + {"v2.5.8", "v3", false}, + {"v2.5.8", "v2.6", false}, + {"v2.5.8", "v2.6.0", false}, + {"v2.5.8", "v2.6.6", false}, + + {"v0.13.0-dev", "v0.13.0", true}, + } + + for _, tc := range tt { + if result := minVersion(tc.Version, tc.MinVersion); result != tc.Result { + t.Errorf("unexpected result: minVersion(%q, %q) = %v", tc.Version, tc.MinVersion, result) + } + } +} + +func TestParseMajorMinorVersion(t *testing.T) { + tt := []struct { + Version string + Major int + Minor int + Error bool + }{ + {"v0.7", 0, 7, false}, + {"v0.7.0", 0, 7, false}, + {"v0.7.5", 0, 7, false}, + {"v0.7.0-dev", 0, 7, false}, + {"v0.7.x-dev", 0, 7, false}, + + {"v1", 1, 0, false}, + {"v1.0", 1, 0, false}, + {"v1.1", 1, 1, false}, + {"v1.0.1", 1, 0, false}, + {"v1.1.1", 1, 1, false}, + {"v2", 2, 0, false}, + {"v2.0", 2, 0, false}, + {"v2.2", 2, 2, false}, + {"v2.2.0", 2, 2, false}, + {"v2.2.2", 2, 2, false}, + + {"1", 1, 0, false}, + {"1.0", 1, 0, false}, + {"1.1", 1, 1, false}, + {"1.0.1", 1, 0, false}, + {"1.1.1", 1, 1, false}, + {"2", 2, 0, false}, + {"2.0", 2, 0, false}, + {"2.2", 2, 2, false}, + {"2.2.0", 2, 2, false}, + {"2.2.2", 2, 2, false}, + + // Errors: + {"", 0, 0, true}, + {"err", 0, 0, true}, + } + + for _, tc := range tt { + major, minor, err := parseMajorMinorVersion(tc.Version) + if err != nil && !tc.Error { + t.Errorf("unexpected error: parseMajorMinorVersion(%q): %v", tc.Version, err) + } + if err == nil && tc.Error { + t.Errorf("expected error: parseMajorMinorVersion(%q)", tc.Version) + } + if major != tc.Major { + t.Errorf("major, _, _ := parseMajorMinorVersion(%q): got %v, expected %v", tc.Version, major, tc.Major) + } + if minor != tc.Minor { + t.Errorf("_, minor, _ := parseMajorMinorVersion(%q): got %v, expected %v", tc.Version, minor, tc.Minor) + } + } +} + +func TestArray(t *testing.T) { + var a, b, c, d interface{} = "a", "b", "c", "d" + + arr := array(c, d) + if got := len(arr); got != 2 { + t.Errorf("array: expected two elements, got %v", got) + } + + arr = appendFn(arr, a, b) + if got := len(arr); got != 4 { + t.Errorf("append: expected four elements, got %v", got) + } + + sorted := sortFn(arr) + if got := fmt.Sprintf("%v", sorted); got != "[a b c d]" { + t.Errorf("sort: expected sorted array, got %v", got) + } + + if got, _ := first(sorted); got != "a" { + t.Errorf("first: expected a, got %v", got) + } + + if got, _ := last(sorted); got != "d" { + t.Errorf("last: expected d, got %v", got) + } + + if got, err := first([]interface{}{}); err == nil || got != "" { + t.Errorf("first(empty): expected error") + } + + if got, err := last([]interface{}{}); err == nil || got != "" { + t.Errorf("last(empty): expected error") + } +} + +func TestLastIndex(t *testing.T) { + big := [...]int{3, 4, 5, 6} + tt := []struct { + Array interface{} + Result int + }{ + {[...]int{3, 4, 5}, 2}, + {[...]string{"a", "b", "c"}, 2}, + {[...]int{}, -1}, + {[...]int{1}, 0}, + {big, 3}, + {big[0:1], 0}, + {big[1:], 2}, + } + + for _, tc := range tt { + got := lastIndex(tc.Array) + if got != tc.Result { + t.Errorf("lastIndex of %v expected %d but got %d", tc.Array, tc.Result, got) + } + } +} diff --git a/gen/funcmap_types.go b/gen/funcmap_types.go new file mode 100644 index 00000000..04603fcd --- /dev/null +++ b/gen/funcmap_types.go @@ -0,0 +1,109 @@ +package gen + +import ( + "fmt" + "strings" + + "github.com/webrpc/webrpc/schema" +) + +// Returns true if given type is core type. +func isCoreType(v interface{}) bool { + _, isCoreType := schema.CoreTypeFromString[toString(v)] + return isCoreType +} + +// Returns true if given type is struct. +func isStructType(v interface{}) bool { + switch t := v.(type) { + case schema.Type: + return t.Kind == "struct" + case *schema.Type: + return t.Kind == "struct" + case schema.VarType: + return t.Type == schema.T_Struct + case *schema.VarType: + if t != nil { + return t.Type == schema.T_Struct + } + return false + default: + return false + } +} + +// Returns true if given type is enum. +func isEnumType(v interface{}) bool { + switch t := v.(type) { + case schema.Type: + return t.Kind == "enum" + case *schema.Type: + return t.Kind == "enum" + case schema.VarType: + if t.Enum == nil { + return t.Type == schema.T_Enum + } + return t.Enum.Type.Kind == "enum" + case *schema.VarType: + if t.Enum == nil { + return t.Type == schema.T_Enum + } + return t.Enum.Type.Kind == "enum" + default: + return false + } +} + +// Returns true if given type is list (ie. `[]T`). +func isListType(v interface{}) bool { + return strings.HasPrefix(toString(v), "[]") +} + +// Return true if given type is map (ie. map). +func isMapType(v interface{}) bool { + key, value, found := stringsCut(toString(v), ",") + return found && strings.HasPrefix(key, "map<") && strings.HasSuffix(value, ">") +} + +// Returns given map's key type (ie. `T1` from `map`) +func mapKeyType(v interface{}) string { + switch t := v.(type) { + case schema.VarType: + return toString(t.Map.Key) + case *schema.VarType: + return toString(t.Map.Key) + default: + str := toString(v) + key, value, found := stringsCut(str, ",") + if !found || !strings.HasPrefix(key, "map<") || !strings.HasSuffix(value, ">") { + panic(fmt.Errorf("mapKeyValue: expected map, got %v", str)) + } + return strings.TrimPrefix(key, "map<") + } +} + +// Returns given map's value type (ie. `T2` from `map`) +func mapValueType(v interface{}) string { + str := toString(v) + key, value, found := stringsCut(str, ",") + if !found || !strings.HasPrefix(key, "map<") || !strings.HasSuffix(value, ">") { + panic(fmt.Errorf("mapKeyValue: expected map, got %v", str)) + } + return strings.TrimSuffix(value, ">") +} + +// Returns list's element type (ie. `T` from `[]T`) +func listElemType(v interface{}) any { + switch t := v.(type) { + case schema.VarType: + return t.List.Elem + case *schema.VarType: + return t.List.Elem + default: + str := toString(v) + if !strings.HasPrefix(str, "[]") { + panic(fmt.Errorf("listElemType: expected []Type, got %v", str)) + } + return strings.TrimPrefix(str, "[]") + } +} diff --git a/gen/gen.go b/gen/gen.go index 8e1d6f2f..67baaeae 100644 --- a/gen/gen.go +++ b/gen/gen.go @@ -1,28 +1,152 @@ package gen import ( + "bytes" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/davecgh/go-spew/spew" + "github.com/webrpc/webrpc" "github.com/webrpc/webrpc/schema" ) -type Generator interface { - Gen(proto *schema.WebRPCSchema, opts TargetOptions) (string, error) +type Config struct { + RefreshCache bool + Format bool + TemplateOptions map[string]interface{} } -var Generators = map[string]Generator{} +type GenOutput struct { + Code string + TemplateSource + FormatErr error +} -func Register(target string, generator Generator) { - Generators[target] = generator +type TemplateVars struct { + *schema.WebRPCSchema + SchemaHash string + WebrpcGenVersion string + WebrpcGenCommand string + WebrpcHeader string + WebrpcTarget string + WebrpcErrors []*schema.Error + Opts map[string]interface{} + CodeGen string + CodeGenVersion string + CodeGenName string } -func GetGenerator(target string) Generator { - g, _ := Generators[target] - return g +func Generate(proto *schema.WebRPCSchema, target string, config *Config) (out *GenOutput, err error) { + defer func() { + if err != nil { + err = fmt.Errorf("%v\n\tcommand failed: %w", getWebrpcGenCommand(), err) + } + }() + + // Generate deterministic schema hash of the proto file + schemaHash, err := proto.SchemaHash() + if err != nil { + return nil, err + } + + vars := TemplateVars{ + WebRPCSchema: proto, + SchemaHash: schemaHash, + WebrpcGenVersion: webrpc.VERSION, + WebrpcGenCommand: getWebrpcGenCommand(), + WebrpcTarget: target, + WebrpcErrors: WebrpcErrors, + Opts: config.TemplateOptions, + } + if isLocalDir(target) { + vars.WebrpcTarget = target + } + + genOutput := &GenOutput{} + + // Built-in targets + switch target { + case "json": + genJSON, err := proto.ToJSON() + if err != nil { + return genOutput, err + } + genOutput.TmplVersion = target + genOutput.IsLocal = true + genOutput.Code = genJSON + return genOutput, nil + + case "debug": + debug := spew.NewDefaultConfig() + debug.DisableMethods = true + debug.DisablePointerAddresses = true + debug.Indent = "\t" + debug.SortKeys = true + genOutput.TmplVersion = target + genOutput.IsLocal = true + genOutput.Code = debug.Sdump(vars) + return genOutput, nil + } + + // webrpc-gen v0.6.0 + target = getOldTarget(target) + + tmpl, tmplSource, err := loadTemplates(proto, target, config) + if err != nil { + return genOutput, err + } + genOutput.TemplateSource = *tmplSource + + v := strings.Split(tmplSource.TmplVersion, "/") + codeGenName, codeGenVersion, _ := strings.Cut(v[len(v)-1], "@") + + vars.CodeGen = v[len(v)-1] + vars.CodeGenVersion = codeGenVersion + vars.CodeGenName = codeGenName + vars.WebrpcHeader = webRPCGenHeader(vars) + + // Generate the template + var b bytes.Buffer + err = tmpl.ExecuteTemplate(&b, "main", vars) + if err != nil { + return genOutput, err + } + + if config.Format && isGolangTarget(target) { + genOutput.Code, genOutput.FormatErr = formatGoSource(b.Bytes()) + } else { + genOutput.Code = b.String() + } + + return genOutput, nil } -type TargetOptions struct { - PkgName string - Client bool - Server bool - Extra string - Websocket bool +func getWebrpcGenCommand() string { + cmd := filepath.Base(os.Args[0]) + if len(os.Args) > 1 { + cmd = fmt.Sprintf("%s %s", cmd, strings.Join(os.Args[1:], " ")) + } + return cmd +} + +func webRPCGenHeader(t TemplateVars) string { + webrpcGenVersion := fmt.Sprintf("webrpc@%s", t.WebrpcGenVersion) + + codeGenVersion := t.CodeGenVersion + if codeGenVersion == "" { + codeGenVersion = "unknown" + } + codeGen := fmt.Sprintf("%s@%s", t.CodeGenName, codeGenVersion) + + var schemaVersion string + // schema version is empty we use schemaHash instead + if t.SchemaVersion == "" { + schemaVersion = fmt.Sprintf("%s@v0.0.0-%s", t.SchemaName, t.SchemaHash) + } else { + schemaVersion = fmt.Sprintf("%s@%s", t.SchemaName, t.SchemaVersion) + } + + return fmt.Sprintf("%s;%s;%s", webrpcGenVersion, codeGen, schemaVersion) } diff --git a/gen/golang/README.md b/gen/golang/README.md deleted file mode 100644 index 1e5807de..00000000 --- a/gen/golang/README.md +++ /dev/null @@ -1,8 +0,0 @@ -webrpc Go generator -=================== - -Go client and server generator from webrpc schema. - -Please see the [./_examples](../../_examples) - - diff --git a/gen/golang/embed/static.go b/gen/golang/embed/static.go deleted file mode 100644 index b629d72a..00000000 --- a/gen/golang/embed/static.go +++ /dev/null @@ -1,6 +0,0 @@ -// Code generated by statik. DO NOT EDIT. - -// Package contains static assets. -package embed - -var Asset = "PK\x03\x04\x14\x00\x08\x00\x00\x00\x92y\xecN\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00 \x00client.go.tmplUT\x05\x00\x01d\xa3(]{{define \"client\"}}\n{{if .Services}}\n//\n// Client\n//\n\n{{range .Services}}\nconst {{.Name | constPathPrefix}} = \"/rpc/{{.Name}}/\"\n{{end}}\n\n{{range .Services}}\n {{ $serviceName := .Name | clientServiceName}}\n type {{$serviceName}} struct {\n client HTTPClient\n urls [{{.Methods | countMethods}}]string\n }\n\n func {{.Name | newClientServiceName }}(addr string, client HTTPClient) {{.Name}} {\n prefix := urlBase(addr) + {{.Name | constPathPrefix}}\n urls := [{{.Methods | countMethods}}]string{\n {{- range .Methods}}\n prefix + \"{{.Name}}\",\n {{- end}}\n }\n return &{{$serviceName}}{\n client: client,\n urls: urls,\n }\n }\n\n {{range $i, $method := .Methods}}\n func (c *{{$serviceName}}) {{.Name}}({{.Inputs | methodInputs}}) ({{.Outputs | methodOutputs }}) {\n {{- $inputVar := \"nil\" -}}\n {{- $outputVar := \"nil\" -}}\n {{- if .Inputs | len}}\n {{- $inputVar = \"in\"}}\n in := struct {\n {{- range $i, $input := .Inputs}}\n Arg{{$i}} {{$input | methodArgType}} `json:\"{{$input.Name | downcaseName}}\"`\n {{- end}} \n }{ {{.Inputs | methodArgNames}} }\n {{- end}}\n {{- if .Outputs | len}}\n {{- $outputVar = \"&out\"}}\n out := struct {\n {{- range $i, $output := .Outputs}}\n Ret{{$i}} {{$output | methodArgType}} `json:\"{{$output.Name | downcaseName}}\"`\n {{- end}} \n }{}\n {{- end}}\n\n err := doJSONRequest(ctx, c.client, c.urls[{{$i}}], {{$inputVar}}, {{$outputVar}})\n return {{argsList .Outputs \"out.Ret\"}}{{commaIfLen .Outputs}} err\n }\n {{end}}\n{{end}}\n\n// HTTPClient is the interface used by generated clients to send HTTP requests.\n// It is fulfilled by *(net/http).Client, which is sufficient for most users.\n// Users can provide their own implementation for special retry policies.\ntype HTTPClient interface {\n Do(req *http.Request) (*http.Response, error)\n}\n\n// urlBase helps ensure that addr specifies a scheme. If it is unparsable\n// as a URL, it returns addr unchanged.\nfunc urlBase(addr string) string {\n // If the addr specifies a scheme, use it. If not, default to\n // http. If url.Parse fails on it, return it unchanged.\n url, err := url.Parse(addr)\n if err != nil {\n return addr\n }\n if url.Scheme == \"\" {\n url.Scheme = \"http\"\n }\n return url.String()\n}\n\n// newRequest makes an http.Request from a client, adding common headers.\nfunc newRequest(ctx context.Context, url string, reqBody io.Reader, contentType string) (*http.Request, error) {\n req, err := http.NewRequest(\"POST\", url, reqBody)\n if err != nil {\n return nil, err\n }\n req.Header.Set(\"Accept\", contentType)\n req.Header.Set(\"Content-Type\", contentType)\n if headers, ok := HTTPRequestHeaders(ctx); ok {\n for k := range headers {\n for _, v := range headers[k] {\n req.Header.Add(k, v)\n }\n }\n }\n return req, nil\n}\n\n// doJSONRequest is common code to make a request to the remote service.\nfunc doJSONRequest(ctx context.Context, client HTTPClient, url string, in, out interface{}) error {\n reqBody, err := json.Marshal(in)\n if err != nil {\n return clientError(\"failed to marshal json request\", err)\n }\n if err = ctx.Err(); err != nil {\n return clientError(\"aborted because context was done\", err)\n }\n\n req, err := newRequest(ctx, url, bytes.NewBuffer(reqBody), \"application/json\")\n if err != nil {\n return clientError(\"could not build request\", err)\n }\n resp, err := client.Do(req)\n if err != nil {\n return clientError(\"request failed\", err)\n }\n\n defer func() {\n cerr := resp.Body.Close()\n if err == nil && cerr != nil {\n err = clientError(\"failed to close response body\", cerr)\n }\n }()\n\n if err = ctx.Err(); err != nil {\n return clientError(\"aborted because context was done\", err)\n }\n\n if resp.StatusCode != 200 {\n return errorFromResponse(resp)\n }\n\n if out != nil {\n respBody, err := ioutil.ReadAll(resp.Body)\n if err != nil {\n return clientError(\"failed to read response body\", err)\n }\n\n err = json.Unmarshal(respBody, &out)\n if err != nil {\n return clientError(\"failed to unmarshal json response body\", err)\n }\n if err = ctx.Err(); err != nil {\n return clientError(\"aborted because context was done\", err)\n }\n }\n\n return nil\n}\n\n// errorFromResponse builds a webrpc Error from a non-200 HTTP response.\nfunc errorFromResponse(resp *http.Response) Error {\n respBody, err := ioutil.ReadAll(resp.Body)\n if err != nil {\n return clientError(\"failed to read server error response body\", err)\n }\n\n var respErr ErrorPayload\n if err := json.Unmarshal(respBody, &respErr); err != nil {\n return clientError(\"failed unmarshal error response\", err)\n }\n\n errCode := ErrorCode(respErr.Code)\n\n if HTTPStatusFromErrorCode(errCode) == 0 {\n return ErrorInternal(\"invalid code returned from server error response: %s\", respErr.Code)\n }\n\n return &rpcErr{\n code: errCode,\n msg: respErr.Msg,\n cause: errors.New(respErr.Cause),\n }\n}\n\nfunc clientError(desc string, err error) Error {\n return WrapError(ErrInternal, err, desc)\n}\n\nfunc WithHTTPRequestHeaders(ctx context.Context, h http.Header) (context.Context, error) {\n if _, ok := h[\"Accept\"]; ok {\n return nil, errors.New(\"provided header cannot set Accept\")\n }\n if _, ok := h[\"Content-Type\"]; ok {\n return nil, errors.New(\"provided header cannot set Content-Type\")\n }\n\n copied := make(http.Header, len(h))\n for k, vv := range h {\n if vv == nil {\n copied[k] = nil\n continue\n }\n copied[k] = make([]string, len(vv))\n copy(copied[k], vv)\n }\n\n return context.WithValue(ctx, HTTPClientRequestHeadersCtxKey, copied), nil\n}\n\nfunc HTTPRequestHeaders(ctx context.Context) (http.Header, bool) {\n h, ok := ctx.Value(HTTPClientRequestHeadersCtxKey).(http.Header)\n return h, ok\n}\n{{end}}\n{{end}}\nPK\x07\x08\xc5\xc9w\xb8]\x16\x00\x00]\x16\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x8c\x9d,O\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0f\x00 \x00helpers.go.tmplUT\x05\x00\x01\x19\xa0z]{{define \"helpers\"}}\n\n//\n// Helpers\n//\n\ntype ErrorPayload struct {\n Status int `json:\"status\"`\n Code string `json:\"code\"`\n Cause string `json:\"cause,omitempty\"`\n Msg string `json:\"msg\"`\n Error string `json:\"error\"`\n}\n\ntype Error interface {\n // Code is of the valid error codes\n Code() ErrorCode\n\n // Msg returns a human-readable, unstructured messages describing the error\n Msg() string\n\n // Cause is reason for the error\n Cause() error\n\n // Error returns a string of the form \"webrpc error : \"\n Error() string\n\n // Error response payload\n Payload() ErrorPayload\n}\n\nfunc Errorf(code ErrorCode, msgf string, args ...interface{}) Error {\n msg := fmt.Sprintf(msgf, args...)\n if IsValidErrorCode(code) {\n return &rpcErr{code: code, msg: msg}\n }\n return &rpcErr{code: ErrInternal, msg: \"invalid error type \" + string(code)}\n}\n\nfunc WrapError(code ErrorCode, cause error, format string, args ...interface{}) Error {\n msg := fmt.Sprintf(format, args...)\n if IsValidErrorCode(code) {\n return &rpcErr{code: code, msg: msg, cause: cause}\n }\n return &rpcErr{code: ErrInternal, msg: \"invalid error type \" + string(code), cause: cause}\n}\n\nfunc Failf(format string, args ...interface{}) Error {\n return Errorf(ErrFail, format, args...)\n}\n\nfunc WrapFailf(cause error, format string, args ...interface{}) Error {\n return WrapError(ErrFail, cause, format, args...)\n}\n\nfunc ErrorNotFound(format string, args ...interface{}) Error {\n return Errorf(ErrNotFound, format, args...)\n}\n\nfunc ErrorInvalidArgument(argument string, validationMsg string) Error {\n return Errorf(ErrInvalidArgument, argument+\" \"+validationMsg)\n}\n\nfunc ErrorRequiredArgument(argument string) Error {\n return ErrorInvalidArgument(argument, \"is required\")\n}\n\nfunc ErrorInternal(format string, args ...interface{}) Error {\n return Errorf(ErrInternal, format, args...)\n}\n\ntype ErrorCode string\n\nconst (\n // Unknown error. For example when handling errors raised by APIs that do not\n // return enough error information.\n ErrUnknown ErrorCode = \"unknown\"\n\n // Fail error. General failure error type.\n ErrFail ErrorCode = \"fail\"\n\n // Canceled indicates the operation was cancelled (typically by the caller).\n ErrCanceled ErrorCode = \"canceled\"\n\n // InvalidArgument indicates client specified an invalid argument. It\n // indicates arguments that are problematic regardless of the state of the\n // system (i.e. a malformed file name, required argument, number out of range,\n // etc.).\n ErrInvalidArgument ErrorCode = \"invalid argument\"\n\n // DeadlineExceeded means operation expired before completion. For operations\n // that change the state of the system, this error may be returned even if the\n // operation has completed successfully (timeout).\n ErrDeadlineExceeded ErrorCode = \"deadline exceeded\"\n\n // NotFound means some requested entity was not found.\n ErrNotFound ErrorCode = \"not found\"\n\n // BadRoute means that the requested URL path wasn't routable to a webrpc\n // service and method. This is returned by the generated server, and usually\n // shouldn't be returned by applications. Instead, applications should use\n // NotFound or Unimplemented.\n ErrBadRoute ErrorCode = \"bad route\"\n\n // AlreadyExists means an attempt to create an entity failed because one\n // already exists.\n ErrAlreadyExists ErrorCode = \"already exists\"\n\n // PermissionDenied indicates the caller does not have permission to execute\n // the specified operation. It must not be used if the caller cannot be\n // identified (Unauthenticated).\n ErrPermissionDenied ErrorCode = \"permission denied\"\n\n // Unauthenticated indicates the request does not have valid authentication\n // credentials for the operation.\n ErrUnauthenticated ErrorCode = \"unauthenticated\"\n\n // ResourceExhausted indicates some resource has been exhausted, perhaps a\n // per-user quota, or perhaps the entire file system is out of space.\n ErrResourceExhausted ErrorCode = \"resource exhausted\"\n\n // FailedPrecondition indicates operation was rejected because the system is\n // not in a state required for the operation's execution. For example, doing\n // an rmdir operation on a directory that is non-empty, or on a non-directory\n // object, or when having conflicting read-modify-write on the same resource.\n ErrFailedPrecondition ErrorCode = \"failed precondition\"\n\n // Aborted indicates the operation was aborted, typically due to a concurrency\n // issue like sequencer check failures, transaction aborts, etc.\n ErrAborted ErrorCode = \"aborted\"\n\n // OutOfRange means operation was attempted past the valid range. For example,\n // seeking or reading past end of a paginated collection.\n //\n // Unlike InvalidArgument, this error indicates a problem that may be fixed if\n // the system state changes (i.e. adding more items to the collection).\n //\n // There is a fair bit of overlap between FailedPrecondition and OutOfRange.\n // We recommend using OutOfRange (the more specific error) when it applies so\n // that callers who are iterating through a space can easily look for an\n // OutOfRange error to detect when they are done.\n ErrOutOfRange ErrorCode = \"out of range\"\n\n // Unimplemented indicates operation is not implemented or not\n // supported/enabled in this service.\n ErrUnimplemented ErrorCode = \"unimplemented\"\n\n // Internal errors. When some invariants expected by the underlying system\n // have been broken. In other words, something bad happened in the library or\n // backend service. Do not confuse with HTTP Internal Server Error; an\n // Internal error could also happen on the client code, i.e. when parsing a\n // server response.\n ErrInternal ErrorCode = \"internal\"\n\n // Unavailable indicates the service is currently unavailable. This is a most\n // likely a transient condition and may be corrected by retrying with a\n // backoff.\n ErrUnavailable ErrorCode = \"unavailable\"\n\n // DataLoss indicates unrecoverable data loss or corruption.\n ErrDataLoss ErrorCode = \"data loss\"\n\n // ErrNone is the zero-value, is considered an empty error and should not be\n // used.\n ErrNone ErrorCode = \"\"\n)\n\nfunc HTTPStatusFromErrorCode(code ErrorCode) int {\n switch code {\n case ErrCanceled:\n return 408 // RequestTimeout\n case ErrUnknown:\n return 400 // Bad Request\n case ErrFail:\n return 422 // Unprocessable Entity\n case ErrInvalidArgument:\n return 400 // BadRequest\n case ErrDeadlineExceeded:\n return 408 // RequestTimeout\n case ErrNotFound:\n return 404 // Not Found\n case ErrBadRoute:\n return 404 // Not Found\n case ErrAlreadyExists:\n return 409 // Conflict\n case ErrPermissionDenied:\n return 403 // Forbidden\n case ErrUnauthenticated:\n return 401 // Unauthorized\n case ErrResourceExhausted:\n return 403 // Forbidden\n case ErrFailedPrecondition:\n return 412 // Precondition Failed\n case ErrAborted:\n return 409 // Conflict\n case ErrOutOfRange:\n return 400 // Bad Request\n case ErrUnimplemented:\n return 501 // Not Implemented\n case ErrInternal:\n return 500 // Internal Server Error\n case ErrUnavailable:\n return 503 // Service Unavailable\n case ErrDataLoss:\n return 500 // Internal Server Error\n case ErrNone:\n return 200 // OK\n default:\n return 0 // Invalid!\n }\n}\n\nfunc IsErrorCode(err error, code ErrorCode) bool {\n if rpcErr, ok := err.(Error); ok {\n if rpcErr.Code() == code {\n return true\n }\n }\n return false\n}\n\nfunc IsValidErrorCode(code ErrorCode) bool {\n return HTTPStatusFromErrorCode(code) != 0\n}\n\ntype rpcErr struct {\n code ErrorCode\n msg string\n cause error\n}\n\nfunc (e *rpcErr) Code() ErrorCode {\n return e.code\n}\n\nfunc (e *rpcErr) Msg() string {\n return e.msg\n}\n\nfunc (e *rpcErr) Cause() error {\n return e.cause\n}\n\nfunc (e *rpcErr) Error() string {\n if e.cause != nil && e.cause.Error() != \"\" {\n if e.msg != \"\" {\n return fmt.Sprintf(\"webrpc %s error: %s -- %s\", e.code, e.cause.Error(), e.msg)\n } else {\n return fmt.Sprintf(\"webrpc %s error: %s\", e.code, e.cause.Error())\n }\n } else {\n return fmt.Sprintf(\"webrpc %s error: %s\", e.code, e.msg)\n }\n}\n\nfunc (e *rpcErr) Payload() ErrorPayload {\n statusCode := HTTPStatusFromErrorCode(e.Code())\n errPayload := ErrorPayload{\n Status: statusCode,\n Code: string(e.Code()),\n Msg: e.Msg(),\n Error: e.Error(),\n }\n if e.Cause() != nil {\n errPayload.Cause = e.Cause().Error()\n }\n return errPayload\n}\n\ntype contextKey struct {\n name string\n}\n\nfunc (k *contextKey) String() string {\n return \"webrpc context value \" + k.name\n}\n\nvar (\n // For Client\n HTTPClientRequestHeadersCtxKey = &contextKey{\"HTTPClientRequestHeaders\"}\n\n // For Server\n HTTPResponseWriterCtxKey = &contextKey{\"HTTPResponseWriter\"}\n\n HTTPRequestCtxKey = &contextKey{\"HTTPRequest\"}\n\n ServiceNameCtxKey = &contextKey{\"ServiceName\"}\n\n MethodNameCtxKey = &contextKey{\"MethodName\"}\n)\n\n{{end}}\nPK\x07\x08\x83\xea\x053\xde!\x00\x00\xde!\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\xb1\x83\x08O\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11\x00 \x00proto.gen.go.tmplUT\x05\x00\x01\xeeML]{{- define \"proto\" -}}\n// {{.Name}} {{.SchemaVersion}} {{.SchemaHash}}\n// --\n// This file has been generated by https://github.com/webrpc/webrpc using gen/golang\n// Do not edit by hand. Update your webrpc schema and re-generate.\npackage {{.TargetOpts.PkgName}}\n\nimport (\n \"context\"\n \"encoding/json\"\n \"fmt\"\n \"io/ioutil\"\n \"net/http\"\n \"time\"\n \"strings\"\n \"bytes\"\n \"errors\"\n \"io\"\n \"net/url\"\n)\n\n// WebRPC description and code-gen version\nfunc WebRPCVersion() string {\n return \"{{.WebRPCVersion}}\"\n}\n\n// Schema version of your RIDL schema\nfunc WebRPCSchemaVersion() string {\n return \"{{.SchemaVersion}}\"\n}\n\n// Schema hash generated from your RIDL schema\nfunc WebRPCSchemaHash() string {\n return \"{{.SchemaHash}}\"\n}\n\n{{template \"types\" .}}\n\n{{if .TargetOpts.Server}}\n {{template \"server\" .}}\n{{end}}\n\n{{if .TargetOpts.Client}}\n {{template \"client\" .}}\n{{end}}\n\n{{template \"helpers\" .}}\n\n{{- end}}\nPK\x07\x08g4\x9a/\x89\x03\x00\x00\x89\x03\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\xb5\x81\x08O\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00 \x00server.go.tmplUT\x05\x00\x016JL]{{define \"server\"}}\n{{if .Services}}\n//\n// Server\n//\n\ntype WebRPCServer interface {\n http.Handler\n}\n\n{{- range .Services}}\n {{$name := .Name}}\n {{$serviceName := .Name | serverServiceName}}\n\n type {{$serviceName}} struct {\n {{.Name}}\n }\n\n func {{ .Name | newServerServiceName }}(svc {{.Name}}) WebRPCServer {\n return &{{$serviceName}}{\n {{.Name}}: svc,\n }\n }\n\n func (s *{{$serviceName}}) ServeHTTP(w http.ResponseWriter, r *http.Request) {\n ctx := r.Context()\n ctx = context.WithValue(ctx, HTTPResponseWriterCtxKey, w)\n ctx = context.WithValue(ctx, HTTPRequestCtxKey, r)\n ctx = context.WithValue(ctx, ServiceNameCtxKey, \"{{.Name}}\")\n\n if r.Method != \"POST\" {\n err := Errorf(ErrBadRoute, \"unsupported method %q (only POST is allowed)\", r.Method)\n RespondWithError(w, err)\n return\n }\n\n switch r.URL.Path {\n {{- range .Methods}}\n case \"/rpc/{{$name}}/{{.Name}}\":\n s.{{.Name | serviceMethodName}}(ctx, w, r)\n return\n {{- end}}\n default:\n err := Errorf(ErrBadRoute, \"no handler for path %q\", r.URL.Path)\n RespondWithError(w, err)\n return\n }\n }\n\n {{range .Methods}}\n func (s *{{$serviceName}}) {{.Name | serviceMethodName}}(ctx context.Context, w http.ResponseWriter, r *http.Request) {\n header := r.Header.Get(\"Content-Type\")\n i := strings.Index(header, \";\")\n if i == -1 {\n i = len(header)\n }\n\n switch strings.TrimSpace(strings.ToLower(header[:i])) {\n case \"application/json\":\n s.{{ .Name | serviceMethodJSONName }}(ctx, w, r)\n default:\n err := Errorf(ErrBadRoute, \"unexpected Content-Type: %q\", r.Header.Get(\"Content-Type\"))\n RespondWithError(w, err)\n }\n }\n\n func (s *{{$serviceName}}) {{.Name | serviceMethodJSONName}}(ctx context.Context, w http.ResponseWriter, r *http.Request) {\n var err error\n ctx = context.WithValue(ctx, MethodNameCtxKey, \"{{.Name}}\")\n\n {{- if .Inputs|len}}\n reqContent := struct {\n {{- range $i, $input := .Inputs}}\n Arg{{$i}} {{. | methodArgType}} `json:\"{{$input.Name | downcaseName}}\"`\n {{- end}}\n }{}\n\n reqBody, err := ioutil.ReadAll(r.Body)\n if err != nil {\n err = WrapError(ErrInternal, err, \"failed to read request data\")\n RespondWithError(w, err)\n return\n }\n defer r.Body.Close()\n\n err = json.Unmarshal(reqBody, &reqContent)\n if err != nil {\n err = WrapError(ErrInvalidArgument, err, \"failed to unmarshal request data\")\n RespondWithError(w, err)\n return\n }\n {{- end}}\n\n // Call service method\n {{- range $i, $output := .Outputs}}\n var ret{{$i}} {{$output | methodArgType}}\n {{- end}}\n func() {\n defer func() {\n // In case of a panic, serve a 500 error and then panic.\n if rr := recover(); rr != nil {\n RespondWithError(w, ErrorInternal(\"internal service panic\"))\n panic(rr)\n }\n }()\n {{argsList .Outputs \"ret\"}}{{.Outputs | commaIfLen}} err = s.{{$name}}.{{.Name}}(ctx{{.Inputs | commaIfLen}}{{argsList .Inputs \"reqContent.Arg\"}})\n }()\n {{- if .Outputs | len}}\n respContent := struct {\n {{- range $i, $output := .Outputs}}\n Ret{{$i}} {{$output | methodArgType}} `json:\"{{$output.Name | downcaseName}}\"`\n {{- end}} \n }{ {{argsList .Outputs \"ret\"}} }\n {{- end}}\n\n if err != nil {\n RespondWithError(w, err)\n return\n }\n\n {{- if .Outputs | len}}\n respBody, err := json.Marshal(respContent)\n if err != nil {\n err = WrapError(ErrInternal, err, \"failed to marshal json response\")\n RespondWithError(w, err)\n return\n }\n {{- end}}\n\n w.Header().Set(\"Content-Type\", \"application/json\")\n w.WriteHeader(http.StatusOK)\n\n {{- if .Outputs | len}}\n w.Write(respBody)\n {{- end}}\n }\n {{end}}\n{{- end}}\n\nfunc RespondWithError(w http.ResponseWriter, err error) {\n rpcErr, ok := err.(Error)\n if !ok {\n rpcErr = WrapError(ErrInternal, err, \"webrpc error\")\n }\n\n statusCode := HTTPStatusFromErrorCode(rpcErr.Code())\n\n w.Header().Set(\"Content-Type\", \"application/json\")\n w.WriteHeader(statusCode)\n\n respBody, _ := json.Marshal(rpcErr.Payload())\n w.Write(respBody)\n}\n{{end}}\n{{end}}\nPK\x07\x08{\x8fd\xdd\xe8\x10\x00\x00\xe8\x10\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x92y\xecN\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0d\x00 \x00types.go.tmplUT\x05\x00\x01d\xa3(]{{define \"types\"}}\n\n{{if .Messages}}\n//\n// Types\n//\n\n{{range .Messages}}\n {{if .Type | isEnum}}\n {{$enumName := .Name}}\n {{$enumType := .EnumType}}\n type {{$enumName}} {{$enumType}}\n\n const (\n {{- range .Fields}}\n {{$enumName}}_{{.Name}} {{$enumName}} = {{.Value}}\n {{- end}}\n )\n\n var {{$enumName}}_name = map[{{$enumType}}]string {\n {{- range .Fields}}\n {{.Value}}: \"{{.Name}}\",\n {{- end}}\n }\n\n var {{$enumName}}_value = map[string]{{$enumType}} {\n {{- range .Fields}}\n \"{{.Name}}\": {{.Value}},\n {{- end}}\n }\n\n func (x {{$enumName}}) String() string {\n return {{$enumName}}_name[{{$enumType}}(x)]\n }\n\n func (x {{$enumName}}) MarshalJSON() ([]byte, error) {\n buf := bytes.NewBufferString(`\"`)\n buf.WriteString({{$enumName}}_name[{{$enumType}}(x)])\n buf.WriteString(`\"`)\n return buf.Bytes(), nil\n }\n\n func (x *{{$enumName}}) UnmarshalJSON(b []byte) error {\n var j string\n err := json.Unmarshal(b, &j)\n if err != nil {\n return err\n }\n *x = {{$enumName}}({{$enumName}}_value[j])\n return nil\n }\n {{end}}\n {{if .Type | isStruct }}\n type {{.Name}} struct {\n {{- range .Fields}}\n {{. | exportedField}} {{. | fieldOptional}}{{. | fieldTypeDef}} {{. | fieldTags}}\n {{- end}}\n }\n {{end}}\n{{end}}\n{{end}}\n{{if .Services}}\n {{range .Services}}\n type {{.Name}} interface {\n {{- range .Methods}}\n {{.Name}}({{.Inputs | methodInputs}}) ({{.Outputs | methodOutputs}})\n {{- end}}\n }\n {{end}}\n var WebRPCServices = map[string][]string{\n {{- range .Services}}\n \"{{.Name}}\": {\n {{- range .Methods}}\n \"{{.Name}}\",\n {{- end}}\n },\n {{- end}}\n }\n{{end}}\n\n{{end}}\nPK\x07\x08\xf8\xf7\x1e\xb7\xff\x06\x00\x00\xff\x06\x00\x00PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x92y\xecN\xc5\xc9w\xb8]\x16\x00\x00]\x16\x00\x00\x0e\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x00\x00\x00\x00client.go.tmplUT\x05\x00\x01d\xa3(]PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x8c\x9d,O\x83\xea\x053\xde!\x00\x00\xde!\x00\x00\x0f\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xa2\x16\x00\x00helpers.go.tmplUT\x05\x00\x01\x19\xa0z]PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\xb1\x83\x08Og4\x9a/\x89\x03\x00\x00\x89\x03\x00\x00\x11\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xc68\x00\x00proto.gen.go.tmplUT\x05\x00\x01\xeeML]PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\xb5\x81\x08O{\x8fd\xdd\xe8\x10\x00\x00\xe8\x10\x00\x00\x0e\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x97<\x00\x00server.go.tmplUT\x05\x00\x016JL]PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x92y\xecN\xf8\xf7\x1e\xb7\xff\x06\x00\x00\xff\x06\x00\x00\x0d\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xc4M\x00\x00types.go.tmplUT\x05\x00\x01d\xa3(]PK\x05\x06\x00\x00\x00\x00\x05\x00\x05\x00\\\x01\x00\x00\x07U\x00\x00\x00\x00" diff --git a/gen/golang/funcmap.go b/gen/golang/funcmap.go deleted file mode 100644 index d49fef12..00000000 --- a/gen/golang/funcmap.go +++ /dev/null @@ -1,341 +0,0 @@ -package golang - -import ( - "errors" - "fmt" - "sort" - "strconv" - "strings" - - "github.com/webrpc/webrpc/schema" -) - -var fieldTypeMap = map[schema.DataType]string{ - schema.T_Uint: "uint", - schema.T_Uint8: "uint8", - schema.T_Uint16: "uint16", - schema.T_Uint32: "uint32", - schema.T_Uint64: "uint64", - schema.T_Int: "int", - schema.T_Int8: "int8", - schema.T_Int16: "int16", - schema.T_Int32: "int32", - schema.T_Int64: "int64", - schema.T_Float32: "float32", - schema.T_Float64: "float64", - schema.T_String: "string", - schema.T_Timestamp: "time.Time", - schema.T_Null: "struct{}", - schema.T_Any: "interface{}", - schema.T_Byte: "byte", - schema.T_Bool: "bool", -} - -func serviceMethodName(in schema.VarName) (string, error) { - s := string(in) - return "serve" + strings.ToUpper(s[0:1]) + s[1:], nil -} - -func serviceMethodJSONName(in schema.VarName) (string, error) { - s := string(in) - return "serve" + strings.ToUpper(s[0:1]) + s[1:] + "JSON", nil -} - -func newServerServiceName(in schema.VarName) (string, error) { - return "New" + string(in) + "Server", nil -} - -func newClientServiceName(in schema.VarName) (string, error) { - return "New" + string(in) + "Client", nil -} - -func fieldType(in *schema.VarType) (string, error) { - switch in.Type { - case schema.T_Map: - typK, ok := fieldTypeMap[in.Map.Key] - if !ok { - return "", fmt.Errorf("unknown type mapping %v", in.Map.Key) - } - typV, err := fieldType(in.Map.Value) - if err != nil { - return "", err - } - return fmt.Sprintf("map[%v]%s", typK, typV), nil - - case schema.T_List: - z, err := fieldType(in.List.Elem) - if err != nil { - return "", err - } - return "[]" + z, nil - - case schema.T_Struct: - return "*" + in.Struct.Name, nil - - default: - if fieldTypeMap[in.Type] != "" { - return fieldTypeMap[in.Type], nil - } - } - return "", fmt.Errorf("could not represent type: %#v", in) -} - -func fieldOptional(field *schema.MessageField) (string, error) { - if !field.Optional { - return "", nil - } - switch field.Type.Type { - case schema.T_Map: - return "", nil // noop - case schema.T_List: - return "", nil // noop - case schema.T_Struct: - return "", nil // noop because by default struct uses '*' prefix - default: - if fieldTypeMap[field.Type.Type] != "" { - return "*", nil - } - } - return "", fmt.Errorf("could not represent type: %#v", field) -} - -func fieldTypeDef(in *schema.MessageField) (string, error) { - goFieldType := "" - - meta := in.Meta - for kk := range meta { - for k, v := range meta[kk] { - if k == "go.field.type" { - goFieldType = fmt.Sprintf("%v", v) - } - } - } - - if goFieldType != "" { - return goFieldType, nil - } - - return fieldType(in.Type) -} - -func fieldTags(in *schema.MessageField) (string, error) { - fieldTags := map[string]interface{}{} - - jsonFieldName, err := downcaseName(in.Name) - if err != nil { - return "", err - } - fieldTags["json"] = fmt.Sprintf("%s", jsonFieldName) - - goTagJSON := "" - - meta := in.Meta - for kk := range meta { - for k, v := range meta[kk] { - - switch { - case k == "json": - if goTagJSON == "" { - fieldTags["json"] = fmt.Sprintf("%v", v) - } - - case strings.HasPrefix(k, "go.tag.json"): - goTagJSON = fmt.Sprintf("%v", v) - if !strings.HasPrefix(goTagJSON, fmt.Sprintf("%v", fieldTags["json"])) { - return "", errors.New("go.tag.json is invalid, it must match the json fieldname") - } - fieldTags[k[7:]] = fmt.Sprintf("%v", v) - - case strings.HasPrefix(k, "go.tag."): - if k == "go.tag.json" { - goTagJSON = fmt.Sprintf("%v", v) - } - fieldTags[k[7:]] = fmt.Sprintf("%v", v) - } - - } - } - - tagKeys := []string{} - for k, _ := range fieldTags { - if k != "json" { - tagKeys = append(tagKeys, k) - } - } - sort.StringSlice(tagKeys).Sort() - tagKeys = append([]string{"json"}, tagKeys...) - - tags := []string{} - for _, k := range tagKeys { - tags = append(tags, fmt.Sprintf(`%s:"%v"`, k, fieldTags[k])) - } - - return "`" + strings.Join(tags, " ") + "`", nil -} - -func constPathPrefix(in schema.VarName) (string, error) { - return string(in) + "PathPrefix", nil -} - -func countMethods(in []*schema.Method) (string, error) { - return strconv.Itoa(len(in)), nil -} - -func clientServiceName(in schema.VarName) (string, error) { - s := string(in) - return strings.ToLower(s[0:1]) + s[1:] + "Client", nil -} - -func serverServiceName(in schema.VarName) (string, error) { - s := string(in) - return strings.ToLower(s[0:1]) + s[1:] + "Server", nil -} - -func methodArgName(in *schema.MethodArgument) string { - name := string(in.Name) - if name == "" && in.Type != nil { - name = in.Type.String() - } - if name != "" { - return name - } - return "" -} - -func methodArgType(in *schema.MethodArgument) string { - z, err := fieldType(in.Type) - if err != nil { - panic(err.Error()) - } - - var prefix string - typ := in.Type.Type - - if in.Optional { - prefix = "*" - } - if typ == schema.T_Struct { - prefix = "" // noop, as already pointer applied elsewhere - } - if typ == schema.T_List || typ == schema.T_Map { - prefix = "" - } - - return prefix + z -} - -func methodInputs(in []*schema.MethodArgument) (string, error) { - inputs := []string{"ctx context.Context"} - for i := range in { - inputs = append(inputs, fmt.Sprintf("%s %s", methodArgName(in[i]), methodArgType(in[i]))) - } - return strings.Join(inputs, ", "), nil -} - -func methodOutputs(in []*schema.MethodArgument) (string, error) { - outputs := []string{} - for i := range in { - outputs = append(outputs, methodArgType(in[i])) - } - outputs = append(outputs, "error") - return strings.Join(outputs, ", "), nil -} - -func methodArgNames(in []*schema.MethodArgument) (string, error) { - inputs := []string{} - for i := range in { - inputs = append(inputs, fmt.Sprintf("%s", methodArgName(in[i]))) - } - return strings.Join(inputs, ", "), nil -} - -func argsList(in []*schema.MethodArgument, prefix string) (string, error) { - ins := []string{} - for i := range in { - ins = append(ins, fmt.Sprintf("%s%d", prefix, i)) - } - return strings.Join(ins, ", "), nil -} - -func commaIfLen(in []*schema.MethodArgument) string { - if len(in) > 0 { - return "," - } - return "" -} - -func isStruct(t schema.MessageType) bool { - return t == "struct" -} - -func exportedField(in *schema.MessageField) (string, error) { - s := string(in.Name) - s = strings.ToUpper(s[0:1]) + s[1:] - - nameTag := "go.field.name" - for k := range in.Meta { - for k, v := range in.Meta[k] { - if k == nameTag { - s = fmt.Sprintf("%v", v) - } - } - } - - return s, nil -} - -func downcaseName(v interface{}) (string, error) { - downFn := func(s string) string { - if s == "" { - return "" - } - return strings.ToLower(s[0:1]) + s[1:] - } - switch t := v.(type) { - case schema.VarName: - return downFn(string(t)), nil - case string: - return downFn(t), nil - default: - return "", errors.New("downcaseFieldName, unknown arg type") - } -} - -func isEnum(t schema.MessageType) bool { - return t == "enum" -} - -func hasFieldType(proto *schema.WebRPCSchema) func(fieldType string) (bool, error) { - return func(fieldType string) (bool, error) { - return proto.HasFieldType(fieldType) - } -} - -func templateFuncMap(proto *schema.WebRPCSchema) map[string]interface{} { - return map[string]interface{}{ - "serviceMethodName": serviceMethodName, - "serviceMethodJSONName": serviceMethodJSONName, - "hasFieldType": hasFieldType(proto), - "fieldTags": fieldTags, - "fieldType": fieldType, - "fieldOptional": fieldOptional, - "fieldTypeDef": fieldTypeDef, - "newClientServiceName": newClientServiceName, - "newServerServiceName": newServerServiceName, - "constPathPrefix": constPathPrefix, - "countMethods": countMethods, - "clientServiceName": clientServiceName, - "serverServiceName": serverServiceName, - "methodInputs": methodInputs, - "methodOutputs": methodOutputs, - "methodArgName": methodArgName, - "methodArgType": methodArgType, - "methodArgNames": methodArgNames, - "argsList": argsList, - "commaIfLen": commaIfLen, - "isStruct": isStruct, - "isEnum": isEnum, - "exportedField": exportedField, - "downcaseName": downcaseName, - } -} diff --git a/gen/golang/gen.go b/gen/golang/gen.go deleted file mode 100644 index 77ddde6c..00000000 --- a/gen/golang/gen.go +++ /dev/null @@ -1,103 +0,0 @@ -//go:generate statik -src=./templates -dest=. -f -Z -p=embed -package golang - -import ( - "bytes" - "io/ioutil" - "os" - "text/template" - - "github.com/goware/statik/fs" - "github.com/pkg/errors" - "github.com/webrpc/webrpc/gen" - "github.com/webrpc/webrpc/gen/golang/embed" - "github.com/webrpc/webrpc/schema" -) - -func init() { - gen.Register("go", &generator{}) -} - -type generator struct{} - -func (g *generator) Gen(proto *schema.WebRPCSchema, opts gen.TargetOptions) (string, error) { - // Get templates from `embed` asset package - // NOTE: make sure to `go generate` whenever you change the files in `templates/` folder - templates, err := getTemplates() - if err != nil { - return "", err - } - - // TODO: we can move a bunch of this code to the core gen package at githb.com/webrpc/webrpc/gen - // .. then typescript gen, and others can use it too.. - - // Load templates - tmpl := template. - New("webrpc-gen-go"). - Funcs(templateFuncMap(proto)) - - for _, tmplData := range templates { - _, err = tmpl.Parse(tmplData) - if err != nil { - return "", err - } - } - - // generate deterministic schema hash of the proto file - schemaHash, err := proto.SchemaHash() - if err != nil { - return "", err - } - - // template vars - vars := struct { - *schema.WebRPCSchema - SchemaHash string - TargetOpts gen.TargetOptions - }{ - proto, schemaHash, opts, - } - - // generate the template - genBuf := bytes.NewBuffer(nil) - err = tmpl.ExecuteTemplate(genBuf, "proto", vars) - if err != nil { - return "", err - } - - // return string(genBuf.Bytes()), nil - - src, err := FormatSource(genBuf.Bytes()) - if err != nil { - return "", errors.Errorf("gofmt is failing to format the Go code because: %v", err) - } - - return string(src), nil -} - -func getTemplates() (map[string]string, error) { - data := map[string]string{} - - statikFS, err := fs.New(embed.Asset) - if err != nil { - return nil, err - } - - fs.Walk(statikFS, "/", func(path string, info os.FileInfo, err error) error { - if path == "/" { - return nil - } - f, err := statikFS.Open(path) - if err != nil { - return err - } - buf, err := ioutil.ReadAll(f) - if err != nil { - return err - } - data[path] = string(buf) - return nil - }) - - return data, nil -} diff --git a/gen/golang/gen_test.go b/gen/golang/gen_test.go deleted file mode 100644 index 6725e386..00000000 --- a/gen/golang/gen_test.go +++ /dev/null @@ -1,183 +0,0 @@ -package golang - -import ( - "log" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/webrpc/webrpc/gen" - "github.com/webrpc/webrpc/schema" -) - -const input = ` -{ - "webrpc": "v1", - "name": "example", - "version":" v0.0.1", - "messages": [ - { - "name": "Kind", - "type": "enum", - "fields": [ - { - "name": "USER", - "type": "uint32", - "value": "1" - }, - { - "name": "ADMIN", - "type": "uint32", - "value": "2" - } - ] - }, - { - "name": "Empty", - "type": "struct", - "fields": [ - ] - }, - { - "name": "GetUserRequest", - "type": "struct", - "fields": [ - { - "name": "userID", - "type": "uint64", - "optional": false - } - ] - }, - { - "name": "User", - "type": "struct", - "fields": [ - { - "name": "ID", - "type": "uint64", - "optional": false, - "meta": [ - { "json": "id" }, - { "go.tag.db": "id" } - ] - }, - { - "name": "username", - "type": "string", - "optional": false, - "meta": [ - { "json": "USERNAME" }, - { "go.tag.db": "username" } - ] - }, - { - "name": "createdAt", - "type": "timestamp", - "optional": true, - "meta": [ - { "go.tag.json": "createdAt,omitempty" }, - { "go.tag.db": "created_at" } - ] - } - - ] - }, - { - "name": "RandomStuff", - "type": "struct", - "fields": [ - { - "name": "meta", - "type": "map" - }, - { - "name": "metaNestedExample", - "type": "map>" - }, - { - "name": "namesList", - "type": "[]string" - }, - { - "name": "numsList", - "type": "[]int64" - }, - { - "name": "doubleArray", - "type": "[][]string" - }, - { - "name": "listOfMaps", - "type": "[]map" - }, - { - "name": "listOfUsers", - "type": "[]User" - }, - { - "name": "mapOfUsers", - "type": "map" - }, - { - "name": "user", - "type": "User" - } - ] - } - ], - "services": [ - { - "name": "ExampleService", - "methods": [ - { - "name": "Ping", - "inputs": [], - "outputs": [ - { - "name": "status", - "type": "bool" - } - ] - }, - { - "name": "GetUser", - "name": "GetUser", - "inputs": [ - { - "name": "header", - "type": "map" - }, - { - "name": "req", - "type": "GetUserRequest" - } - ], - "outputs": [ - { - "name": "code", - "type": "uint32" - }, - { - "name": "user", - "type": "User" - } - ] - } - ] - } - ] -} -` - -func TestGenTypescript(t *testing.T) { - g := &generator{} - - s, err := schema.ParseSchemaJSON([]byte(input)) - assert.NoError(t, err) - - o, err := g.Gen(s, gen.TargetOptions{PkgName: "test", Client: true, Server: true}) - assert.NoError(t, err) - _ = o - - log.Printf("o: %v", o) -} diff --git a/gen/golang/helpers.go b/gen/golang/helpers.go deleted file mode 100644 index 252f9221..00000000 --- a/gen/golang/helpers.go +++ /dev/null @@ -1,12 +0,0 @@ -package golang - -import ( - "golang.org/x/tools/imports" -) - -// FormatSource is gofmt with addition of removing any unused imports. -func FormatSource(source []byte) ([]byte, error) { - return imports.Process("", source, &imports.Options{ - AllErrors: true, Comments: true, TabIndent: true, TabWidth: 8, - }) -} diff --git a/gen/golang/templates/client.go.tmpl b/gen/golang/templates/client.go.tmpl deleted file mode 100644 index cf27193f..00000000 --- a/gen/golang/templates/client.go.tmpl +++ /dev/null @@ -1,205 +0,0 @@ -{{define "client"}} -{{if .Services}} -// -// Client -// - -{{range .Services}} -const {{.Name | constPathPrefix}} = "/rpc/{{.Name}}/" -{{end}} - -{{range .Services}} - {{ $serviceName := .Name | clientServiceName}} - type {{$serviceName}} struct { - client HTTPClient - urls [{{.Methods | countMethods}}]string - } - - func {{.Name | newClientServiceName }}(addr string, client HTTPClient) {{.Name}} { - prefix := urlBase(addr) + {{.Name | constPathPrefix}} - urls := [{{.Methods | countMethods}}]string{ - {{- range .Methods}} - prefix + "{{.Name}}", - {{- end}} - } - return &{{$serviceName}}{ - client: client, - urls: urls, - } - } - - {{range $i, $method := .Methods}} - func (c *{{$serviceName}}) {{.Name}}({{.Inputs | methodInputs}}) ({{.Outputs | methodOutputs }}) { - {{- $inputVar := "nil" -}} - {{- $outputVar := "nil" -}} - {{- if .Inputs | len}} - {{- $inputVar = "in"}} - in := struct { - {{- range $i, $input := .Inputs}} - Arg{{$i}} {{$input | methodArgType}} `json:"{{$input.Name | downcaseName}}"` - {{- end}} - }{ {{.Inputs | methodArgNames}} } - {{- end}} - {{- if .Outputs | len}} - {{- $outputVar = "&out"}} - out := struct { - {{- range $i, $output := .Outputs}} - Ret{{$i}} {{$output | methodArgType}} `json:"{{$output.Name | downcaseName}}"` - {{- end}} - }{} - {{- end}} - - err := doJSONRequest(ctx, c.client, c.urls[{{$i}}], {{$inputVar}}, {{$outputVar}}) - return {{argsList .Outputs "out.Ret"}}{{commaIfLen .Outputs}} err - } - {{end}} -{{end}} - -// HTTPClient is the interface used by generated clients to send HTTP requests. -// It is fulfilled by *(net/http).Client, which is sufficient for most users. -// Users can provide their own implementation for special retry policies. -type HTTPClient interface { - Do(req *http.Request) (*http.Response, error) -} - -// urlBase helps ensure that addr specifies a scheme. If it is unparsable -// as a URL, it returns addr unchanged. -func urlBase(addr string) string { - // If the addr specifies a scheme, use it. If not, default to - // http. If url.Parse fails on it, return it unchanged. - url, err := url.Parse(addr) - if err != nil { - return addr - } - if url.Scheme == "" { - url.Scheme = "http" - } - return url.String() -} - -// newRequest makes an http.Request from a client, adding common headers. -func newRequest(ctx context.Context, url string, reqBody io.Reader, contentType string) (*http.Request, error) { - req, err := http.NewRequest("POST", url, reqBody) - if err != nil { - return nil, err - } - req.Header.Set("Accept", contentType) - req.Header.Set("Content-Type", contentType) - if headers, ok := HTTPRequestHeaders(ctx); ok { - for k := range headers { - for _, v := range headers[k] { - req.Header.Add(k, v) - } - } - } - return req, nil -} - -// doJSONRequest is common code to make a request to the remote service. -func doJSONRequest(ctx context.Context, client HTTPClient, url string, in, out interface{}) error { - reqBody, err := json.Marshal(in) - if err != nil { - return clientError("failed to marshal json request", err) - } - if err = ctx.Err(); err != nil { - return clientError("aborted because context was done", err) - } - - req, err := newRequest(ctx, url, bytes.NewBuffer(reqBody), "application/json") - if err != nil { - return clientError("could not build request", err) - } - resp, err := client.Do(req) - if err != nil { - return clientError("request failed", err) - } - - defer func() { - cerr := resp.Body.Close() - if err == nil && cerr != nil { - err = clientError("failed to close response body", cerr) - } - }() - - if err = ctx.Err(); err != nil { - return clientError("aborted because context was done", err) - } - - if resp.StatusCode != 200 { - return errorFromResponse(resp) - } - - if out != nil { - respBody, err := ioutil.ReadAll(resp.Body) - if err != nil { - return clientError("failed to read response body", err) - } - - err = json.Unmarshal(respBody, &out) - if err != nil { - return clientError("failed to unmarshal json response body", err) - } - if err = ctx.Err(); err != nil { - return clientError("aborted because context was done", err) - } - } - - return nil -} - -// errorFromResponse builds a webrpc Error from a non-200 HTTP response. -func errorFromResponse(resp *http.Response) Error { - respBody, err := ioutil.ReadAll(resp.Body) - if err != nil { - return clientError("failed to read server error response body", err) - } - - var respErr ErrorPayload - if err := json.Unmarshal(respBody, &respErr); err != nil { - return clientError("failed unmarshal error response", err) - } - - errCode := ErrorCode(respErr.Code) - - if HTTPStatusFromErrorCode(errCode) == 0 { - return ErrorInternal("invalid code returned from server error response: %s", respErr.Code) - } - - return &rpcErr{ - code: errCode, - msg: respErr.Msg, - cause: errors.New(respErr.Cause), - } -} - -func clientError(desc string, err error) Error { - return WrapError(ErrInternal, err, desc) -} - -func WithHTTPRequestHeaders(ctx context.Context, h http.Header) (context.Context, error) { - if _, ok := h["Accept"]; ok { - return nil, errors.New("provided header cannot set Accept") - } - if _, ok := h["Content-Type"]; ok { - return nil, errors.New("provided header cannot set Content-Type") - } - - copied := make(http.Header, len(h)) - for k, vv := range h { - if vv == nil { - copied[k] = nil - continue - } - copied[k] = make([]string, len(vv)) - copy(copied[k], vv) - } - - return context.WithValue(ctx, HTTPClientRequestHeadersCtxKey, copied), nil -} - -func HTTPRequestHeaders(ctx context.Context) (http.Header, bool) { - h, ok := ctx.Value(HTTPClientRequestHeadersCtxKey).(http.Header) - return h, ok -} -{{end}} -{{end}} diff --git a/gen/golang/templates/helpers.go.tmpl b/gen/golang/templates/helpers.go.tmpl deleted file mode 100644 index 1831d51e..00000000 --- a/gen/golang/templates/helpers.go.tmpl +++ /dev/null @@ -1,292 +0,0 @@ -{{define "helpers"}} - -// -// Helpers -// - -type ErrorPayload struct { - Status int `json:"status"` - Code string `json:"code"` - Cause string `json:"cause,omitempty"` - Msg string `json:"msg"` - Error string `json:"error"` -} - -type Error interface { - // Code is of the valid error codes - Code() ErrorCode - - // Msg returns a human-readable, unstructured messages describing the error - Msg() string - - // Cause is reason for the error - Cause() error - - // Error returns a string of the form "webrpc error : " - Error() string - - // Error response payload - Payload() ErrorPayload -} - -func Errorf(code ErrorCode, msgf string, args ...interface{}) Error { - msg := fmt.Sprintf(msgf, args...) - if IsValidErrorCode(code) { - return &rpcErr{code: code, msg: msg} - } - return &rpcErr{code: ErrInternal, msg: "invalid error type " + string(code)} -} - -func WrapError(code ErrorCode, cause error, format string, args ...interface{}) Error { - msg := fmt.Sprintf(format, args...) - if IsValidErrorCode(code) { - return &rpcErr{code: code, msg: msg, cause: cause} - } - return &rpcErr{code: ErrInternal, msg: "invalid error type " + string(code), cause: cause} -} - -func Failf(format string, args ...interface{}) Error { - return Errorf(ErrFail, format, args...) -} - -func WrapFailf(cause error, format string, args ...interface{}) Error { - return WrapError(ErrFail, cause, format, args...) -} - -func ErrorNotFound(format string, args ...interface{}) Error { - return Errorf(ErrNotFound, format, args...) -} - -func ErrorInvalidArgument(argument string, validationMsg string) Error { - return Errorf(ErrInvalidArgument, argument+" "+validationMsg) -} - -func ErrorRequiredArgument(argument string) Error { - return ErrorInvalidArgument(argument, "is required") -} - -func ErrorInternal(format string, args ...interface{}) Error { - return Errorf(ErrInternal, format, args...) -} - -type ErrorCode string - -const ( - // Unknown error. For example when handling errors raised by APIs that do not - // return enough error information. - ErrUnknown ErrorCode = "unknown" - - // Fail error. General failure error type. - ErrFail ErrorCode = "fail" - - // Canceled indicates the operation was cancelled (typically by the caller). - ErrCanceled ErrorCode = "canceled" - - // InvalidArgument indicates client specified an invalid argument. It - // indicates arguments that are problematic regardless of the state of the - // system (i.e. a malformed file name, required argument, number out of range, - // etc.). - ErrInvalidArgument ErrorCode = "invalid argument" - - // DeadlineExceeded means operation expired before completion. For operations - // that change the state of the system, this error may be returned even if the - // operation has completed successfully (timeout). - ErrDeadlineExceeded ErrorCode = "deadline exceeded" - - // NotFound means some requested entity was not found. - ErrNotFound ErrorCode = "not found" - - // BadRoute means that the requested URL path wasn't routable to a webrpc - // service and method. This is returned by the generated server, and usually - // shouldn't be returned by applications. Instead, applications should use - // NotFound or Unimplemented. - ErrBadRoute ErrorCode = "bad route" - - // AlreadyExists means an attempt to create an entity failed because one - // already exists. - ErrAlreadyExists ErrorCode = "already exists" - - // PermissionDenied indicates the caller does not have permission to execute - // the specified operation. It must not be used if the caller cannot be - // identified (Unauthenticated). - ErrPermissionDenied ErrorCode = "permission denied" - - // Unauthenticated indicates the request does not have valid authentication - // credentials for the operation. - ErrUnauthenticated ErrorCode = "unauthenticated" - - // ResourceExhausted indicates some resource has been exhausted, perhaps a - // per-user quota, or perhaps the entire file system is out of space. - ErrResourceExhausted ErrorCode = "resource exhausted" - - // FailedPrecondition indicates operation was rejected because the system is - // not in a state required for the operation's execution. For example, doing - // an rmdir operation on a directory that is non-empty, or on a non-directory - // object, or when having conflicting read-modify-write on the same resource. - ErrFailedPrecondition ErrorCode = "failed precondition" - - // Aborted indicates the operation was aborted, typically due to a concurrency - // issue like sequencer check failures, transaction aborts, etc. - ErrAborted ErrorCode = "aborted" - - // OutOfRange means operation was attempted past the valid range. For example, - // seeking or reading past end of a paginated collection. - // - // Unlike InvalidArgument, this error indicates a problem that may be fixed if - // the system state changes (i.e. adding more items to the collection). - // - // There is a fair bit of overlap between FailedPrecondition and OutOfRange. - // We recommend using OutOfRange (the more specific error) when it applies so - // that callers who are iterating through a space can easily look for an - // OutOfRange error to detect when they are done. - ErrOutOfRange ErrorCode = "out of range" - - // Unimplemented indicates operation is not implemented or not - // supported/enabled in this service. - ErrUnimplemented ErrorCode = "unimplemented" - - // Internal errors. When some invariants expected by the underlying system - // have been broken. In other words, something bad happened in the library or - // backend service. Do not confuse with HTTP Internal Server Error; an - // Internal error could also happen on the client code, i.e. when parsing a - // server response. - ErrInternal ErrorCode = "internal" - - // Unavailable indicates the service is currently unavailable. This is a most - // likely a transient condition and may be corrected by retrying with a - // backoff. - ErrUnavailable ErrorCode = "unavailable" - - // DataLoss indicates unrecoverable data loss or corruption. - ErrDataLoss ErrorCode = "data loss" - - // ErrNone is the zero-value, is considered an empty error and should not be - // used. - ErrNone ErrorCode = "" -) - -func HTTPStatusFromErrorCode(code ErrorCode) int { - switch code { - case ErrCanceled: - return 408 // RequestTimeout - case ErrUnknown: - return 400 // Bad Request - case ErrFail: - return 422 // Unprocessable Entity - case ErrInvalidArgument: - return 400 // BadRequest - case ErrDeadlineExceeded: - return 408 // RequestTimeout - case ErrNotFound: - return 404 // Not Found - case ErrBadRoute: - return 404 // Not Found - case ErrAlreadyExists: - return 409 // Conflict - case ErrPermissionDenied: - return 403 // Forbidden - case ErrUnauthenticated: - return 401 // Unauthorized - case ErrResourceExhausted: - return 403 // Forbidden - case ErrFailedPrecondition: - return 412 // Precondition Failed - case ErrAborted: - return 409 // Conflict - case ErrOutOfRange: - return 400 // Bad Request - case ErrUnimplemented: - return 501 // Not Implemented - case ErrInternal: - return 500 // Internal Server Error - case ErrUnavailable: - return 503 // Service Unavailable - case ErrDataLoss: - return 500 // Internal Server Error - case ErrNone: - return 200 // OK - default: - return 0 // Invalid! - } -} - -func IsErrorCode(err error, code ErrorCode) bool { - if rpcErr, ok := err.(Error); ok { - if rpcErr.Code() == code { - return true - } - } - return false -} - -func IsValidErrorCode(code ErrorCode) bool { - return HTTPStatusFromErrorCode(code) != 0 -} - -type rpcErr struct { - code ErrorCode - msg string - cause error -} - -func (e *rpcErr) Code() ErrorCode { - return e.code -} - -func (e *rpcErr) Msg() string { - return e.msg -} - -func (e *rpcErr) Cause() error { - return e.cause -} - -func (e *rpcErr) Error() string { - if e.cause != nil && e.cause.Error() != "" { - if e.msg != "" { - return fmt.Sprintf("webrpc %s error: %s -- %s", e.code, e.cause.Error(), e.msg) - } else { - return fmt.Sprintf("webrpc %s error: %s", e.code, e.cause.Error()) - } - } else { - return fmt.Sprintf("webrpc %s error: %s", e.code, e.msg) - } -} - -func (e *rpcErr) Payload() ErrorPayload { - statusCode := HTTPStatusFromErrorCode(e.Code()) - errPayload := ErrorPayload{ - Status: statusCode, - Code: string(e.Code()), - Msg: e.Msg(), - Error: e.Error(), - } - if e.Cause() != nil { - errPayload.Cause = e.Cause().Error() - } - return errPayload -} - -type contextKey struct { - name string -} - -func (k *contextKey) String() string { - return "webrpc context value " + k.name -} - -var ( - // For Client - HTTPClientRequestHeadersCtxKey = &contextKey{"HTTPClientRequestHeaders"} - - // For Server - HTTPResponseWriterCtxKey = &contextKey{"HTTPResponseWriter"} - - HTTPRequestCtxKey = &contextKey{"HTTPRequest"} - - ServiceNameCtxKey = &contextKey{"ServiceName"} - - MethodNameCtxKey = &contextKey{"MethodName"} -) - -{{end}} diff --git a/gen/golang/templates/proto.gen.go.tmpl b/gen/golang/templates/proto.gen.go.tmpl deleted file mode 100644 index 9dfd00a2..00000000 --- a/gen/golang/templates/proto.gen.go.tmpl +++ /dev/null @@ -1,49 +0,0 @@ -{{- define "proto" -}} -// {{.Name}} {{.SchemaVersion}} {{.SchemaHash}} -// -- -// This file has been generated by https://github.com/webrpc/webrpc using gen/golang -// Do not edit by hand. Update your webrpc schema and re-generate. -package {{.TargetOpts.PkgName}} - -import ( - "context" - "encoding/json" - "fmt" - "io/ioutil" - "net/http" - "time" - "strings" - "bytes" - "errors" - "io" - "net/url" -) - -// WebRPC description and code-gen version -func WebRPCVersion() string { - return "{{.WebRPCVersion}}" -} - -// Schema version of your RIDL schema -func WebRPCSchemaVersion() string { - return "{{.SchemaVersion}}" -} - -// Schema hash generated from your RIDL schema -func WebRPCSchemaHash() string { - return "{{.SchemaHash}}" -} - -{{template "types" .}} - -{{if .TargetOpts.Server}} - {{template "server" .}} -{{end}} - -{{if .TargetOpts.Client}} - {{template "client" .}} -{{end}} - -{{template "helpers" .}} - -{{- end}} diff --git a/gen/golang/templates/server.go.tmpl b/gen/golang/templates/server.go.tmpl deleted file mode 100644 index b6708a93..00000000 --- a/gen/golang/templates/server.go.tmpl +++ /dev/null @@ -1,155 +0,0 @@ -{{define "server"}} -{{if .Services}} -// -// Server -// - -type WebRPCServer interface { - http.Handler -} - -{{- range .Services}} - {{$name := .Name}} - {{$serviceName := .Name | serverServiceName}} - - type {{$serviceName}} struct { - {{.Name}} - } - - func {{ .Name | newServerServiceName }}(svc {{.Name}}) WebRPCServer { - return &{{$serviceName}}{ - {{.Name}}: svc, - } - } - - func (s *{{$serviceName}}) ServeHTTP(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - ctx = context.WithValue(ctx, HTTPResponseWriterCtxKey, w) - ctx = context.WithValue(ctx, HTTPRequestCtxKey, r) - ctx = context.WithValue(ctx, ServiceNameCtxKey, "{{.Name}}") - - if r.Method != "POST" { - err := Errorf(ErrBadRoute, "unsupported method %q (only POST is allowed)", r.Method) - RespondWithError(w, err) - return - } - - switch r.URL.Path { - {{- range .Methods}} - case "/rpc/{{$name}}/{{.Name}}": - s.{{.Name | serviceMethodName}}(ctx, w, r) - return - {{- end}} - default: - err := Errorf(ErrBadRoute, "no handler for path %q", r.URL.Path) - RespondWithError(w, err) - return - } - } - - {{range .Methods}} - func (s *{{$serviceName}}) {{.Name | serviceMethodName}}(ctx context.Context, w http.ResponseWriter, r *http.Request) { - header := r.Header.Get("Content-Type") - i := strings.Index(header, ";") - if i == -1 { - i = len(header) - } - - switch strings.TrimSpace(strings.ToLower(header[:i])) { - case "application/json": - s.{{ .Name | serviceMethodJSONName }}(ctx, w, r) - default: - err := Errorf(ErrBadRoute, "unexpected Content-Type: %q", r.Header.Get("Content-Type")) - RespondWithError(w, err) - } - } - - func (s *{{$serviceName}}) {{.Name | serviceMethodJSONName}}(ctx context.Context, w http.ResponseWriter, r *http.Request) { - var err error - ctx = context.WithValue(ctx, MethodNameCtxKey, "{{.Name}}") - - {{- if .Inputs|len}} - reqContent := struct { - {{- range $i, $input := .Inputs}} - Arg{{$i}} {{. | methodArgType}} `json:"{{$input.Name | downcaseName}}"` - {{- end}} - }{} - - reqBody, err := ioutil.ReadAll(r.Body) - if err != nil { - err = WrapError(ErrInternal, err, "failed to read request data") - RespondWithError(w, err) - return - } - defer r.Body.Close() - - err = json.Unmarshal(reqBody, &reqContent) - if err != nil { - err = WrapError(ErrInvalidArgument, err, "failed to unmarshal request data") - RespondWithError(w, err) - return - } - {{- end}} - - // Call service method - {{- range $i, $output := .Outputs}} - var ret{{$i}} {{$output | methodArgType}} - {{- end}} - func() { - defer func() { - // In case of a panic, serve a 500 error and then panic. - if rr := recover(); rr != nil { - RespondWithError(w, ErrorInternal("internal service panic")) - panic(rr) - } - }() - {{argsList .Outputs "ret"}}{{.Outputs | commaIfLen}} err = s.{{$name}}.{{.Name}}(ctx{{.Inputs | commaIfLen}}{{argsList .Inputs "reqContent.Arg"}}) - }() - {{- if .Outputs | len}} - respContent := struct { - {{- range $i, $output := .Outputs}} - Ret{{$i}} {{$output | methodArgType}} `json:"{{$output.Name | downcaseName}}"` - {{- end}} - }{ {{argsList .Outputs "ret"}} } - {{- end}} - - if err != nil { - RespondWithError(w, err) - return - } - - {{- if .Outputs | len}} - respBody, err := json.Marshal(respContent) - if err != nil { - err = WrapError(ErrInternal, err, "failed to marshal json response") - RespondWithError(w, err) - return - } - {{- end}} - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - - {{- if .Outputs | len}} - w.Write(respBody) - {{- end}} - } - {{end}} -{{- end}} - -func RespondWithError(w http.ResponseWriter, err error) { - rpcErr, ok := err.(Error) - if !ok { - rpcErr = WrapError(ErrInternal, err, "webrpc error") - } - - statusCode := HTTPStatusFromErrorCode(rpcErr.Code()) - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(statusCode) - - respBody, _ := json.Marshal(rpcErr.Payload()) - w.Write(respBody) -} -{{end}} -{{end}} diff --git a/gen/golang/templates/types.go.tmpl b/gen/golang/templates/types.go.tmpl deleted file mode 100644 index 3faed213..00000000 --- a/gen/golang/templates/types.go.tmpl +++ /dev/null @@ -1,81 +0,0 @@ -{{define "types"}} - -{{if .Messages}} -// -// Types -// - -{{range .Messages}} - {{if .Type | isEnum}} - {{$enumName := .Name}} - {{$enumType := .EnumType}} - type {{$enumName}} {{$enumType}} - - const ( - {{- range .Fields}} - {{$enumName}}_{{.Name}} {{$enumName}} = {{.Value}} - {{- end}} - ) - - var {{$enumName}}_name = map[{{$enumType}}]string { - {{- range .Fields}} - {{.Value}}: "{{.Name}}", - {{- end}} - } - - var {{$enumName}}_value = map[string]{{$enumType}} { - {{- range .Fields}} - "{{.Name}}": {{.Value}}, - {{- end}} - } - - func (x {{$enumName}}) String() string { - return {{$enumName}}_name[{{$enumType}}(x)] - } - - func (x {{$enumName}}) MarshalJSON() ([]byte, error) { - buf := bytes.NewBufferString(`"`) - buf.WriteString({{$enumName}}_name[{{$enumType}}(x)]) - buf.WriteString(`"`) - return buf.Bytes(), nil - } - - func (x *{{$enumName}}) UnmarshalJSON(b []byte) error { - var j string - err := json.Unmarshal(b, &j) - if err != nil { - return err - } - *x = {{$enumName}}({{$enumName}}_value[j]) - return nil - } - {{end}} - {{if .Type | isStruct }} - type {{.Name}} struct { - {{- range .Fields}} - {{. | exportedField}} {{. | fieldOptional}}{{. | fieldTypeDef}} {{. | fieldTags}} - {{- end}} - } - {{end}} -{{end}} -{{end}} -{{if .Services}} - {{range .Services}} - type {{.Name}} interface { - {{- range .Methods}} - {{.Name}}({{.Inputs | methodInputs}}) ({{.Outputs | methodOutputs}}) - {{- end}} - } - {{end}} - var WebRPCServices = map[string][]string{ - {{- range .Services}} - "{{.Name}}": { - {{- range .Methods}} - "{{.Name}}", - {{- end}} - }, - {{- end}} - } -{{end}} - -{{end}} diff --git a/gen/helpers.go b/gen/helpers.go new file mode 100644 index 00000000..1023427e --- /dev/null +++ b/gen/helpers.go @@ -0,0 +1,55 @@ +package gen + +import ( + "fmt" + "strings" + + "golang.org/x/tools/imports" +) + +// Backward compatibility with webrpc-gen v0.6.0. +func getOldTarget(target string) string { + switch target { + case "go": + return "github.com/webrpc/gen-golang@v0.6.0" + case "ts": + return "github.com/webrpc/gen-typescript@v0.6.0" + case "js": + return "github.com/webrpc/gen-javascript@v0.6.0" + } + return target +} + +func isGolangTarget(target string) bool { + target, _, _ = stringsCut(target, "@") + + if target == "golang" || target == "go" { + return true + } + + if strings.HasSuffix(target, "gen-golang") { + return true + } + + return false +} + +// Format Go source and & update imports. +func formatGoSource(source []byte) (string, error) { + formatted, err := imports.Process("", source, &imports.Options{ + AllErrors: true, Comments: true, TabIndent: true, TabWidth: 8, + }) + if err != nil { + return string(source), fmt.Errorf("failed to format generated Go source: %w", err) + } + return string(formatted), nil +} + +// strings.Cut was added in Go 1.18. +// Replace this once we bump up go version directive in go.mod. +func stringsCut(s, sep string) (before, after string, found bool) { + if i := strings.Index(s, sep); i >= 0 { + return s[:i], s[i+len(sep):], true + } + return s, "", false +} diff --git a/gen/javascript/README.md b/gen/javascript/README.md deleted file mode 100644 index 826757f4..00000000 --- a/gen/javascript/README.md +++ /dev/null @@ -1,33 +0,0 @@ -webrpc Javascript (ES6) generator -================================= - -> NOTE: the javascript generator is almost identical to the typescript one, -> just without the types.. we could have even omitted this generator -> and just relied on `tsc` to compile to es5/es6 JS versions, but -> for ease of use for devs that don't use TS, we offer this generator. - -This generator, from a webrpc schema/design file will code-generate: - -1. Client -- an isomorphic/universal Javascript client to speak to a webrpc server using the -provided schema. This client is compatible with any webrpc server language (ie. Go, nodejs, etc.). -As the client is isomorphic, means you can use this within a Web browser or use the client in a -server like nodejs -- both without needing any dependencies. I suggest to read the generated TS -output of the generated code, and you shall see, its nothing fancy, just the sort of thing you'd -write by hand. - -2. Server -- a nodejs Javascript server handler. See examples. - - -## webrpc-gen extras - -The `webrpc-gen` tool accepts an `-extra` cli flag, which is passed down to the generator, and -we use it for some basic toggling during code-generation. For `javascript` generator, we make -es6 code that includes module "exports" by default, but, if you'd like to use the client in a -UMD scenario without having to use babel or something like it, you can pass `-extra=noexports` -option to the cli.. like we do in the [hello-webrpc](../../_examples/hello-webrpc) example. - -ie.. - -``` -webrpc-gen -schema=hello-api.webrpc.json -target=js -extra=noexports -client -out=./webapp/client.gen.js -``` diff --git a/gen/javascript/embed/static.go b/gen/javascript/embed/static.go deleted file mode 100644 index 1a28e614..00000000 --- a/gen/javascript/embed/static.go +++ /dev/null @@ -1,6 +0,0 @@ -// Code generated by statik. DO NOT EDIT. - -// Package contains static assets. -package embed - -var Asset = "PK\x03\x04\x14\x00\x08\x00\x00\x00o\x85\x08O\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00 \x00client.js.tmplUT\x05\x00\x012QL]{{define \"client\"}}\n{{- if .Services}}\n//\n// Client\n//\n{{ range .Services}}\n{{exportKeyword}}class {{.Name}} {\n constructor(hostname, fetch) {\n this.path = '/rpc/{{.Name}}/'\n this.hostname = hostname\n this.fetch = fetch\n }\n\n url(name) {\n return this.hostname + this.path + name\n }\n {{range .Methods}}\n {{.Name | methodName}} = ({{.Inputs | methodInputs}}) => {\n return this.fetch(\n this.url('{{.Name}}'),\n {{- if .Inputs | len}}\n createHTTPRequest(args, headers)\n {{- else}}\n createHTTPRequest({}, headers)\n {{- end}}\n ).then((res) => {\n return buildResponse(res).then(_data => {\n return {\n {{- $outputsCount := .Outputs|len -}}\n {{- range $i, $output := .Outputs}}\n {{$output | newOutputArgResponse}}{{listComma $i $outputsCount}}\n {{- end}}\n }\n })\n })\n }\n {{end}}\n}\n{{end -}}\n{{end -}}\n{{end}}PK\x07\x08&\xd2\xd7\x13\x8c\x03\x00\x00\x8c\x03\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x92y\xecN\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x16\x00 \x00client_helpers.js.tmplUT\x05\x00\x01d\xa3(]{{define \"client_helpers\"}}\nconst createHTTPRequest = (body = {}, headers = {}) => {\n return {\n method: 'POST',\n headers: { ...headers, 'Content-Type': 'application/json' },\n body: JSON.stringify(body || {})\n }\n}\n\nconst buildResponse = (res) => {\n return res.text().then(text => {\n let data\n try {\n data = JSON.parse(text)\n } catch(err) {\n throw { code: 'unknown', msg: `expecting JSON, got: ${text}`, status: res.status }\n }\n if (!res.ok) {\n throw data // webrpc error response\n }\n return data\n })\n}\n{{end}}\nPK\x07\x08\xb2\x9b\x81\xf5.\x02\x00\x00.\x02\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00B\x85\x08O\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11\x00 \x00proto.gen.js.tmplUT\x05\x00\x01\xdcPL]{{- define \"proto\" -}}\n// {{.Name}} {{.SchemaVersion}} {{.SchemaHash}}\n// --\n// This file has been generated by https://github.com/webrpc/webrpc using gen/javascript\n// Do not edit by hand. Update your webrpc schema and re-generate.\n\n// WebRPC description and code-gen version\nexport const WebRPCVersion = \"{{.WebRPCVersion}}\"\n\n// Schema version of your RIDL schema\nexport const WebRPCSchemaVersion = \"{{.SchemaVersion}}\"\n\n// Schema hash generated from your RIDL schema\nexport const WebRPCSchemaHash = \"{{.SchemaHash}}\"\n\n{{template \"types\" .}}\n{{- if .TargetOpts.Client}}\n {{template \"client\" .}}\n {{template \"client_helpers\" .}}\n{{- end}}\n{{- if .TargetOpts.Server}}\n {{template \"server\" .}}\n{{- end}}\n{{- end}}\nPK\x07\x08\x98\xa5\xe9i\xcc\x02\x00\x00\xcc\x02\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00J\x85\x08O\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00 \x00server.js.tmplUT\x05\x00\x01\xecPL]{{define \"server\"}}\n\n{{- if .Services}}\n//\n// Server\n//\n\nclass WebRPCError extends Error {\n constructor(msg = \"error\", statusCode) {\n super(\"webrpc eror: \" + msg);\n\n this.statusCode = statusCode\n }\n}\n\nimport express from 'express'\n\n {{- range .Services}}\n {{$name := .Name}}\n {{$serviceName := .Name | serviceInterfaceName}}\n\n export const create{{$serviceName}}App = (serviceImplementation) => {\n const app = express();\n\n app.use(express.json())\n\n app.post('/*', async (req, res) => {\n const requestPath = req.baseUrl + req.path\n\n if (!req.body) {\n res.status(400).send(\"webrpc error: missing body\");\n\n return\n }\n\n switch(requestPath) {\n {{range .Methods}}\n\n case \"/rpc/{{$name}}/{{.Name}}\": { \n try {\n {{ range .Inputs }}\n {{- if not .Optional}}\n if (!(\"{{ .Name }}\" in req.body)) {\n throw new WebRPCError(\"Missing Argument `{{ .Name }}`\")\n }\n {{end -}}\n\n if (typeof req.body[\"{{.Name}}\"] !== \"{{ .Type | jsFieldType }}\") {\n throw new WebRPCError(\"Invalid arg: {{ .Name }}, got type \" + typeof req.body[\"{{ .Name }}\"] + \" expected \" + \"{{ .Type | jsFieldType }}\", 400);\n }\n {{end}}\n\n const response = await serviceImplementation[\"{{.Name}}\"](req.body);\n\n {{ range .Outputs}}\n if (!(\"{{ .Name }}\" in response)) {\n throw new WebRPCError(\"internal\", 500);\n }\n {{end}}\n\n res.status(200).json(response);\n } catch (err) {\n if (err instanceof WebRPCError) {\n const statusCode = err.statusCode || 400\n const message = err.message\n\n res.status(statusCode).json({\n msg: message,\n status: statusCode,\n code: \"\"\n });\n\n return\n }\n\n if (err.message) {\n res.status(400).send(err.message);\n\n return;\n }\n\n res.status(400).end();\n }\n }\n\n return;\n {{end}}\n\n default: {\n res.status(404).end()\n }\n }\n });\n\n return app;\n };\n {{- end}}\n{{end -}}\n{{end}}\nPK\x07\x08>E\\\xb6s\x0c\x00\x00s\x0c\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00x\x85\x08O\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0d\x00 \x00types.js.tmplUT\x05\x00\x01EQL]{{define \"types\"}}\n//\n// Types\n//\n{{ if .Messages -}}\n{{range .Messages -}}\n\n{{if .Type | isEnum -}}\n{{$enumName := .Name}}\n{{exportKeyword}}var {{$enumName}};\n(function ({{$enumName}}) {\n{{- range $i, $field := .Fields}}\n {{$enumName}}[\"{{$field.Name}}\"] = \"{{$field.Name}}\"\n{{- end}}\n})({{$enumName}} || ({{$enumName}} = {}))\n{{end -}}\n\n{{- if .Type | isStruct }}\n{{exportKeyword}}class {{.Name}} {\n constructor(_data) {\n this._data = {}\n if (_data) {\n {{range .Fields -}}\n this._data['{{. | exportedJSONField}}'] = _data['{{. | exportedJSONField}}']\n {{end}}\n }\n }\n {{ range .Fields -}}\n get {{. | exportedJSONField}}() {\n return this._data['{{. | exportedJSONField }}']\n }\n set {{. | exportedJSONField}}(value) {\n this._data['{{. | exportedJSONField}}'] = value\n }\n {{end}}\n toJSON() {\n return this._data\n }\n}\n{{end -}}\n{{end -}}\n{{end -}}\n\n{{end}}\nPK\x07\x08r\x06\xac\x87\x82\x03\x00\x00\x82\x03\x00\x00PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00o\x85\x08O&\xd2\xd7\x13\x8c\x03\x00\x00\x8c\x03\x00\x00\x0e\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x00\x00\x00\x00client.js.tmplUT\x05\x00\x012QL]PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x92y\xecN\xb2\x9b\x81\xf5.\x02\x00\x00.\x02\x00\x00\x16\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xd1\x03\x00\x00client_helpers.js.tmplUT\x05\x00\x01d\xa3(]PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00B\x85\x08O\x98\xa5\xe9i\xcc\x02\x00\x00\xcc\x02\x00\x00\x11\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81L\x06\x00\x00proto.gen.js.tmplUT\x05\x00\x01\xdcPL]PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00J\x85\x08O>E\\\xb6s\x0c\x00\x00s\x0c\x00\x00\x0e\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81` \x00\x00server.js.tmplUT\x05\x00\x01\xecPL]PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00x\x85\x08Or\x06\xac\x87\x82\x03\x00\x00\x82\x03\x00\x00\x0d\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x18\x16\x00\x00types.js.tmplUT\x05\x00\x01EQL]PK\x05\x06\x00\x00\x00\x00\x05\x00\x05\x00c\x01\x00\x00\xde\x19\x00\x00\x00\x00" diff --git a/gen/javascript/funcmap.go b/gen/javascript/funcmap.go deleted file mode 100644 index 4291984a..00000000 --- a/gen/javascript/funcmap.go +++ /dev/null @@ -1,199 +0,0 @@ -package javascript - -import ( - "errors" - "fmt" - "strings" - - "github.com/webrpc/webrpc/gen" - "github.com/webrpc/webrpc/schema" -) - -var fieldTypeMap = map[schema.DataType]string{ - schema.T_Uint: "number", - schema.T_Uint8: "number", - schema.T_Uint16: "number", - schema.T_Uint32: "number", - schema.T_Uint64: "number", - schema.T_Int: "number", - schema.T_Int8: "number", - schema.T_Int16: "number", - schema.T_Int32: "number", - schema.T_Int64: "number", - schema.T_Float32: "number", - schema.T_Float64: "number", - schema.T_String: "string", - schema.T_Timestamp: "string", - schema.T_Null: "null", - schema.T_Any: "any", - schema.T_Byte: "string", - schema.T_Bool: "boolean", -} - -func fieldConcreteType(in *schema.VarType) (string, error) { - switch in.Type { - case schema.T_Struct: - return in.Struct.Name, nil - default: - return "", nil - } - return "", fmt.Errorf("could not represent type: %#v", in) -} - -func constPathPrefix(in schema.VarName) (string, error) { - return string(in) + "PathPrefix", nil -} - -func methodName(in interface{}) string { - v, _ := downcaseName(in) - return v -} - -func methodInputName(in *schema.MethodArgument) string { - name := string(in.Name) - if name != "" { - return name - } - if in.Type != nil { - return in.Type.String() - } - return "" -} - -func methodInputs(in []*schema.MethodArgument) (string, error) { - inputs := []string{} - - if len(in) > 0 { - inputs = append(inputs, fmt.Sprintf("args")) - } - - inputs = append(inputs, "headers") - - return strings.Join(inputs, ", "), nil -} - -func isStruct(t schema.MessageType) bool { - return t == "struct" -} - -func exportedField(in schema.VarName) (string, error) { - s := string(in) - return strings.ToUpper(s[0:1]) + s[1:], nil -} - -func exportedJSONField(in schema.MessageField) (string, error) { - for i := range in.Meta { - for k := range in.Meta[i] { - if k == "json" { - s := strings.Split(fmt.Sprintf("%v", in.Meta[i][k]), ",") - if len(s) > 0 { - return s[0], nil - } - } - } - } - return string(in.Name), nil -} - -func isEnum(t schema.MessageType) bool { - return t == "enum" -} - -func downcaseName(v interface{}) (string, error) { - downFn := func(s string) string { - if s == "" { - return "" - } - return strings.ToLower(s[0:1]) + s[1:] - } - switch t := v.(type) { - case schema.VarName: - return downFn(string(t)), nil - case string: - return downFn(t), nil - default: - return "", errors.New("downcaseFieldName, unknown arg type") - } -} - -func listComma(item int, count int) string { - if item+1 < count { - return ", " - } - return "" -} - -func newOutputArgResponse(in *schema.MethodArgument) (string, error) { - z, err := fieldConcreteType(in.Type) - if err != nil { - return "", err - } - - typ := "" - switch in.Type.Type { - case schema.T_Struct: - typ = fmt.Sprintf("new %s", z) - default: - typ = "" - // typ = fmt.Sprintf("<%s>", z) - } - - line := fmt.Sprintf("%s: %s(_data.%s)", in.Name, typ, in.Name) - - return line, nil -} - -func exportKeyword(opts gen.TargetOptions) func() string { - return func() string { - if opts.Extra == "noexports" { - return "" - } else { - return "export " - } - } -} - -func serverServiceName(in schema.VarName) (string, error) { - s := string(in) - return strings.ToLower(s[0:1]) + s[1:] + "Server", nil -} - -func jsFieldType(in *schema.VarType) (string, error) { - switch in.Type { - case schema.T_Map: - return "object", nil - - case schema.T_Struct: - return in.Struct.Name, nil - - default: - if fieldTypeMap[in.Type] != "" { - return fieldTypeMap[in.Type], nil - } - } - - return "", fmt.Errorf("could not represent type: %#v", in) -} - -func serviceInterfaceName(in schema.VarName) (string, error) { - s := string(in) - return s, nil -} - -func templateFuncMap(opts gen.TargetOptions) map[string]interface{} { - return map[string]interface{}{ - "constPathPrefix": constPathPrefix, - "methodName": methodName, - "methodInputs": methodInputs, - "isStruct": isStruct, - "isEnum": isEnum, - "listComma": listComma, - "exportedField": exportedField, - "exportedJSONField": exportedJSONField, - "newOutputArgResponse": newOutputArgResponse, - "exportKeyword": exportKeyword(opts), - "serverServiceName": serverServiceName, - "jsFieldType": jsFieldType, - "serviceInterfaceName": serviceInterfaceName, - } -} diff --git a/gen/javascript/gen.go b/gen/javascript/gen.go deleted file mode 100644 index 321d94fc..00000000 --- a/gen/javascript/gen.go +++ /dev/null @@ -1,92 +0,0 @@ -//go:generate statik -src=./templates -dest=. -f -Z -p=embed -package javascript - -import ( - "bytes" - "io/ioutil" - "os" - "text/template" - - "github.com/goware/statik/fs" - "github.com/webrpc/webrpc/gen" - "github.com/webrpc/webrpc/gen/javascript/embed" - "github.com/webrpc/webrpc/schema" -) - -func init() { - gen.Register("js", &generator{}) -} - -type generator struct{} - -func (g *generator) Gen(proto *schema.WebRPCSchema, opts gen.TargetOptions) (string, error) { - // Get templates from `embed` asset package - // NOTE: make sure to `go generate` whenever you change the files in `templates/` folder - templates, err := getTemplates() - if err != nil { - return "", err - } - - // Load templates - tmpl := template. - New("webrpc-gen-js"). - Funcs(templateFuncMap(opts)) - - for _, tmplData := range templates { - _, err = tmpl.Parse(tmplData) - if err != nil { - return "", err - } - } - - // generate deterministic schema hash of the proto file - schemaHash, err := proto.SchemaHash() - if err != nil { - return "", err - } - - // template vars - vars := struct { - *schema.WebRPCSchema - SchemaHash string - TargetOpts gen.TargetOptions - }{ - proto, schemaHash, opts, - } - - // Generate the template - genBuf := bytes.NewBuffer(nil) - err = tmpl.ExecuteTemplate(genBuf, "proto", vars) - if err != nil { - return "", err - } - - return string(genBuf.Bytes()), nil -} - -func getTemplates() (map[string]string, error) { - data := map[string]string{} - - statikFS, err := fs.New(embed.Asset) - if err != nil { - return nil, err - } - - fs.Walk(statikFS, "/", func(path string, info os.FileInfo, err error) error { - if path == "/" { - return nil - } - f, err := statikFS.Open(path) - if err != nil { - return err - } - buf, err := ioutil.ReadAll(f) - if err != nil { - return err - } - data[path] = string(buf) - return nil - }) - - return data, nil -} diff --git a/gen/javascript/gen_test.go b/gen/javascript/gen_test.go deleted file mode 100644 index 522d16f3..00000000 --- a/gen/javascript/gen_test.go +++ /dev/null @@ -1,173 +0,0 @@ -package javascript - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/webrpc/webrpc/gen" - "github.com/webrpc/webrpc/schema" -) - -const input = ` -{ - "webrpc": "v1", - "name": "example", - "version":" v0.0.1", - "messages": [ - { - "name": "Kind", - "type": "enum", - "fields": [ - { - "name": "USER", - "type": "uint32", - "value": "1" - }, - { - "name": "ADMIN", - "type": "uint32", - "value": "2" - } - ] - }, - { - "name": "Empty", - "type": "struct", - "fields": [ - ] - }, - { - "name": "GetUserRequest", - "type": "struct", - "fields": [ - { - "name": "userID", - "type": "uint64", - "optional": false - } - ] - }, - { - "name": "User", - "type": "struct", - "fields": [ - { - "name": "ID", - "type": "uint64", - "optional": false, - "meta": [ - { "json": "id" }, - { "go.tag.db": "id" } - ] - }, - { - "name": "username", - "type": "string", - "optional": false, - "meta": [ - { "json": "USERNAME" }, - { "go.tag.db": "username" } - ] - }, - { - "name": "createdAt", - "type": "timestamp", - "optional": true, - "meta": [ - { "go.tag.json": "created_at,omitempty" }, - { "go.tag.db": "created_at" } - ] - } - - ] - }, - { - "name": "RandomStuff", - "type": "struct", - "fields": [ - { - "name": "meta", - "type": "map" - }, - { - "name": "metaNestedExample", - "type": "map>" - }, - { - "name": "namesList", - "type": "[]string" - }, - { - "name": "numsList", - "type": "[]int64" - }, - { - "name": "doubleArray", - "type": "[][]string" - }, - { - "name": "listOfMaps", - "type": "[]map" - }, - { - "name": "listOfUsers", - "type": "[]User" - }, - { - "name": "mapOfUsers", - "type": "map" - }, - { - "name": "user", - "type": "User" - } - ] - } - ], - "services": [ - { - "name": "ExampleService", - "methods": [ - { - "name": "Ping", - "inputs": [], - "outputs": [ - { - "name": "status", - "type": "bool" - } - ] - }, - { - "name": "GetUser", - "inputs": [ - { - "name": "req", - "type": "GetUserRequest" - } - ], - "outputs": [ - { - "name": "user", - "type": "User" - } - ] - } - ] - } - ] -} -` - -func TestGenJavascript(t *testing.T) { - s, err := schema.ParseSchemaJSON([]byte(input)) - assert.NoError(t, err) - - g := &generator{} - - o, err := g.Gen(s, gen.TargetOptions{}) - assert.NoError(t, err) - _ = o - - // t.Logf("%s", o) -} diff --git a/gen/javascript/templates/client.js.tmpl b/gen/javascript/templates/client.js.tmpl deleted file mode 100644 index 8f42df1c..00000000 --- a/gen/javascript/templates/client.js.tmpl +++ /dev/null @@ -1,41 +0,0 @@ -{{define "client"}} -{{- if .Services}} -// -// Client -// -{{ range .Services}} -{{exportKeyword}}class {{.Name}} { - constructor(hostname, fetch) { - this.path = '/rpc/{{.Name}}/' - this.hostname = hostname - this.fetch = fetch - } - - url(name) { - return this.hostname + this.path + name - } - {{range .Methods}} - {{.Name | methodName}} = ({{.Inputs | methodInputs}}) => { - return this.fetch( - this.url('{{.Name}}'), - {{- if .Inputs | len}} - createHTTPRequest(args, headers) - {{- else}} - createHTTPRequest({}, headers) - {{- end}} - ).then((res) => { - return buildResponse(res).then(_data => { - return { - {{- $outputsCount := .Outputs|len -}} - {{- range $i, $output := .Outputs}} - {{$output | newOutputArgResponse}}{{listComma $i $outputsCount}} - {{- end}} - } - }) - }) - } - {{end}} -} -{{end -}} -{{end -}} -{{end}} \ No newline at end of file diff --git a/gen/javascript/templates/client_helpers.js.tmpl b/gen/javascript/templates/client_helpers.js.tmpl deleted file mode 100644 index 84d28a2a..00000000 --- a/gen/javascript/templates/client_helpers.js.tmpl +++ /dev/null @@ -1,24 +0,0 @@ -{{define "client_helpers"}} -const createHTTPRequest = (body = {}, headers = {}) => { - return { - method: 'POST', - headers: { ...headers, 'Content-Type': 'application/json' }, - body: JSON.stringify(body || {}) - } -} - -const buildResponse = (res) => { - return res.text().then(text => { - let data - try { - data = JSON.parse(text) - } catch(err) { - throw { code: 'unknown', msg: `expecting JSON, got: ${text}`, status: res.status } - } - if (!res.ok) { - throw data // webrpc error response - } - return data - }) -} -{{end}} diff --git a/gen/javascript/templates/proto.gen.js.tmpl b/gen/javascript/templates/proto.gen.js.tmpl deleted file mode 100644 index 0ce5a4b3..00000000 --- a/gen/javascript/templates/proto.gen.js.tmpl +++ /dev/null @@ -1,24 +0,0 @@ -{{- define "proto" -}} -// {{.Name}} {{.SchemaVersion}} {{.SchemaHash}} -// -- -// This file has been generated by https://github.com/webrpc/webrpc using gen/javascript -// Do not edit by hand. Update your webrpc schema and re-generate. - -// WebRPC description and code-gen version -export const WebRPCVersion = "{{.WebRPCVersion}}" - -// Schema version of your RIDL schema -export const WebRPCSchemaVersion = "{{.SchemaVersion}}" - -// Schema hash generated from your RIDL schema -export const WebRPCSchemaHash = "{{.SchemaHash}}" - -{{template "types" .}} -{{- if .TargetOpts.Client}} - {{template "client" .}} - {{template "client_helpers" .}} -{{- end}} -{{- if .TargetOpts.Server}} - {{template "server" .}} -{{- end}} -{{- end}} diff --git a/gen/javascript/templates/server.js.tmpl b/gen/javascript/templates/server.js.tmpl deleted file mode 100644 index 948e456f..00000000 --- a/gen/javascript/templates/server.js.tmpl +++ /dev/null @@ -1,99 +0,0 @@ -{{define "server"}} - -{{- if .Services}} -// -// Server -// - -class WebRPCError extends Error { - constructor(msg = "error", statusCode) { - super("webrpc eror: " + msg); - - this.statusCode = statusCode - } -} - -import express from 'express' - - {{- range .Services}} - {{$name := .Name}} - {{$serviceName := .Name | serviceInterfaceName}} - - export const create{{$serviceName}}App = (serviceImplementation) => { - const app = express(); - - app.use(express.json()) - - app.post('/*', async (req, res) => { - const requestPath = req.baseUrl + req.path - - if (!req.body) { - res.status(400).send("webrpc error: missing body"); - - return - } - - switch(requestPath) { - {{range .Methods}} - - case "/rpc/{{$name}}/{{.Name}}": { - try { - {{ range .Inputs }} - {{- if not .Optional}} - if (!("{{ .Name }}" in req.body)) { - throw new WebRPCError("Missing Argument `{{ .Name }}`") - } - {{end -}} - - if (typeof req.body["{{.Name}}"] !== "{{ .Type | jsFieldType }}") { - throw new WebRPCError("Invalid arg: {{ .Name }}, got type " + typeof req.body["{{ .Name }}"] + " expected " + "{{ .Type | jsFieldType }}", 400); - } - {{end}} - - const response = await serviceImplementation["{{.Name}}"](req.body); - - {{ range .Outputs}} - if (!("{{ .Name }}" in response)) { - throw new WebRPCError("internal", 500); - } - {{end}} - - res.status(200).json(response); - } catch (err) { - if (err instanceof WebRPCError) { - const statusCode = err.statusCode || 400 - const message = err.message - - res.status(statusCode).json({ - msg: message, - status: statusCode, - code: "" - }); - - return - } - - if (err.message) { - res.status(400).send(err.message); - - return; - } - - res.status(400).end(); - } - } - - return; - {{end}} - - default: { - res.status(404).end() - } - } - }); - - return app; - }; - {{- end}} -{{end -}} -{{end}} diff --git a/gen/javascript/templates/types.js.tmpl b/gen/javascript/templates/types.js.tmpl deleted file mode 100644 index 0f7660c2..00000000 --- a/gen/javascript/templates/types.js.tmpl +++ /dev/null @@ -1,44 +0,0 @@ -{{define "types"}} -// -// Types -// -{{ if .Messages -}} -{{range .Messages -}} - -{{if .Type | isEnum -}} -{{$enumName := .Name}} -{{exportKeyword}}var {{$enumName}}; -(function ({{$enumName}}) { -{{- range $i, $field := .Fields}} - {{$enumName}}["{{$field.Name}}"] = "{{$field.Name}}" -{{- end}} -})({{$enumName}} || ({{$enumName}} = {})) -{{end -}} - -{{- if .Type | isStruct }} -{{exportKeyword}}class {{.Name}} { - constructor(_data) { - this._data = {} - if (_data) { - {{range .Fields -}} - this._data['{{. | exportedJSONField}}'] = _data['{{. | exportedJSONField}}'] - {{end}} - } - } - {{ range .Fields -}} - get {{. | exportedJSONField}}() { - return this._data['{{. | exportedJSONField }}'] - } - set {{. | exportedJSONField}}(value) { - this._data['{{. | exportedJSONField}}'] = value - } - {{end}} - toJSON() { - return this._data - } -} -{{end -}} -{{end -}} -{{end -}} - -{{end}} diff --git a/gen/template_source.go b/gen/template_source.go new file mode 100644 index 00000000..5dd76899 --- /dev/null +++ b/gen/template_source.go @@ -0,0 +1,254 @@ +package gen + +import ( + "context" + "fmt" + "hash/fnv" + "net/http" + "os" + "path/filepath" + "strconv" + "strings" + "text/template" + "time" + + "github.com/posener/gitfs" + "github.com/shurcooL/httpfs/path/vfspath" + "github.com/shurcooL/httpfs/text/vfstemplate" + "github.com/shurcooL/httpfs/vfsutil" + "github.com/webrpc/webrpc/schema" +) + +func loadTemplates(proto *schema.WebRPCSchema, target string, config *Config) (*template.Template, *TemplateSource, error) { + s, err := NewTemplateSource(target, config) + if err != nil { + return nil, nil, err + } + tmpl, err := s.loadTemplates() + if err != nil { + return nil, nil, err + } + return tmpl, s, nil +} + +// period of time before we attempt to refetch from git source again. +// in case of a failure, we will use the local cache. +const ( + templateCacheTTL = 1 * time.Hour + templateCacheTimestampFilename = ".webrpc-gen-timestamp" + templateCacheInfoFilename = ".webrpc-gen-info" +) + +type TemplateSource struct { + tmpl *template.Template + target string + config *Config + + IsLocal bool + TmplDir string + TmplVersion string // git url and hash, or the local dir same as TmplDir + CacheAge time.Duration + CacheRefreshErr error +} + +func NewTemplateSource(target string, config *Config) (*TemplateSource, error) { + tmpl := template.New(target).Funcs(templateFuncMap(config.TemplateOptions)) + return &TemplateSource{ + tmpl: tmpl, + target: target, + config: config, + }, nil +} + +func (s *TemplateSource) loadTemplates() (*template.Template, error) { + // from go:embed + if target, ok := EmbeddedTargets[s.target]; ok { + s.IsLocal = true + s.TmplVersion = target.ImportTag + tmpl, err := s.tmpl.ParseFS(target.FS, "*.go.tmpl") + if err != nil { + return nil, fmt.Errorf("failed to load embedded templates: %w", err) + } + return tmpl, nil + } + + // from local directory + if isLocalDir(s.target) { + s.IsLocal = true + tmpl, err := s.tmpl.ParseGlob(filepath.Join(s.target, "/*.go.tmpl")) + if err != nil { + return nil, fmt.Errorf("failed to load templates from %s: %w", s.target, err) + } + s.TmplDir = s.target + s.TmplVersion = s.target + return tmpl, nil + } + + // from remote git or cache source + s.IsLocal = false + s.target = s.inferRemoteTarget(s.target) + tmpl, err := s.loadRemote() + if err != nil { + return nil, fmt.Errorf("failed to load templates from %s: %w", s.target, err) + } + return tmpl, nil +} + +func (s *TemplateSource) loadRemote() (*template.Template, error) { + var err error + var sourceFS http.FileSystem + + cacheAvailable, cacheDir, cacheFS, cacheAge := s.openCacheDir() + s.TmplDir = cacheDir + + if !cacheAvailable || s.config.RefreshCache || cacheAge > templateCacheTTL { + s.TmplVersion = s.target + // fetch from remote git + sourceFS, err = gitfs.New(context.Background(), s.target, gitfs.OptPrefetch(true), gitfs.OptGlob("*.go.tmpl")) + if err != nil { + s.CacheRefreshErr = err + // load from cache, if available + if cacheAvailable { + sourceFS = cacheFS + s.CacheAge = cacheAge + } else { + return nil, fmt.Errorf("failed to fetch from remote git: %w", err) + } + } else { + // cache remote git + if err := s.cacheTemplates(s.target, sourceFS, cacheDir); err != nil { + s.CacheRefreshErr = err + } + } + } else { + // load from cache + sourceFS = cacheFS + s.CacheAge = cacheAge + } + + // read template version info + if cacheAvailable && s.TmplVersion == "" { + tmplVersion, _ := os.ReadFile(filepath.Join(cacheDir, templateCacheInfoFilename)) + s.TmplVersion = strings.TrimSpace(string(tmplVersion)) + } + + // parse the template files from the source + tmpl, err := vfstemplate.ParseGlob(sourceFS, s.tmpl, "/*.go.tmpl") + if err != nil { + return nil, fmt.Errorf("failed to parse templates: %w", err) + } + + return tmpl, nil +} + +func (s *TemplateSource) cacheTemplates(target string, remoteFS http.FileSystem, cacheDir string) error { + // create empty cache directory + if _, err := os.Stat(cacheDir); os.IsExist(err) { + if err := os.RemoveAll(cacheDir); err != nil { + return err + } + } + + filenames, err := vfspath.Glob(remoteFS, "/*.go.tmpl") + if err != nil { + return err + } + + if len(filenames) == 0 { + return fmt.Errorf("no template files were found in target %s", target) + } + + if err := os.MkdirAll(cacheDir, 0755); err != nil { + return fmt.Errorf("unable to create directory for template cache at %s: %w", cacheDir, err) + } + + for _, filename := range filenames { + data, err := vfsutil.ReadFile(remoteFS, filename) + if err != nil { + return err + } + + err = os.WriteFile(filepath.Join(cacheDir, filename), data, 0755) + if err != nil { + return err + } + } + + err = os.WriteFile(filepath.Join(cacheDir, templateCacheInfoFilename), []byte(strings.TrimSpace(target)), 0755) + if err != nil { + return err + } + + now := time.Now() + data := []byte(fmt.Sprintf("%d", now.Unix())) + + err = os.WriteFile(filepath.Join(cacheDir, templateCacheTimestampFilename), data, 0755) + if err != nil { + return err + } + + return nil +} + +func (s *TemplateSource) openCacheDir() (bool, string, http.FileSystem, time.Duration) { + cacheDir, _ := s.getTmpCacheDir() + if cacheDir == "" { + // unable to find OS temp dir, but we don't error -- although + // we probably should print a warning + return false, "", nil, 0 + } + + // convert local fs to http filesystem + cacheFS := http.Dir(cacheDir) + + // read cache timestamp file to determine availability + available := false + ts := int64(0) + b, _ := vfsutil.ReadFile(cacheFS, templateCacheTimestampFilename) + if len(b) > 0 { + ts, _ = strconv.ParseInt(strings.TrimSpace(string(b)), 10, 64) + if ts > 0 { + available = true + } + } + + return available, cacheDir, cacheFS, time.Since(time.Unix(ts, 0)) +} + +func (s *TemplateSource) getTmpCacheDir() (string, error) { + dir := os.TempDir() + if dir == "" { + return "", fmt.Errorf("unable to determine OS temp dir") + } + + // derive a deterministic folder for this template source + hash := fnv.New32a() + hash.Write([]byte(s.target)) + + parts := strings.Split(s.target, "/") + name := parts[len(parts)-1] + + return filepath.Join(dir, "webrpc-cache", fmt.Sprintf("%d-%s", hash.Sum32(), name)), nil +} + +func (s *TemplateSource) inferRemoteTarget(target string) string { + // extra check to ensure its not a local dir + if isLocalDir(target) { + return target + } + + // determine if a url is passed or just a gen-XXX name + parts := strings.Split(target, "/") + + // just a name, so by convention assume the default target of the webrpc org + if len(parts) == 1 { + return fmt.Sprintf("github.com/webrpc/gen-%s", strings.ToLower(target)) + } + + // accept the target as is + return target +} + +func isLocalDir(target string) bool { + return strings.HasPrefix(target, "/") || strings.HasPrefix(target, ".") +} diff --git a/gen/typescript/README.md b/gen/typescript/README.md deleted file mode 100644 index 4cf99e36..00000000 --- a/gen/typescript/README.md +++ /dev/null @@ -1,13 +0,0 @@ -webrpc Typescript generator -=========================== - -This generator, from a webrpc schema/design file will code-generate: - -1. Client -- an isomorphic/universal Typescript client to speak to a webrpc server using the -provided schema. This client is compatible with any webrpc server language (ie. Go, nodejs, etc.). -As the client is isomorphic, means you can use this within a Web browser or use the client in a -server like nodejs -- both without needing any dependencies. I suggest to read the generated TS -output of the generated code, and you shall see, its nothing fancy, just the sort of thing you'd -write by hand. - -2. Server -- a nodejs Typescript server handler. See examples. diff --git a/gen/typescript/embed/static.go b/gen/typescript/embed/static.go deleted file mode 100644 index 5ddda994..00000000 --- a/gen/typescript/embed/static.go +++ /dev/null @@ -1,6 +0,0 @@ -// Code generated by statik. DO NOT EDIT. - -// Package contains static assets. -package embed - -var Asset = "PK\x03\x04\x14\x00\x08\x00\x00\x00\xcc\x84\x08O\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00 \x00client.ts.tmplUT\x05\x00\x01\x00PL]{{define \"client\"}}\n{{- if .Services}}\n//\n// Client\n//\n\n{{- range .Services}}\nexport class {{.Name}} implements {{.Name | serviceInterfaceName}} {\n private hostname: string\n private fetch: Fetch\n private path = '/rpc/{{.Name}}/'\n\n constructor(hostname: string, fetch: Fetch) {\n this.hostname = hostname\n this.fetch = fetch\n }\n\n private url(name: string): string {\n return this.hostname + this.path + name\n }\n {{range .Methods}}\n {{.Name | methodName}} = ({{. | methodInputs}}): {{. | methodOutputs}} => {\n return this.fetch(\n this.url('{{.Name}}'),\n {{- if .Inputs | len}}\n createHTTPRequest(args, headers)\n {{- else}}\n createHTTPRequest({}, headers)\n {{end -}}\n ).then((res) => {\n return buildResponse(res).then(_data => {\n return {\n {{- $outputsCount := .Outputs|len -}}\n {{- range $i, $output := .Outputs}}\n {{$output | newOutputArgResponse}}{{listComma $i $outputsCount}}\n {{- end}}\n }\n })\n })\n }\n {{end}}\n}\n{{end -}}\n{{end -}}\n{{end}}\nPK\x07\x08\x1d\xf3\xd5f\x1d\x04\x00\x00\x1d\x04\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x92y\xecN\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x16\x00 \x00client_helpers.ts.tmplUT\x05\x00\x01d\xa3(]{{define \"client_helpers\"}}\nexport interface WebRPCError extends Error {\n code: string\n msg: string\n status: number\n}\n\nconst createHTTPRequest = (body: object = {}, headers: object = {}): object => {\n return {\n method: 'POST',\n headers: { ...headers, 'Content-Type': 'application/json' },\n body: JSON.stringify(body || {})\n }\n}\n\nconst buildResponse = (res: Response): Promise => {\n return res.text().then(text => {\n let data\n try {\n data = JSON.parse(text)\n } catch(err) {\n throw { code: 'unknown', msg: `expecting JSON, got: ${text}`, status: res.status } as WebRPCError\n }\n if (!res.ok) {\n throw data // webrpc error response\n }\n return data\n })\n}\n\nexport type Fetch = (input: RequestInfo, init?: RequestInit) => Promise\n{{end}}\nPK\x07\x08\xcd\xaet\xa8\x1d\x03\x00\x00\x1d\x03\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\xfc\x84\x08O\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11\x00 \x00proto.gen.ts.tmplUT\x05\x00\x01]PL]{{- define \"proto\" -}}\n/* tslint:disable */\n// {{.Name}} {{.SchemaVersion}} {{.SchemaHash}}\n// --\n// This file has been generated by https://github.com/webrpc/webrpc using gen/typescript\n// Do not edit by hand. Update your webrpc schema and re-generate.\n\n// WebRPC description and code-gen version\nexport const WebRPCVersion = \"{{.WebRPCVersion}}\"\n\n// Schema version of your RIDL schema\nexport const WebRPCSchemaVersion = \"{{.SchemaVersion}}\"\n\n// Schema hash generated from your RIDL schema\nexport const WebRPCSchemaHash = \"{{.SchemaHash}}\"\n\n{{template \"types\" .}}\n\n{{- if .TargetOpts.Client}}\n {{template \"client\" .}}\n {{template \"client_helpers\" .}}\n{{- end}}\n\n{{- if .TargetOpts.Server}}\n {{template \"server\" .}}\n {{template \"server_helpers\" .}}\n{{- end}}\n\n{{- end}}\nPK\x07\x08mw\xe7\xa3\x06\x03\x00\x00\x06\x03\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\xd4\x84\x08O\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00 \x00server.ts.tmplUT\x05\x00\x01\x11PL]{{define \"server\"}}\n\n{{- if .Services}}\n//\n// Server\n//\nexport class WebRPCError extends Error {\n statusCode?: number\n\n constructor(msg: string = \"error\", statusCode?: number) {\n super(\"webrpc error: \" + msg);\n\n Object.setPrototypeOf(this, WebRPCError.prototype);\n\n this.statusCode = statusCode;\n }\n}\n\nimport express from 'express'\n\n {{- range .Services}}\n {{$name := .Name}}\n {{$serviceName := .Name | serviceInterfaceName}}\n\n export type {{$serviceName}}Service = {\n {{range .Methods}}\n {{.Name}}: (args: {{.Name}}Args) => {{.Name}}Return | Promise<{{.Name}}Return>\n {{end}}\n }\n\n export const create{{$serviceName}}App = (serviceImplementation: {{$serviceName}}Service) => {\n const app = express();\n\n app.use(express.json())\n\n app.post('/*', async (req, res) => {\n const requestPath = req.baseUrl + req.path\n\n if (!req.body) {\n res.status(400).send(\"webrpc error: missing body\");\n\n return\n }\n\n switch(requestPath) {\n {{range .Methods}}\n\n case \"/rpc/{{$name}}/{{.Name}}\": { \n try {\n {{ range .Inputs }}\n {{- if not .Optional}}\n if (!(\"{{ .Name }}\" in req.body)) {\n throw new WebRPCError(\"Missing Argument `{{ .Name }}`\")\n }\n {{end -}}\n\n if (\"{{ .Name }}\" in req.body && !validateType(req.body[\"{{ .Name }}\"], \"{{ .Type | jsFieldType }}\")) {\n throw new WebRPCError(\"Invalid Argument: {{ .Name }}\")\n }\n {{end}}\n\n const response = await serviceImplementation[\"{{.Name}}\"](req.body);\n\n {{ range .Outputs}}\n if (!(\"{{ .Name }}\" in response)) {\n throw new WebRPCError(\"internal\", 500);\n }\n {{end}}\n\n res.status(200).json(response);\n } catch (err) {\n if (err instanceof WebRPCError) {\n const statusCode = err.statusCode || 400\n const message = err.message\n\n res.status(statusCode).json({\n msg: message,\n status: statusCode,\n code: \"\"\n });\n\n return\n }\n\n if (err.message) {\n res.status(400).send(err.message);\n\n return;\n }\n\n res.status(400).end();\n }\n }\n\n return;\n {{end}}\n\n default: {\n res.status(404).end()\n }\n }\n });\n\n return app;\n };\n {{- end}}\n{{end -}}\n{{end}}\nPK\x07\x08XT\xde\xa9\x92\x0d\x00\x00\x92\x0d\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x92y\xecN\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x16\x00 \x00server_helpers.ts.tmplUT\x05\x00\x01d\xa3(]{{ define \"server_helpers\" }}\n\nconst JS_TYPES = [\n \"bigint\",\n \"boolean\",\n \"function\",\n \"number\",\n \"object\",\n \"string\",\n \"symbol\",\n \"undefined\"\n]\n\n{{ range .Messages }}\n const validate{{ .Name }} = (value: any) => {\n {{ range .Fields }}\n {{ if .Optional }}\n if (\"{{ . | exportedJSONField }}\" in value && !validateType(value[\"{{ . | exportedJSONField }}\"], \"{{ .Type | jsFieldType }}\")) {\n return false\n }\n {{ else }}\n if (!(\"{{ . | exportedJSONField }}\" in value) || !validateType(value[\"{{ . | exportedJSONField }}\"], \"{{ .Type | jsFieldType }}\")) {\n return false\n }\n {{ end }}\n {{ end }}\n\n return true\n }\n{{ end }}\n\nconst TYPE_VALIDATORS: { [type: string]: (value: any) => boolean } = {\n {{ range .Messages }}\n {{ .Name }}: validate{{ .Name }},\n {{ end }}\n}\n\nconst validateType = (value: any, type: string) => {\n if (JS_TYPES.indexOf(type) > -1) {\n return typeof value === type;\n }\n\n const validator = TYPE_VALIDATORS[type];\n\n if (!validator) {\n return false;\n }\n\n return validator(value);\n}\n\n{{ end }}PK\x07\x08\x93\xb2\xd6w\xce\x04\x00\x00\xce\x04\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\xc8\x84\x08O\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0d\x00 \x00types.ts.tmplUT\x05\x00\x01\xf8OL]{{define \"types\"}}\n//\n// Types\n//\n\n{{- if .Messages -}}\n{{range .Messages -}}\n\n{{if .Type | isEnum -}}\n{{$enumName := .Name}}\nexport enum {{$enumName}} {\n{{- range $i, $field := .Fields}}\n {{- if $i}},{{end}}\n {{$field.Name}} = '{{$field.Name}}'\n{{- end}}\n}\n{{end -}}\n\n{{- if .Type | isStruct }}\nexport interface {{.Name | interfaceName}} {\n {{- range .Fields}}\n {{if . | exportableField -}}{{. | exportedJSONField}}{{if .Optional}}?{{end}}: {{.Type | fieldType}}{{- end -}}\n {{- end}}\n}\n{{end -}}\n{{end -}}\n{{end -}}\n\n{{if .Services}}\n{{- range .Services}}\nexport interface {{.Name | serviceInterfaceName}} {\n{{- range .Methods}}\n {{.Name | methodName}}({{. | methodInputs}}): {{. | methodOutputs}}\n{{- end}}\n}\n\n{{range .Methods -}}\nexport interface {{. | methodArgumentInputInterfaceName}} {\n{{- range .Inputs}}\n {{.Name}}{{if .Optional}}?{{end}}: {{.Type | fieldType}}\n{{- end}}\n}\n\nexport interface {{. | methodArgumentOutputInterfaceName}} {\n{{- range .Outputs}}\n {{.Name}}{{if .Optional}}?{{end}}: {{.Type | fieldType}}\n{{- end}} \n}\n{{end}}\n\n{{- end}}\n{{end -}}\n{{end}}\nPK\x07\x08=A*\xed=\x04\x00\x00=\x04\x00\x00PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\xcc\x84\x08O\x1d\xf3\xd5f\x1d\x04\x00\x00\x1d\x04\x00\x00\x0e\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x00\x00\x00\x00client.ts.tmplUT\x05\x00\x01\x00PL]PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x92y\xecN\xcd\xaet\xa8\x1d\x03\x00\x00\x1d\x03\x00\x00\x16\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81b\x04\x00\x00client_helpers.ts.tmplUT\x05\x00\x01d\xa3(]PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\xfc\x84\x08Omw\xe7\xa3\x06\x03\x00\x00\x06\x03\x00\x00\x11\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xcc\x07\x00\x00proto.gen.ts.tmplUT\x05\x00\x01]PL]PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\xd4\x84\x08OXT\xde\xa9\x92\x0d\x00\x00\x92\x0d\x00\x00\x0e\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x1a\x0b\x00\x00server.ts.tmplUT\x05\x00\x01\x11PL]PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x92y\xecN\x93\xb2\xd6w\xce\x04\x00\x00\xce\x04\x00\x00\x16\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xf1\x18\x00\x00server_helpers.ts.tmplUT\x05\x00\x01d\xa3(]PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\xc8\x84\x08O=A*\xed=\x04\x00\x00=\x04\x00\x00\x0d\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x0c\x1e\x00\x00types.ts.tmplUT\x05\x00\x01\xf8OL]PK\x05\x06\x00\x00\x00\x00\x06\x00\x06\x00\xb0\x01\x00\x00\x8d\"\x00\x00\x00\x00" diff --git a/gen/typescript/funcmap.go b/gen/typescript/funcmap.go deleted file mode 100644 index e1ac328a..00000000 --- a/gen/typescript/funcmap.go +++ /dev/null @@ -1,275 +0,0 @@ -package typescript - -import ( - "errors" - "fmt" - "strings" - - "github.com/webrpc/webrpc/schema" -) - -var fieldTypeMap = map[schema.DataType]string{ - schema.T_Uint: "number", - schema.T_Uint8: "number", - schema.T_Uint16: "number", - schema.T_Uint32: "number", - schema.T_Uint64: "number", - schema.T_Int: "number", - schema.T_Int8: "number", - schema.T_Int16: "number", - schema.T_Int32: "number", - schema.T_Int64: "number", - schema.T_Float32: "number", - schema.T_Float64: "number", - schema.T_String: "string", - schema.T_Timestamp: "string", - schema.T_Null: "null", - schema.T_Any: "any", - schema.T_Byte: "string", - schema.T_Bool: "boolean", -} - -func jsFieldType(in *schema.VarType) (string, error) { - switch in.Type { - case schema.T_Map: - return "object", nil - - case schema.T_List: - z, err := fieldType(in.List.Elem) - - if err != nil { - return "", err - } - - return z + "[]", nil - - case schema.T_Struct: - return in.Struct.Name, nil - - default: - if fieldTypeMap[in.Type] != "" { - return fieldTypeMap[in.Type], nil - } - } - - return "", fmt.Errorf("could not represent type: %#v", in) -} - -func fieldType(in *schema.VarType) (string, error) { - switch in.Type { - case schema.T_Map: - typK, ok := fieldTypeMap[in.Map.Key] - if !ok { - return "", fmt.Errorf("unknown type mapping %v", in.Map.Key) - } - typV, err := fieldType(in.Map.Value) - if err != nil { - return "", err - } - return fmt.Sprintf("{[key: %s]: %s}", typK, typV), nil - - case schema.T_List: - z, err := fieldType(in.List.Elem) - if err != nil { - return "", err - } - return "Array<" + z + ">", nil - - case schema.T_Struct: - return in.Struct.Name, nil - - default: - if fieldTypeMap[in.Type] != "" { - return fieldTypeMap[in.Type], nil - } - } - return "", fmt.Errorf("could not represent type: %#v", in) -} - -func constPathPrefix(in schema.VarName) (string, error) { - return string(in) + "PathPrefix", nil -} - -func methodInputName(in *schema.MethodArgument) string { - name := string(in.Name) - if name != "" { - return name - } - if in.Type != nil { - return in.Type.String() - } - return "" -} - -func methodInputType(in *schema.MethodArgument) string { - z, _ := fieldType(in.Type) - return z -} - -func methodArgumentInputInterfaceName(in *schema.Method) string { - if len(in.Service.Schema.Services) == 1 { - return fmt.Sprintf("%s%s", in.Name, "Args") - } else { - return fmt.Sprintf("%s%s%s", in.Service.Name, in.Name, "Args") - } -} - -func methodArgumentOutputInterfaceName(in *schema.Method) string { - if len(in.Service.Schema.Services) == 1 { - return fmt.Sprintf("%s%s", in.Name, "Return") - } else { - return fmt.Sprintf("%s%s%s", in.Service.Name, in.Name, "Return") - } -} - -func methodInputs(in *schema.Method) (string, error) { - inputs := []string{} - if len(in.Inputs) > 0 { - inputs = append(inputs, fmt.Sprintf("args: %s", methodArgumentInputInterfaceName(in))) - } - inputs = append(inputs, "headers?: object") - return strings.Join(inputs, ", "), nil -} - -func methodOutputs(in *schema.Method) (string, error) { - return fmt.Sprintf("Promise<%s>", methodArgumentOutputInterfaceName(in)), nil -} - -func methodName(in interface{}) string { - v, _ := downcaseName(in) - return v -} - -func isStruct(t schema.MessageType) bool { - return t == "struct" -} - -func exportedField(in schema.VarName) (string, error) { - return string(in), nil -} - -func exportableField(in schema.MessageField) bool { - for _, meta := range in.Meta { - for k := range meta { - if k == "json" { - if meta[k] == "-" { - return false - } - } - } - } - return true -} - -func exportedJSONField(in schema.MessageField) (string, error) { - for _, meta := range in.Meta { - for k := range meta { - if k == "json" { - s := strings.Split(fmt.Sprintf("%v", meta[k]), ",") - return s[0], nil - } - } - } - return string(in.Name), nil -} - -func interfaceName(in schema.VarName) (string, error) { - s := string(in) - return s, nil -} - -func isEnum(t schema.MessageType) bool { - return t == "enum" -} - -func downcaseName(v interface{}) (string, error) { - downFn := func(s string) string { - if s == "" { - return "" - } - return strings.ToLower(s[0:1]) + s[1:] - } - switch t := v.(type) { - case schema.VarName: - return downFn(string(t)), nil - case string: - return downFn(t), nil - default: - return "", errors.New("downcaseFieldName, unknown arg type") - } -} - -func listComma(item int, count int) string { - if item+1 < count { - return ", " - } - return "" -} - -func serviceInterfaceName(in schema.VarName) (string, error) { - s := string(in) - return s, nil -} - -func newOutputArgResponse(in *schema.MethodArgument) (string, error) { - z, err := fieldType(in.Type) - if err != nil { - return "", err - } - - typ := fmt.Sprintf("<%s>", z) - line := fmt.Sprintf("%s: %s(_data.%s)", in.Name, typ, in.Name) - - return line, nil -} - -func serverServiceName(in schema.VarName) (string, error) { - s := string(in) - return strings.ToLower(s[0:1]) + s[1:] + "Server", nil -} - -func methodArgType(in *schema.MethodArgument) string { - z, err := fieldType(in.Type) - - if err != nil { - panic(err.Error()) - } - - var prefix string - typ := in.Type.Type - - if in.Optional { - prefix = "*" - } - if typ == schema.T_Struct { - prefix = "" // noop, as already pointer applied elsewhere - } - if typ == schema.T_List || typ == schema.T_Map { - prefix = "" - } - - return prefix + z -} - -var templateFuncMap = map[string]interface{}{ - "fieldType": fieldType, - "constPathPrefix": constPathPrefix, - "interfaceName": interfaceName, - "methodName": methodName, - "methodInputs": methodInputs, - "methodOutputs": methodOutputs, - "methodArgumentInputInterfaceName": methodArgumentInputInterfaceName, - "methodArgumentOutputInterfaceName": methodArgumentOutputInterfaceName, - "isStruct": isStruct, - "isEnum": isEnum, - "listComma": listComma, - "serviceInterfaceName": serviceInterfaceName, - "exportableField": exportableField, - "exportedField": exportedField, - "exportedJSONField": exportedJSONField, - "newOutputArgResponse": newOutputArgResponse, - "downcaseName": downcaseName, - "serverServiceName": serverServiceName, - "methodArgType": methodArgType, - "jsFieldType": jsFieldType, -} diff --git a/gen/typescript/gen.go b/gen/typescript/gen.go deleted file mode 100644 index c4bdd32f..00000000 --- a/gen/typescript/gen.go +++ /dev/null @@ -1,92 +0,0 @@ -//go:generate statik -src=./templates -dest=. -f -Z -p=embed -package typescript - -import ( - "bytes" - "io/ioutil" - "os" - "text/template" - - "github.com/goware/statik/fs" - "github.com/webrpc/webrpc/gen" - "github.com/webrpc/webrpc/gen/typescript/embed" - "github.com/webrpc/webrpc/schema" -) - -func init() { - gen.Register("ts", &generator{}) -} - -type generator struct{} - -func (g *generator) Gen(proto *schema.WebRPCSchema, opts gen.TargetOptions) (string, error) { - // Get templates from `embed` asset package - // NOTE: make sure to `go generate` whenever you change the files in `templates/` folder - templates, err := getTemplates() - if err != nil { - return "", err - } - - // Load templates - tmpl := template. - New("webrpc-gen-ts"). - Funcs(templateFuncMap) - - for _, tmplData := range templates { - _, err = tmpl.Parse(tmplData) - if err != nil { - return "", err - } - } - - // generate deterministic schema hash of the proto file - schemaHash, err := proto.SchemaHash() - if err != nil { - return "", err - } - - // template vars - vars := struct { - *schema.WebRPCSchema - SchemaHash string - TargetOpts gen.TargetOptions - }{ - proto, schemaHash, opts, - } - - // Generate the template - genBuf := bytes.NewBuffer(nil) - err = tmpl.ExecuteTemplate(genBuf, "proto", vars) - if err != nil { - return "", err - } - - return string(genBuf.Bytes()), nil -} - -func getTemplates() (map[string]string, error) { - data := map[string]string{} - - statikFS, err := fs.New(embed.Asset) - if err != nil { - return nil, err - } - - fs.Walk(statikFS, "/", func(path string, info os.FileInfo, err error) error { - if path == "/" { - return nil - } - f, err := statikFS.Open(path) - if err != nil { - return err - } - buf, err := ioutil.ReadAll(f) - if err != nil { - return err - } - data[path] = string(buf) - return nil - }) - - return data, nil -} diff --git a/gen/typescript/gen_test.go b/gen/typescript/gen_test.go deleted file mode 100644 index 5747b745..00000000 --- a/gen/typescript/gen_test.go +++ /dev/null @@ -1,173 +0,0 @@ -package typescript - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/webrpc/webrpc/gen" - "github.com/webrpc/webrpc/schema" -) - -const input = ` -{ - "webrpc": "v1", - "name": "example", - "version":" v0.0.1", - "messages": [ - { - "name": "Kind", - "type": "enum", - "fields": [ - { - "name": "USER", - "type": "uint32", - "value": "1" - }, - { - "name": "ADMIN", - "type": "uint32", - "value": "2" - } - ] - }, - { - "name": "Empty", - "type": "struct", - "fields": [ - ] - }, - { - "name": "GetUserRequest", - "type": "struct", - "fields": [ - { - "name": "userID", - "type": "uint64", - "optional": false - } - ] - }, - { - "name": "User", - "type": "struct", - "fields": [ - { - "name": "ID", - "type": "uint64", - "optional": false, - "meta": [ - { "json": "id" }, - { "go.tag.db": "id" } - ] - }, - { - "name": "username", - "type": "string", - "optional": false, - "meta": [ - { "json": "USERNAME" }, - { "go.tag.db": "username" } - ] - }, - { - "name": "createdAt", - "type": "timestamp", - "optional": true, - "meta": [ - { "go.tag.json": "created_at,omitempty" }, - { "go.tag.db": "created_at" } - ] - } - - ] - }, - { - "name": "RandomStuff", - "type": "struct", - "fields": [ - { - "name": "meta", - "type": "map" - }, - { - "name": "metaNestedExample", - "type": "map>" - }, - { - "name": "namesList", - "type": "[]string" - }, - { - "name": "numsList", - "type": "[]int64" - }, - { - "name": "doubleArray", - "type": "[][]string" - }, - { - "name": "listOfMaps", - "type": "[]map" - }, - { - "name": "listOfUsers", - "type": "[]User" - }, - { - "name": "mapOfUsers", - "type": "map" - }, - { - "name": "user", - "type": "User" - } - ] - } - ], - "services": [ - { - "name": "ExampleService", - "methods": [ - { - "name": "Ping", - "inputs": [], - "outputs": [ - { - "name": "status", - "type": "bool" - } - ] - }, - { - "name": "GetUser", - "inputs": [ - { - "name": "req", - "type": "GetUserRequest" - } - ], - "outputs": [ - { - "name": "user", - "type": "User" - } - ] - } - ] - } - ] -} -` - -func TestGenTypescript(t *testing.T) { - s, err := schema.ParseSchemaJSON([]byte(input)) - assert.NoError(t, err) - - g := &generator{} - - o, err := g.Gen(s, gen.TargetOptions{}) - assert.NoError(t, err) - _ = o - - // t.Logf("%s", o) -} diff --git a/gen/typescript/templates/client.ts.tmpl b/gen/typescript/templates/client.ts.tmpl deleted file mode 100644 index 2b962f64..00000000 --- a/gen/typescript/templates/client.ts.tmpl +++ /dev/null @@ -1,45 +0,0 @@ -{{define "client"}} -{{- if .Services}} -// -// Client -// - -{{- range .Services}} -export class {{.Name}} implements {{.Name | serviceInterfaceName}} { - private hostname: string - private fetch: Fetch - private path = '/rpc/{{.Name}}/' - - constructor(hostname: string, fetch: Fetch) { - this.hostname = hostname - this.fetch = fetch - } - - private url(name: string): string { - return this.hostname + this.path + name - } - {{range .Methods}} - {{.Name | methodName}} = ({{. | methodInputs}}): {{. | methodOutputs}} => { - return this.fetch( - this.url('{{.Name}}'), - {{- if .Inputs | len}} - createHTTPRequest(args, headers) - {{- else}} - createHTTPRequest({}, headers) - {{end -}} - ).then((res) => { - return buildResponse(res).then(_data => { - return { - {{- $outputsCount := .Outputs|len -}} - {{- range $i, $output := .Outputs}} - {{$output | newOutputArgResponse}}{{listComma $i $outputsCount}} - {{- end}} - } - }) - }) - } - {{end}} -} -{{end -}} -{{end -}} -{{end}} diff --git a/gen/typescript/templates/client_helpers.ts.tmpl b/gen/typescript/templates/client_helpers.ts.tmpl deleted file mode 100644 index 4a2f3fa2..00000000 --- a/gen/typescript/templates/client_helpers.ts.tmpl +++ /dev/null @@ -1,32 +0,0 @@ -{{define "client_helpers"}} -export interface WebRPCError extends Error { - code: string - msg: string - status: number -} - -const createHTTPRequest = (body: object = {}, headers: object = {}): object => { - return { - method: 'POST', - headers: { ...headers, 'Content-Type': 'application/json' }, - body: JSON.stringify(body || {}) - } -} - -const buildResponse = (res: Response): Promise => { - return res.text().then(text => { - let data - try { - data = JSON.parse(text) - } catch(err) { - throw { code: 'unknown', msg: `expecting JSON, got: ${text}`, status: res.status } as WebRPCError - } - if (!res.ok) { - throw data // webrpc error response - } - return data - }) -} - -export type Fetch = (input: RequestInfo, init?: RequestInit) => Promise -{{end}} diff --git a/gen/typescript/templates/proto.gen.ts.tmpl b/gen/typescript/templates/proto.gen.ts.tmpl deleted file mode 100644 index 6afb1f2d..00000000 --- a/gen/typescript/templates/proto.gen.ts.tmpl +++ /dev/null @@ -1,29 +0,0 @@ -{{- define "proto" -}} -/* tslint:disable */ -// {{.Name}} {{.SchemaVersion}} {{.SchemaHash}} -// -- -// This file has been generated by https://github.com/webrpc/webrpc using gen/typescript -// Do not edit by hand. Update your webrpc schema and re-generate. - -// WebRPC description and code-gen version -export const WebRPCVersion = "{{.WebRPCVersion}}" - -// Schema version of your RIDL schema -export const WebRPCSchemaVersion = "{{.SchemaVersion}}" - -// Schema hash generated from your RIDL schema -export const WebRPCSchemaHash = "{{.SchemaHash}}" - -{{template "types" .}} - -{{- if .TargetOpts.Client}} - {{template "client" .}} - {{template "client_helpers" .}} -{{- end}} - -{{- if .TargetOpts.Server}} - {{template "server" .}} - {{template "server_helpers" .}} -{{- end}} - -{{- end}} diff --git a/gen/typescript/templates/server.ts.tmpl b/gen/typescript/templates/server.ts.tmpl deleted file mode 100644 index fe35a0fa..00000000 --- a/gen/typescript/templates/server.ts.tmpl +++ /dev/null @@ -1,108 +0,0 @@ -{{define "server"}} - -{{- if .Services}} -// -// Server -// -export class WebRPCError extends Error { - statusCode?: number - - constructor(msg: string = "error", statusCode?: number) { - super("webrpc error: " + msg); - - Object.setPrototypeOf(this, WebRPCError.prototype); - - this.statusCode = statusCode; - } -} - -import express from 'express' - - {{- range .Services}} - {{$name := .Name}} - {{$serviceName := .Name | serviceInterfaceName}} - - export type {{$serviceName}}Service = { - {{range .Methods}} - {{.Name}}: (args: {{.Name}}Args) => {{.Name}}Return | Promise<{{.Name}}Return> - {{end}} - } - - export const create{{$serviceName}}App = (serviceImplementation: {{$serviceName}}Service) => { - const app = express(); - - app.use(express.json()) - - app.post('/*', async (req, res) => { - const requestPath = req.baseUrl + req.path - - if (!req.body) { - res.status(400).send("webrpc error: missing body"); - - return - } - - switch(requestPath) { - {{range .Methods}} - - case "/rpc/{{$name}}/{{.Name}}": { - try { - {{ range .Inputs }} - {{- if not .Optional}} - if (!("{{ .Name }}" in req.body)) { - throw new WebRPCError("Missing Argument `{{ .Name }}`") - } - {{end -}} - - if ("{{ .Name }}" in req.body && !validateType(req.body["{{ .Name }}"], "{{ .Type | jsFieldType }}")) { - throw new WebRPCError("Invalid Argument: {{ .Name }}") - } - {{end}} - - const response = await serviceImplementation["{{.Name}}"](req.body); - - {{ range .Outputs}} - if (!("{{ .Name }}" in response)) { - throw new WebRPCError("internal", 500); - } - {{end}} - - res.status(200).json(response); - } catch (err) { - if (err instanceof WebRPCError) { - const statusCode = err.statusCode || 400 - const message = err.message - - res.status(statusCode).json({ - msg: message, - status: statusCode, - code: "" - }); - - return - } - - if (err.message) { - res.status(400).send(err.message); - - return; - } - - res.status(400).end(); - } - } - - return; - {{end}} - - default: { - res.status(404).end() - } - } - }); - - return app; - }; - {{- end}} -{{end -}} -{{end}} diff --git a/gen/typescript/templates/server_helpers.ts.tmpl b/gen/typescript/templates/server_helpers.ts.tmpl deleted file mode 100644 index 9763a4f2..00000000 --- a/gen/typescript/templates/server_helpers.ts.tmpl +++ /dev/null @@ -1,52 +0,0 @@ -{{ define "server_helpers" }} - -const JS_TYPES = [ - "bigint", - "boolean", - "function", - "number", - "object", - "string", - "symbol", - "undefined" -] - -{{ range .Messages }} - const validate{{ .Name }} = (value: any) => { - {{ range .Fields }} - {{ if .Optional }} - if ("{{ . | exportedJSONField }}" in value && !validateType(value["{{ . | exportedJSONField }}"], "{{ .Type | jsFieldType }}")) { - return false - } - {{ else }} - if (!("{{ . | exportedJSONField }}" in value) || !validateType(value["{{ . | exportedJSONField }}"], "{{ .Type | jsFieldType }}")) { - return false - } - {{ end }} - {{ end }} - - return true - } -{{ end }} - -const TYPE_VALIDATORS: { [type: string]: (value: any) => boolean } = { - {{ range .Messages }} - {{ .Name }}: validate{{ .Name }}, - {{ end }} -} - -const validateType = (value: any, type: string) => { - if (JS_TYPES.indexOf(type) > -1) { - return typeof value === type; - } - - const validator = TYPE_VALIDATORS[type]; - - if (!validator) { - return false; - } - - return validator(value); -} - -{{ end }} \ No newline at end of file diff --git a/gen/typescript/templates/types.ts.tmpl b/gen/typescript/templates/types.ts.tmpl deleted file mode 100644 index a6870bac..00000000 --- a/gen/typescript/templates/types.ts.tmpl +++ /dev/null @@ -1,53 +0,0 @@ -{{define "types"}} -// -// Types -// - -{{- if .Messages -}} -{{range .Messages -}} - -{{if .Type | isEnum -}} -{{$enumName := .Name}} -export enum {{$enumName}} { -{{- range $i, $field := .Fields}} - {{- if $i}},{{end}} - {{$field.Name}} = '{{$field.Name}}' -{{- end}} -} -{{end -}} - -{{- if .Type | isStruct }} -export interface {{.Name | interfaceName}} { - {{- range .Fields}} - {{if . | exportableField -}}{{. | exportedJSONField}}{{if .Optional}}?{{end}}: {{.Type | fieldType}}{{- end -}} - {{- end}} -} -{{end -}} -{{end -}} -{{end -}} - -{{if .Services}} -{{- range .Services}} -export interface {{.Name | serviceInterfaceName}} { -{{- range .Methods}} - {{.Name | methodName}}({{. | methodInputs}}): {{. | methodOutputs}} -{{- end}} -} - -{{range .Methods -}} -export interface {{. | methodArgumentInputInterfaceName}} { -{{- range .Inputs}} - {{.Name}}{{if .Optional}}?{{end}}: {{.Type | fieldType}} -{{- end}} -} - -export interface {{. | methodArgumentOutputInterfaceName}} { -{{- range .Outputs}} - {{.Name}}{{if .Optional}}?{{end}}: {{.Type | fieldType}} -{{- end}} -} -{{end}} - -{{- end}} -{{end -}} -{{end}} diff --git a/go.mod b/go.mod index 15243305..aa582ff6 100644 --- a/go.mod +++ b/go.mod @@ -1,13 +1,67 @@ module github.com/webrpc/webrpc -go 1.13 +go 1.19 + +// replace github.com/webrpc/gen-golang => ../gen-golang +// replace github.com/webrpc/gen-typescript => ../gen-typescript +// replace github.com/webrpc/gen-javascript => ../gen-javascript +// replace github.com/webrpc/gen-openapi => ../gen-openapi +// replace github.com/webrpc/gen-kotlin => ../gen-kotlin + +require ( + github.com/Masterminds/sprig/v3 v3.2.3 + github.com/davecgh/go-spew v1.1.1 + github.com/golang-cz/textcase v1.2.1 + github.com/google/go-cmp v0.6.0 + github.com/posener/gitfs v1.2.2 + github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c + github.com/stretchr/testify v1.9.0 + github.com/webrpc/gen-dart v0.1.1 + github.com/webrpc/gen-golang v0.17.0 + github.com/webrpc/gen-javascript v0.13.0 + github.com/webrpc/gen-kotlin v0.1.0 + github.com/webrpc/gen-openapi v0.15.0 + github.com/webrpc/gen-typescript v0.16.2 + golang.org/x/tools v0.21.0 +) require ( - github.com/go-chi/chi v4.0.2+incompatible - github.com/goware/statik v0.2.0 - github.com/pkg/errors v0.8.1 - github.com/stretchr/objx v0.2.0 // indirect - github.com/stretchr/testify v1.4.0 - golang.org/x/tools v0.0.0-20191101200257-8dbcdeb83d3f - gopkg.in/yaml.v2 v2.2.4 // indirect + dario.cat/mergo v1.0.0 // indirect + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver/v3 v3.2.1 // indirect + github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/ProtonMail/go-crypto v1.0.0 // indirect + github.com/cloudflare/circl v1.3.7 // indirect + github.com/cyphar/filepath-securejoin v0.2.5 // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/go-billy/v5 v5.5.0 // indirect + github.com/go-git/go-git/v5 v5.12.0 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/google/go-github v17.0.0+incompatible // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/huandu/xstrings v1.4.0 // indirect + github.com/imdario/mergo v0.3.16 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/kevinburke/ssh_config v1.2.0 // indirect + github.com/kr/fs v0.1.0 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/pjbgf/sha1cd v0.3.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/posener/diff v0.0.1 // indirect + github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect + github.com/shopspring/decimal v1.4.0 // indirect + github.com/skeema/knownhosts v1.2.2 // indirect + github.com/spf13/cast v1.6.0 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect + golang.org/x/crypto v0.23.0 // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/net v0.25.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.20.0 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index c6cbbe04..28d34911 100644 --- a/go.sum +++ b/go.sum @@ -1,35 +1,406 @@ -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= +github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= +github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= +github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= +github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= +github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/cyphar/filepath-securejoin v0.2.5 h1:6iR5tXJ/e6tJZzzdMc1km3Sa7RRIVBKAK32O2s7AYfo= +github.com/cyphar/filepath-securejoin v0.2.5/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/go-chi/chi v4.0.2+incompatible h1:maB6vn6FqCxrpz4FqWdh4+lwpyZIQS7YEAUcHlgXVRs= -github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= -github.com/goware/statik v0.2.0 h1:2dKnJIawSr/qbd4TdSgRtNc6mdVZrTOR56aSiL47460= -github.com/goware/statik v0.2.0/go.mod h1:Fktf+coYRC3SB2RfBB++LAG6ojA/VzuDp0Jfd064ICs= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= +github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= +github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= +github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.4.1/go.mod h1:vjbugF6Fz7JIflbVpl1hJsGjSHNltrSw45YK/ukIvQg= +github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= +github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= +github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= +github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/golang-cz/textcase v1.2.1 h1:0xRtKo+abtJojre5ONjuMzyg9fSfiKBj5bWZ6fpTYxI= +github.com/golang-cz/textcase v1.2.1/go.mod h1:aWsQknYwxtTS2zSCrGGoRIsxmzjsHomRqLeMeVb+SKU= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= +github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU= +github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= +github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/mmcloughlin/avo v0.5.0/go.mod h1:ChHFdoV7ql95Wi7vuq2YT1bwCJqiWdZrQ1im3VujLYM= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= +github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47RKZmLU= +github.com/onsi/ginkgo/v2 v2.1.6/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk= +github.com/onsi/ginkgo/v2 v2.3.0/go.mod h1:Eew0uilEqZmIEZr8JrvYlvOM7Rr6xzTmMV8AyFNU9d0= +github.com/onsi/ginkgo/v2 v2.4.0/go.mod h1:iHkDK1fKGcBoEHT5W7YBq4RFWaQulw+caOMkAt4OrFo= +github.com/onsi/ginkgo/v2 v2.5.0/go.mod h1:Luc4sArBICYCS8THh8v3i3i5CuSZO+RaQRaJoeNwomw= +github.com/onsi/ginkgo/v2 v2.7.0/go.mod h1:yjiuMwPokqY1XauOgju45q3sJt6VzQ/Fict1LFVcsAo= +github.com/onsi/ginkgo/v2 v2.8.1/go.mod h1:N1/NbDngAFcSLdyZ+/aYTYGSlq9qMCS/cNKGJjy+csc= +github.com/onsi/ginkgo/v2 v2.9.0/go.mod h1:4xkjoL/tZv4SMWeww56BU5kAt19mVB47gTWxmrTcxyk= +github.com/onsi/ginkgo/v2 v2.9.1/go.mod h1:FEcmzVcCHl+4o9bQZVab+4dC9+j+91t2FHSzmGAPfuo= +github.com/onsi/ginkgo/v2 v2.9.2/go.mod h1:WHcJJG2dIlcCqVfBAwUCrJxSPFb6v4azBwgxeMeDuts= +github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= +github.com/onsi/ginkgo/v2 v2.9.7/go.mod h1:cxrmXWykAwTwhQsJOPfdIDiJ+l2RYq7U8hFU+M/1uw0= +github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= +github.com/onsi/gomega v1.20.1/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo= +github.com/onsi/gomega v1.21.1/go.mod h1:iYAIXgPSaDHak0LCMA+AWBpIKBr8WZicMxnE8luStNc= +github.com/onsi/gomega v1.22.1/go.mod h1:x6n7VNe4hw0vkyYUM4mjIXx3JbLiPaBPNgB7PRQ1tuM= +github.com/onsi/gomega v1.24.0/go.mod h1:Z/NWtiqwBrwUt4/2loMmHL63EDLnYHmVbuBpDr2vQAg= +github.com/onsi/gomega v1.24.1/go.mod h1:3AOiACssS3/MajrniINInwbfOOtfZvplPzuRSmvt1jM= +github.com/onsi/gomega v1.26.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM= +github.com/onsi/gomega v1.27.1/go.mod h1:aHX5xOykVYzWOV4WqQy0sy8BQptgukenXpCXfadcIAw= +github.com/onsi/gomega v1.27.3/go.mod h1:5vG284IBtfDAmDyrK+eGyZmUgUlmi+Wngqo557cZ6Gw= +github.com/onsi/gomega v1.27.4/go.mod h1:riYq/GJKh8hhoM01HN6Vmuy93AarCXCBGpvFDK3q3fQ= +github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= +github.com/onsi/gomega v1.27.7/go.mod h1:1p8OOlwo2iUUDsHnOrjE5UKYJ+e3W8eQ3qSlRahPmr4= +github.com/onsi/gomega v1.27.8/go.mod h1:2J8vzI/s+2shY9XHRApDkdgPo1TKT7P2u6fXeJKFnNQ= +github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= +github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= +github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= +github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= +github.com/posener/diff v0.0.1 h1:rjxZ4l6g5DixF+LKqFFEZpTXY2kitoiGfph/sVRjqoM= +github.com/posener/diff v0.0.1/go.mod h1:hZraNYAlXkt6AyFW523B2inR/zd+gmL9WNJB45sKFzQ= +github.com/posener/gitfs v1.2.2 h1:BqkjTzqV5v/wTWt39Z4hmLPj9ZJyMgIMxXJj7Yy6TWY= +github.com/posener/gitfs v1.2.2/go.mod h1:NESLm7QEkxEA65GGkItljj5rss2gSLdqp+A/0pWo2xI= +github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c h1:aqg5Vm5dwtvL+YgDpBcK1ITf3o96N/K7/wsRXQnUTEs= +github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c/go.mod h1:owqhoLW1qZoYLZzLnBw+QkPP9WZnjlSWihhxAJC1+/M= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= +github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= -github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= -github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/webrpc/gen-dart v0.1.1 h1:PZh5oNNdA84Qxu8ixKDf1cT8Iv6t0g7x6q9aeX1bti4= +github.com/webrpc/gen-dart v0.1.1/go.mod h1:yq0ThW3ANNulJLyR50jx1aZMEVBDp19VUHucK65ayPs= +github.com/webrpc/gen-golang v0.17.0 h1:qCvkhrIdEalQNYJEyqUhKiZ/wK5yEOhx0TJ5X0fzoKM= +github.com/webrpc/gen-golang v0.17.0/go.mod h1:qy1qEWMlTvrRzjSuQLy+176RqNaX1ymUULDtlo7Dapo= +github.com/webrpc/gen-javascript v0.13.0 h1:tw7U1xueUjZz3cQAAA4/DZ90BHydkQKiJC4VXd/j2hg= +github.com/webrpc/gen-javascript v0.13.0/go.mod h1:5EhapSJgzbiWrIGlqzZN9Lg9mE9209wwX+Du2dgn4EU= +github.com/webrpc/gen-kotlin v0.1.0 h1:tnlinqbDgowEoSy8E3VovTdP2OjyOIbgACCbahRjNcc= +github.com/webrpc/gen-kotlin v0.1.0/go.mod h1:PIPys9Gn1Ro7q7uoacydEX8CtqBlAJSV98A++tdj4ak= +github.com/webrpc/gen-openapi v0.15.0 h1:RrHAcDTlm0YH+YCz12p0KLCYIwfHrxf4x6/LjJ6kePY= +github.com/webrpc/gen-openapi v0.15.0/go.mod h1:fwY3ylZmdiCr+WXjR8Ek8wm08CFRr2/GaXI7Zd/Ou4Y= +github.com/webrpc/gen-typescript v0.16.2 h1:SfQ/S6S2/N855g6iqjWOkjNrVrV/Abm2bgXQj4zaYL0= +github.com/webrpc/gen-typescript v0.16.2/go.mod h1:xQzYnVaSMfcygDXA5SuW8eYyCLHBHkj15wCF7gcJF5Y= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/arch v0.1.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= +golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= +golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= +golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/tools v0.0.0-20191101160922-229318561b07 h1:ZWbexfw5+qfjZEV4Nak71v9VhK3fnydWcDRFmH61G74= -golang.org/x/tools v0.0.0-20191101160922-229318561b07/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191101200257-8dbcdeb83d3f h1:+QO45yvqhfD79HVNFPAgvstYLFye8zA+rd0mHFsGV9s= -golang.org/x/tools v0.0.0-20191101200257-8dbcdeb83d3f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= +golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= +golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= +golang.org/x/tools v0.9.3/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw= +golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1 h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3I= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/schema/README.md b/schema/README.md index 5ba14602..72ffc8fd 100644 --- a/schema/README.md +++ b/schema/README.md @@ -1,7 +1,7 @@ -WebRPC Schema +webrpc schema ============= -WebRPC is a design/schema driven approach to writing backend servers, with fully-generated +webrpc is a design/schema driven approach to writing backend servers, with fully-generated client libraries. Write your schema, and it will generate strongly-typed bindings between your server and client. The type system is described below. @@ -10,10 +10,20 @@ Some example webrpc schemas: in [JSON](../_examples/golang-basics/example.webrpc.json) * ..find more in ./_examples +- [Type system](#type-system) + - [Core types](#core-types) + - [Integers](#integers) + - [Floats](#floats) + - [Strings](#strings) + - [Timestamps (date/time)](#timestamps-datetime) + - [List](#list) + - [Map](#map) + - [Enum](#enum) + - [Struct](#struct) -## Type system +# Type system -### Basics +## Core types - `byte` (aka uint8) - `bool` @@ -47,11 +57,12 @@ Some example webrpc schemas: ### Timestamps (date/time) -- `timestamp` - for date/time +- `timestamp` - for date/time (serialized to [ECMA Script ISO 8601 format](https://tc39.es/ecma262/multipage/numbers-and-dates.html#sec-date-time-string-format): `YYYY-MM-DDTHH:mm:ss.sssZ`) -### Lists +## List +- List represents a JSON array over the wire - form: `[]` - ie. * `[]string` @@ -60,8 +71,9 @@ Some example webrpc schemas: * .. -### Map +## Map +- Map represents a JSON object with 0..N properties (key:value pairs) over the wire - form: `map` - ie. * `map` @@ -71,27 +83,16 @@ Some example webrpc schemas: * `map` - where `User` is a struct type defined in schema -### Enums +## Enum - enum, see examples -### Binary (future / v2) - -- `blob` aka.. `[]byte` - * TODO: https://github.com/PsychoLlama/bin-json might have some ideas for us - - -### Structs aka Objects / Messages - -- struct or object - * think of it just as a Javascript object or JSON object - - -#### Some notes on structs - -- fields of an object can be `optional` -- fields of an object are by default required, unless made optional -- fields of an object always return default values by default, ie. default of int is 0, string is "", etc. (like in Go) - - otherwise someone should make it optional which will have it be nullable +## Struct +- struct represents a JSON object over the wire +- struct has 0..N fields + - field can be `optional` + - fields are by default required, unless made optional + - fields always return default values by default, ie. default of int is 0, string is "", etc. (like in Go) + - otherwise someone should make it optional which will have it be nullable diff --git a/schema/data_type.go b/schema/core_type.go similarity index 73% rename from schema/data_type.go rename to schema/core_type.go index 096f79dc..2c12d1e6 100644 --- a/schema/data_type.go +++ b/schema/core_type.go @@ -5,10 +5,10 @@ import ( "encoding/json" ) -type DataType int +type CoreType int const ( - T_Unknown DataType = iota + T_Unknown CoreType = iota T_Null T_Any @@ -37,11 +37,11 @@ const ( T_List T_Map - T_Primitive - T_Struct // aka, a reference to our own webrpc proto struct/message + T_Struct // aka, a reference to our own webrpc struct type + T_Enum ) -var DataTypeToString = map[DataType]string{ +var CoreTypeToString = map[CoreType]string{ T_Null: "null", T_Any: "any", T_Byte: "byte", @@ -70,7 +70,7 @@ var DataTypeToString = map[DataType]string{ T_List: "[]", } -var DataTypeFromString = map[string]DataType{ +var CoreTypeFromString = map[string]CoreType{ "null": T_Null, "any": T_Any, "byte": T_Byte, @@ -99,23 +99,23 @@ var DataTypeFromString = map[string]DataType{ "[]": T_List, } -func (t DataType) String() string { - return DataTypeToString[t] +func (t CoreType) String() string { + return CoreTypeToString[t] } -func (t DataType) MarshalJSON() ([]byte, error) { +func (t CoreType) MarshalJSON() ([]byte, error) { buf := bytes.NewBufferString(`"`) - buf.WriteString(DataTypeToString[t]) + buf.WriteString(CoreTypeToString[t]) buf.WriteString(`"`) return buf.Bytes(), nil } -func (t *DataType) UnmarshalJSON(b []byte) error { +func (t *CoreType) UnmarshalJSON(b []byte) error { var j string err := json.Unmarshal(b, &j) if err != nil { return err } - *t = DataTypeFromString[j] + *t = CoreTypeFromString[j] return nil } diff --git a/schema/error.go b/schema/error.go new file mode 100644 index 00000000..1edaf9b0 --- /dev/null +++ b/schema/error.go @@ -0,0 +1,54 @@ +package schema + +import ( + "fmt" + "strings" +) + +type Error struct { + Code int `json:"code"` + Name string `json:"name"` + Message string `json:"message"` + HTTPStatus int `json:"httpStatus"` + + // Schema *WebRPCSchema `json:"-"` // denormalize/back-reference +} + +func (s *Error) Parse(schema *WebRPCSchema) error { + s.Name = strings.TrimSpace(s.Name) + if s.Name == "" { + return fmt.Errorf("schema error name cannot be empty") + } + if s.Code <= 0 { + return fmt.Errorf("schema error code must be positive number") + } + if !startsWithUpper(s.Name) { + return fmt.Errorf("schema error name must start with upper case: '%s'", s.Name) + } + if strings.HasPrefix(strings.ToLower(s.Name), "webrpc") { + return fmt.Errorf("schema error name cannot start with 'Webrpc': '%s'", s.Name) + } + if s.Message == "" { + return fmt.Errorf("schema error: message cannot be empty") + } + if s.HTTPStatus < 400 || s.HTTPStatus > 599 { + return fmt.Errorf("schema error: invalid HTTP status code '%v' for error type '%s' (must be number between 400-599)", s.HTTPStatus, s.Name) + } + + // check for duplicate codes or names + nameList := map[string]struct{}{} + codeList := map[int]struct{}{} + for _, e := range schema.Errors { + name := strings.ToLower(e.Name) + if _, ok := nameList[name]; ok { + return fmt.Errorf("schema error: detected duplicate error name of '%s'", e.Name) + } + if _, ok := codeList[e.Code]; ok { + return fmt.Errorf("schema error: detected duplicate error code of '%d'", e.Code) + } + nameList[name] = struct{}{} + codeList[e.Code] = struct{}{} + } + + return nil +} diff --git a/schema/message.go b/schema/message.go deleted file mode 100644 index fe2ded16..00000000 --- a/schema/message.go +++ /dev/null @@ -1,123 +0,0 @@ -package schema - -import ( - "strings" - - "github.com/pkg/errors" -) - -type Message struct { - Name VarName `json:"name"` - Type MessageType `json:"type"` - Fields []*MessageField `json:"fields"` - - // EnumType determined for enum types during parsing time - EnumType *VarType `json:"-"` -} - -type MessageType string // "enum" | "struct" - -type MessageField struct { - Name VarName `json:"name"` - Type *VarType `json:"type"` - - Optional bool `json:"optional"` - Value string `json:"value"` // used by enums - - // Meta store extra metadata on a field for plugins - Meta []MessageFieldMeta `json:"meta"` -} - -type MessageFieldMeta map[string]interface{} - -func (m *Message) Parse(schema *WebRPCSchema) error { - // Message name - msgName := string(m.Name) - if msgName == "" { - return errors.Errorf("schema error: message name cannot be empty") - } - - // Ensure we don't have dupe message types (w/ normalization) - name := strings.ToLower(msgName) - for _, msg := range schema.Messages { - if msg != m && name == strings.ToLower(string(msg.Name)) { - return errors.Errorf("schema error: duplicate message type detected, '%s'", msgName) - } - } - - // Ensure we have a message type - if string(m.Type) != "enum" && string(m.Type) != "struct" { - return errors.Errorf("schema error: message type must be 'enum' or 'struct' for '%s'", msgName) - } - - // NOTE: so far, lets allow messages with no fields.. so just empty object, why, I dunno, but gRPC allows it - // Ensure we have some fields - // if len(m.Fields) == 0 { - // return errors.Errorf("schema error: message type must contain at least one field for '%s'", msgName) - // } - - // Verify field names and ensure we don't have any duplicate field names - fieldList := map[string]string{} - for _, field := range m.Fields { - if string(field.Name) == "" { - return errors.Errorf("schema error: detected empty field name in message '%s", msgName) - } - - fieldName := string(field.Name) - nFieldName := strings.ToLower(fieldName) - - // Verify name format - if !IsValidArgName(fieldName) { - return errors.Errorf("schema error: invalid field name of '%s' in message '%s'", fieldName, msgName) - } - - // Ensure no dupes - if _, ok := fieldList[nFieldName]; ok { - return errors.Errorf("schema error: detected duplicate field name of '%s' in message '%s'", fieldName, msgName) - } - fieldList[nFieldName] = fieldName - } - - // Parse+validate message fields - for _, field := range m.Fields { - err := field.Type.Parse(schema) - if err != nil { - return err - } - } - - // For enums only, ensure all field types are the same - if m.Type == "enum" { - // ensure enum fields have value key set, and are all of the same type - fieldTypes := map[string]struct{}{} - for _, field := range m.Fields { - fieldType := field.Type.expr - fieldTypes[fieldType] = struct{}{} - if field.Value == "" { - return errors.Errorf("schema error: enum message '%s' with field '%s' is missing value", m.Name, field.Name) - } - } - if len(fieldTypes) > 1 { - return errors.Errorf("schema error: enum message '%s' must all have the same field type", m.Name) - } - - // ensure enum type is one of the allowed types.. aka integer - fieldType := m.Fields[0].Type - if !isValidVarType(fieldType.String(), VarIntegerDataTypes) { - return errors.Errorf("schema error: enum message '%s' field '%s' is invalid. must be an integer type.", m.Name, fieldType.String()) - } - m.EnumType = fieldType - } - - // For structs only - if m.Type == "struct" { - for _, field := range m.Fields { - if field.Value != "" { - return errors.Errorf("schema error: struct message '%s' with field '%s' cannot contain value field - please remove it", m.Name, field.Name) - } - } - - } - - return nil -} diff --git a/schema/ridl/README.md b/schema/ridl/README.md index f79a4b5a..95667963 100644 --- a/schema/ridl/README.md +++ b/schema/ridl/README.md @@ -2,4 +2,31 @@ ridl, pronounced "riddle" ========================= a ridl file is a "rpc interface design language" schema file that -describes a webrpc client/server app. +describes a webrpc client/server app. + +## Define webrpc schema errors + +You can now define your own custom schema errors in RIDL file, for example: + +```ridl +error 1 Unauthorized "unauthorized" HTTP 401 +error 2 ExpiredToken "expired token" HTTP 401 +error 3 InvalidToken "invalid token" HTTP 401 +error 4 Deactivated "account deactivated" HTTP 403 +error 5 ConfirmAccount "confirm your email" HTTP 403 +error 6 AccessDenied "access denied" HTTP 403 +error 7 MissingArgument "missing argument" HTTP 400 +error 8 UnexpectedValue "unexpected value" HTTP 400 +error 100 RateLimited "too many requests" HTTP 429 +error 101 DatabaseDown "service outage" HTTP 503 +error 102 ElasticDown "search is degraded" HTTP 503 +error 103 NotImplemented "not implemented" HTTP 501 +error 200 UserNotFound "user not found" +error 201 UserBusy "user busy" +error 202 InvalidUsername "invalid username" +error 300 FileTooBig "file is too big (max 1GB)" +error 301 FileInfected "file is infected" +error 302 FileType "unsupported file type" +``` + +Note: Unless specified, the default HTTP status for webrpc errors is `HTTP 400`. diff --git a/schema/ridl/_example/comments.ridl b/schema/ridl/_example/comments.ridl new file mode 100644 index 00000000..601e1722 --- /dev/null +++ b/schema/ridl/_example/comments.ridl @@ -0,0 +1,30 @@ +webrpc = v1 + +name = hello-webrpc +version = v0.0.1 + +# this is a comment +# yep +enum Kind: uint32 + # user more + - USER = 1 # user + - ADMIN = 2 # comment.. + +#! or.. just.. +enum Kind2: uint32 + - USER # aka, = 0 + - ADMIN # aka, = 1 + +struct Role + # role name line first + - name: string # role name + - perms: []string # permissions + +# ExampleService first line +# ExampleService second line +# ExampleService third line +service ExampleService + + # comment can go here + # too .. :) + - Ping() => (status: bool) diff --git a/schema/ridl/_example/example0.ridl b/schema/ridl/_example/example0.ridl index 181bce32..72c8e6ad 100644 --- a/schema/ridl/_example/example0.ridl +++ b/schema/ridl/_example/example0.ridl @@ -15,14 +15,14 @@ enum Kind2: uint32 - ADMIN # aka, = 1 -message Empty +struct Empty -message GetUserRequest +struct GetUserRequest - userID: uint64 -message Role +struct Role - name: string - users: map @@ -32,7 +32,7 @@ message Role - other: map> # comment -message User +struct User - ID: uint64 + json = id + go.tag.db = id @@ -50,7 +50,7 @@ message User + go.tag.db = created_at -message Notice +struct Notice - msg: string diff --git a/schema/ridl/_example/example1-golden.json b/schema/ridl/_example/example1-golden.json index e2abad8e..397abe28 100644 --- a/schema/ridl/_example/example1-golden.json +++ b/schema/ridl/_example/example1-golden.json @@ -1,352 +1,475 @@ { - "imports": [ + "webrpc": "v1", + "name": "hello-webrpc", + "version": "v0.0.1", + "types": [ + { + "kind": "enum", + "name": "Kind", + "type": "uint32", + "fields": [ + { + "comments": [ + "comment" + ], + "name": "USER", + "value": "1" + }, + { + "comments": [ + "comment.." + ], + "name": "ADMIN", + "value": "2" + } + ] + }, + { + "kind": "enum", + "name": "Kind2", + "type": "uint32", + "fields": [ + { + "comments": [ + "aka, = 0" + ], + "name": "USER", + "value": "0" + }, + { + "comments": [ + "aka, = 1" + ], + "name": "ADMIN", + "value": "1" + } + ] + }, + { + "kind": "struct", + "name": "Empty" + }, + { + "kind": "struct", + "name": "GetUserRequest", + "fields": [ + { + "name": "userID", + "type": "uint64" + } + ] + }, + { + "kind": "struct", + "name": "Role", + "fields": [ + { + "name": "name", + "type": "string" + }, + { + "name": "users", + "type": "map" + }, + { + "comments": [ + "comment" + ], + "name": "perms", + "type": "[]string" + }, + { + "comments": [ + "comment" + ], + "name": "other", + "type": "map>" + } + ] + }, + { + "kind": "struct", + "name": "User", + "fields": [ + { + "name": "ID", + "type": "uint64", + "meta": [ + { + "json": "id" + }, + { + "go.tag.db": "id" + } + ] + }, + { + "name": "username", + "type": "string", + "meta": [ + { + "json": "USERNAME" + }, + { + "go.tag.db": "username" + } + ] + }, + { + "name": "amount", + "type": "uint64" + }, + { + "name": "role", + "type": "string", + "meta": [ + { + "go.tag.db": "-" + } + ] + }, + { + "name": "createdAt", + "type": "timestamp", + "optional": true, + "meta": [ + { + "json": "created_at" + }, + { + "go.tag.json": "created_at,omitempty" + }, + { + "go.tag.db": "created_at" + } + ] + } + ] + }, + { + "kind": "struct", + "name": "Notice", + "fields": [ + { + "name": "msg", + "type": "string" + } + ] + }, + { + "kind": "struct", + "name": "FlattenRequest", + "fields": [ + { + "name": "name", + "type": "string", + "meta": [ + { + "go.tag.db": "name" + } + ] + }, + { + "name": "amount", + "type": "uint64", + "optional": true, + "meta": [ + { + "go.tag.db": "amount" + } + ] + } + ] + }, + { + "kind": "struct", + "name": "FlattenResponse", + "fields": [ + { + "name": "id", + "type": "uint64", + "meta": [ + { + "go.field.name": "ID" + } + ] + }, + { + "name": "count", + "type": "uint64", + "meta": [ + { + "json": "counter" + } + ] + } + ] + } + ], + "errors": [], + "services": [ + { + "name": "PingerService", + "methods": [ + { + "name": "Ping", + "annotations": {}, + "comments": [ + "comment can go here", + "too .. :)" + ], + "inputs": [], + "outputs": [ + { + "name": "status", + "type": "bool", + "optional": false + } + ] + } + ], + "comments": [] + }, + { + "name": "ExampleService", + "methods": [ + { + "name": "Ping", + "annotations": {}, + "comments": [ + "comment can go here", + "too .. :)" + ], + "inputs": [], + "outputs": [ + { + "name": "status", + "type": "bool", + "optional": false + } + ] + }, + { + "name": "GetUser", + "annotations": {}, + "comments": [], + "inputs": [ + { + "name": "req", + "type": "GetUserRequest", + "optional": false + } + ], + "outputs": [ + { + "name": "user", + "type": "User", + "optional": false + } + ] + }, + { + "name": "Recv", + "annotations": {}, + "comments": [], + "streamInput": true, + "inputs": [ + { + "name": "req", + "type": "string", + "optional": false + } + ], + "outputs": [] + }, + { + "name": "Send", + "annotations": {}, + "comments": [], + "streamInput": true, + "inputs": [], + "outputs": [ + { + "name": "resp", + "type": "string", + "optional": false + } + ] + }, + { + "name": "SendAndRecv", + "annotations": {}, + "comments": [], + "streamInput": true, + "streamOutput": true, + "inputs": [ + { + "name": "req", + "type": "string", + "optional": false + } + ], + "outputs": [ + { + "name": "resp", + "type": "string", + "optional": false + } + ] + }, + { + "name": "Broadcast", + "annotations": {}, + "comments": [], + "streamOutput": true, + "inputs": [], + "outputs": [ + { + "name": "resp", + "type": "Notice", + "optional": false + } + ] + }, + { + "name": "VerifyUsers", + "annotations": {}, + "comments": [], + "inputs": [ + { + "name": "seq", + "type": "int32", + "optional": false + }, + { + "name": "header", + "type": "map", + "optional": true + }, + { + "name": "ids", + "type": "[]uint64", + "optional": false + } + ], + "outputs": [ + { + "name": "code", + "type": "bool", + "optional": false + }, + { + "name": "ids", + "type": "[]bool", + "optional": false + } + ] + }, + { + "name": "MoreTest", + "annotations": {}, + "comments": [], + "inputs": [ + { + "name": "n", + "type": "uint64", + "optional": false + }, + { + "name": "stuff", + "type": "[]map", + "optional": false + }, + { + "name": "etc", + "type": "string", + "optional": false + } + ], + "outputs": [ + { + "name": "code", + "type": "bool", + "optional": true + } + ] + } + ], + "comments": [] + }, + { + "name": "Another", + "methods": [ + { + "name": "Flatten", + "annotations": {}, + "comments": [], + "inputs": [ + { + "name": "name", + "type": "string", + "optional": false, + "meta": [ { - "members": [], - "path": "example1-definitions.ridl" - }, - { - "members": [ - "Empty", - "GetUserRequest" - ], - "path": "example1-partials.ridl" + "go.tag.db": "name" } - ], - "messages": [ - { - "fields": [ - { - "meta": null, - "name": "USER", - "optional": false, - "type": "uint32", - "value": "1" - }, - { - "meta": null, - "name": "ADMIN", - "optional": false, - "type": "uint32", - "value": "2" - } - ], - "name": "Kind", - "type": "enum" - }, - { - "fields": [ - { - "meta": null, - "name": "USER", - "optional": false, - "type": "uint32", - "value": "0" - }, - { - "meta": null, - "name": "ADMIN", - "optional": false, - "type": "uint32", - "value": "1" - } - ], - "name": "Kind2", - "type": "enum" - }, - { - "fields": null, - "name": "Empty", - "type": "struct" - }, + ] + }, + { + "name": "amount", + "type": "uint64", + "optional": true, + "meta": [ { - "fields": [ - { - "meta": null, - "name": "userID", - "optional": false, - "type": "uint64", - "value": "" - } - ], - "name": "GetUserRequest", - "type": "struct" - }, - { - "fields": [ - { - "meta": null, - "name": "name", - "optional": false, - "type": "string", - "value": "" - }, - { - "meta": null, - "name": "users", - "optional": false, - "type": "map", - "value": "" - }, - { - "meta": null, - "name": "perms", - "optional": false, - "type": "[]string", - "value": "" - }, - { - "meta": null, - "name": "other", - "optional": false, - "type": "map>", - "value": "" - } - ], - "name": "Role", - "type": "struct" - }, - { - "fields": [ - { - "meta": [ - { - "json": "id" - }, - { - "go.tag.db": "id" - } - ], - "name": "ID", - "optional": false, - "type": "uint64", - "value": "" - }, - { - "meta": [ - { - "json": "USERNAME" - }, - { - "go.tag.db": "username" - } - ], - "name": "username", - "optional": false, - "type": "string", - "value": "" - }, - { - "meta": [ - { - "go.tag.db": "-" - } - ], - "name": "role", - "optional": false, - "type": "string", - "value": "" - }, - { - "meta": [ - { - "json": "created_at" - }, - { - "go.tag.json": "created_at,omitempty" - }, - { - "go.tag.db": "created_at" - } - ], - "name": "createdAt", - "optional": true, - "type": "timestamp", - "value": "" - } - ], - "name": "User", - "type": "struct" - }, - { - "fields": [ - { - "meta": null, - "name": "msg", - "optional": false, - "type": "string", - "value": "" - } - ], - "name": "Notice", - "type": "struct" + "go.tag.db": "amount" } - ], - "name": "hello-webrpc", - "services": [ + ] + } + ], + "outputs": [ + { + "name": "id", + "type": "uint64", + "optional": false, + "meta": [ { - "methods": [ - { - "inputs": [], - "name": "Ping", - "outputs": [ - { - "name": "status", - "optional": false, - "type": "bool" - } - ] - } - ], - "name": "PingerService" - }, + "go.field.name": "ID" + } + ] + }, + { + "name": "count", + "type": "uint64", + "optional": false, + "meta": [ { - "methods": [ - { - "inputs": [], - "name": "Ping", - "outputs": [ - { - "name": "status", - "optional": false, - "type": "bool" - } - ] - }, - { - "inputs": [ - { - "name": "req", - "optional": false, - "type": "GetUserRequest" - } - ], - "name": "GetUser", - "outputs": [ - { - "name": "user", - "optional": false, - "type": "User" - } - ] - }, - { - "inputs": [ - { - "name": "req", - "optional": false, - "type": "string" - } - ], - "name": "Recv", - "outputs": [], - "streamInput": true - }, - { - "inputs": [], - "name": "Send", - "outputs": [ - { - "name": "resp", - "optional": false, - "type": "string" - } - ], - "streamInput": true - }, - { - "inputs": [ - { - "name": "req", - "optional": false, - "type": "string" - } - ], - "name": "SendAndRecv", - "outputs": [ - { - "name": "resp", - "optional": false, - "type": "string" - } - ], - "streamInput": true, - "streamOutput": true - }, - { - "inputs": [], - "name": "Broadcast", - "outputs": [ - { - "name": "resp", - "optional": false, - "type": "Notice" - } - ], - "streamOutput": true - }, - { - "inputs": [ - { - "name": "seq", - "optional": false, - "type": "int32" - }, - { - "name": "header", - "optional": true, - "type": "map" - }, - { - "name": "ids", - "optional": false, - "type": "[]uint64" - } - ], - "name": "VerifyUsers", - "outputs": [ - { - "name": "code", - "optional": false, - "type": "bool" - }, - { - "name": "ids", - "optional": false, - "type": "[]bool" - } - ] - }, - { - "inputs": [ - { - "name": "n", - "optional": false, - "type": "uint64" - }, - { - "name": "stuff", - "optional": false, - "type": "[]map" - }, - { - "name": "etc", - "optional": false, - "type": "string" - } - ], - "name": "MoreTest", - "outputs": [ - { - "name": "code", - "optional": true, - "type": "bool" - } - ] - } - ], - "name": "ExampleService" + "json": "counter" } - ], - "version": "v0.0.1", - "webrpc": "v1" + ] + } + ] + }, + { + "name": "GetAccountBalance", + "annotations": {}, + "comments": [], + "inputs": [ + { + "name": "name", + "type": "string", + "optional": false + } + ], + "outputs": [ + { + "name": "balance", + "type": "uint64", + "optional": false + } + ] + } + ], + "comments": [] + } + ] } diff --git a/schema/ridl/_example/example1-partials.ridl b/schema/ridl/_example/example1-partials.ridl index 1b18d4aa..bad9e874 100644 --- a/schema/ridl/_example/example1-partials.ridl +++ b/schema/ridl/_example/example1-partials.ridl @@ -18,14 +18,14 @@ enum Kind2: uint32 - ADMIN # aka, = 1 -message Empty +struct Empty -message GetUserRequest +struct GetUserRequest - userID: uint64 -message Role +struct Role - name: string - users: map @@ -35,7 +35,7 @@ message Role - other: map> # comment -message User +struct User - ID: uint64 + json = id + go.tag.db = id @@ -53,7 +53,7 @@ message User + go.tag.db = created_at -message Notice +struct Notice - msg: string diff --git a/schema/ridl/_example/example1.ridl b/schema/ridl/_example/example1.ridl index 37da6fa2..8d579e2d 100644 --- a/schema/ridl/_example/example1.ridl +++ b/schema/ridl/_example/example1.ridl @@ -11,7 +11,8 @@ import example1-partials.ridl - Empty - GetUserRequest -message Role + +struct Role - name: string - users: map @@ -21,7 +22,7 @@ message Role - other: map> # comment -message User +struct User - ID: uint64 + json = id + go.tag.db = id @@ -30,6 +31,8 @@ message User + json = USERNAME + go.tag.db = username + - amount: uint64 + - role: string + go.tag.db = - @@ -39,7 +42,7 @@ message User + go.tag.db = created_at -message Notice +struct Notice - msg: string @@ -62,3 +65,20 @@ service ExampleService - VerifyUsers(seq: int32, header?: map, ids: []uint64) => (code: bool, ids: []bool) - MoreTest(n: uint64, stuff: []map, etc: string) => (code?: bool) + + +struct FlattenRequest + - name: string + + go.tag.db = name + - amount?: uint64 + + go.tag.db = amount + +struct FlattenResponse + - id: uint64 + + go.field.name = ID + - count: uint64 + + json = counter + +service Another + - Flatten(FlattenRequest) => (FlattenResponse) + - GetAccountBalance(name: string) => (balance: uint64) diff --git a/schema/ridl/_example/example2-golden.json b/schema/ridl/_example/example2-golden.json new file mode 100644 index 00000000..40207807 --- /dev/null +++ b/schema/ridl/_example/example2-golden.json @@ -0,0 +1,339 @@ +{ + "webrpc": "v1", + "name": "hello-webrpc", + "version": "v0.0.1", + "types": [ + { + "kind": "struct", + "name": "Empty" + }, + { + "kind": "struct", + "name": "GetUserRequest", + "fields": [ + { + "name": "userID", + "type": "uint64" + } + ] + }, + { + "kind": "struct", + "name": "Role", + "fields": [ + { + "name": "name", + "type": "string" + }, + { + "name": "users", + "type": "map" + }, + { + "comments": [ + "comment" + ], + "name": "perms", + "type": "[]string" + }, + { + "comments": [ + "comment" + ], + "name": "other", + "type": "map>" + } + ] + }, + { + "kind": "struct", + "name": "User", + "fields": [ + { + "name": "ID", + "type": "uint64", + "meta": [ + { + "json": "id" + }, + { + "go.tag.db": "id" + } + ] + }, + { + "name": "username", + "type": "string", + "meta": [ + { + "json": "USERNAME" + }, + { + "go.tag.db": "username" + } + ] + }, + { + "name": "role", + "type": "string", + "meta": [ + { + "go.tag.db": "-" + } + ] + }, + { + "name": "createdAt", + "type": "timestamp", + "optional": true, + "meta": [ + { + "json": "created_at" + }, + { + "go.tag.json": "created_at,omitempty" + }, + { + "go.tag.db": "created_at" + } + ] + } + ] + }, + { + "kind": "struct", + "name": "Notice", + "fields": [ + { + "name": "msg", + "type": "string" + } + ] + } + ], + "errors": [ + { + "code": 1000, + "name": "Unauthorized", + "message": "Unauthorized access", + "httpStatus": 401 + }, + { + "code": 1001, + "name": "PermissionDenied", + "message": "Permission denied", + "httpStatus": 403 + }, + { + "code": 1002, + "name": "SessionExpired", + "message": "Session expired", + "httpStatus": 403 + }, + { + "code": 1003, + "name": "MethodNotFound", + "message": "Method not found", + "httpStatus": 404 + }, + { + "code": 1004, + "name": "RequestConflict", + "message": "Conflict with target resource", + "httpStatus": 409 + }, + { + "code": 1005, + "name": "Aborted", + "message": "Request aborted", + "httpStatus": 400 + }, + { + "code": 1006, + "name": "Geoblocked", + "message": "Geoblocked region", + "httpStatus": 451 + }, + { + "code": 1007, + "name": "RateLimited", + "message": "Rate-limited. Please slow down.", + "httpStatus": 429 + }, + { + "code": 1008, + "name": "ProjectNotFound", + "message": "Project not found", + "httpStatus": 401 + } + ], + "services": [ + { + "name": "ExampleService", + "methods": [ + { + "name": "Ping", + "annotations": {}, + "comments": [ + "comment can go here", + "too .. :)" + ], + "inputs": [], + "outputs": [ + { + "name": "status", + "type": "bool", + "optional": false + } + ] + }, + { + "name": "GetUser", + "annotations": {}, + "comments": [], + "inputs": [ + { + "name": "req", + "type": "GetUserRequest", + "optional": false + } + ], + "outputs": [ + { + "name": "user", + "type": "User", + "optional": false + } + ] + }, + { + "name": "Recv", + "annotations": {}, + "comments": [], + "streamInput": true, + "inputs": [ + { + "name": "req", + "type": "string", + "optional": false + } + ], + "outputs": [] + }, + { + "name": "Send", + "annotations": {}, + "comments": [], + "streamInput": true, + "inputs": [], + "outputs": [ + { + "name": "resp", + "type": "string", + "optional": false + } + ] + }, + { + "name": "SendAndRecv", + "annotations": {}, + "comments": [], + "streamInput": true, + "streamOutput": true, + "inputs": [ + { + "name": "req", + "type": "string", + "optional": false + } + ], + "outputs": [ + { + "name": "resp", + "type": "string", + "optional": false + } + ] + }, + { + "name": "Broadcast", + "annotations": {}, + "comments": [], + "streamOutput": true, + "inputs": [], + "outputs": [ + { + "name": "resp", + "type": "Notice", + "optional": false + } + ] + }, + { + "name": "VerifyUsers", + "annotations": {}, + "comments": [], + "inputs": [ + { + "name": "seq", + "type": "int32", + "optional": false + }, + { + "name": "header", + "type": "map", + "optional": true + }, + { + "name": "ids", + "type": "[]uint64", + "optional": false + } + ], + "outputs": [ + { + "name": "code", + "type": "bool", + "optional": false + }, + { + "name": "ids", + "type": "[]bool", + "optional": false + } + ] + }, + { + "name": "MoreTest", + "annotations": {}, + "comments": [], + "inputs": [ + { + "name": "n", + "type": "uint64", + "optional": false + }, + { + "name": "stuff", + "type": "[]map", + "optional": false + }, + { + "name": "etc", + "type": "string", + "optional": false + } + ], + "outputs": [ + { + "name": "code", + "type": "bool", + "optional": true + } + ] + } + ], + "comments": [] + } + ] +} diff --git a/schema/ridl/_example/example2.ridl b/schema/ridl/_example/example2.ridl index e2fe2647..c91e5758 100644 --- a/schema/ridl/_example/example2.ridl +++ b/schema/ridl/_example/example2.ridl @@ -3,11 +3,13 @@ webrpc = v1 name = hello-webrpc version = v0.0.1 +import ./lib/errors.ridl -import - - ./lib/types.ridl +import example1-partials.ridl + - Empty + - GetUserRequest -message Role +struct Role - name: string - users: map @@ -16,8 +18,7 @@ message Role - other: map> # comment - -message User +struct User - ID: uint64 + json = id + go.tag.db = id @@ -33,12 +34,10 @@ message User + json = created_at + go.tag.json = created_at,omitempty + go.tag.db = created_at - -message Notice +struct Notice - msg: string - service ExampleService # comment can go here @@ -55,6 +54,7 @@ service ExampleService - Broadcast() => stream (resp: Notice) - - VerifyUsers(seq: int32, header?: map, ids: []uint64) => (code: bool, ids: []bool) - + - VerifyUsers(seq: int32, header?: map, ids: []uint64) => (code: bool, ids: []bool) + - MoreTest(n: uint64, stuff: []map, etc: string) => (code?: bool) + diff --git a/schema/ridl/_example/lib/errors.ridl b/schema/ridl/_example/lib/errors.ridl new file mode 100644 index 00000000..f606acf9 --- /dev/null +++ b/schema/ridl/_example/lib/errors.ridl @@ -0,0 +1,14 @@ +webrpc = v1 +name = errors +version = v0.1 + +error 1000 Unauthorized "Unauthorized access" HTTP 401 +error 1001 PermissionDenied "Permission denied" HTTP 403 +error 1002 SessionExpired "Session expired" HTTP 403 +error 1003 MethodNotFound "Method not found" HTTP 404 +error 1004 RequestConflict "Conflict with target resource" HTTP 409 +error 1005 Aborted "Request aborted" HTTP 400 +error 1006 Geoblocked "Geoblocked region" HTTP 451 +error 1007 RateLimited "Rate-limited. Please slow down." HTTP 429 +error 1008 ProjectNotFound "Project not found" HTTP 401 + diff --git a/schema/ridl/_example/lib/messages.ridl b/schema/ridl/_example/lib/structs.ridl similarity index 73% rename from schema/ridl/_example/lib/messages.ridl rename to schema/ridl/_example/lib/structs.ridl index ca973ce6..d6496dda 100644 --- a/schema/ridl/_example/lib/messages.ridl +++ b/schema/ridl/_example/lib/structs.ridl @@ -1,15 +1,14 @@ webrpc = v1 - -name = messages +name = structs version = v0.1 import - ../example1-partials.ridl -message MessageA +struct StructA - name: string - last_name: string -message MessageB +struct StructB - foo: string - bar: string diff --git a/schema/ridl/_example/lib/types.ridl b/schema/ridl/_example/lib/types.ridl index 452dd456..b49600d6 100644 --- a/schema/ridl/_example/lib/types.ridl +++ b/schema/ridl/_example/lib/types.ridl @@ -1,15 +1,9 @@ webrpc = v1 - - - - - name = types - version = v0.1 import - - messages.ridl + - structs.ridl enum TypeA: uint32 - FOO = 1 diff --git a/schema/ridl/definition_parser.go b/schema/ridl/definition_parser.go index 89dc6301..34bc0e4d 100644 --- a/schema/ridl/definition_parser.go +++ b/schema/ridl/definition_parser.go @@ -24,5 +24,6 @@ func parserStateDefinition(p *parser) parserState { return parserStateDefinitionValue(&DefinitionNode{ leftNode: newTokenNode(tokens[0]), + comment: parseComments(p.comments, tokens[0].line), }) } diff --git a/schema/ridl/enum_parser.go b/schema/ridl/enum_parser.go index 80ab53c9..df532d9e 100644 --- a/schema/ridl/enum_parser.go +++ b/schema/ridl/enum_parser.go @@ -11,6 +11,7 @@ func parserStateEnumExplicitValue(en *EnumNode, dn *DefinitionNode) parserState return p.stateError(err) } dn.rightNode = newTokenNode(explicitValue) + en.comment = parseComments(p.comments, explicitValue.line) } en.values = append(en.values, dn) @@ -35,6 +36,7 @@ func parserStateEnumDefinition(et *EnumNode) parserState { return parserStateEnumExplicitValue(et, &DefinitionNode{ leftNode: newTokenNode(matches[2]), + comment: parseComments(p.comments, matches[0].line), }) case tokenNewLine, tokenWhitespace: @@ -68,5 +70,6 @@ func parserStateEnum(p *parser) parserState { name: newTokenNode(matches[2]), enumType: newTokenNode(matches[5]), values: []*DefinitionNode{}, + comment: parseComments(p.comments, matches[0].line), }) } diff --git a/schema/ridl/error_parser.go b/schema/ridl/error_parser.go new file mode 100644 index 00000000..b96c7823 --- /dev/null +++ b/schema/ridl/error_parser.go @@ -0,0 +1,102 @@ +package ridl + +import ( + "fmt" +) + +// error [HTTP ] +func parserStateError(p *parser) parserState { + // error + matches, err := p.match(tokenWord, tokenWhitespace, tokenWord, tokenWhitespace, tokenWord, tokenWhitespace) + if err != nil { + return p.stateError(err) + } + + if matches[0].val != wordError { + return p.stateError(errUnexpectedToken) + } + + if err := expectNumber(matches[2], matches[2].val); err != nil { + return p.stateError(fmt.Errorf("expecting error code to be a number but got '%s'", matches[2].val)) + } + + return parserStateErrorMessage(&ErrorNode{ + code: newTokenNode(matches[2]), + name: newTokenNode(matches[4]), + }) +} + +// [HTTP ] +func parserStateErrorMessage(et *ErrorNode) parserState { + return func(p *parser) parserState { + var err error + tok := p.cursor() + + // message, err := p.expectLiteralValue() + // if err != nil { + // return p.stateError(err) + // } + + switch tok.tt { + + // "" + case tokenQuote: + tok, err = p.expectStringValue() + if err != nil { + return p.stateError(err) + } + et.message = newTokenNode(tok) + + // + case tokenWord: + et.message = newTokenNode(tok) + p.next() + + default: + return p.stateError(fmt.Errorf("expected but got %v", tok)) + } + + return parserStateErrorExplicitStatusCode(et) + } +} + +// [HTTP ] +func parserStateErrorExplicitStatusCode(et *ErrorNode) parserState { + return func(p *parser) parserState { + // Try to match HTTP + matches, err := p.match(tokenWhitespace, tokenWord) + if err != nil { + if err := p.expectOptionalCommentOrEOL(); err != nil { + return p.stateError(err) + } + + p.emit(et) + return parserDefaultState + } + + if err := expectWord(matches[1], "HTTP"); err != nil { + p.rewind(1) + return p.stateError(fmt.Errorf("expecting optional 'HTTP ' but got '%s'", matches[1].val)) + } + + // Match + matches, err = p.match(tokenWhitespace, tokenWord) + if err != nil { + return p.stateError(fmt.Errorf("expecting '': %w", err)) + } + + if err := expectNumber(matches[1], matches[1].val); err != nil { + p.rewind(1) + return p.stateError(fmt.Errorf("expecting HTTP '' to be a number but got '%s'", matches[1].val)) + } + + et.httpStatus = newTokenNode(matches[1]) + + if err := p.expectOptionalCommentOrEOL(); err != nil { + return p.stateError(err) + } + + p.emit(et) + return parserDefaultState + } +} diff --git a/schema/ridl/lexer.go b/schema/ridl/lexer.go index 0ee8e9c3..ca84dbd5 100644 --- a/schema/ridl/lexer.go +++ b/schema/ridl/lexer.go @@ -11,6 +11,7 @@ var ( var ( wordBeginning = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_") wordBreak = []rune("\x00 \t\r\n[]()<>{}=:¿?¡!,\"") + wordNumber = []rune("0123456789") ) type tokenType uint8 @@ -62,6 +63,7 @@ const ( tokenDot // "." tokenQuestionMark // "?" tokenRocket // "=>" + tokenBang // "!" tokenWord // ..wordCharset.. tokenExtra // other @@ -70,6 +72,7 @@ const ( tokenEOL tokenEOF + tokenAt ) const tokenDash = tokenMinusSign @@ -96,32 +99,36 @@ var tokenTypeName = map[tokenType]string{ tokenSlash: "[slash]", tokenQuestionMark: "[question mark]", tokenRocket: "[rocket]", + tokenBang: "[bang]", tokenWord: "[word]", tokenExtra: "[extra]", tokenComposed: "[composed]", tokenEOF: "[EOF]", + tokenAt: "[at]", } var tokenTypeValue = map[tokenType][]rune{ - tokenWhitespace: []rune{' ', '\t', '\r'}, - tokenNewLine: []rune{'\n'}, - tokenEqual: []rune{'='}, - tokenOpenParen: []rune{'('}, - tokenCloseParen: []rune{')'}, - tokenOpenBracket: []rune{'['}, - tokenCloseBracket: []rune{']'}, - tokenOpenAngleBracket: []rune{'<'}, - tokenCloseAngleBracket: []rune{'>'}, - tokenPlusSign: []rune{'+'}, - tokenMinusSign: []rune{'-'}, - tokenHash: []rune{'#'}, - tokenColon: []rune{':'}, - tokenQuote: []rune{'"'}, - tokenBackslash: []rune{'\\'}, - tokenSlash: []rune{'/'}, - tokenComma: []rune{','}, - tokenDot: []rune{'.'}, - tokenQuestionMark: []rune{'?'}, + tokenWhitespace: {' ', '\t', '\r'}, + tokenNewLine: {'\n'}, + tokenEqual: {'='}, + tokenOpenParen: {'('}, + tokenCloseParen: {')'}, + tokenOpenBracket: {'['}, + tokenCloseBracket: {']'}, + tokenOpenAngleBracket: {'<'}, + tokenCloseAngleBracket: {'>'}, + tokenPlusSign: {'+'}, + tokenMinusSign: {'-'}, + tokenHash: {'#'}, + tokenColon: {':'}, + tokenQuote: {'"'}, + tokenBackslash: {'\\'}, + tokenSlash: {'/'}, + tokenComma: {','}, + tokenDot: {'.'}, + tokenQuestionMark: {'?'}, + tokenBang: {'!'}, + tokenAt: {'@'}, } var ( @@ -144,6 +151,8 @@ var ( isBackslash = isTokenType(tokenBackslash) isSlash = isTokenType(tokenSlash) isDot = isTokenType(tokenDot) + isBang = isTokenType(tokenBang) + isAt = isTokenType(tokenAt) ) func isTokenType(tt tokenType) func(r rune) bool { @@ -215,6 +224,10 @@ func lexStateRocket(lx *lexer) lexState { return lexPushTokenState(tokenRocket) } +func lexStateAt(lx *lexer) lexState { + return lexPushTokenState(tokenAt) +} + func lexStateHash(lx *lexer) lexState { return lexPushTokenState(tokenHash) } @@ -307,7 +320,6 @@ func lexDefaultState(lx *lexer) lexState { r := lx.peek() switch { - case isEmpty(r): return nil @@ -371,12 +383,12 @@ func lexDefaultState(lx *lexer) lexState { case isWord(r): return lexStateWord + case isAt(r): + return lexStateAt + default: return lexStateExtra - } - - panic("unreachable") } type lexer struct { diff --git a/schema/ridl/lexer_test.go b/schema/ridl/lexer_test.go index fe48d288..c7b8a1ec 100644 --- a/schema/ridl/lexer_test.go +++ b/schema/ridl/lexer_test.go @@ -1,7 +1,6 @@ package ridl import ( - "strings" "testing" "github.com/stretchr/testify/assert" @@ -533,7 +532,7 @@ func TestLexerSimpleTokens(t *testing.T) { } for _, input := range inputs { - tokens, err := tokenize(strings.NewReader(input.in)) + tokens, _, err := tokenize([]byte(input.in)) assert.NoError(t, err) assert.Equal(t, len(input.out), len(tokens)) @@ -616,7 +615,7 @@ func TestLexerRIDLTokens(t *testing.T) { } for _, input := range inputs { - tokens, err := tokenize(strings.NewReader(input.in)) + tokens, _, err := tokenize([]byte(input.in)) assert.NoError(t, err) for i, tok := range tokens { diff --git a/schema/ridl/mock.go b/schema/ridl/mock.go deleted file mode 100644 index b12f15d4..00000000 --- a/schema/ridl/mock.go +++ /dev/null @@ -1,11 +0,0 @@ -package ridl - -var mockImport bool - -func enableMockImport() { - mockImport = true -} - -func disableMockImport() { - mockImport = false -} diff --git a/schema/ridl/parser.go b/schema/ridl/parser.go index e94b1046..33d2f720 100644 --- a/schema/ridl/parser.go +++ b/schema/ridl/parser.go @@ -3,7 +3,6 @@ package ridl import ( "errors" "fmt" - "io" "log" "reflect" "runtime" @@ -11,10 +10,11 @@ import ( ) const ( + wordError = "error" wordEnum = "enum" wordImport = "import" wordMap = "map" - wordMessage = "message" + wordStruct = "struct" wordName = "name" wordProxy = "proxy" wordService = "service" @@ -38,21 +38,23 @@ type parser struct { length int pos int - words chan interface{} + words chan interface{} + comments map[int]string root RootNode } -func newParser(r io.Reader) (*parser, error) { - tokens, err := tokenize(r) +func newParser(src []byte) (*parser, error) { + tokens, comments, err := tokenize(src) if err != nil { return nil, err } p := &parser{ - words: make(chan interface{}), - tokens: tokens, - length: len(tokens), + words: make(chan interface{}), + tokens: tokens, + length: len(tokens), + comments: comments, } return p, nil } @@ -104,12 +106,16 @@ func (p *parser) run() error { } func (p *parser) continueUntilEOL() error { + words := []string{} + for { tok := p.cursor() switch tok.tt { case tokenNewLine, tokenEOF: return nil + case tokenWord: + words = append(words, tok.String()) } p.next() @@ -148,7 +154,7 @@ func (p *parser) expectOptionalCommentOrEOL() error { func (p *parser) stateError(err error) parserState { cur := p.cursor() - err = fmt.Errorf("parse error: %q near %q (line: %d, col: %d)", err, cur.val, cur.line, cur.col) + err = fmt.Errorf("%d:%d: error near %q: %v", cur.line, cur.col, cur.val, err) p.emit(err) return nil } @@ -390,15 +396,21 @@ func parserStateDeclaration(p *parser) parserState { // import // - [<# comment>] return parserStateImport + case wordError: + // error [HTTP ] + return parserStateError case wordEnum: // enum : // - [=][<#comment>] return parserStateEnum - case wordMessage: - // message + case "message": + // Deprecated in v0.9.0. + return p.stateError(fmt.Errorf("keyword \"message\" was renamed to \"struct\", see https://github.com/webrpc/webrpc/blob/master/CHANGELOG.md#ridl-v090-migration-guide")) + case wordStruct: + // struct // - : // + = - return parserStateMessage + return parserStateStruct case wordService: // service // - ([arguments]) [=> ([arguments])] @@ -499,3 +511,48 @@ func composedValue(tokens []*token) (*token, error) { col: baseToken.col, }, nil } + +func parseComments(comments map[int]string, currentLine int) string { + iteration := 0 + c := []string{} + + if len(comments) == 0 { + return "" + } + + for ; currentLine >= 0; currentLine-- { + comment, ok := comments[currentLine] + if ok { + if !strings.HasPrefix(comment, "!") { + c = append(c, comment) + } + delete(comments, currentLine) + iteration = 0 + + if len(comments) == 0 { + break + } + } + + // if we already found a comment and there is one empty line we don't read more lines + if !ok && len(c) > 0 { + break + } + + iteration++ + + // if there are 2 lines of empty space => no comment we don't read more lines + if iteration > 1 { + break + } + } + + if len(c) > 0 { + // slices.Reverse is introduced with go 1.22 + for i, j := 0, len(c)-1; i < j; i, j = i+1, j-1 { + c[i], c[j] = c[j], c[i] + } + } + + return strings.Join(c, "\n") +} diff --git a/schema/ridl/parser_node.go b/schema/ridl/parser_node.go index 255f6dc6..36dc46aa 100644 --- a/schema/ridl/parser_node.go +++ b/schema/ridl/parser_node.go @@ -9,10 +9,12 @@ const ( DefinitionNodeType ImportNodeType EnumNodeType - MessageNodeType + StructNodeType + ErrorNodeType ArgumentNodeType MethodNodeType ServiceNodeType + AnnotationType ) // Node represents a parser tree node @@ -81,15 +83,26 @@ func (rn RootNode) Imports() []*ImportNode { return importNodes } -func (rn RootNode) Messages() []*MessageNode { - nodes := rn.Filter(MessageNodeType) +func (rn RootNode) Structs() []*StructNode { + nodes := rn.Filter(StructNodeType) - messageNodes := make([]*MessageNode, 0, len(nodes)) + structNodes := make([]*StructNode, 0, len(nodes)) for i := range nodes { - messageNodes = append(messageNodes, nodes[i].(*MessageNode)) + structNodes = append(structNodes, nodes[i].(*StructNode)) } - return messageNodes + return structNodes +} + +func (rn RootNode) Errors() []*ErrorNode { + nodes := rn.Filter(ErrorNodeType) + + errorNodes := make([]*ErrorNode, 0, len(nodes)) + for i := range nodes { + errorNodes = append(errorNodes, nodes[i].(*ErrorNode)) + } + + return errorNodes } func (rn RootNode) Enums() []*EnumNode { @@ -126,7 +139,8 @@ type DefinitionNode struct { optional bool - meta []*DefinitionNode + meta []*DefinitionNode + comment string } func (dn DefinitionNode) Meta() []*DefinitionNode { @@ -155,6 +169,8 @@ func (dn DefinitionNode) Optional() bool { return dn.optional } +func (dn DefinitionNode) Comment() string { return dn.comment } + type TokenNode struct { node @@ -214,6 +230,7 @@ type EnumNode struct { name *TokenNode enumType *TokenNode values []*DefinitionNode + comment string } func (en EnumNode) Type() NodeType { @@ -232,34 +249,55 @@ func (en EnumNode) Values() []*DefinitionNode { return en.values } -type MessageNode struct { +func (en EnumNode) Comments() string { return en.comment } + +type StructNode struct { node - name *TokenNode - fields []*DefinitionNode + name *TokenNode + fields []*DefinitionNode + comment string } -func (mn MessageNode) Name() *TokenNode { +func (mn StructNode) Name() *TokenNode { return mn.name } -func (mn *MessageNode) Type() NodeType { - return MessageNodeType +func (mn *StructNode) Type() NodeType { + return StructNodeType } -func (mn *MessageNode) Fields() []*DefinitionNode { +func (mn *StructNode) Fields() []*DefinitionNode { return mn.fields } +func (mn *StructNode) Comment() string { return mn.comment } + +type ErrorNode struct { + node + + code *TokenNode + name *TokenNode + message *TokenNode + httpStatus *TokenNode +} + +func (en ErrorNode) Type() NodeType { + return ErrorNodeType +} + +func (en ErrorNode) Name() *TokenNode { + return en.name +} + type ArgumentNode struct { node name *TokenNode argumentType *TokenNode + optional bool - optional bool - - stream bool //TODO: should be deprecated + inlineStruct *TokenNode } func (an *ArgumentNode) Name() *TokenNode { @@ -284,13 +322,28 @@ func (an *ArgumentNode) Type() NodeType { return ArgumentNodeType } +type AnnotationNode struct { + annotationType *TokenNode + value *TokenNode +} + +func (a *AnnotationNode) AnnotationType() *TokenNode { + return a.annotationType +} + +func (a *AnnotationNode) Value() *TokenNode { + return a.value +} + type MethodNode struct { name *TokenNode proxy bool - inputs argumentList - outputs argumentList + comment string + annotations []*AnnotationNode + inputs argumentList + outputs argumentList } func (mn *MethodNode) Name() *TokenNode { @@ -317,12 +370,21 @@ func (mn *MethodNode) Outputs() []*ArgumentNode { return mn.outputs.arguments } +func (mn *MethodNode) Comment() string { + return mn.comment +} + +func (mn *MethodNode) Annotations() []*AnnotationNode { + return mn.annotations +} + type ServiceNode struct { node - name *TokenNode - - methods []*MethodNode + name *TokenNode + methods []*MethodNode + methodAnnotations []*AnnotationNode + comment string } func (sn ServiceNode) Type() NodeType { @@ -337,6 +399,8 @@ func (sn ServiceNode) Methods() []*MethodNode { return sn.methods } +func (sn ServiceNode) Comment() string { return sn.comment } + type argumentList struct { stream bool diff --git a/schema/ridl/parser_test.go b/schema/ridl/parser_test.go index 41961ff1..19dc983c 100644 --- a/schema/ridl/parser_test.go +++ b/schema/ridl/parser_test.go @@ -4,10 +4,14 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func TestParserTopLevelDefinitions(t *testing.T) { +func newStringParser(src string) (*parser, error) { + return newParser([]byte(src)) +} +func TestParserTopLevelDefinitions(t *testing.T) { type expectation struct { left string right string @@ -105,6 +109,56 @@ func TestParserTopLevelDefinitions(t *testing.T) { } +func TestParserError(t *testing.T) { + p, err := newStringParser(` + error 12345 InvalidUsername "username is invalid" HTTP 401 + error 12345 InvalidUsername "username is invalid" HTTP 1 # comment + error 12345 InvalidUsername InvalidUsername + error 12345 InvalidUsername InvalidUsername # comment + error 45678 Unauthorized "unauthorized access" HTTP 401 + error 45678 Unauthorized "unauthorized access" HTTP 401 # comment + error 45678 Unauthorized Unauthorized HTTP 401 + error 45678 Unauthorized Unauthorized HTTP 401 # comment + `) + assert.NoError(t, err) + + err = p.run() + assert.NoError(t, err) + + if !assert.Equal(t, 8, len(p.root.Errors())) { + for _, e := range p.root.Errors() { + t.Logf("%v", e.message) + } + } +} + +func TestParserErrorInvalid(t *testing.T) { + tt := []string{ + `error`, + `error WRONG`, + `error 12345`, + `error 12345 Unauthorized`, // missing + `error 12345 Unauthorized unauthorized access`, // missing quotes for multi-word + `error 12345 Unauthorized "unauthorized access" WRONG 401`, + `error 12345 Unauthorized "unauthorized access" HTTP STATUS`, + `error 12345 Unauthorized "unauthorized access" HTTP 401 EXTRA`, + } + + for _, str := range tt { + p, err := newStringParser(str) + assert.NoError(t, err) + + err = p.run() + assert.Error(t, err) + + if !assert.Equal(t, 0, len(p.root.Errors())) { + for _, e := range p.root.Errors() { + t.Logf("%v", e.message) + } + } + } +} + func TestParserImport(t *testing.T) { { p, err := newStringParser(`import`) @@ -154,7 +208,9 @@ func TestParserImport(t *testing.T) { assert.NoError(t, err) assert.NotZero(t, len(p.root.Children())) - assert.Equal(t, "packageName.ridl", p.root.Imports()[0].Path().String()) + if assert.Equal(t, 1, len(p.root.Imports())) { + assert.Equal(t, "packageName.ridl", p.root.Imports()[0].Path().String()) + } } { @@ -167,7 +223,9 @@ func TestParserImport(t *testing.T) { err = p.run() assert.NoError(t, err) - assert.Equal(t, "foo", p.root.Imports()[0].Path().String()) + if assert.Equal(t, 1, len(p.root.Imports())) { + assert.Equal(t, "foo", p.root.Imports()[0].Path().String()) + } } { @@ -182,7 +240,9 @@ func TestParserImport(t *testing.T) { err = p.run() assert.NoError(t, err) - assert.Equal(t, "foo", p.root.Imports()[0].Path().String()) + if assert.Equal(t, 1, len(p.root.Imports())) { + assert.Equal(t, "foo", p.root.Imports()[0].Path().String()) + } } { @@ -191,13 +251,21 @@ func TestParserImport(t *testing.T) { # comment #comment - foo + # comment with spaces + - bar + - baz # # # comment `) assert.NoError(t, err) err = p.run() assert.NoError(t, err) - assert.Equal(t, "foo", p.root.Imports()[0].Path().String()) + imports := p.root.Imports() + if assert.Equal(t, 3, len(imports)) { + assert.Equal(t, "foo", imports[0].Path().String()) + assert.Equal(t, "bar", imports[1].Path().String()) + assert.Equal(t, "baz", imports[2].Path().String()) + } } { @@ -211,9 +279,10 @@ func TestParserImport(t *testing.T) { err = p.run() assert.NoError(t, err) - assert.Equal(t, 1, len(p.root.Imports())) assert.Equal(t, 1, len(p.root.Children())) - assert.Equal(t, "foo.ridl", p.root.Imports()[0].Path().String()) + if assert.Equal(t, 1, len(p.root.Imports())) { + assert.Equal(t, "foo.ridl", p.root.Imports()[0].Path().String()) + } } { @@ -228,6 +297,7 @@ func TestParserImport(t *testing.T) { assert.Error(t, err) assert.Zero(t, len(p.root.Children())) + assert.Zero(t, len(p.root.Imports())) } { @@ -242,7 +312,10 @@ func TestParserImport(t *testing.T) { assert.NoError(t, err) assert.NotZero(t, len(p.root.Children())) - assert.Equal(t, "x", p.root.Imports()[0].Path().String()) + imports := p.root.Imports() + if assert.Equal(t, 1, len(imports)) { + assert.Equal(t, "x", imports[0].Path().String()) + } } { @@ -254,17 +327,21 @@ func TestParserImport(t *testing.T) { - ./path/to/bar.ridl # comment - baz_- #comment - `) + - ../parent.ridl + # comment + `) assert.NoError(t, err) err = p.run() assert.NoError(t, err) - assert.Equal(t, 3, len(p.root.Imports())) - - assert.Equal(t, "foo.ridl", p.root.Imports()[0].Path().String()) - assert.Equal(t, "./path/to/bar.ridl", p.root.Imports()[1].Path().String()) - assert.Equal(t, "baz_-", p.root.Imports()[2].Path().String()) + imports := p.root.Imports() + if assert.Equal(t, 4, len(imports)) { + assert.Equal(t, "foo.ridl", imports[0].Path().String()) + assert.Equal(t, "./path/to/bar.ridl", imports[1].Path().String()) + assert.Equal(t, "baz_-", imports[2].Path().String()) + assert.Equal(t, "../parent.ridl", imports[3].Path().String()) + } } { @@ -296,13 +373,16 @@ func TestParserImport(t *testing.T) { err = p.run() assert.NoError(t, err) - assert.Equal(t, "/path/to/file.ridl", p.root.Imports()[0].Path().String()) - assert.Equal(t, "Member", p.root.Imports()[0].Members()[0].String()) - assert.Equal(t, "Name", p.root.Imports()[0].Members()[1].String()) + imports := p.root.Imports() + if assert.Equal(t, 2, len(imports)) { + assert.Equal(t, "/path/to/file.ridl", imports[0].Path().String()) + assert.Equal(t, "Member", imports[0].Members()[0].String()) + assert.Equal(t, "Name", imports[0].Members()[1].String()) - assert.Equal(t, "/path/to/file2.ridl", p.root.Imports()[1].Path().String()) - assert.Equal(t, "Member", p.root.Imports()[1].Members()[0].String()) - assert.Equal(t, "Name", p.root.Imports()[1].Members()[1].String()) + assert.Equal(t, "/path/to/file2.ridl", imports[1].Path().String()) + assert.Equal(t, "Member", imports[1].Members()[0].String()) + assert.Equal(t, "Name", imports[1].Members()[1].String()) + } } { @@ -315,7 +395,11 @@ func TestParserImport(t *testing.T) { err = p.run() assert.Error(t, err, "expecting import") - assert.Equal(t, "/path /to foo.ridl", p.root.Imports()[0].Path().String()) + + imports := p.root.Imports() + if assert.Equal(t, 1, len(imports)) { + assert.Equal(t, "/path /to foo.ridl", imports[0].Path().String()) + } } { @@ -332,10 +416,14 @@ func TestParserImport(t *testing.T) { err = p.run() assert.NoError(t, err) - assert.Equal(t, "/path /to foo.ridl", p.root.Imports()[0].Path().String()) - assert.Equal(t, "skywalker.ridl", p.root.Imports()[1].Path().String()) - assert.Equal(t, " ", p.root.Imports()[2].Path().String()) - assert.Equal(t, " \nABC \" DEF ", p.root.Imports()[3].Path().String()) + + imports := p.root.Imports() + if assert.Equal(t, 4, len(imports)) { + assert.Equal(t, "/path /to foo.ridl", imports[0].Path().String()) + assert.Equal(t, "skywalker.ridl", imports[1].Path().String()) + assert.Equal(t, " ", imports[2].Path().String()) + assert.Equal(t, " \nABC \" DEF ", imports[3].Path().String()) + } } { @@ -361,10 +449,13 @@ func TestParserImport(t *testing.T) { assert.Equal(t, "webrpc", p.root.Definitions()[0].Left().String()) assert.Equal(t, "v1", p.root.Definitions()[0].Right().String()) - assert.Equal(t, "./users.ridl", p.root.Imports()[0].Path().String()) - assert.Equal(t, "./users # .ridl", p.root.Imports()[1].Path().String()) - assert.Equal(t, "uno", p.root.Imports()[2].Path().String()) - assert.Equal(t, "dos", p.root.Imports()[3].Path().String()) + imports := p.root.Imports() + if assert.Equal(t, 4, len(imports)) { + assert.Equal(t, "./users.ridl", imports[0].Path().String()) + assert.Equal(t, "./users # .ridl", imports[1].Path().String()) + assert.Equal(t, "uno", imports[2].Path().String()) + assert.Equal(t, "dos", imports[3].Path().String()) + } } { @@ -463,13 +554,20 @@ func TestParserEnum(t *testing.T) { err = p.run() assert.NoError(t, err) - assert.Equal(t, "hello", p.root.Imports()[0].Path().String()) - assert.Equal(t, "Value", p.root.Enums()[0].values[0].Left().String()) - assert.Equal(t, "value2", p.root.Enums()[0].values[1].Left().String()) + imports := p.root.Imports() + if assert.Equal(t, 1, len(imports)) { + assert.Equal(t, "hello", imports[0].Path().String()) + } + + enums := p.root.Enums() + if assert.Equal(t, 2, len(enums)) { + assert.Equal(t, "Value", enums[0].values[0].Left().String()) + assert.Equal(t, "value2", enums[0].values[1].Left().String()) - assert.Equal(t, "USER", p.root.Enums()[1].values[0].Left().String()) - assert.Equal(t, "ADMIN", p.root.Enums()[1].values[1].Left().String()) + assert.Equal(t, "USER", enums[1].values[0].Left().String()) + assert.Equal(t, "ADMIN", enums[1].values[1].Left().String()) + } } { @@ -510,36 +608,43 @@ func TestParserEnum(t *testing.T) { err = p.run() assert.NoError(t, err) - assert.Equal(t, "hello", p.root.Imports()[0].Path().String()) - assert.Equal(t, "Value", p.root.Enums()[0].values[0].Left().String()) - assert.Equal(t, "SKY", p.root.Enums()[0].values[0].Right().String()) + imports := p.root.Imports() + if assert.Equal(t, 1, len(imports)) { + assert.Equal(t, "hello", imports[0].Path().String()) + } + + enums := p.root.Enums() + if assert.Equal(t, 4, len(enums)) { + assert.Equal(t, "Value", enums[0].values[0].Left().String()) + assert.Equal(t, "SKY", enums[0].values[0].Right().String()) - assert.Equal(t, "value2", p.root.Enums()[0].values[1].Left().String()) - assert.Equal(t, "WALKER", p.root.Enums()[0].values[1].Right().String()) + assert.Equal(t, "value2", enums[0].values[1].Left().String()) + assert.Equal(t, "WALKER", enums[0].values[1].Right().String()) - assert.Equal(t, "USER", p.root.Enums()[1].values[0].Left().String()) - assert.Equal(t, "ADMIN", p.root.Enums()[1].values[1].Left().String()) + assert.Equal(t, "USER", enums[1].values[0].Left().String()) + assert.Equal(t, "ADMIN", enums[1].values[1].Left().String()) - assert.Equal(t, "USER", p.root.Enums()[3].values[0].Left().String()) - assert.Equal(t, "ADMIN", p.root.Enums()[3].values[1].Left().String()) + assert.Equal(t, "USER", enums[3].values[0].Left().String()) + assert.Equal(t, "ADMIN", enums[3].values[1].Left().String()) + } } } -func TestParserMessage(t *testing.T) { +func TestParserStruct(t *testing.T) { { p, err := newStringParser(` - message + struct `) assert.NoError(t, err) err = p.run() - assert.Error(t, err, "expecting message name") + assert.Error(t, err, "expecting struct name") } { p, err := newStringParser(` - message Empty + struct Empty `) assert.NoError(t, err) @@ -549,7 +654,7 @@ func TestParserMessage(t *testing.T) { { p, err := newStringParser(` - message Role + struct Role - name: string `) assert.NoError(t, err) @@ -557,13 +662,16 @@ func TestParserMessage(t *testing.T) { err = p.run() assert.NoError(t, err) - assert.Equal(t, "name", p.root.Messages()[0].Fields()[0].Left().String()) - assert.Equal(t, "string", p.root.Messages()[0].Fields()[0].Right().String()) + structs := p.root.Structs() + if assert.Equal(t, 1, len(structs)) { + assert.Equal(t, "name", structs[0].Fields()[0].Left().String()) + assert.Equal(t, "string", structs[0].Fields()[0].Right().String()) + } } { p, err := newStringParser(` - message ComplexTypes + struct ComplexTypes - arrayOfStrings: []string - arrayOfArrayOfStrings: [][]string - arrayOfArrayOfArrayOfStrings: [][][]string @@ -581,19 +689,22 @@ func TestParserMessage(t *testing.T) { err = p.run() assert.NoError(t, err) - assert.Equal(t, "[]string", p.root.Messages()[0].Fields()[0].Right().String()) - assert.Equal(t, "[][]string", p.root.Messages()[0].Fields()[1].Right().String()) - assert.Equal(t, "[][][]string", p.root.Messages()[0].Fields()[2].Right().String()) - assert.Equal(t, "map", p.root.Messages()[0].Fields()[3].Right().String()) - assert.Equal(t, "map", p.root.Messages()[0].Fields()[4].Right().String()) - assert.Equal(t, "map", p.root.Messages()[0].Fields()[5].Right().String()) - assert.Equal(t, "map>", p.root.Messages()[0].Fields()[6].Right().String()) - assert.Equal(t, "map,uint64>>", p.root.Messages()[0].Fields()[7].Right().String()) + structFields := p.root.Structs()[0].Fields() + if assert.Equal(t, 8, len(structFields)) { + assert.Equal(t, "[]string", structFields[0].Right().String()) + assert.Equal(t, "[][]string", structFields[1].Right().String()) + assert.Equal(t, "[][][]string", structFields[2].Right().String()) + assert.Equal(t, "map", structFields[3].Right().String()) + assert.Equal(t, "map", structFields[4].Right().String()) + assert.Equal(t, "map", structFields[5].Right().String()) + assert.Equal(t, "map>", structFields[6].Right().String()) + assert.Equal(t, "map,uint64>>", structFields[7].Right().String()) + } } { p, err := newStringParser(` - message Role # comment + struct Role # comment # comment - name: string # comment - age: uint32 # comment @@ -603,16 +714,19 @@ func TestParserMessage(t *testing.T) { err = p.run() assert.NoError(t, err) - assert.Equal(t, "name", p.root.Messages()[0].Fields()[0].Left().String()) - assert.Equal(t, "string", p.root.Messages()[0].Fields()[0].Right().String()) + structFields := p.root.Structs()[0].Fields() + if assert.Equal(t, 2, len(structFields)) { + assert.Equal(t, "name", structFields[0].Left().String()) + assert.Equal(t, "string", structFields[0].Right().String()) - assert.Equal(t, "age", p.root.Messages()[0].Fields()[1].Left().String()) - assert.Equal(t, "uint32", p.root.Messages()[0].Fields()[1].Right().String()) + assert.Equal(t, "age", structFields[1].Left().String()) + assert.Equal(t, "uint32", structFields[1].Right().String()) + } } { p, err := newStringParser(` - message Role + struct Role - name: string # comment + go.tag.db = id # comment @@ -626,22 +740,25 @@ func TestParserMessage(t *testing.T) { err = p.run() assert.NoError(t, err) - assert.Equal(t, "name", p.root.Messages()[0].Fields()[0].Left().String()) - assert.Equal(t, "string", p.root.Messages()[0].Fields()[0].Right().String()) + structFields := p.root.Structs()[0].Fields() + if assert.Equal(t, 1, len(structFields)) { + assert.Equal(t, "name", structFields[0].Left().String()) + assert.Equal(t, "string", structFields[0].Right().String()) - assert.Equal(t, "go.tag.db", p.root.Messages()[0].Fields()[0].Meta()[0].Left().String()) - assert.Equal(t, "id", p.root.Messages()[0].Fields()[0].Meta()[0].Right().String()) + assert.Equal(t, "go.tag.db", structFields[0].Meta()[0].Left().String()) + assert.Equal(t, "id", structFields[0].Meta()[0].Right().String()) - assert.Equal(t, "json", p.root.Messages()[0].Fields()[0].Meta()[1].Left().String()) - assert.Equal(t, "id", p.root.Messages()[0].Fields()[0].Meta()[1].Right().String()) + assert.Equal(t, "json", structFields[0].Meta()[1].Left().String()) + assert.Equal(t, "id", structFields[0].Meta()[1].Right().String()) - assert.Equal(t, "go.tag.json", p.root.Messages()[0].Fields()[0].Meta()[2].Left().String()) - assert.Equal(t, "created_at,omitempty", p.root.Messages()[0].Fields()[0].Meta()[2].Right().String()) + assert.Equal(t, "go.tag.json", structFields[0].Meta()[2].Left().String()) + assert.Equal(t, "created_at,omitempty", structFields[0].Meta()[2].Right().String()) + } } { p, err := newStringParser(` - message User + struct User - ID: uint64 +json = id +go.tag.db = id @@ -661,7 +778,7 @@ func TestParserMessage(t *testing.T) { + go.tag.other = created_at,omitempty - message Notice + struct Notice - msg:string `) assert.NoError(t, err) @@ -669,44 +786,47 @@ func TestParserMessage(t *testing.T) { err = p.run() assert.NoError(t, err) - assert.Equal(t, "ID", p.root.Messages()[0].Fields()[0].Left().String()) - assert.Equal(t, "uint64", p.root.Messages()[0].Fields()[0].Right().String()) + structFields := p.root.Structs()[0].Fields() + if assert.Equal(t, 4, len(structFields)) { + assert.Equal(t, "ID", structFields[0].Left().String()) + assert.Equal(t, "uint64", structFields[0].Right().String()) - assert.Equal(t, "json", p.root.Messages()[0].Fields()[0].Meta()[0].Left().String()) - assert.Equal(t, "id", p.root.Messages()[0].Fields()[0].Meta()[0].Right().String()) + assert.Equal(t, "json", structFields[0].Meta()[0].Left().String()) + assert.Equal(t, "id", structFields[0].Meta()[0].Right().String()) - assert.Equal(t, "go.tag.db", p.root.Messages()[0].Fields()[0].Meta()[1].Left().String()) - assert.Equal(t, "id", p.root.Messages()[0].Fields()[0].Meta()[1].Right().String()) + assert.Equal(t, "go.tag.db", structFields[0].Meta()[1].Left().String()) + assert.Equal(t, "id", structFields[0].Meta()[1].Right().String()) - assert.Equal(t, "username", p.root.Messages()[0].Fields()[1].Left().String()) - assert.Equal(t, "string", p.root.Messages()[0].Fields()[1].Right().String()) + assert.Equal(t, "username", structFields[1].Left().String()) + assert.Equal(t, "string", structFields[1].Right().String()) - assert.Equal(t, "json", p.root.Messages()[0].Fields()[1].Meta()[0].Left().String()) - assert.Equal(t, "USERNAME", p.root.Messages()[0].Fields()[1].Meta()[0].Right().String()) + assert.Equal(t, "json", structFields[1].Meta()[0].Left().String()) + assert.Equal(t, "USERNAME", structFields[1].Meta()[0].Right().String()) - assert.Equal(t, "go.tag.db", p.root.Messages()[0].Fields()[1].Meta()[1].Left().String()) - assert.Equal(t, "username", p.root.Messages()[0].Fields()[1].Meta()[1].Right().String()) + assert.Equal(t, "go.tag.db", structFields[1].Meta()[1].Left().String()) + assert.Equal(t, "username", structFields[1].Meta()[1].Right().String()) - assert.Equal(t, "role", p.root.Messages()[0].Fields()[2].Left().String()) - assert.Equal(t, "string", p.root.Messages()[0].Fields()[2].Right().String()) + assert.Equal(t, "role", structFields[2].Left().String()) + assert.Equal(t, "string", structFields[2].Right().String()) - assert.Equal(t, "go.tag.db", p.root.Messages()[0].Fields()[2].Meta()[0].Left().String()) - assert.Equal(t, "-", p.root.Messages()[0].Fields()[2].Meta()[0].Right().String()) + assert.Equal(t, "go.tag.db", structFields[2].Meta()[0].Left().String()) + assert.Equal(t, "-", structFields[2].Meta()[0].Right().String()) - assert.Equal(t, "createdAt", p.root.Messages()[0].Fields()[3].Left().String()) - assert.Equal(t, "timestamp", p.root.Messages()[0].Fields()[3].Right().String()) + assert.Equal(t, "createdAt", structFields[3].Left().String()) + assert.Equal(t, "timestamp", structFields[3].Right().String()) - assert.Equal(t, "json", p.root.Messages()[0].Fields()[3].Meta()[0].Left().String()) - assert.Equal(t, "created_at", p.root.Messages()[0].Fields()[3].Meta()[0].Right().String()) + assert.Equal(t, "json", structFields[3].Meta()[0].Left().String()) + assert.Equal(t, "created_at", structFields[3].Meta()[0].Right().String()) - assert.Equal(t, "go.tag.json", p.root.Messages()[0].Fields()[3].Meta()[1].Left().String()) - assert.Equal(t, "created_at,omitempty", p.root.Messages()[0].Fields()[3].Meta()[1].Right().String()) + assert.Equal(t, "go.tag.json", structFields[3].Meta()[1].Left().String()) + assert.Equal(t, "created_at,omitempty", structFields[3].Meta()[1].Right().String()) - assert.Equal(t, "go.tag.db", p.root.Messages()[0].Fields()[3].Meta()[2].Left().String()) - assert.Equal(t, "created_at", p.root.Messages()[0].Fields()[3].Meta()[2].Right().String()) + assert.Equal(t, "go.tag.db", structFields[3].Meta()[2].Left().String()) + assert.Equal(t, "created_at", structFields[3].Meta()[2].Right().String()) - assert.Equal(t, "go.tag.other", p.root.Messages()[0].Fields()[3].Meta()[3].Left().String()) - assert.Equal(t, "created_at,omitempty", p.root.Messages()[0].Fields()[3].Meta()[3].Right().String()) + assert.Equal(t, "go.tag.other", structFields[3].Meta()[3].Left().String()) + assert.Equal(t, "created_at,omitempty", structFields[3].Meta()[3].Right().String()) + } } } @@ -769,17 +889,41 @@ func TestParserService(t *testing.T) { } } +func TestParserServiceSuccint(t *testing.T) { + p, err := newStringParser(` + struct FlattenRequest + - name: string + + go.tag.db = name + - amount: uint64 + + go.tag.db = amount + + struct FlattenResponse + - id: uint64 + + go.field.name = ID + - count: uint64 + + json = counter + + service Demo + - DemoService(in: input) => (out: output) + - Flatten(FlattenRequest) => (FlattenResponse) + `) + assert.NoError(t, err) + + err = p.run() + assert.NoError(t, err) +} + func TestParserExamples(t *testing.T) { { p, err := newStringParser(` # contacts.ridl webrpc = v1 - message Contact + struct Contact - id: int - name: string - message Counter + struct Counter - counter: int service ContactsService @@ -799,7 +943,7 @@ func TestParserExamples(t *testing.T) { import "../contacts/proto/contacts.ridl" - message PingResponse + struct PingResponse - pong: string - counter: Counter # Counter is available here from the import @@ -813,3 +957,125 @@ func TestParserExamples(t *testing.T) { assert.NoError(t, err) } } + +func TestParseStructComments(t *testing.T) { + p, err := newStringParser(` + # Defines role in our application + struct Role # => role + # role name line first + # role name line second + # role name line third + - name: string # role name + - perms: []string # permissions + #- codesSubmitted: bool + # + go.tag.db = codes_submitted + + - countryCode: string # ie. US + + go.tag.db = country_code + `) + assert.NoError(t, err) + + err = p.run() + assert.NoError(t, err) + + structNode, ok := p.root.node.children[0].(*StructNode) + if !ok { + t.Errorf("expected type StructNode") + } + + assert.Equal(t, "Defines role in our application\n=> role", structNode.comment) + + assert.Equal(t, "role name line first\nrole name line second\nrole name line third\nrole name", structNode.fields[0].comment) + assert.Equal(t, "permissions", structNode.fields[1].comment) + assert.Equal(t, "ie. US", structNode.fields[2].comment) +} + +func TestParseServiceComments(t *testing.T) { + p, err := newStringParser(` + # Contacts service line 1 + # Contacts service line 2 + service ContactsService # Contacts service line 3 + #! skip this line + # GetContact gives you contact for specific id + # see https://www.example.com/?first=1&second=12#help + - GetContact(id: int) => (contact: Contact) + # Version returns you current deployed version + # + #! skip this line as its internal comment + #! skip more lines + #! more + - Version() => (details: any) + # test comment two lines above + + - Version2() => (details: any) + `) + assert.NoError(t, err) + + err = p.run() + assert.NoError(t, err) + + serviceNode, ok := p.root.node.children[0].(*ServiceNode) + if !ok { + t.Errorf("expected type ServiceNode") + } + + assert.Equal(t, "Contacts service line 1\nContacts service line 2\nContacts service line 3", serviceNode.comment) + assert.Equal(t, "GetContact gives you contact for specific id\nsee https://www.example.com/?first=1&second=12#help", serviceNode.methods[0].comment) + assert.Equal(t, "Version returns you current deployed version\n", serviceNode.methods[1].comment) + assert.Equal(t, "", serviceNode.methods[2].comment) +} + +func TestParseAnnotations(t *testing.T) { + p, err := newStringParser(` + service ContactsService + # Version returns you current deployed version + @auth:x-access-key + @deprecated:Version2 @acl:"admin,member" + @internal + - Version() => (details: any) + @internal + - Version2() => (details: any) + `) + assert.NoError(t, err) + + err = p.run() + assert.NoError(t, err) + + serviceNode, ok := p.root.node.children[0].(*ServiceNode) + if !ok { + t.Errorf("expected type ServiceNode") + } + + require.Len(t, serviceNode.methods[0].annotations, 4) + require.Equal(t, "auth", serviceNode.methods[0].annotations[0].AnnotationType().String()) + require.Equal(t, "x-access-key", serviceNode.methods[0].annotations[0].Value().String()) + + require.Equal(t, "deprecated", serviceNode.methods[0].annotations[1].AnnotationType().String()) + require.Equal(t, "Version2", serviceNode.methods[0].annotations[1].Value().String()) + + require.Equal(t, "acl", serviceNode.methods[0].annotations[2].AnnotationType().String()) + require.Equal(t, "admin,member", serviceNode.methods[0].annotations[2].Value().String()) + + require.Equal(t, "internal", serviceNode.methods[0].annotations[3].AnnotationType().String()) + require.Nil(t, serviceNode.methods[0].annotations[3].Value()) + + require.Len(t, serviceNode.methods[1].annotations, 1) + require.Equal(t, "internal", serviceNode.methods[1].annotations[0].AnnotationType().String()) +} + +func TestFailParsingAnnotationsOnDuplicate(t *testing.T) { + p, err := newStringParser(` + service ContactsService + # Version returns you current deployed version + @auth:x-access-key + @auth:cookies + - Version() => (details: any) + @internal + - Version2() => (details: any) + `) + assert.NoError(t, err) + + err = p.run() + assert.Error(t, err) + assert.ErrorContains(t, err, "duplicate annotation") +} diff --git a/schema/ridl/parser_util.go b/schema/ridl/parser_util.go index 2d53f35e..7bc0b35e 100644 --- a/schema/ridl/parser_util.go +++ b/schema/ridl/parser_util.go @@ -12,6 +12,24 @@ func expectWord(tok *token, value string) error { return nil } +func expectNumber(tok *token, value string) error { + if tok.tt != tokenWord { + return errUnexpectedToken + } + for _, r := range value { + found := false + for _, m := range wordNumber { + if r == m { + found = true + } + } + if !found { + return errUnexpectedToken + } + } + return nil +} + func unescapeString(in string) (string, error) { size := len(in) out := "" diff --git a/schema/ridl/ridl.go b/schema/ridl/ridl.go index 59c39ef5..1f3e6a30 100644 --- a/schema/ridl/ridl.go +++ b/schema/ridl/ridl.go @@ -2,31 +2,36 @@ package ridl import ( "fmt" - "os" - "path/filepath" + "io" + "io/fs" + "path" "strconv" + "strings" "github.com/webrpc/webrpc/schema" ) var ( - schemaMessageTypeEnum = schema.MessageType("enum") - schemaMessageTypeStruct = schema.MessageType("struct") + schemaTypeKindEnum = "enum" + schemaTypeKindStruct = "struct" ) type Parser struct { parent *Parser imports map[string]struct{} - reader *schema.Reader + reader io.Reader + path string + fsys fs.FS } -func NewParser(r *schema.Reader) *Parser { +func NewParser(fsys fs.FS, path string) *Parser { return &Parser{ - reader: r, + fsys: fsys, + path: path, imports: map[string]struct{}{ // this file imports itself - r.File: struct{}{}, + path: {}, }, } } @@ -46,31 +51,30 @@ func (p *Parser) Parse() (*schema.WebRPCSchema, error) { return s, nil } -func (p *Parser) importRIDLFile(path string) (*schema.WebRPCSchema, error) { - if mockImport { - return &schema.WebRPCSchema{}, nil - } - +func (p *Parser) importRIDLFile(filename string) (*schema.WebRPCSchema, error) { for node := p; node != nil; node = node.parent { - if _, imported := node.imports[path]; imported { - return nil, fmt.Errorf("circular import %q in file %q", filepath.Base(path), p.reader.File) + if _, imported := node.imports[filename]; imported { + return nil, fmt.Errorf("circular import %q in file %q", path.Base(filename), p.path) } - node.imports[path] = struct{}{} + node.imports[filename] = struct{}{} } - fp, err := os.Open(path) - if err != nil { - return nil, err - } - defer fp.Close() - - m := NewParser(schema.NewReader(fp, path)) + m := NewParser(p.fsys, filename) m.parent = p return m.Parse() } func (p *Parser) parse() (*schema.WebRPCSchema, error) { - q, err := newParser(p.reader) + if !fs.ValidPath(p.path) { + return nil, fmt.Errorf("invalid fs.FS path %q, see https://pkg.go.dev/io/fs#ValidPath", p.path) + } + + src, err := fs.ReadFile(p.fsys, p.path) + if err != nil { + return nil, fmt.Errorf("failed to read file: %w", err) + } + + q, err := newParser(src) if err != nil { return nil, err } @@ -80,8 +84,8 @@ func (p *Parser) parse() (*schema.WebRPCSchema, error) { } s := &schema.WebRPCSchema{ - Imports: []*schema.Import{}, - Messages: []*schema.Message{}, + Types: []*schema.Type{}, + Errors: []*schema.Error{}, Services: []*schema.Service{}, } @@ -91,15 +95,15 @@ func (p *Parser) parse() (*schema.WebRPCSchema, error) { switch key { case wordWebRPC: - if s.WebRPCVersion != "" { + if s.WebrpcVersion != "" { return nil, fmt.Errorf(`webrpc was previously declared`) } - s.WebRPCVersion = value + s.WebrpcVersion = value case wordName: - if s.Name != "" { + if s.SchemaName != "" { return nil, fmt.Errorf(`name was previously declared`) } - s.Name = value + s.SchemaName = value case wordVersion: if s.SchemaVersion != "" { return nil, fmt.Errorf(`version was previously declared`) @@ -112,64 +116,67 @@ func (p *Parser) parse() (*schema.WebRPCSchema, error) { // imports for _, line := range q.root.Imports() { - importPath := filepath.Join(filepath.Dir(p.reader.File), line.Path().String()) + importPath := path.Join(path.Dir(p.path), line.Path().String()) - importDef := &schema.Import{ - Path: importPath, - Members: []string{}, + imported, err := p.importRIDLFile(importPath) + if err != nil { + return nil, p.trace(err, line.Path()) } + + members := []string{} for _, member := range line.Members() { - importDef.Members = append(importDef.Members, member.String()) + members = append(members, member.String()) } - imported, err := p.importRIDLFile(importDef.Path) - if err != nil { - return nil, p.trace(err, line.Path()) + for i := range imported.Types { + if isImportAllowed(imported.Types[i].Name, members) { + s.Types = append(s.Types, imported.Types[i]) + } } - - for i := range imported.Messages { - if isImportAllowed(string(imported.Messages[i].Name), importDef.Members) { - s.Messages = append(s.Messages, imported.Messages[i]) + for i := range imported.Errors { + if isImportAllowed(imported.Errors[i].Name, members) { + s.Errors = append(s.Errors, imported.Errors[i]) } } for i := range imported.Services { - if isImportAllowed(string(imported.Services[i].Name), importDef.Members) { + if isImportAllowed(imported.Services[i].Name, members) { s.Services = append(s.Services, imported.Services[i]) } } - - s.Imports = append(s.Imports, importDef) } // pushing enums (1st pass) for _, line := range q.root.Enums() { - s.Messages = append(s.Messages, &schema.Message{ - Name: schema.VarName(line.Name().String()), - Type: schemaMessageTypeEnum, - Fields: []*schema.MessageField{}, + s.Types = append(s.Types, &schema.Type{ + Kind: schemaTypeKindEnum, + Name: line.Name().String(), + Fields: []*schema.TypeField{}, }) } - // pushing messages (1st pass) - for _, line := range q.root.Messages() { - s.Messages = append(s.Messages, &schema.Message{ - Name: schema.VarName(line.Name().String()), - Type: schemaMessageTypeStruct, + // pushing types (1st pass) + for _, line := range q.root.Structs() { + s.Types = append(s.Types, &schema.Type{ + Kind: schemaTypeKindStruct, + Name: line.Name().String(), + Comments: parseComment(line.Comment()), }) } // pushing services (1st pass) for _, service := range q.root.Services() { - // push service - s.Services = append(s.Services, &schema.Service{ - Name: schema.VarName(service.Name().String()), - }) + srv := &schema.Service{ + Name: service.Name().String(), + Comments: parseComment(service.Comment()), + } + + s.Services = append(s.Services, srv) } // enum fields for _, line := range q.root.Enums() { - name := schema.VarName(line.Name().String()) - enumDef := s.GetMessageByName(string(name)) + name := line.Name().String() + enumDef := s.GetTypeByName(name) if enumDef == nil { return nil, fmt.Errorf("unexpected error, could not find definition for: %v", name) @@ -178,32 +185,50 @@ func (p *Parser) parse() (*schema.WebRPCSchema, error) { var enumType schema.VarType err := schema.ParseVarTypeExpr(s, line.TypeName().String(), &enumType) if err != nil { - return nil, fmt.Errorf("unknown data type: %v", line.TypeName()) + return nil, fmt.Errorf("enum %q: unknown type: %v", name, line.TypeName()) } + enumDef.Type = &enumType for i, def := range line.Values() { key, val := def.Left().String(), def.Right().String() - if val == "" { val = strconv.Itoa(i) } - enumDef.Fields = append(enumDef.Fields, &schema.MessageField{ - Name: schema.VarName(key), - Type: &enumType, - Value: val, - }) + elems := &schema.TypeField{ + Name: key, + TypeExtra: schema.TypeExtra{ + Value: val, + }, + Comments: parseComment(def.Comment()), + } + + enumDef.Fields = append(enumDef.Fields, elems) } + } - enumDef.EnumType = &enumType + // error types + for _, line := range q.root.Errors() { + var errorType schema.Error + code, _ := strconv.ParseInt(line.code.String(), 10, 32) + errorType.Code = int(code) + errorType.Name = line.name.String() + errorType.Message = line.message.String() + if line.httpStatus != nil { + httpStatus, _ := strconv.ParseInt(line.httpStatus.String(), 10, 32) + errorType.HTTPStatus = int(httpStatus) + } else { + errorType.HTTPStatus = 400 // Default HTTP status code + } + s.Errors = append(s.Errors, &errorType) } - // message fields - for _, line := range q.root.Messages() { - name := schema.VarName(line.Name().String()) - messageDef := s.GetMessageByName(string(name)) + // struct fields + for _, line := range q.root.Structs() { + name := line.Name().String() + structDef := s.GetTypeByName(string(name)) - if messageDef == nil { + if structDef == nil { return nil, fmt.Errorf("unexpected error, could not find definition for: %v", name) } @@ -213,21 +238,24 @@ func (p *Parser) parse() (*schema.WebRPCSchema, error) { var varType schema.VarType err := schema.ParseVarTypeExpr(s, fieldType, &varType) if err != nil { - return nil, fmt.Errorf("unknown data type: %v", fieldType) + return nil, fmt.Errorf("struct %q: unknown type of field %q: %v", name, fieldName, fieldType) } - field := &schema.MessageField{ - Name: schema.VarName(fieldName), - Optional: def.Optional(), - Type: &varType, + field := &schema.TypeField{ + Name: fieldName, + Type: &varType, + TypeExtra: schema.TypeExtra{ + Optional: def.Optional(), + }, + Comments: parseComment(def.Comment()), } for _, meta := range def.Meta() { key, val := meta.Left().String(), meta.Right().String() - field.Meta = append(field.Meta, schema.MessageFieldMeta{ + field.Meta = append(field.Meta, schema.TypeFieldMeta{ key: val, }) } - messageDef.Fields = append(messageDef.Fields, field) + structDef.Fields = append(structDef.Fields, field) } } @@ -236,7 +264,6 @@ func (p *Parser) parse() (*schema.WebRPCSchema, error) { methods := []*schema.Method{} for _, method := range service.Methods() { - inputs, err := buildArgumentsList(s, method.Inputs()) if err != nil { return nil, err @@ -247,14 +274,18 @@ func (p *Parser) parse() (*schema.WebRPCSchema, error) { return nil, err } - // push method - methods = append(methods, &schema.Method{ - Name: schema.VarName(method.Name().String()), + // push m + m := &schema.Method{ + Name: method.Name().String(), StreamInput: method.StreamInput(), StreamOutput: method.StreamOutput(), Inputs: inputs, Outputs: outputs, - }) + Comments: parseComment(method.Comment()), + Annotations: buildAnnotations(method), + } + + methods = append(methods, m) } serviceDef := s.GetServiceByName(service.Name().String()) @@ -269,7 +300,7 @@ func (p *Parser) trace(err error, tok *TokenNode) error { "%v\nnear string %q\n\tfrom %v:%d:%d", err, tok.tok.val, - p.reader.File, + p.path, tok.tok.line, tok.tok.col, ) @@ -290,22 +321,69 @@ func isImportAllowed(name string, whitelist []string) bool { func buildArgumentsList(s *schema.WebRPCSchema, args []*ArgumentNode) ([]*schema.MethodArgument, error) { output := []*schema.MethodArgument{} - for _, arg := range args { + // succint form + if len(args) == 1 && args[0].inlineStruct != nil { + node := args[0].inlineStruct + structName := node.tok.val + + typ := s.GetTypeByName(structName) + if typ.Kind != "struct" { + return nil, fmt.Errorf("expecting struct type for inline definition of '%s'", structName) + } + for _, arg := range typ.Fields { + methodArgument := &schema.MethodArgument{ + Name: arg.Name, + Type: arg.Type, + Optional: arg.Optional, + TypeExtra: arg.TypeExtra, + } + output = append(output, methodArgument) + } + + return output, nil + } + + // normal form + for _, arg := range args { var varType schema.VarType err := schema.ParseVarTypeExpr(s, arg.TypeName().String(), &varType) if err != nil { - return nil, fmt.Errorf("unknown data type: %v", arg.TypeName().String()) + return nil, fmt.Errorf("parsing argument %v: %w", arg.TypeName(), err) } methodArgument := &schema.MethodArgument{ - Name: schema.VarName(arg.Name().String()), + Name: arg.Name().String(), Type: &varType, Optional: arg.Optional(), } - output = append(output, methodArgument) } return output, nil } + +func parseComment(comment string) []string { + if comment == "" { + return []string{} + } + + return strings.Split(comment, "\n") +} + +func buildAnnotations(method *MethodNode) schema.Annotations { + annotations := make(map[string]*schema.Annotation) + + for _, a := range method.Annotations() { + an := &schema.Annotation{ + AnnotationType: a.AnnotationType().String(), + } + if a.Value() != nil { + an.Value = a.Value().String() + } + + annotations[a.AnnotationType().String()] = an + } + + return schema.Annotations(annotations) +} diff --git a/schema/ridl/ridl_test.go b/schema/ridl/ridl_test.go index 14de7419..8befae4d 100644 --- a/schema/ridl/ridl_test.go +++ b/schema/ridl/ridl_test.go @@ -1,35 +1,26 @@ package ridl import ( - "bytes" - "encoding/json" - "fmt" - "io/ioutil" + "flag" + "io" "os" - "strings" "testing" + "testing/fstest" + "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" "github.com/webrpc/webrpc/schema" ) -func newStringParser(s string) (*parser, error) { - return newParser(strings.NewReader(s)) -} - -func parseString(s string) (*schema.WebRPCSchema, error) { - return NewParser(schema.NewReader(strings.NewReader(s), "./main.ridl")).Parse() -} +var updateFlag = flag.String("update", "", "update golden file to match tests' current behavior") -func compactJSON(src []byte) string { - buf := bytes.NewBuffer(nil) - - err := json.Compact(buf, src) - if err != nil { - panic(fmt.Sprintf("json.Compact: %v", err)) +func parseString(src string) (*schema.WebRPCSchema, error) { + fsys := fstest.MapFS{ + "main.ridl": { + Data: []byte(src), + }, } - - return buf.String() + return NewParser(fsys, "main.ridl").Parse() } func TestRIDLHeader(t *testing.T) { @@ -63,60 +54,89 @@ func TestRIDLHeader(t *testing.T) { s, err := parseString(buf) assert.NoError(t, err) - assert.Equal(t, "v1", s.WebRPCVersion) - assert.Equal(t, "h_ello-webrpc", s.Name) + assert.Equal(t, "v1", s.WebrpcVersion) + assert.Equal(t, "h_ello-webrpc", s.SchemaName) assert.Equal(t, "v0.1.1", s.SchemaVersion) } } -func TestRIDLImport(t *testing.T) { - enableMockImport() - defer disableMockImport() - - { - input := ` - webrpc = v1 - version = v0.1.1 - name = hello-webrpc - - import - - foo # ko ment - # ko ment - - - bar - # comment - ` - - s, err := parseString(input) - assert.NoError(t, err) - - assert.Equal(t, "v1", s.WebRPCVersion) - assert.Equal(t, "hello-webrpc", s.Name) - assert.Equal(t, "v0.1.1", s.SchemaVersion) - - assert.Equal(t, "foo", s.Imports[0].Path) - assert.Equal(t, "bar", s.Imports[1].Path) +func TestRIDLImports(t *testing.T) { + fsys := fstest.MapFS{ + "schema/import-service.ridl": {Data: []byte(` + webrpc = v1 + version = v0.1.1 + name = ImportService + + import + - types.ridl + `)}, + "schema/types.ridl": {Data: []byte(` + webrpc = v1 + version = v1.0.0 + name = types + + import + - foo.ridl # import from current directory + - subdir/bar.ridl # import from subdirectory + - ../common.ridl # import from parent directory + + struct ExtraType + - name: string + + error 1000 Unauthorized "Unauthorized access" HTTP 401 + `)}, + "schema/foo.ridl": {Data: []byte(` + webrpc = v1 + version = v0.8.0 + name = foo + + struct Foo + - name: string + + error 2000 FooError "Foo, not enough access" HTTP 403 + `)}, + "schema/subdir/bar.ridl": {Data: []byte(` + webrpc = v1 + version = v0.8.0 + name = bar + + struct Bar + - name: string + + struct Baz + - name: string + + error 3000 BarError "The bar is too high" HTTP 400 + `)}, + "common.ridl": {Data: []byte(` + webrpc = v1 + version = v1.0.0 + name = common + + struct Common + - name: string + `)}, } - { - input := ` - webrpc = v1 - version = v0.1.1 # version number - name = hello-webrpc + s, err := NewParser(fsys, "schema/import-service.ridl").Parse() + assert.NoError(t, err) - import # import line - - foo1 # foo-comment with spaces - - bar2 # # # bar-comment - ` - s, err := parseString(input) - assert.NoError(t, err) + assert.Equal(t, "v1", s.WebrpcVersion) + assert.Equal(t, "ImportService", s.SchemaName) + assert.Equal(t, "v0.1.1", s.SchemaVersion) - assert.Equal(t, "v1", s.WebRPCVersion) - assert.Equal(t, "hello-webrpc", s.Name) - assert.Equal(t, "v0.1.1", s.SchemaVersion) + if assert.Equal(t, 5, len(s.Types)) { + assert.Equal(t, "Foo", string(s.Types[0].Name)) + assert.Equal(t, "Bar", string(s.Types[1].Name)) + assert.Equal(t, "Baz", string(s.Types[2].Name)) + assert.Equal(t, "Common", string(s.Types[3].Name)) + assert.Equal(t, "ExtraType", string(s.Types[4].Name)) + } - assert.Equal(t, "foo1", s.Imports[0].Path) - assert.Equal(t, "bar2", s.Imports[1].Path) + if assert.Equal(t, 3, len(s.Errors)) { + assert.Equal(t, "FooError", string(s.Errors[0].Name)) + assert.Equal(t, "BarError", string(s.Errors[1].Name)) + assert.Equal(t, "Unauthorized", string(s.Errors[2].Name)) } } @@ -142,49 +162,123 @@ func TestRIDLEnum(t *testing.T) { s, err := parseString(input) assert.NoError(t, err) - assert.Equal(t, "v1", s.WebRPCVersion) - assert.Equal(t, "hello-webrpc", s.Name) + assert.Equal(t, "v1", s.WebrpcVersion) + assert.Equal(t, "hello-webrpc", s.SchemaName) assert.Equal(t, "v0.1.1", s.SchemaVersion) - assert.Equal(t, "Kind", string(s.Messages[0].Name)) - assert.Equal(t, "enum", string(s.Messages[0].Type)) + assert.Equal(t, "Kind", string(s.Types[0].Name)) + assert.Equal(t, "enum", string(s.Types[0].Kind)) + assert.Equal(t, "uint32", string(s.Types[0].Type.String())) + + assert.Equal(t, "USER", string(s.Types[0].Fields[0].Name)) + assert.Equal(t, "ADMIN", string(s.Types[0].Fields[1].Name)) + + assert.Equal(t, "33", string(s.Types[0].Fields[0].Value)) + assert.Equal(t, "44", string(s.Types[0].Fields[1].Value)) + + assert.Equal(t, (*schema.VarType)(nil), s.Types[0].Fields[0].Type) + assert.Equal(t, (*schema.VarType)(nil), s.Types[0].Fields[1].Type) + + assert.Equal(t, "KindTwo", string(s.Types[1].Name)) + assert.Equal(t, "enum", string(s.Types[1].Kind)) + assert.Equal(t, "uint32", string(s.Types[1].Type.String())) + + assert.Equal(t, (*schema.VarType)(nil), s.Types[1].Fields[0].Type) + assert.Equal(t, (*schema.VarType)(nil), s.Types[1].Fields[1].Type) + assert.Equal(t, (*schema.VarType)(nil), s.Types[1].Fields[2].Type) + + assert.Equal(t, "0", string(s.Types[1].Fields[0].Value)) + assert.Equal(t, "1", string(s.Types[1].Fields[1].Value)) + assert.Equal(t, "2", string(s.Types[1].Fields[2].Value)) + } +} - assert.Equal(t, "USER", string(s.Messages[0].Fields[0].Name)) - assert.Equal(t, "ADMIN", string(s.Messages[0].Fields[1].Name)) +func TestRIDLErrors(t *testing.T) { + { + input := ` + webrpc = v1 + version = v0.1.1 + name = webrpc-errors + + error 500100 MissingArgument "missing argument" + error 500101 InvalidUsername "invalid username" + error 400100 MemoryFull "system memory is full" + error 400200 Unauthorized "Unauthorized" HTTP 401 + error 400300 UserNotFound "user not found" + ` + s, err := parseString(input) + assert.NoError(t, err) - assert.Equal(t, "33", string(s.Messages[0].Fields[0].Value)) - assert.Equal(t, "44", string(s.Messages[0].Fields[1].Value)) + if assert.NotNil(t, s) && assert.Equal(t, 5, len(s.Errors)) { + assert.Equal(t, 500100, s.Errors[0].Code) + assert.Equal(t, 500101, s.Errors[1].Code) + assert.Equal(t, 400100, s.Errors[2].Code) + assert.Equal(t, 400200, s.Errors[3].Code) + assert.Equal(t, 400300, s.Errors[4].Code) + + assert.Equal(t, "MissingArgument", s.Errors[0].Name) + assert.Equal(t, "InvalidUsername", s.Errors[1].Name) + assert.Equal(t, "MemoryFull", s.Errors[2].Name) + assert.Equal(t, "Unauthorized", s.Errors[3].Name) + assert.Equal(t, "UserNotFound", s.Errors[4].Name) + + assert.Equal(t, "missing argument", s.Errors[0].Message) + assert.Equal(t, "invalid username", s.Errors[1].Message) + assert.Equal(t, "system memory is full", s.Errors[2].Message) + assert.Equal(t, "Unauthorized", s.Errors[3].Message) + assert.Equal(t, "user not found", s.Errors[4].Message) + + assert.Equal(t, 400, s.Errors[0].HTTPStatus) + assert.Equal(t, 400, s.Errors[1].HTTPStatus) + assert.Equal(t, 400, s.Errors[2].HTTPStatus) + assert.Equal(t, 401, s.Errors[3].HTTPStatus) + assert.Equal(t, 400, s.Errors[4].HTTPStatus) + } + } - assert.Equal(t, "uint32", string(s.Messages[0].Fields[0].Type.String())) - assert.Equal(t, "uint32", string(s.Messages[0].Fields[1].Type.String())) + { + input := ` + webrpc = v1 + version = v0.1.1 + name = webrpc-errors - assert.Equal(t, "KindTwo", string(s.Messages[1].Name)) - assert.Equal(t, "enum", string(s.Messages[1].Type)) + error 500100 MissingArgument "missing argument" + error 500100 InvalidUsername "invalid username" # duplicated error code + ` + s, err := parseString(input) + assert.Error(t, err) + assert.Nil(t, s) + } - assert.Equal(t, "uint32", string(s.Messages[1].Fields[0].Type.String())) - assert.Equal(t, "uint32", string(s.Messages[1].Fields[1].Type.String())) - assert.Equal(t, "uint32", string(s.Messages[1].Fields[2].Type.String())) + { + input := ` + webrpc = v1 + version = v0.1.1 + name = webrpc-errors - assert.Equal(t, "0", string(s.Messages[1].Fields[0].Value)) - assert.Equal(t, "1", string(s.Messages[1].Fields[1].Value)) - assert.Equal(t, "2", string(s.Messages[1].Fields[2].Value)) + error 500100 MissingArgument "missing argument 1" + error 500101 MissingArgument "missing argument 2" # duplicated error name + ` + s, err := parseString(input) + assert.Error(t, err) + assert.Nil(t, s) } } -func TestRIDLMessages(t *testing.T) { +func TestRIDLTypes(t *testing.T) { { input := ` webrpc = v1 version = v0.1.1 name = hello-webrpc - message Empty + struct Empty ` s, err := parseString(input) assert.NoError(t, err) - assert.Equal(t, "Empty", string(s.Messages[0].Name)) - assert.Equal(t, "struct", string(s.Messages[0].Type)) + assert.Equal(t, "struct", string(s.Types[0].Kind)) + assert.Equal(t, "Empty", string(s.Types[0].Name)) } { @@ -193,13 +287,13 @@ func TestRIDLMessages(t *testing.T) { version = v0.1.1 name = hello-webrpc - message Empty # with a, comment + struct Empty # with a, comment ` s, err := parseString(input) assert.NoError(t, err) - assert.Equal(t, "Empty", string(s.Messages[0].Name)) - assert.Equal(t, "struct", string(s.Messages[0].Type)) + assert.Equal(t, "struct", string(s.Types[0].Kind)) + assert.Equal(t, "Empty", string(s.Types[0].Name)) } @@ -209,23 +303,23 @@ func TestRIDLMessages(t *testing.T) { version = v0.1.1 name = hello-webrpc - message Simple # with a, comment + struct Simple # with a, comment - ID: uint32 - Value?: uint32 ` s, err := parseString(input) assert.NoError(t, err) - assert.Equal(t, "Simple", string(s.Messages[0].Name)) - assert.Equal(t, "struct", string(s.Messages[0].Type)) + assert.Equal(t, "struct", string(s.Types[0].Kind)) + assert.Equal(t, "Simple", string(s.Types[0].Name)) - assert.Equal(t, "ID", string(s.Messages[0].Fields[0].Name)) - assert.Equal(t, "uint32", string(s.Messages[0].Fields[0].Type.String())) - assert.Equal(t, false, s.Messages[0].Fields[0].Optional) + assert.Equal(t, "ID", string(s.Types[0].Fields[0].Name)) + assert.Equal(t, "uint32", string(s.Types[0].Fields[0].Type.String())) + assert.Equal(t, false, s.Types[0].Fields[0].Optional) - assert.Equal(t, "Value", string(s.Messages[0].Fields[1].Name)) - assert.Equal(t, "uint32", string(s.Messages[0].Fields[1].Type.String())) - assert.Equal(t, true, s.Messages[0].Fields[1].Optional) + assert.Equal(t, "Value", string(s.Types[0].Fields[1].Name)) + assert.Equal(t, "uint32", string(s.Types[0].Fields[1].Type.String())) + assert.Equal(t, true, s.Types[0].Fields[1].Optional) } { @@ -234,7 +328,7 @@ func TestRIDLMessages(t *testing.T) { version = v0.1.1 name = hello-webrpc - message Simple # with a-comment an,d meta fields + struct Simple # with a-comment an,d meta fields - ID: uint32 - Field2: uint64 # one two #t + json = field_2 # a comment @@ -245,25 +339,25 @@ func TestRIDLMessages(t *testing.T) { + go.tag.db = - # omits the field from db - message Simple2 # with a-comment an,d meta fields + struct Simple2 # with a-comment an,d meta fields ` s, err := parseString(input) assert.NoError(t, err) - assert.Equal(t, "Simple", string(s.Messages[0].Name)) - assert.Equal(t, "struct", string(s.Messages[0].Type)) + assert.Equal(t, "struct", string(s.Types[0].Kind)) + assert.Equal(t, "Simple", string(s.Types[0].Name)) - assert.Equal(t, "Simple2", string(s.Messages[1].Name)) - assert.Equal(t, "struct", string(s.Messages[1].Type)) + assert.Equal(t, "struct", string(s.Types[1].Kind)) + assert.Equal(t, "Simple2", string(s.Types[1].Name)) - assert.Equal(t, "ID", string(s.Messages[0].Fields[0].Name)) - assert.Equal(t, "Field2", string(s.Messages[0].Fields[1].Name)) - assert.Equal(t, "Field3", string(s.Messages[0].Fields[2].Name)) + assert.Equal(t, "ID", string(s.Types[0].Fields[0].Name)) + assert.Equal(t, "Field2", string(s.Types[0].Fields[1].Name)) + assert.Equal(t, "Field3", string(s.Types[0].Fields[2].Name)) - assert.Equal(t, "field_2", s.Messages[0].Fields[1].Meta[0]["json"]) - assert.Equal(t, "field_3", s.Messages[0].Fields[1].Meta[1]["go.tag.db"]) + assert.Equal(t, "field_2", s.Types[0].Fields[1].Meta[0]["json"]) + assert.Equal(t, "field_3", s.Types[0].Fields[1].Meta[1]["go.tag.db"]) - assert.Equal(t, "-", s.Messages[0].Fields[2].Meta[0]["go.tag.db"]) + assert.Equal(t, "-", s.Types[0].Fields[2].Meta[0]["go.tag.db"]) } { @@ -272,26 +366,26 @@ func TestRIDLMessages(t *testing.T) { version = v0.1.1 name = hello-webrpc - message Simple # with a-comment an,d meta fields + struct Simple # with a-comment an,d meta fields - ID: uint32 - Field2: map # one two #t + json = field_2 # a comment + go.tag.db = field_2 - Field3: []bool # one two #t - + json = field_2 # a comment - + go.tag.db = field_2 + + json = field_3 # a comment + + go.tag.db = field_3 - Field4: [][][]bool # one two #t - + json = field_2 # a comment - + go.tag.db = field_2 + + json = field_4 # a comment + + go.tag.db = field_4 - message Simple2 # with a-comment an,d meta fields + struct Simple2 # with a-comment an,d meta fields ` s, err := parseString(input) assert.NoError(t, err) - assert.Equal(t, "map", string(s.Messages[0].Fields[1].Type.String())) - assert.Equal(t, "[]bool", string(s.Messages[0].Fields[2].Type.String())) - assert.Equal(t, "[][][]bool", string(s.Messages[0].Fields[3].Type.String())) + assert.Equal(t, "map", string(s.Types[0].Fields[1].Type.String())) + assert.Equal(t, "[]bool", string(s.Types[0].Fields[2].Type.String())) + assert.Equal(t, "[][][]bool", string(s.Types[0].Fields[3].Type.String())) } { @@ -300,7 +394,7 @@ func TestRIDLMessages(t *testing.T) { version = v0.1.1 name = hello-webrpc - message Simple # with a-comment an,d meta fields + struct Simple # with a-comment an,d meta fields - ID: uint32 - Field2: map # one two #t + json = field_2 # a comment @@ -313,12 +407,12 @@ func TestRIDLMessages(t *testing.T) { s, err := parseString(input) assert.NoError(t, err) - assert.Equal(t, "map", string(s.Messages[0].Fields[1].Type.String())) - assert.Equal(t, "field_2", s.Messages[0].Fields[1].Meta[1]["go.tag.db"]) - assert.Equal(t, "default**:**now**()**,use_zero#000", s.Messages[0].Fields[1].Meta[2]["go.tag.db.1"]) - assert.Equal(t, `default**:**now**()**,use_zero,"//`, s.Messages[0].Fields[1].Meta[3]["go.tag.db.2"]) - assert.Equal(t, "default**:**now**()**,use_zero,// # not a comment", s.Messages[0].Fields[1].Meta[4]["go.tag.db.3"]) - assert.Equal(t, "default**:**now**()**,use_zero", s.Messages[0].Fields[1].Meta[5]["go.tag.db.4"]) + assert.Equal(t, "map", string(s.Types[0].Fields[1].Type.String())) + assert.Equal(t, "field_2", s.Types[0].Fields[1].Meta[1]["go.tag.db"]) + assert.Equal(t, "default**:**now**()**,use_zero#000", s.Types[0].Fields[1].Meta[2]["go.tag.db.1"]) + assert.Equal(t, `default**:**now**()**,use_zero,"//`, s.Types[0].Fields[1].Meta[3]["go.tag.db.2"]) + assert.Equal(t, "default**:**now**()**,use_zero,// # not a comment", s.Types[0].Fields[1].Meta[4]["go.tag.db.3"]) + assert.Equal(t, "default**:**now**()**,use_zero", s.Types[0].Fields[1].Meta[5]["go.tag.db.4"]) } } @@ -414,116 +508,66 @@ func TestRIDLParse(t *testing.T) { fp, err := os.Open("_example/example0.ridl") assert.NoError(t, err) - buf, err := ioutil.ReadAll(fp) + buf, err := io.ReadAll(fp) assert.NoError(t, err) s, err := parseString(string(buf)) assert.NoError(t, err) - jout, err := s.ToJSON(true) + jout, err := s.ToJSON() assert.NoError(t, err) assert.NotZero(t, jout) } -func TestRIDLTables(t *testing.T) { - enableMockImport() - defer disableMockImport() - - table := []struct { - Input string - Output []byte - }{ - { - // Whitespace bug - "webrpc = v1\n \nname = test\n \nversion=v1.1\n", - []byte(` - { - "webrpc": "v1", - "name": "test", - "version": "v1.1", - "imports": [], - "messages": [], - "services": [] - } - `), - }, - { - "webrpc = v1\n \nname = test\n", - []byte(` - { - "webrpc": "v1", - "name": "test", - "version": "", - "imports": [], - "messages": [], - "services": [] - } - `), - }, - { - ` - webrpc = v1 - - name = hello-webrpc - version = v0.0.1 - - import - - ./blah.ridl - - ./abc.json - `, - []byte(` - { - "webrpc": "v1", - "name": "hello-webrpc", - "version": "v0.0.1", - "imports": [ - { - "path": "blah.ridl", - "members": [] - }, - { - "path": "abc.json", - "members": [] - } - ], - "messages": [], - "services": [] - } - `), - }, - } +func TestRIDLImportsExample1(t *testing.T) { + exampleDirFS := os.DirFS("./_example") - for i := range table { - s, err := parseString(table[i].Input) - assert.NoError(t, err) + r := NewParser(exampleDirFS, "example1.ridl") + s, err := r.Parse() + assert.NoError(t, err) - jout, err := s.ToJSON(true) - assert.NoError(t, err) + jout, err := s.ToJSON() + assert.NoError(t, err) + + current := []byte(jout) - assert.JSONEq(t, compactJSON(table[i].Output), compactJSON([]byte(jout)), fmt.Sprintf("GOT:\n\n%s\n\nEXPECTING:\n\n%s\n\n", jout, string(table[i].Output))) + golden, err := os.ReadFile("./_example/example1-golden.json") + assert.NoError(t, err) + + if *updateFlag == "./_example/example1-golden.json" { + assert.NoError(t, os.WriteFile("./_example/example1-golden.json", current, 0644)) + return + } + + if !cmp.Equal(golden, current) { + t.Error(cmp.Diff(golden, current)) + t.Log("To update the golden file, run go test -update=./_example/example1-golden.json") } } -func TestRIDLImports(t *testing.T) { - os.Chdir("_example") +func TestRIDLImportsExample2(t *testing.T) { + exampleDirFS := os.DirFS("./_example") - fp, err := os.Open("example1.ridl") + r := NewParser(exampleDirFS, "example2.ridl") + s, err := r.Parse() assert.NoError(t, err) - buf, err := ioutil.ReadAll(fp) + jout, err := s.ToJSON() assert.NoError(t, err) - s, err := parseString(string(buf)) - assert.NoError(t, err) + current := []byte(jout) - jout, err := s.ToJSON(true) + golden, err := os.ReadFile("./_example/example2-golden.json") assert.NoError(t, err) - assert.NotZero(t, jout) - - golden, err := ioutil.ReadFile("example1-golden.json") - assert.NoError(t, err) + if *updateFlag == "./_example/example2-golden.json" { + assert.NoError(t, os.WriteFile("./_example/example2-golden.json", current, 0644)) + return + } - assert.JSONEq(t, compactJSON(golden), compactJSON([]byte(jout))) + if !cmp.Equal(golden, current) { + t.Error(cmp.Diff(golden, current)) + t.Log("To update the golden file, run go test -update=./_example/example2-golden.json") + } } diff --git a/schema/ridl/service_parser.go b/schema/ridl/service_parser.go index a6b37766..fabaa536 100644 --- a/schema/ridl/service_parser.go +++ b/schema/ridl/service_parser.go @@ -1,9 +1,25 @@ package ridl +import "fmt" + func parseStateServiceMethodDefinition(sn *ServiceNode) parserState { return func(p *parser) parserState { var streamInput, proxy bool + defer func() { + // clear annotation buffer + sn.methodAnnotations = []*AnnotationNode{} + }() + + // check for annotation duplicates + annotations := make(map[string]struct{}) + for _, ann := range sn.methodAnnotations { + if _, ok := annotations[ann.AnnotationType().String()]; ok { + return p.stateError(fmt.Errorf("duplicate annotation type: %v", ann.AnnotationType())) + } + + annotations[ann.AnnotationType().String()] = struct{}{} + } // - ([arguments]) [=> [([ return values ])]] matches, err := p.match(tokenDash, tokenWhitespace, tokenWord) if err != nil { @@ -34,9 +50,16 @@ func parseStateServiceMethodDefinition(sn *ServiceNode) parserState { methodName = matches[1] } + commentLine := matches[0].line + // we have to start parsing comments from the line of last annotation + if len(sn.methodAnnotations) > 0 { + commentLine = sn.methodAnnotations[len(sn.methodAnnotations)-1].AnnotationType().tok.line + } + mn := &MethodNode{ - name: newTokenNode(methodName), - proxy: proxy, + name: newTokenNode(methodName), + proxy: proxy, + comment: parseComments(p.comments, commentLine), inputs: argumentList{ stream: streamInput, arguments: []*ArgumentNode{}, @@ -44,6 +67,7 @@ func parseStateServiceMethodDefinition(sn *ServiceNode) parserState { outputs: argumentList{ arguments: []*ArgumentNode{}, }, + annotations: sn.methodAnnotations, } if proxy { @@ -91,15 +115,25 @@ func parserStateServiceMethod(s *ServiceNode) parserState { tok := p.cursor() switch tok.tt { - case tokenNewLine, tokenWhitespace: p.next() + case tokenAt: + anns, err := parseAnnotations(p) + if err != nil { + return p.stateError(err) + } + s.methodAnnotations = append(s.methodAnnotations, anns...) + case tokenHash: - p.continueUntilEOL() + err := p.continueUntilEOL() + if err != nil { + return p.stateError(err) + } case tokenDash: - return parseStateServiceMethodDefinition(s) + state := parseStateServiceMethodDefinition(s) + return state default: p.emit(s) @@ -112,7 +146,6 @@ func parserStateServiceMethod(s *ServiceNode) parserState { } func parserStateService(p *parser) parserState { - matches, err := p.match(tokenWord, tokenWhitespace, tokenWord, tokenEOL) if err != nil { return p.stateError(err) @@ -125,5 +158,60 @@ func parserStateService(p *parser) parserState { return parserStateServiceMethod(&ServiceNode{ name: newTokenNode(matches[2]), methods: []*MethodNode{}, + comment: parseComments(p.comments, matches[0].line), }) } + +func parseAnnotations(p *parser) ([]*AnnotationNode, error) { + annotations := []*AnnotationNode{} + annotationsMap := map[string]struct{}{} + + // @acl:admin + // @auth:"cookies,authorization,query" @internal + matcher := []tokenType{tokenAt, tokenWord} + + for { + annotationMatches, err := p.match(matcher...) + if err != nil { + break + } + + annotation := &AnnotationNode{ + annotationType: newTokenNode(annotationMatches[1]), + } + + if _, ok := annotationsMap[annotation.annotationType.String()]; ok { + return nil, fmt.Errorf("duplicate annotation type: %s", annotation.annotationType.String()) + } + + annotationsMap[annotation.annotationType.String()] = struct{}{} + + if p.cursor().tt != tokenColon { + annotations = append(annotations, annotation) + continue + } + + p.next() + + // @acl:admin + if p.cursor() != eofToken && p.cursor().tt == tokenWord { + annotation.value = newTokenNode(p.cursor()) + } + + // @auth:"cookies,authorization,query" + if p.cursor() != eofToken && p.cursor().tt == tokenQuote { + annotationValue, err := p.expectStringValue() + if err != nil { + return nil, fmt.Errorf("parse string value: %w", err) + } + + annotation.value = newTokenNode(annotationValue) + } + + p.next() + + annotations = append(annotations, annotation) + } + + return annotations, nil +} diff --git a/schema/ridl/message_parser.go b/schema/ridl/struct_parser.go similarity index 69% rename from schema/ridl/message_parser.go rename to schema/ridl/struct_parser.go index 18a189c7..561703a7 100644 --- a/schema/ridl/message_parser.go +++ b/schema/ridl/struct_parser.go @@ -1,6 +1,6 @@ package ridl -func parserStateMessageFieldMetaDefinition(mn *MessageNode) parserState { +func parserStateStructFieldMetaDefinition(mn *StructNode) parserState { return func(p *parser) parserState { // add to latest field field := mn.fields[len(mn.fields)-1] @@ -34,11 +34,11 @@ func parserStateMessageFieldMetaDefinition(mn *MessageNode) parserState { rightNode: newTokenNode(right), }) - return parserStateMessageFieldMeta(mn) + return parserStateStructFieldMeta(mn) } } -func parserStateMessageFieldMeta(mn *MessageNode) parserState { +func parserStateStructFieldMeta(mn *StructNode) parserState { return func(p *parser) parserState { tok := p.cursor() @@ -52,18 +52,18 @@ func parserStateMessageFieldMeta(mn *MessageNode) parserState { p.continueUntilEOL() case tokenPlusSign: - return parserStateMessageFieldMetaDefinition(mn) + return parserStateStructFieldMetaDefinition(mn) default: - return parserStateMessageField(mn) + return parserStateStructField(mn) } - return parserStateMessageFieldMeta(mn) + return parserStateStructFieldMeta(mn) } } -func parserStateMessageFieldDefinition(mn *MessageNode) parserState { +func parserStateStructFieldDefinition(mn *StructNode) parserState { return func(p *parser) parserState { // - : [<# comment>][EOL] matches, err := p.match(tokenDash, tokenWhitespace, tokenWord) @@ -73,6 +73,7 @@ func parserStateMessageFieldDefinition(mn *MessageNode) parserState { field := &DefinitionNode{ leftNode: newTokenNode(matches[2]), + comment: parseComments(p.comments, matches[0].line), } // ? @@ -96,11 +97,11 @@ func parserStateMessageFieldDefinition(mn *MessageNode) parserState { mn.fields = append(mn.fields, field) - return parserStateMessageFieldMeta(mn) + return parserStateStructFieldMeta(mn) } } -func parserStateMessageField(mn *MessageNode) parserState { +func parserStateStructField(mn *StructNode) parserState { return func(p *parser) parserState { tok := p.cursor() @@ -113,7 +114,7 @@ func parserStateMessageField(mn *MessageNode) parserState { p.continueUntilEOL() case tokenDash: - return parserStateMessageFieldDefinition(mn) + return parserStateStructFieldDefinition(mn) default: p.emit(mn) @@ -121,23 +122,24 @@ func parserStateMessageField(mn *MessageNode) parserState { } - return parserStateMessageField(mn) + return parserStateStructField(mn) } } -func parserStateMessage(p *parser) parserState { - // message +func parserStateStruct(p *parser) parserState { + // struct matches, err := p.match(tokenWord, tokenWhitespace, tokenWord) if err != nil { return p.stateError(err) } - if matches[0].val != wordMessage { + if matches[0].val != wordStruct { return p.stateError(errUnexpectedToken) } - return parserStateMessageField(&MessageNode{ - name: newTokenNode(matches[2]), - fields: []*DefinitionNode{}, + return parserStateStructField(&StructNode{ + name: newTokenNode(matches[2]), + fields: []*DefinitionNode{}, + comment: parseComments(p.comments, matches[0].line), }) } diff --git a/schema/ridl/error_test.go b/schema/ridl/syntax_error_test.go similarity index 90% rename from schema/ridl/error_test.go rename to schema/ridl/syntax_error_test.go index 3e15b110..1ee6a859 100644 --- a/schema/ridl/error_test.go +++ b/schema/ridl/syntax_error_test.go @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestError(t *testing.T) { +func TestSyntaxError(t *testing.T) { syntaxErrors := []string{ ` @@ -44,6 +44,5 @@ func TestError(t *testing.T) { for i := range syntaxErrors { _, err := parseString(syntaxErrors[i]) assert.Error(t, err) - t.Logf("%v", err) } } diff --git a/schema/ridl/tokenizer.go b/schema/ridl/tokenizer.go index f32a8b6e..5cab16be 100644 --- a/schema/ridl/tokenizer.go +++ b/schema/ridl/tokenizer.go @@ -1,25 +1,72 @@ package ridl -import ( - "io" - "io/ioutil" -) - -func tokenize(r io.Reader) ([]token, error) { - in, err := ioutil.ReadAll(r) - if err != nil { - return nil, err - } - - lx := newLexer(string(in)) +import "strings" +// tokenize +// Returns: +// - []token: A slice containing all the tokens parsed from the source. +// - map[int]string: A map where the key is the line number and the value is the comment present in that line. +// +// This function extracts tokens from it. The comments are associated with the line +// numbers where they occur, providing a convenient map for comment retrieval +func tokenize(src []byte) ([]token, map[int]string, error) { + lx := newLexer(string(src)) tokens := []token{} + lineComments := make(map[int]string) + + parsingComment := false + commentTokens := []string{} + for tok := range lx.tokens { if tok.tt == tokenEOF { break } + + // start parsing comment tokens until new line + if tok.tt == tokenHash { + parsingComment = true + tokens = append(tokens, tok) + continue + } + + if parsingComment && tok.tt != tokenNewLine && tok.tt != tokenWhitespace { + commentTokens = parseCommentToken(tok, tokens, commentTokens) + } + + if tok.tt == tokenNewLine && parsingComment { + lineComments[tok.line] = strings.Join(commentTokens, " ") + commentTokens = []string{} + parsingComment = false + } + tokens = append(tokens, tok) } - return tokens, nil + return tokens, lineComments, nil +} + +func parseCommentToken(curToken token, tokens []token, commentTokens []string) []string { + tokenLen := len(tokens) + + if tokenLen == 0 { + commentTokens = append(commentTokens, curToken.String()) + return commentTokens + } + + // previous token was whitespace that means new word + if tokens[tokenLen-1].tt == tokenWhitespace { + commentTokens = append(commentTokens, curToken.String()) + return commentTokens + } + + commentLen := len(commentTokens) + if commentLen > 0 { + // previous token was not whitespace that could be slash or any character + // need to append current char to previous comment token + // for instance https://www.google.com has `:`, `/`, `/` .... + commentTokens[commentLen-1] += curToken.String() + } else { + commentTokens = append(commentTokens, curToken.String()) + } + return commentTokens } diff --git a/schema/ridl/type_parser.go b/schema/ridl/type_parser.go index c433cd9e..e6f85a93 100644 --- a/schema/ridl/type_parser.go +++ b/schema/ridl/type_parser.go @@ -82,46 +82,60 @@ loop: p.next() case tokenWord: - var argument []*token - var name *token - - optional := false - - matches, err := p.match(tokenWord, tokenQuestionMark, tokenColon, tokenWhitespace) - if err == nil { - argument = []*token{matches[0], matches[1], matches[2]} - name = matches[0] - optional = true - } else { - matches, err = p.match(tokenWord, tokenColon, tokenWhitespace) - if err != nil { - return nil, err + // succinct form, Method(InRequest) => (OutReponse) + { + matches, err := p.match(tokenWord, tokenCloseParen) + if err == nil { + values = append(values, &ArgumentNode{ + inlineStruct: newTokenNode(matches[0]), + }) + + tokens = append(tokens, tok) + p.next() + break loop } - argument = []*token{matches[0], matches[1]} - name = matches[0] } - varType, err := p.expectType() - if err != nil { - return nil, err - } + // normal form, Method(arg1: type, arg2?: type) => (out: type) + { + var argument []*token + var name *token + + optional := false + + matches, err := p.match(tokenWord, tokenQuestionMark, tokenColon, tokenWhitespace) + if err == nil { + argument = []*token{matches[0], matches[1], matches[2]} + name = matches[0] + optional = true + } else { + matches, err = p.match(tokenWord, tokenColon, tokenWhitespace) + if err != nil { + return nil, err + } + argument = []*token{matches[0], matches[1]} + name = matches[0] + } - values = append(values, &ArgumentNode{ - name: newTokenNode(name), - argumentType: newTokenNode(varType), - optional: optional, - }) + varType, err := p.expectType() + if err != nil { + return nil, err + } + + values = append(values, &ArgumentNode{ + name: newTokenNode(name), + argumentType: newTokenNode(varType), + optional: optional, + }) - tokens = append(tokens, append(argument, varType)...) + tokens = append(tokens, append(argument, varType)...) + } default: return nil, errUnexpectedToken - } } - //return composedValue(tokens) - return values, nil } @@ -201,7 +215,7 @@ loop: switch tok.val { - case "map": + case wordMap: var err error tok, err = p.expectMapDefinition() @@ -211,6 +225,9 @@ loop: tokens = append(tokens, tok) continue loop + + case wordStream, wordStruct: + return nil, errUnexpectedToken } tokens = append(tokens, tok) @@ -220,8 +237,6 @@ loop: default: return nil, errUnexpectedToken } - - p.next() } return composedValue(tokens) diff --git a/schema/schema.go b/schema/schema.go index ea892e9e..52912261 100644 --- a/schema/schema.go +++ b/schema/schema.go @@ -5,44 +5,50 @@ import ( "crypto/sha1" "encoding/hex" "encoding/json" + "fmt" "strings" - - "github.com/pkg/errors" ) const ( - VERSION = "v1" // todo rename to schema_version + SCHEMA_VERSION = "v1" ) // schema of webrpc json file, and validations type WebRPCSchema struct { - WebRPCVersion string `json:"webrpc"` - Name string `json:"name"` - SchemaVersion string `json:"version"` - Imports []*Import `json:"imports"` + WebrpcVersion string `json:"webrpc"` + SchemaName string `json:"name"` + SchemaVersion string `json:"version"` - Messages []*Message `json:"messages"` + Types []*Type `json:"types"` + Errors []*Error `json:"errors"` Services []*Service `json:"services"` -} -type Import struct { - Path string `json:"path"` - Members []string `json:"members"` + // Deprecated. Renamed to Types. Keep this field for now, so we can + // error out & advise users to migrate to v0.9.0+ schema format. + Deprecated_Messages []interface{} `json:"messages,omitempty"` } // Validate validates the schema through the AST, intended to be called after // the json has been unmarshalled func (s *WebRPCSchema) Validate() error { - if s.WebRPCVersion != VERSION { - return errors.Errorf("webrpc schema version, '%s' is invalid, try '%s'", s.WebRPCVersion, VERSION) + if s.WebrpcVersion != SCHEMA_VERSION { + return fmt.Errorf("webrpc schema version, '%s' is invalid, try '%s'", s.WebrpcVersion, SCHEMA_VERSION) } - for _, msg := range s.Messages { + for _, msg := range s.Types { err := msg.Parse(s) if err != nil { return err } } + + for _, e := range s.Errors { + err := e.Parse(s) + if err != nil { + return err + } + } + for _, svc := range s.Services { err := svc.Parse(s) if err != nil { @@ -50,13 +56,18 @@ func (s *WebRPCSchema) Validate() error { } } + if len(s.Deprecated_Messages) > 0 { + return fmt.Errorf(" field \"messages\" was renamed to \"types\", see https://github.com/webrpc/webrpc/blob/master/CHANGELOG.md#json-schema-v090-migration-guide") + } + return nil } func (s *WebRPCSchema) SchemaHash() (string, error) { // TODO: lets later make this even more deterministic in face of re-ordering // definitions within the ridl file - jsonString, err := s.ToJSON(false) + + jsonString, err := s.ToJSON() if err != nil { return "", err } @@ -66,31 +77,24 @@ func (s *WebRPCSchema) SchemaHash() (string, error) { return hex.EncodeToString(h.Sum(nil)), nil } -func (s *WebRPCSchema) ToJSON(optIndent ...bool) (string, error) { - indent := false - if len(optIndent) > 0 { - indent = optIndent[0] - } +func (s *WebRPCSchema) ToJSON() (string, error) { + var buf bytes.Buffer - buf := &bytes.Buffer{} - enc := json.NewEncoder(buf) + enc := json.NewEncoder(&buf) enc.SetEscapeHTML(false) - if indent { - enc.SetIndent("", " ") - } + enc.SetIndent("", " ") - err := enc.Encode(s) - if err != nil { + if err := enc.Encode(s); err != nil { return "", err } - return string(buf.Bytes()), nil + return buf.String(), nil } -func (s *WebRPCSchema) GetMessageByName(name string) *Message { +func (s *WebRPCSchema) GetTypeByName(name string) *Type { name = strings.ToLower(name) - for _, message := range s.Messages { - if strings.ToLower(string(message.Name)) == name { + for _, message := range s.Types { + if strings.ToLower(message.Name) == name { return message } } @@ -100,42 +104,119 @@ func (s *WebRPCSchema) GetMessageByName(name string) *Message { func (s *WebRPCSchema) GetServiceByName(name string) *Service { name = strings.ToLower(name) for _, service := range s.Services { - if strings.ToLower(string(service.Name)) == name { + if strings.ToLower(service.Name) == name { return service } } return nil } -func (s *WebRPCSchema) HasFieldType(fieldType string) (bool, error) { - fieldType = strings.ToLower(fieldType) - _, ok := DataTypeFromString[fieldType] - if !ok { - return false, errors.Errorf("webrpc: invalid data type '%s'", fieldType) +func MatchServices(s *WebRPCSchema, services []string) *WebRPCSchema { + if s == nil { + return nil } - for _, m := range s.Messages { - for _, f := range m.Fields { - if DataTypeToString[f.Type.Type] == fieldType { - return true, nil - } + if len(services) == 0 { + return s + } + + serviceMap := map[string]struct{}{} + for _, srv := range services { + serviceMap[srv] = struct{}{} + } + + var matchedServices []*Service + for _, srv := range s.Services { + if _, ok := serviceMap[srv.Name]; ok { + matchedServices = append(matchedServices, srv) } } - for _, s := range s.Services { - for _, m := range s.Methods { - for _, i := range m.Inputs { - if DataTypeToString[i.Type.Type] == fieldType { - return true, nil + s.Services = matchedServices + + return s +} + +func MatchMethodsWithAnnotations(s *WebRPCSchema, annotations map[string]string) *WebRPCSchema { + if s == nil { + return nil + } + + for i, srv := range s.Services { + var methods []*Method + for _, method := range srv.Methods { + match := false + for name, value := range annotations { + ann, ok := method.Annotations[name] + if !ok { + continue + } + + trimmedAnnVal := strings.Trim(value, " ") + if trimmedAnnVal == "" { + // match annotation types only + if name == ann.AnnotationType { + match = true + break + } + } else { + // match @key:value + if name == ann.AnnotationType && value == ann.Value { + match = true + break + } } } - for _, o := range m.Outputs { - if DataTypeToString[o.Type.Type] == fieldType { - return true, nil + + if match { + methods = append(methods, method) + } + } + + s.Services[i].Methods = methods + } + + return s +} + +func IgnoreMethodsWithAnnotations(s *WebRPCSchema, annotations map[string]string) *WebRPCSchema { + if s == nil { + return nil + } + + for i, srv := range s.Services { + var methods []*Method + for _, method := range srv.Methods { + ignore := false + for name, value := range annotations { + ann, ok := method.Annotations[name] + if !ok { + continue + } + + trimmedAnnVal := strings.Trim(value, " ") + if trimmedAnnVal == "" { + // match annotation types only + if name == ann.AnnotationType { + ignore = true + break + } + } else { + // match @key:value + if name == ann.AnnotationType && value == ann.Value { + ignore = true + break + } } } + + if !ignore { + methods = append(methods, method) + } } + + s.Services[i].Methods = methods } - return false, nil + return s } diff --git a/schema/schema_test.go b/schema/schema_test.go index cb5090df..3bc927fa 100644 --- a/schema/schema_test.go +++ b/schema/schema_test.go @@ -2,6 +2,7 @@ package schema import ( "fmt" + "github.com/stretchr/testify/require" "testing" "github.com/stretchr/testify/assert" @@ -12,32 +13,31 @@ func TestSchema(t *testing.T) { "webrpc": "v1", "name": "example", "version": "v0.0.1", - "messages": [ + "types": [ { + "kind": "enum", "name": "Kind", - "type": "enum", + "type": "uint32", "fields": [ { "name": "USER", - "type": "uint32", "value": "1" }, { "name": "ADMIN", - "type": "uint32", "value": "2" } ] }, { + "kind": "struct", "name": "Empty", - "type": "struct", "fields": [ ] }, { + "kind": "struct", "name": "GetUserRequest", - "type": "struct", "fields": [ { "name": "userID", @@ -47,8 +47,8 @@ func TestSchema(t *testing.T) { ] }, { + "kind": "struct", "name": "RandomStuff", - "type": "struct", "fields": [ { "name": "meta", @@ -89,8 +89,8 @@ func TestSchema(t *testing.T) { ] }, { + "kind": "struct", "name": "User", - "type": "struct", "fields": [ { "name": "ID", @@ -125,7 +125,7 @@ func TestSchema(t *testing.T) { ], "services": [ { - "name": "ExampleService", + "name": "Example", "methods": [ { "name": "Ping", @@ -171,7 +171,7 @@ func TestSchema(t *testing.T) { // spew.Dump(schema) - jout, err := schema.ToJSON(true) + jout, err := schema.ToJSON() assert.NoError(t, err) fmt.Println("schema JSON:", jout) } @@ -192,3 +192,510 @@ func TestFoo(t *testing.T) { assert.True(t, IsValidArgName("a55_____cdDDDD")) assert.False(t, IsValidArgName("asSS_E_##$")) } + +func TestMatchServices(t *testing.T) { + type args struct { + s *WebRPCSchema + services []string + } + tests := []struct { + name string + args args + want *WebRPCSchema + }{ + { + name: "match services", + args: args{ + s: &WebRPCSchema{ + WebrpcVersion: "v0.1.0", + SchemaName: "dev", + SchemaVersion: "dev-v0.1.0", + Services: []*Service{ + {Name: "ExampleService"}, + {Name: "AdminService"}, + {Name: "PublicService"}, + }, + }, + services: []string{"ExampleService", "AdminService"}, + }, + want: &WebRPCSchema{ + WebrpcVersion: "v0.1.0", + SchemaName: "dev", + SchemaVersion: "dev-v0.1.0", + Services: []*Service{ + {Name: "ExampleService"}, + {Name: "AdminService"}, + }, + }, + }, + { + name: "no services to match", + args: args{ + s: &WebRPCSchema{ + WebrpcVersion: "v0.1.0", + SchemaName: "dev", + SchemaVersion: "dev-v0.1.0", + Services: []*Service{ + {Name: "ExampleService"}, + {Name: "AdminService"}, + }, + }, + services: []string{}, + }, + want: &WebRPCSchema{ + WebrpcVersion: "v0.1.0", + SchemaName: "dev", + SchemaVersion: "dev-v0.1.0", + Services: []*Service{ + {Name: "ExampleService"}, + {Name: "AdminService"}, + }, + }, + }, + { + name: "no matching services", + args: args{ + s: &WebRPCSchema{ + WebrpcVersion: "v0.1.0", + SchemaName: "dev", + SchemaVersion: "dev-v0.1.0", + Services: []*Service{ + {Name: "ExampleService"}, + {Name: "AdminService"}, + }, + }, + services: []string{"NonExistentService"}, + }, + want: &WebRPCSchema{ + WebrpcVersion: "v0.1.0", + SchemaName: "dev", + SchemaVersion: "dev-v0.1.0", + Services: []*Service(nil), + }, + }, + { + name: "all services match", + args: args{ + s: &WebRPCSchema{ + WebrpcVersion: "v0.1.0", + SchemaName: "dev", + SchemaVersion: "dev-v0.1.0", + Services: []*Service{ + {Name: "ExampleService"}, + {Name: "AdminService"}, + }, + }, + services: []string{"ExampleService", "AdminService"}, + }, + want: &WebRPCSchema{ + WebrpcVersion: "v0.1.0", + SchemaName: "dev", + SchemaVersion: "dev-v0.1.0", + Services: []*Service{ + {Name: "ExampleService"}, + {Name: "AdminService"}, + }, + }, + }, + { + name: "empty service list in schema", + args: args{ + s: &WebRPCSchema{ + WebrpcVersion: "v0.1.0", + SchemaName: "dev", + SchemaVersion: "dev-v0.1.0", + Services: []*Service{}, // No services in schema + }, + services: []string{"ExampleService", "AdminService"}, + }, + want: &WebRPCSchema{ + WebrpcVersion: "v0.1.0", + SchemaName: "dev", + SchemaVersion: "dev-v0.1.0", + Services: []*Service(nil), // Nothing to match, return empty + }, + }, + { + name: "nil schema", + args: args{ + s: nil, // Schema is nil + services: []string{"ExampleService", "AdminService"}, + }, + want: nil, // Expect nil in return + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.EqualValues(t, tt.want, MatchServices(tt.args.s, tt.args.services)) + }) + } +} + +func TestIgnoreMethodsWithAnnotations(t *testing.T) { + type args struct { + s *WebRPCSchema + ignoreAnnotations map[string]string + } + tests := []struct { + name string + args args + want *WebRPCSchema + }{ + { + name: "no methods to ignore", + args: args{ + s: &WebRPCSchema{ + Services: []*Service{ + { + Name: "ExampleService", + Methods: []*Method{ + { + Name: "Ping", + Annotations: map[string]*Annotation{}, + }, + { + Name: "Version", + Annotations: map[string]*Annotation{}, + }, + }, + }, + }, + }, + ignoreAnnotations: map[string]string{"deprecated": ""}, + }, + want: &WebRPCSchema{ + Services: []*Service{ + { + Name: "ExampleService", + Methods: []*Method{ + { + Name: "Ping", + Annotations: map[string]*Annotation{}, + }, + { + Name: "Version", + Annotations: map[string]*Annotation{}, + }, + }, + }, + }, + }, + }, + { + name: "all methods ignored", + args: args{ + s: &WebRPCSchema{ + Services: []*Service{ + { + Name: "ExampleService", + Methods: []*Method{ + { + Name: "Ping", + Annotations: map[string]*Annotation{ + "deprecated": { + AnnotationType: "deprecated", + Value: "PingV2", + }, + }, + }, + { + Name: "Version", + Annotations: map[string]*Annotation{ + "deprecated": { + AnnotationType: "deprecated", + Value: "", + }, + }, + }, + }, + }, + }, + }, + ignoreAnnotations: map[string]string{"deprecated": ""}, + }, + want: &WebRPCSchema{ + Services: []*Service{ + { + Name: "ExampleService", + Methods: []*Method(nil), // All methods are ignored + }, + }, + }, + }, + { + name: "no matching annotations", + args: args{ + s: &WebRPCSchema{ + Services: []*Service{ + { + Name: "ExampleService", + Methods: []*Method{ + { + Name: "Ping", + Annotations: map[string]*Annotation{ + "internal": { + AnnotationType: "internal", + Value: "", + }, + }, + }, + }, + }, + }, + }, + ignoreAnnotations: map[string]string{"deprecated": ""}, + }, + want: &WebRPCSchema{ + Services: []*Service{ + { + Name: "ExampleService", + Methods: []*Method{ + { + Name: "Ping", + Annotations: map[string]*Annotation{ + "internal": { + AnnotationType: "internal", + Value: "", + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "empty schema", + args: args{ + s: &WebRPCSchema{ + Services: []*Service{}, // No services in the schema + }, + ignoreAnnotations: map[string]string{"deprecated": ""}, + }, + want: &WebRPCSchema{ + Services: []*Service{}, // Expect no services + }, + }, + { + name: "empty annotations map", + args: args{ + s: &WebRPCSchema{ + Services: []*Service{ + { + Name: "ExampleService", + Methods: []*Method{ + { + Name: "Ping", + Annotations: map[string]*Annotation{}, + }, + }, + }, + }, + }, + ignoreAnnotations: map[string]string{}, + }, + want: &WebRPCSchema{ + Services: []*Service{ + { + Name: "ExampleService", + Methods: []*Method{ + { + Name: "Ping", + Annotations: map[string]*Annotation{}, + }, + }, + }, + }, + }, + }, + { + name: "nil schema", + args: args{ + s: nil, + ignoreAnnotations: map[string]string{"deprecated": ""}, + }, + want: nil, // Expect nil when the schema is nil + }, + { + name: "nil annotations map", + args: args{ + s: &WebRPCSchema{ + Services: []*Service{ + { + Name: "ExampleService", + Methods: []*Method{ + { + Name: "Ping", + Annotations: map[string]*Annotation{ + "deprecated": { + AnnotationType: "deprecated", + Value: "v1", + }, + }, + }, + }, + }, + }, + }, + ignoreAnnotations: nil, // Annotations map is nil + }, + want: &WebRPCSchema{ + Services: []*Service{ + { + Name: "ExampleService", + Methods: []*Method{ + { + Name: "Ping", + Annotations: map[string]*Annotation{ + "deprecated": { + AnnotationType: "deprecated", + Value: "v1", + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "filter deprecated methods", + args: args{ + s: &WebRPCSchema{ + Services: []*Service{ + { + Name: "ExampleService", + Methods: []*Method{ + { + Name: "Ping", + Annotations: map[string]*Annotation{ + "internal": { + AnnotationType: "internal", + Value: "", + }, + "auth": { + AnnotationType: "auth", + Value: "Cookies,Jwt,Authorization", + }, + }, + }, + { + Name: "Version", + Annotations: map[string]*Annotation{ + "internal": { + AnnotationType: "internal", + Value: "", + }, + "deprecated": { + AnnotationType: "deprecated", + }, + }, + }, + { + Name: "Version2", + Annotations: map[string]*Annotation{}, + }, + }, + }, + }, + }, + ignoreAnnotations: map[string]string{"deprecated": ""}, + }, + want: &WebRPCSchema{ + Services: []*Service{ + { + Name: "ExampleService", + Methods: []*Method{ + { + Name: "Ping", + Annotations: map[string]*Annotation{ + "internal": { + AnnotationType: "internal", + Value: "", + }, + "auth": { + AnnotationType: "auth", + Value: "Cookies,Jwt,Authorization", + }, + }, + }, + { + Name: "Version2", + Annotations: map[string]*Annotation{}, + }, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.EqualValues(t, tt.want, IgnoreMethodsWithAnnotations(tt.args.s, tt.args.ignoreAnnotations)) + }) + } +} + +func TestMatchMethodsWithAnnotations(t *testing.T) { + type args struct { + s *WebRPCSchema + matchAnnotations map[string]string + } + tests := []struct { + name string + args args + want *WebRPCSchema + }{ + { + name: "no methods to ignore", + args: args{ + s: &WebRPCSchema{ + Services: []*Service{ + { + Name: "ExampleService", + Methods: []*Method{ + { + Name: "Ping", + Annotations: map[string]*Annotation{}, + }, + { + Name: "Version", + Annotations: map[string]*Annotation{ + "deprecated": { + AnnotationType: "deprecated", + Value: "", + }, + }, + }, + }, + }, + }, + }, + matchAnnotations: map[string]string{"deprecated": ""}, + }, + want: &WebRPCSchema{ + Services: []*Service{ + { + Name: "ExampleService", + Methods: []*Method{ + { + Name: "Version", + Annotations: map[string]*Annotation{ + "deprecated": { + AnnotationType: "deprecated", + Value: "", + }, + }, + }, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.EqualValues(t, tt.want, MatchMethodsWithAnnotations(tt.args.s, tt.args.matchAnnotations)) + }) + } +} diff --git a/schema/service.go b/schema/service.go index c4f566d0..5cf88bdd 100644 --- a/schema/service.go +++ b/schema/service.go @@ -1,20 +1,22 @@ package schema import ( + "fmt" "strings" - - "github.com/pkg/errors" ) type Service struct { - Name VarName `json:"name"` - Methods []*Method `json:"methods"` + Name string `json:"name"` + Methods []*Method `json:"methods"` + Comments []string `json:"comments"` Schema *WebRPCSchema `json:"-"` // denormalize/back-reference } type Method struct { - Name VarName `json:"name"` + Name string `json:"name"` + Annotations Annotations `json:"annotations"` + Comments []string `json:"comments"` StreamInput bool `json:"streamInput,omitempty"` StreamOutput bool `json:"streamOutput,omitempty"` @@ -27,50 +29,59 @@ type Method struct { } type MethodArgument struct { - Name VarName `json:"name"` + Name string `json:"name"` Type *VarType `json:"type"` Optional bool `json:"optional"` InputArg bool `json:"-"` // denormalize/back-reference OutputArg bool `json:"-"` // denormalize/back-reference + + TypeExtra `json:",omitempty"` +} + +type Annotations map[string]*Annotation + +type Annotation struct { + AnnotationType string `json:"annotationType"` + Value string `json:"value"` } func (s *Service) Parse(schema *WebRPCSchema) error { s.Schema = schema // back-ref // Service name - serviceName := string(s.Name) + serviceName := s.Name if string(s.Name) == "" { - return errors.Errorf("schema error: service name cannot be empty") + return fmt.Errorf("schema error: service name cannot be empty") } // Ensure we don't have dupe service names (w/ normalization) - name := strings.ToLower(string(s.Name)) + name := strings.ToLower(s.Name) for _, svc := range schema.Services { if svc != s && name == strings.ToLower(string(svc.Name)) { - return errors.Errorf("schema error: duplicate service name detected in service '%s'", serviceName) + return fmt.Errorf("schema error: duplicate service name detected in service '%s'", serviceName) } } // Ensure methods is defined if len(s.Methods) == 0 { - return errors.Errorf("schema error: methods cannot be empty for service '%s'", serviceName) + return fmt.Errorf("schema error: methods cannot be empty for service '%s'", serviceName) } // Verify method names and ensure we don't have any duplicate method names methodList := map[string]string{} for _, method := range s.Methods { if string(method.Name) == "" { - return errors.Errorf("schema error: detected empty method name in service '%s", serviceName) + return fmt.Errorf("schema error: detected empty method name in service '%s", serviceName) } - methodName := string(method.Name) - nMethodName := strings.ToLower(methodName) + methodName := method.Name + methodNameLower := strings.ToLower(methodName) - if _, ok := methodList[nMethodName]; ok { - return errors.Errorf("schema error: detected duplicate method name of '%s' in service '%s'", methodName, serviceName) + if _, ok := methodList[methodNameLower]; ok { + return fmt.Errorf("schema error: detected duplicate method name of '%s' in service '%s'", methodName, serviceName) } - methodList[nMethodName] = methodName + methodList[methodNameLower] = methodName } // Parse+validate methods @@ -86,16 +97,16 @@ func (s *Service) Parse(schema *WebRPCSchema) error { func (m *Method) Parse(schema *WebRPCSchema, service *Service) error { if service == nil { - return errors.Errorf("parse error, service arg cannot be nil") + return fmt.Errorf("parse error, service arg cannot be nil") } m.Service = service // back-ref - serviceName := string(service.Name) + serviceName := service.Name // Parse+validate inputs for _, input := range m.Inputs { input.InputArg = true // back-ref if input.Name == "" { - return errors.Errorf("schema error: detected empty input argument name for method '%s' in service '%s'", m.Name, serviceName) + return fmt.Errorf("schema error: detected empty input argument name for method '%s' in service '%s'", m.Name, serviceName) } err := input.Type.Parse(schema) if err != nil { @@ -107,7 +118,7 @@ func (m *Method) Parse(schema *WebRPCSchema, service *Service) error { for _, output := range m.Outputs { output.OutputArg = true // back-ref if output.Name == "" { - return errors.Errorf("schema error: detected empty output name for method '%s' in service '%s'", m.Name, serviceName) + return fmt.Errorf("schema error: detected empty output name for method '%s' in service '%s'", m.Name, serviceName) } err := output.Type.Parse(schema) if err != nil { diff --git a/schema/type.go b/schema/type.go new file mode 100644 index 00000000..484fd268 --- /dev/null +++ b/schema/type.go @@ -0,0 +1,177 @@ +package schema + +import ( + "fmt" + "strings" + "unicode" +) + +const ( + TypeKind_Struct = "struct" + TypeKind_Enum = "enum" +) + +type Type struct { + Kind string `json:"kind"` + Name string `json:"name"` + Type *VarType `json:"type,omitempty"` + Fields []*TypeField `json:"fields,omitempty"` + TypeExtra `json:",omitempty"` + Comments []string `json:"comments,omitempty"` +} + +type TypeField struct { + Comments []string `json:"comments,omitempty"` + Name string `json:"name"` + + Type *VarType `json:"type,omitempty"` + TypeExtra `json:",omitempty"` +} + +type TypeExtra struct { + Optional bool `json:"optional,omitempty"` // used by structs + Value string `json:"value,omitempty"` // used by enums + + // Meta store extra metadata on a field for plugins + Meta []TypeFieldMeta `json:"meta,omitempty"` +} + +type TypeFieldMeta map[string]interface{} + +func (t *Type) Parse(schema *WebRPCSchema) error { + // Type name + typName := string(t.Name) + if typName == "" { + return fmt.Errorf("schema error: type name cannot be empty") + } + if !startsWithUpper(typName) { + return fmt.Errorf("schema error: type name must start with upper case for '%s'", typName) + } + + // Ensure we don't have dupe types (w/ normalization) + name := strings.ToLower(typName) + for _, msg := range schema.Types { + if msg != t && name == strings.ToLower(string(msg.Name)) { + return fmt.Errorf("schema error: duplicate type detected, '%s'", typName) + } + } + + // Ensure we have a valid kind + if t.Kind != TypeKind_Enum && t.Kind != TypeKind_Struct { + return fmt.Errorf("schema error: type must be one of 'enum', or 'struct' for '%s'", typName) + } + + // Verify field names and ensure we don't have any duplicate field names + // NOTE: Allow structs with no fields. + fieldList := map[string]string{} + jsonFieldList := map[string]string{} + for _, field := range t.Fields { + if string(field.Name) == "" { + return fmt.Errorf("schema error: detected empty field name in type '%s", typName) + } + + fieldName := string(field.Name) + + // Verify name format + if !IsValidArgName(fieldName) { + return fmt.Errorf("schema error: invalid field name of '%s' in type '%s'", fieldName, typName) + } + + // Ensure no dupes + fieldNameLower := strings.ToLower(fieldName) + if _, ok := fieldList[fieldNameLower]; ok { + return fmt.Errorf("schema error: detected duplicate field name of '%s' in type '%s'", fieldName, typName) + } + fieldList[fieldNameLower] = fieldName + + // Verify json meta format, as it overrides field name in JSON + // and in JavaScript/TypeScript generated code. + jsonFieldName := fieldName + for _, meta := range field.TypeExtra.Meta { + if jsonMeta, ok := meta["json"]; ok { + jsonMetaString, ok := jsonMeta.(string) + if !ok { + return fmt.Errorf("schema error: invalid json type '%T' in field '%s' in type '%s': must be string", jsonMeta, fieldName, typName) + } + + if jsonMetaString == "-" { + // Skip the special `json = -` value, which makes the field ignored in generated clients. + continue + } + + if !IsValidArgName(jsonMetaString) { + return fmt.Errorf("schema error: invalid json name '%s' in field '%s' in type '%s'", jsonMetaString, fieldName, typName) + } + + jsonFieldName = jsonMetaString + } + } + + // Ensure no dupes + jsonMetaStringLower := strings.ToLower(jsonFieldName) + if _, ok := jsonFieldList[jsonMetaStringLower]; ok { + return fmt.Errorf("schema error: detected duplicate json name '%s' in field '%s' in type '%s'", jsonFieldName, fieldName, typName) + } + jsonFieldList[jsonMetaStringLower] = fieldName + } + + // For enums only, ensure all field types are the same + if t.Kind == TypeKind_Enum { + // ensure enum fields have value key set + for _, field := range t.Fields { + if field.Value == "" { + return fmt.Errorf("schema error: enum '%s' with field '%s' is missing value", t.Name, field.Name) + } + if field.Type != nil { + return fmt.Errorf("schema error: enum '%s' with field '%s', must omit 'type'", t.Name, field.Name) + } + } + + // ensure enum type is one of the allowed types.. aka integer + fieldType := t.Type + validCoreTypes := append(VarIntegerCoreTypes, T_String) + if !isValidVarType(fieldType.String(), validCoreTypes) { + return fmt.Errorf("schema error: enum '%s' field '%s' is invalid. must be an integer type", t.Name, fieldType.String()) + } + } + + // For structs only + if t.Kind == TypeKind_Struct { + for _, field := range t.Fields { + // Parse+validate type fields + err := field.Type.Parse(schema) + if err != nil { + return err + } + + // Ensure value isn't set + if field.Value != "" { + return fmt.Errorf("schema error: struct '%s' with field '%s' cannot contain value field - please remove it", t.Name, field.Name) + } + } + + } + + return nil +} + +func (t *Type) RequiredFields() []*TypeField { + requiredFields := make([]*TypeField, 0) + for _, field := range t.Fields { + if !field.Optional { + requiredFields = append(requiredFields, field) + } + } + + return requiredFields +} + +func startsWithUpper(s string) bool { + if len(s) == 0 { + return false + } + if !unicode.IsUpper(rune(s[0])) { + return false + } + return true +} diff --git a/schema/type_test.go b/schema/type_test.go new file mode 100644 index 00000000..864c7b4b --- /dev/null +++ b/schema/type_test.go @@ -0,0 +1,62 @@ +package schema + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestRequiredFields(t *testing.T) { + tests := []struct { + name string + fields []*TypeField + want []*TypeField + }{ + { + name: "one field is required", + fields: []*TypeField{ + { + Name: "Name", + Type: &VarType{ + Type: T_String, + }, + TypeExtra: TypeExtra{Optional: false}, + }, + { + Name: "Phone number", + Type: &VarType{ + Type: T_String, + }, + TypeExtra: TypeExtra{Optional: true}, + }, + }, + want: []*TypeField{ + { + Name: "Name", + Type: &VarType{ + Type: T_String, + }, + TypeExtra: TypeExtra{Optional: false}, + }, + }, + }, + { + name: "all fields are optional", + fields: []*TypeField{ + { + Name: "Name", + Type: &VarType{ + Type: T_String, + }, + TypeExtra: TypeExtra{Optional: true}, + }, + }, + want: []*TypeField{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ty := &Type{Fields: tt.fields} + assert.Equal(t, tt.want, ty.RequiredFields()) + }) + } +} diff --git a/schema/var_type.go b/schema/var_type.go index b9bbb4e5..8483f6bf 100644 --- a/schema/var_type.go +++ b/schema/var_type.go @@ -1,45 +1,44 @@ package schema import ( - "bytes" "fmt" "strings" - - "github.com/pkg/errors" ) type VarType struct { - expr string - Type DataType + Expr string // Type, ie. map> or []User + Type CoreType // Kind, ie. int, map or struct + Comments []string `json:"comments,omitempty"` List *VarListType Map *VarMapType Struct *VarStructType + Enum *VarEnumType } func (t *VarType) String() string { - return t.expr + if t == nil { + return "" + } + return t.Expr } func (t *VarType) MarshalJSON() ([]byte, error) { - buf := bytes.NewBufferString(`"`) - buf.WriteString(t.String()) - buf.WriteString(`"`) - return buf.Bytes(), nil + return []byte(fmt.Sprintf("%q", t)), nil } func (t *VarType) UnmarshalJSON(b []byte) error { if len(b) <= 2 { - return errors.Errorf("json error: type cannot be empty") + return fmt.Errorf("json error: type cannot be empty") } s := string(b) // string value will be wrapped in quotes // validate its a string value if s[0:1] != "\"" { - return errors.Errorf("json error: string value is expected") + return fmt.Errorf("json error: string value is expected") } if s[len(s)-1:] != "\"" { - return errors.Errorf("json error: string value is expected") + return fmt.Errorf("json error: string value is expected") } // trim string quotes from the json string @@ -47,20 +46,20 @@ func (t *VarType) UnmarshalJSON(b []byte) error { s = s[:len(s)-1] // set the expr from value - t.expr = s + t.Expr = s return nil } func (t *VarType) Parse(schema *WebRPCSchema) error { - if t.expr == "" { - return errors.Errorf("schema error: type expr cannot be empty") + if t.Expr == "" { + return fmt.Errorf("schema error: type expr cannot be empty") } - err := ParseVarTypeExpr(schema, t.expr, t) + err := ParseVarTypeExpr(schema, t.Expr, t) if err != nil { return err } - t.expr = buildVarTypeExpr(t, "") + t.Expr = buildVarTypeExpr(t, "") return nil } @@ -69,24 +68,28 @@ type VarListType struct { } type VarMapType struct { - Key DataType // see, VarMapKeyDataTypes -- only T_String or T_XintX supported + Key *VarType // see, VarMapKeyCoreTypes -- only T_String or T_XintX supported Value *VarType } type VarStructType struct { - Name string - Message *Message + Name string + Type *Type +} + +type VarEnumType struct { + Name string + Type *Type } func ParseVarTypeExpr(schema *WebRPCSchema, expr string, vt *VarType) error { if expr == "" { return nil } - vt.expr = expr + vt.Expr = expr // parse data type from string - dataType, ok := DataTypeFromString[expr] - + dataType, ok := CoreTypeFromString[expr] if !ok { // test for complex datatype if isListExpr(expr) { @@ -106,7 +109,7 @@ func ParseVarTypeExpr(schema *WebRPCSchema, expr string, vt *VarType) error { vt.List = &VarListType{Elem: &VarType{}} // shift expr, and keep parsing - expr = strings.TrimPrefix(expr, DataTypeToString[T_List]) + expr = strings.TrimPrefix(expr, CoreTypeToString[T_List]) err := ParseVarTypeExpr(schema, expr, vt.List.Elem) if err != nil { return err @@ -119,34 +122,56 @@ func ParseVarTypeExpr(schema *WebRPCSchema, expr string, vt *VarType) error { return err } - keyDataType, ok := DataTypeFromString[key] - if !ok { - return errors.Errorf("schema error: invalid map key type '%s' for expr '%s'", key, expr) + if keyType, ok := CoreTypeFromString[key]; ok { + if !isValidVarKeyType(key) { + return fmt.Errorf("schema error: invalid map key '%s' for '%s'", key, expr) + } + // create sub-type object for map + vt.Map = &VarMapType{ + Key: &VarType{Expr: key, Type: keyType}, + Value: &VarType{}, + } + } else { + t := schema.GetTypeByName(key) + if t == nil || t.Kind != "enum" && t.Type.Type != T_Enum { + return fmt.Errorf("schema error: invalid map key '%s' for '%s'", key, expr) + } + vt.Map = &VarMapType{ + Key: &VarType{Expr: key, Type: T_Enum, Enum: &VarEnumType{Name: key, Type: t}}, + Value: &VarType{}, + } + vt.Map.Key.Expr = key } - // create sub-type object for map - vt.Map = &VarMapType{Key: keyDataType, Value: &VarType{}} - // shift expr and keep parsing expr = value + err = ParseVarTypeExpr(schema, expr, vt.Map.Value) if err != nil { return err } - case T_Unknown: + case T_Enum, T_Unknown: + // struct or enum - structExpr := expr - msg, ok := getMessageType(schema, structExpr) - if !ok || msg == nil { - return errors.Errorf("schema error: invalid struct/message type '%s'", structExpr) + typ, ok := getType(schema, expr) + if !ok || typ == nil { + return fmt.Errorf("schema error: invalid type '%s'", expr) } - vt.Type = T_Struct - vt.Struct = &VarStructType{Name: structExpr, Message: msg} + switch typ.Kind { + case TypeKind_Struct: + vt.Type = T_Struct + vt.Struct = &VarStructType{Name: expr, Type: typ} + case TypeKind_Enum: + vt.Type = T_Enum // TODO: T_Enum, see https://github.com/webrpc/webrpc/issues/44 + vt.Enum = &VarEnumType{Name: expr, Type: typ} + default: + return fmt.Errorf("schema error: unexpected type '%s'", expr) + } default: - // basic type, we're done here + // core type, we're done here } return nil @@ -154,32 +179,28 @@ func ParseVarTypeExpr(schema *WebRPCSchema, expr string, vt *VarType) error { func parseMapExpr(expr string) (string, string, error) { if !isMapExpr(expr) { - return "", "", errors.Errorf("schema error: invalid map expr for '%s'", expr) + return "", "", fmt.Errorf("schema error: invalid map expr for '%s'", expr) } - mapKeyword := DataTypeToString[T_Map] + mapKeyword := CoreTypeToString[T_Map] expr = expr[len(mapKeyword):] if expr[0:1] != "<" { - return "", "", errors.Errorf("schema error: invalid map syntax for '%s'", expr) + return "", "", fmt.Errorf("schema error: invalid map syntax for '%s'", expr) } if expr[len(expr)-1:] != ">" { - return "", "", errors.Errorf("schema error: invalid map syntax for '%s'", expr) + return "", "", fmt.Errorf("schema error: invalid map syntax for '%s'", expr) } expr = expr[1 : len(expr)-1] p := strings.Index(expr, ",") if p < 0 { - return "", "", errors.Errorf("schema error: invalid map syntax for '%s'", expr) + return "", "", fmt.Errorf("schema error: invalid map syntax for '%s'", expr) } key := expr[0:p] value := expr[p+1:] - if !isValidVarKeyType(key) { - return "", "", errors.Errorf("schema error: invalid map key '%s' for '%s'", key, expr) - } - return key, value, nil } @@ -200,46 +221,50 @@ func buildVarTypeExpr(vt *VarType, expr string) string { expr += vt.Struct.Name return expr + case T_Enum: + expr += vt.Enum.Name + return expr + default: - // basic type + // core type expr += vt.Type.String() return expr } } func isListExpr(expr string) bool { - listTest := DataTypeToString[T_List] + listTest := CoreTypeToString[T_List] return strings.HasPrefix(expr, listTest) } func isMapExpr(expr string) bool { - mapTest := DataTypeToString[T_Map] + "<" + mapTest := CoreTypeToString[T_Map] + "<" return strings.HasPrefix(expr, mapTest) } -func getMessageType(schema *WebRPCSchema, structExpr string) (*Message, bool) { - for _, msg := range schema.Messages { - if structExpr == string(msg.Name) { - return msg, true +func getType(schema *WebRPCSchema, structExpr string) (*Type, bool) { + for _, typ := range schema.Types { + if structExpr == string(typ.Name) { + return typ, true } } return nil, false } -var VarKeyDataTypes = []DataType{ +var VarKeyCoreTypes = []CoreType{ T_String, T_Uint, T_Uint8, T_Uint16, T_Uint32, T_Uint64, T_Int, T_Int8, T_Int16, T_Int32, T_Int64, } -var VarIntegerDataTypes = []DataType{ +var VarIntegerCoreTypes = []CoreType{ T_Uint, T_Uint8, T_Uint16, T_Uint32, T_Uint64, T_Int, T_Int8, T_Int16, T_Int32, T_Int64, } func isValidVarKeyType(s string) bool { - return isValidVarType(s, VarKeyDataTypes) + return isValidVarType(s, VarKeyCoreTypes) } -func isValidVarType(s string, allowedList []DataType) bool { - dt, ok := DataTypeFromString[s] +func isValidVarType(s string, allowedList []CoreType) bool { + dt, ok := CoreTypeFromString[s] if !ok { return false } diff --git a/tests/_testdata/test.golden.json b/tests/_testdata/test.golden.json new file mode 100644 index 00000000..c2203601 --- /dev/null +++ b/tests/_testdata/test.golden.json @@ -0,0 +1,436 @@ +{ + "webrpc": "v1", + "name": "Test", + "version": "v0.10.0", + "types": [ + { + "kind": "enum", + "name": "Status", + "type": "uint32", + "fields": [ + { + "name": "AVAILABLE", + "value": "0" + }, + { + "name": "NOT_AVAILABLE", + "value": "1" + } + ] + }, + { + "kind": "enum", + "name": "Access", + "type": "uint32", + "fields": [ + { + "name": "NONE", + "value": "0" + }, + { + "name": "READ", + "value": "1" + }, + { + "name": "WRITE", + "value": "2" + }, + { + "name": "ADMIN", + "value": "3" + }, + { + "name": "OWNER", + "value": "4" + } + ] + }, + { + "kind": "struct", + "name": "Simple", + "fields": [ + { + "name": "id", + "type": "int" + }, + { + "name": "name", + "type": "string" + } + ] + }, + { + "kind": "struct", + "name": "User", + "fields": [ + { + "name": "id", + "type": "uint64", + "meta": [ + { + "json": "id" + }, + { + "go.field.name": "ID" + }, + { + "go.tag.db": "id" + } + ] + }, + { + "name": "username", + "type": "string", + "meta": [ + { + "json": "USERNAME" + }, + { + "go.tag.db": "username" + } + ] + }, + { + "name": "role", + "type": "string", + "meta": [ + { + "go.tag.db": "-" + } + ] + } + ] + }, + { + "kind": "struct", + "name": "Complex", + "fields": [ + { + "name": "meta", + "type": "map" + }, + { + "name": "metaNestedExample", + "type": "map>" + }, + { + "name": "namesList", + "type": "[]string" + }, + { + "name": "numsList", + "type": "[]int64" + }, + { + "name": "doubleArray", + "type": "[][]string" + }, + { + "name": "listOfMaps", + "type": "[]map" + }, + { + "name": "listOfUsers", + "type": "[]User" + }, + { + "name": "mapOfUsers", + "type": "map" + }, + { + "name": "user", + "type": "User" + }, + { + "name": "status", + "type": "Status" + } + ] + }, + { + "kind": "struct", + "name": "EnumData", + "fields": [ + { + "name": "dict", + "type": "map" + }, + { + "name": "list", + "type": "[]Status" + } + ] + } + ], + "errors": [ + { + "code": 1, + "name": "Unauthorized", + "message": "unauthorized", + "httpStatus": 401 + }, + { + "code": 2, + "name": "ExpiredToken", + "message": "expired token", + "httpStatus": 401 + }, + { + "code": 3, + "name": "InvalidToken", + "message": "invalid token", + "httpStatus": 401 + }, + { + "code": 4, + "name": "Deactivated", + "message": "account deactivated", + "httpStatus": 403 + }, + { + "code": 5, + "name": "ConfirmAccount", + "message": "confirm your email", + "httpStatus": 403 + }, + { + "code": 6, + "name": "AccessDenied", + "message": "access denied", + "httpStatus": 403 + }, + { + "code": 7, + "name": "MissingArgument", + "message": "missing argument", + "httpStatus": 400 + }, + { + "code": 8, + "name": "UnexpectedValue", + "message": "unexpected value", + "httpStatus": 400 + }, + { + "code": 100, + "name": "RateLimited", + "message": "too many requests", + "httpStatus": 429 + }, + { + "code": 101, + "name": "DatabaseDown", + "message": "service outage", + "httpStatus": 503 + }, + { + "code": 102, + "name": "ElasticDown", + "message": "search is degraded", + "httpStatus": 503 + }, + { + "code": 103, + "name": "NotImplemented", + "message": "not implemented", + "httpStatus": 501 + }, + { + "code": 200, + "name": "UserNotFound", + "message": "user not found", + "httpStatus": 400 + }, + { + "code": 201, + "name": "UserBusy", + "message": "user busy", + "httpStatus": 400 + }, + { + "code": 202, + "name": "InvalidUsername", + "message": "invalid username", + "httpStatus": 400 + }, + { + "code": 300, + "name": "FileTooBig", + "message": "file is too big (max 1GB)", + "httpStatus": 400 + }, + { + "code": 301, + "name": "FileInfected", + "message": "file is infected", + "httpStatus": 400 + }, + { + "code": 302, + "name": "FileType", + "message": "unsupported file type", + "httpStatus": 400 + } + ], + "services": [ + { + "name": "TestApi", + "methods": [ + { + "name": "GetEmpty", + "annotations": {}, + "comments": [], + "inputs": [], + "outputs": [] + }, + { + "name": "GetError", + "annotations": {}, + "comments": [], + "inputs": [], + "outputs": [] + }, + { + "name": "GetOne", + "annotations": {}, + "comments": [], + "inputs": [], + "outputs": [ + { + "name": "one", + "type": "Simple", + "optional": false + } + ] + }, + { + "name": "SendOne", + "annotations": {}, + "comments": [], + "inputs": [ + { + "name": "one", + "type": "Simple", + "optional": false + } + ], + "outputs": [] + }, + { + "name": "GetMulti", + "annotations": {}, + "comments": [], + "inputs": [], + "outputs": [ + { + "name": "one", + "type": "Simple", + "optional": false + }, + { + "name": "two", + "type": "Simple", + "optional": false + }, + { + "name": "three", + "type": "Simple", + "optional": false + } + ] + }, + { + "name": "SendMulti", + "annotations": {}, + "comments": [], + "inputs": [ + { + "name": "one", + "type": "Simple", + "optional": false + }, + { + "name": "two", + "type": "Simple", + "optional": false + }, + { + "name": "three", + "type": "Simple", + "optional": false + } + ], + "outputs": [] + }, + { + "name": "GetComplex", + "annotations": {}, + "comments": [], + "inputs": [], + "outputs": [ + { + "name": "complex", + "type": "Complex", + "optional": false + } + ] + }, + { + "name": "SendComplex", + "annotations": {}, + "comments": [], + "inputs": [ + { + "name": "complex", + "type": "Complex", + "optional": false + } + ], + "outputs": [] + }, + { + "name": "GetEnumList", + "annotations": {}, + "comments": [], + "inputs": [], + "outputs": [ + { + "name": "list", + "type": "[]Status", + "optional": false + } + ] + }, + { + "name": "GetEnumMap", + "annotations": {}, + "comments": [], + "inputs": [], + "outputs": [ + { + "name": "map", + "type": "map", + "optional": false + } + ] + }, + { + "name": "GetSchemaError", + "annotations": {}, + "comments": [ + "added in v0.11.0" + ], + "inputs": [ + { + "name": "code", + "type": "int", + "optional": false + } + ], + "outputs": [] + } + ], + "comments": [] + } + ] +} diff --git a/tests/client/client.gen.go b/tests/client/client.gen.go new file mode 100644 index 00000000..f751ddee --- /dev/null +++ b/tests/client/client.gen.go @@ -0,0 +1,808 @@ +// Test v0.10.0 61c9c8c942e3f6bdffd9929e78ebb3631d924993 +// -- +// Code generated by webrpc-gen with golang generator. DO NOT EDIT. +// +// webrpc-gen -schema=./schema/test.ridl -target=golang -pkg=client -client -out=./client/client.gen.go +package client + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "strings" +) + +const WebrpcHeader = "Webrpc" + +const WebrpcHeaderValue = "webrpc;gen-golang@v0.17.0;Test@v0.10.0" + +// WebRPC description and code-gen version +func WebRPCVersion() string { + return "v1" +} + +// Schema version of your RIDL schema +func WebRPCSchemaVersion() string { + return "v0.10.0" +} + +// Schema hash generated from your RIDL schema +func WebRPCSchemaHash() string { + return "61c9c8c942e3f6bdffd9929e78ebb3631d924993" +} + +type WebrpcGenVersions struct { + WebrpcGenVersion string + CodeGenName string + CodeGenVersion string + SchemaName string + SchemaVersion string +} + +func VersionFromHeader(h http.Header) (*WebrpcGenVersions, error) { + if h.Get(WebrpcHeader) == "" { + return nil, fmt.Errorf("header is empty or missing") + } + + versions, err := parseWebrpcGenVersions(h.Get(WebrpcHeader)) + if err != nil { + return nil, fmt.Errorf("webrpc header is invalid: %w", err) + } + + return versions, nil +} + +func parseWebrpcGenVersions(header string) (*WebrpcGenVersions, error) { + versions := strings.Split(header, ";") + if len(versions) < 3 { + return nil, fmt.Errorf("expected at least 3 parts while parsing webrpc header: %v", header) + } + + _, webrpcGenVersion, ok := strings.Cut(versions[0], "@") + if !ok { + return nil, fmt.Errorf("webrpc gen version could not be parsed from: %s", versions[0]) + } + + tmplTarget, tmplVersion, ok := strings.Cut(versions[1], "@") + if !ok { + return nil, fmt.Errorf("tmplTarget and tmplVersion could not be parsed from: %s", versions[1]) + } + + schemaName, schemaVersion, ok := strings.Cut(versions[2], "@") + if !ok { + return nil, fmt.Errorf("schema name and schema version could not be parsed from: %s", versions[2]) + } + + return &WebrpcGenVersions{ + WebrpcGenVersion: webrpcGenVersion, + CodeGenName: tmplTarget, + CodeGenVersion: tmplVersion, + SchemaName: schemaName, + SchemaVersion: schemaVersion, + }, nil +} + +// +// Common types +// + +type Status uint32 + +const ( + Status_AVAILABLE Status = 0 + Status_NOT_AVAILABLE Status = 1 +) + +var Status_name = map[uint32]string{ + 0: "AVAILABLE", + 1: "NOT_AVAILABLE", +} + +var Status_value = map[string]uint32{ + "AVAILABLE": 0, + "NOT_AVAILABLE": 1, +} + +func (x Status) String() string { + return Status_name[uint32(x)] +} + +func (x Status) MarshalText() ([]byte, error) { + return []byte(Status_name[uint32(x)]), nil +} + +func (x *Status) UnmarshalText(b []byte) error { + *x = Status(Status_value[string(b)]) + return nil +} + +func (x *Status) Is(values ...Status) bool { + if x == nil { + return false + } + for _, v := range values { + if *x == v { + return true + } + } + return false +} + +type Access uint32 + +const ( + Access_NONE Access = 0 + Access_READ Access = 1 + Access_WRITE Access = 2 + Access_ADMIN Access = 3 + Access_OWNER Access = 4 +) + +var Access_name = map[uint32]string{ + 0: "NONE", + 1: "READ", + 2: "WRITE", + 3: "ADMIN", + 4: "OWNER", +} + +var Access_value = map[string]uint32{ + "NONE": 0, + "READ": 1, + "WRITE": 2, + "ADMIN": 3, + "OWNER": 4, +} + +func (x Access) String() string { + return Access_name[uint32(x)] +} + +func (x Access) MarshalText() ([]byte, error) { + return []byte(Access_name[uint32(x)]), nil +} + +func (x *Access) UnmarshalText(b []byte) error { + *x = Access(Access_value[string(b)]) + return nil +} + +func (x *Access) Is(values ...Access) bool { + if x == nil { + return false + } + for _, v := range values { + if *x == v { + return true + } + } + return false +} + +type Simple struct { + Id int `json:"id"` + Name string `json:"name"` +} + +type User struct { + ID uint64 `json:"id" db:"id"` + Username string `json:"USERNAME" db:"username"` + Role string `json:"role" db:"-"` +} + +type Complex struct { + Meta map[string]interface{} `json:"meta"` + MetaNestedExample map[string]map[string]uint32 `json:"metaNestedExample"` + NamesList []string `json:"namesList"` + NumsList []int64 `json:"numsList"` + DoubleArray [][]string `json:"doubleArray"` + ListOfMaps []map[string]uint32 `json:"listOfMaps"` + ListOfUsers []*User `json:"listOfUsers"` + MapOfUsers map[string]*User `json:"mapOfUsers"` + User *User `json:"user"` + Status Status `json:"status"` +} + +type EnumData struct { + Dict map[Access]uint64 `json:"dict"` + List []Status `json:"list"` +} + +var methods = map[string]method{ + "/rpc/TestApi/GetEmpty": { + Name: "GetEmpty", + Service: "TestApi", + Annotations: map[string]string{}, + }, + "/rpc/TestApi/GetError": { + Name: "GetError", + Service: "TestApi", + Annotations: map[string]string{}, + }, + "/rpc/TestApi/GetOne": { + Name: "GetOne", + Service: "TestApi", + Annotations: map[string]string{}, + }, + "/rpc/TestApi/SendOne": { + Name: "SendOne", + Service: "TestApi", + Annotations: map[string]string{}, + }, + "/rpc/TestApi/GetMulti": { + Name: "GetMulti", + Service: "TestApi", + Annotations: map[string]string{}, + }, + "/rpc/TestApi/SendMulti": { + Name: "SendMulti", + Service: "TestApi", + Annotations: map[string]string{}, + }, + "/rpc/TestApi/GetComplex": { + Name: "GetComplex", + Service: "TestApi", + Annotations: map[string]string{}, + }, + "/rpc/TestApi/SendComplex": { + Name: "SendComplex", + Service: "TestApi", + Annotations: map[string]string{}, + }, + "/rpc/TestApi/GetEnumList": { + Name: "GetEnumList", + Service: "TestApi", + Annotations: map[string]string{}, + }, + "/rpc/TestApi/GetEnumMap": { + Name: "GetEnumMap", + Service: "TestApi", + Annotations: map[string]string{}, + }, + "/rpc/TestApi/GetSchemaError": { + Name: "GetSchemaError", + Service: "TestApi", + Annotations: map[string]string{}, + }, +} + +func WebrpcMethods() map[string]method { + res := make(map[string]method, len(methods)) + for k, v := range methods { + res[k] = v + } + + return res +} + +var WebRPCServices = map[string][]string{ + "TestApi": { + "GetEmpty", + "GetError", + "GetOne", + "SendOne", + "GetMulti", + "SendMulti", + "GetComplex", + "SendComplex", + "GetEnumList", + "GetEnumMap", + "GetSchemaError", + }, +} + +// +// Server types +// + +type TestApi interface { + GetEmpty(ctx context.Context) error + GetError(ctx context.Context) error + GetOne(ctx context.Context) (*Simple, error) + SendOne(ctx context.Context, one *Simple) error + GetMulti(ctx context.Context) (*Simple, *Simple, *Simple, error) + SendMulti(ctx context.Context, one *Simple, two *Simple, three *Simple) error + GetComplex(ctx context.Context) (*Complex, error) + SendComplex(ctx context.Context, complex *Complex) error + GetEnumList(ctx context.Context) ([]Status, error) + GetEnumMap(ctx context.Context) (map[Access]uint64, error) + // added in v0.11.0 + GetSchemaError(ctx context.Context, code int) error +} + +// +// Client types +// + +type TestApiClient interface { + GetEmpty(ctx context.Context) error + GetError(ctx context.Context) error + GetOne(ctx context.Context) (*Simple, error) + SendOne(ctx context.Context, one *Simple) error + GetMulti(ctx context.Context) (*Simple, *Simple, *Simple, error) + SendMulti(ctx context.Context, one *Simple, two *Simple, three *Simple) error + GetComplex(ctx context.Context) (*Complex, error) + SendComplex(ctx context.Context, complex *Complex) error + GetEnumList(ctx context.Context) ([]Status, error) + GetEnumMap(ctx context.Context) (map[Access]uint64, error) + // added in v0.11.0 + GetSchemaError(ctx context.Context, code int) error +} + +// +// Client +// + +const TestApiPathPrefix = "/rpc/TestApi/" + +type testApiClient struct { + client HTTPClient + urls [11]string +} + +func NewTestApiClient(addr string, client HTTPClient) TestApiClient { + prefix := urlBase(addr) + TestApiPathPrefix + urls := [11]string{ + prefix + "GetEmpty", + prefix + "GetError", + prefix + "GetOne", + prefix + "SendOne", + prefix + "GetMulti", + prefix + "SendMulti", + prefix + "GetComplex", + prefix + "SendComplex", + prefix + "GetEnumList", + prefix + "GetEnumMap", + prefix + "GetSchemaError", + } + return &testApiClient{ + client: client, + urls: urls, + } +} + +func (c *testApiClient) GetEmpty(ctx context.Context) error { + + resp, err := doHTTPRequest(ctx, c.client, c.urls[0], nil, nil) + if resp != nil { + cerr := resp.Body.Close() + if err == nil && cerr != nil { + err = ErrWebrpcRequestFailed.WithCausef("failed to close response body: %w", cerr) + } + } + + return err +} + +func (c *testApiClient) GetError(ctx context.Context) error { + + resp, err := doHTTPRequest(ctx, c.client, c.urls[1], nil, nil) + if resp != nil { + cerr := resp.Body.Close() + if err == nil && cerr != nil { + err = ErrWebrpcRequestFailed.WithCausef("failed to close response body: %w", cerr) + } + } + + return err +} + +func (c *testApiClient) GetOne(ctx context.Context) (*Simple, error) { + out := struct { + Ret0 *Simple `json:"one"` + }{} + + resp, err := doHTTPRequest(ctx, c.client, c.urls[2], nil, &out) + if resp != nil { + cerr := resp.Body.Close() + if err == nil && cerr != nil { + err = ErrWebrpcRequestFailed.WithCausef("failed to close response body: %w", cerr) + } + } + + return out.Ret0, err +} + +func (c *testApiClient) SendOne(ctx context.Context, one *Simple) error { + in := struct { + Arg0 *Simple `json:"one"` + }{one} + + resp, err := doHTTPRequest(ctx, c.client, c.urls[3], in, nil) + if resp != nil { + cerr := resp.Body.Close() + if err == nil && cerr != nil { + err = ErrWebrpcRequestFailed.WithCausef("failed to close response body: %w", cerr) + } + } + + return err +} + +func (c *testApiClient) GetMulti(ctx context.Context) (*Simple, *Simple, *Simple, error) { + out := struct { + Ret0 *Simple `json:"one"` + Ret1 *Simple `json:"two"` + Ret2 *Simple `json:"three"` + }{} + + resp, err := doHTTPRequest(ctx, c.client, c.urls[4], nil, &out) + if resp != nil { + cerr := resp.Body.Close() + if err == nil && cerr != nil { + err = ErrWebrpcRequestFailed.WithCausef("failed to close response body: %w", cerr) + } + } + + return out.Ret0, out.Ret1, out.Ret2, err +} + +func (c *testApiClient) SendMulti(ctx context.Context, one *Simple, two *Simple, three *Simple) error { + in := struct { + Arg0 *Simple `json:"one"` + Arg1 *Simple `json:"two"` + Arg2 *Simple `json:"three"` + }{one, two, three} + + resp, err := doHTTPRequest(ctx, c.client, c.urls[5], in, nil) + if resp != nil { + cerr := resp.Body.Close() + if err == nil && cerr != nil { + err = ErrWebrpcRequestFailed.WithCausef("failed to close response body: %w", cerr) + } + } + + return err +} + +func (c *testApiClient) GetComplex(ctx context.Context) (*Complex, error) { + out := struct { + Ret0 *Complex `json:"complex"` + }{} + + resp, err := doHTTPRequest(ctx, c.client, c.urls[6], nil, &out) + if resp != nil { + cerr := resp.Body.Close() + if err == nil && cerr != nil { + err = ErrWebrpcRequestFailed.WithCausef("failed to close response body: %w", cerr) + } + } + + return out.Ret0, err +} + +func (c *testApiClient) SendComplex(ctx context.Context, complex *Complex) error { + in := struct { + Arg0 *Complex `json:"complex"` + }{complex} + + resp, err := doHTTPRequest(ctx, c.client, c.urls[7], in, nil) + if resp != nil { + cerr := resp.Body.Close() + if err == nil && cerr != nil { + err = ErrWebrpcRequestFailed.WithCausef("failed to close response body: %w", cerr) + } + } + + return err +} + +func (c *testApiClient) GetEnumList(ctx context.Context) ([]Status, error) { + out := struct { + Ret0 []Status `json:"list"` + }{} + + resp, err := doHTTPRequest(ctx, c.client, c.urls[8], nil, &out) + if resp != nil { + cerr := resp.Body.Close() + if err == nil && cerr != nil { + err = ErrWebrpcRequestFailed.WithCausef("failed to close response body: %w", cerr) + } + } + + return out.Ret0, err +} + +func (c *testApiClient) GetEnumMap(ctx context.Context) (map[Access]uint64, error) { + out := struct { + Ret0 map[Access]uint64 `json:"map"` + }{} + + resp, err := doHTTPRequest(ctx, c.client, c.urls[9], nil, &out) + if resp != nil { + cerr := resp.Body.Close() + if err == nil && cerr != nil { + err = ErrWebrpcRequestFailed.WithCausef("failed to close response body: %w", cerr) + } + } + + return out.Ret0, err +} + +func (c *testApiClient) GetSchemaError(ctx context.Context, code int) error { + in := struct { + Arg0 int `json:"code"` + }{code} + + resp, err := doHTTPRequest(ctx, c.client, c.urls[10], in, nil) + if resp != nil { + cerr := resp.Body.Close() + if err == nil && cerr != nil { + err = ErrWebrpcRequestFailed.WithCausef("failed to close response body: %w", cerr) + } + } + + return err +} + +// HTTPClient is the interface used by generated clients to send HTTP requests. +// It is fulfilled by *(net/http).Client, which is sufficient for most users. +// Users can provide their own implementation for special retry policies. +type HTTPClient interface { + Do(req *http.Request) (*http.Response, error) +} + +// urlBase helps ensure that addr specifies a scheme. If it is unparsable +// as a URL, it returns addr unchanged. +func urlBase(addr string) string { + // If the addr specifies a scheme, use it. If not, default to + // http. If url.Parse fails on it, return it unchanged. + url, err := url.Parse(addr) + if err != nil { + return addr + } + if url.Scheme == "" { + url.Scheme = "http" + } + return url.String() +} + +// newRequest makes an http.Request from a client, adding common headers. +func newRequest(ctx context.Context, url string, reqBody io.Reader, contentType string) (*http.Request, error) { + req, err := http.NewRequestWithContext(ctx, "POST", url, reqBody) + if err != nil { + return nil, err + } + req.Header.Set("Accept", contentType) + req.Header.Set("Content-Type", contentType) + req.Header.Set(WebrpcHeader, WebrpcHeaderValue) + if headers, ok := HTTPRequestHeaders(ctx); ok { + for k := range headers { + for _, v := range headers[k] { + req.Header.Add(k, v) + } + } + } + return req, nil +} + +// doHTTPRequest is common code to make a request to the remote service. +func doHTTPRequest(ctx context.Context, client HTTPClient, url string, in, out interface{}) (*http.Response, error) { + reqBody, err := json.Marshal(in) + if err != nil { + return nil, ErrWebrpcRequestFailed.WithCausef("failed to marshal JSON body: %w", err) + } + if err = ctx.Err(); err != nil { + return nil, ErrWebrpcRequestFailed.WithCausef("aborted because context was done: %w", err) + } + + req, err := newRequest(ctx, url, bytes.NewBuffer(reqBody), "application/json") + if err != nil { + return nil, ErrWebrpcRequestFailed.WithCausef("could not build request: %w", err) + } + + resp, err := client.Do(req) + if err != nil { + return nil, ErrWebrpcRequestFailed.WithCause(err) + } + + if resp.StatusCode != 200 { + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, ErrWebrpcBadResponse.WithCausef("failed to read server error response body: %w", err) + } + + var rpcErr WebRPCError + if err := json.Unmarshal(respBody, &rpcErr); err != nil { + return nil, ErrWebrpcBadResponse.WithCausef("failed to unmarshal server error: %w", err) + } + if rpcErr.Cause != "" { + rpcErr.cause = errors.New(rpcErr.Cause) + } + return nil, rpcErr + } + + if out != nil { + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, ErrWebrpcBadResponse.WithCausef("failed to read response body: %w", err) + } + + err = json.Unmarshal(respBody, &out) + if err != nil { + return nil, ErrWebrpcBadResponse.WithCausef("failed to unmarshal JSON response body: %w", err) + } + } + + return resp, nil +} + +func WithHTTPRequestHeaders(ctx context.Context, h http.Header) (context.Context, error) { + if _, ok := h["Accept"]; ok { + return nil, errors.New("provided header cannot set Accept") + } + if _, ok := h["Content-Type"]; ok { + return nil, errors.New("provided header cannot set Content-Type") + } + + copied := make(http.Header, len(h)) + for k, vv := range h { + if vv == nil { + copied[k] = nil + continue + } + copied[k] = make([]string, len(vv)) + copy(copied[k], vv) + } + + return context.WithValue(ctx, HTTPClientRequestHeadersCtxKey, copied), nil +} + +func HTTPRequestHeaders(ctx context.Context) (http.Header, bool) { + h, ok := ctx.Value(HTTPClientRequestHeadersCtxKey).(http.Header) + return h, ok +} + +// +// Helpers +// + +type method struct { + Name string + Service string + Annotations map[string]string +} + +type contextKey struct { + name string +} + +func (k *contextKey) String() string { + return "webrpc context value " + k.name +} + +var ( + HTTPClientRequestHeadersCtxKey = &contextKey{"HTTPClientRequestHeaders"} + HTTPRequestCtxKey = &contextKey{"HTTPRequest"} + + ServiceNameCtxKey = &contextKey{"ServiceName"} + + MethodNameCtxKey = &contextKey{"MethodName"} +) + +func ServiceNameFromContext(ctx context.Context) string { + service, _ := ctx.Value(ServiceNameCtxKey).(string) + return service +} + +func MethodNameFromContext(ctx context.Context) string { + method, _ := ctx.Value(MethodNameCtxKey).(string) + return method +} + +func RequestFromContext(ctx context.Context) *http.Request { + r, _ := ctx.Value(HTTPRequestCtxKey).(*http.Request) + return r +} + +func MethodCtx(ctx context.Context) (method, bool) { + req := RequestFromContext(ctx) + if req == nil { + return method{}, false + } + + m, ok := methods[req.URL.Path] + if !ok { + return method{}, false + } + + return m, true +} + +// +// Errors +// + +type WebRPCError struct { + Name string `json:"error"` + Code int `json:"code"` + Message string `json:"msg"` + Cause string `json:"cause,omitempty"` + HTTPStatus int `json:"status"` + cause error +} + +var _ error = WebRPCError{} + +func (e WebRPCError) Error() string { + if e.cause != nil { + return fmt.Sprintf("%s %d: %s: %v", e.Name, e.Code, e.Message, e.cause) + } + return fmt.Sprintf("%s %d: %s", e.Name, e.Code, e.Message) +} + +func (e WebRPCError) Is(target error) bool { + if target == nil { + return false + } + if rpcErr, ok := target.(WebRPCError); ok { + return rpcErr.Code == e.Code + } + return errors.Is(e.cause, target) +} + +func (e WebRPCError) Unwrap() error { + return e.cause +} + +func (e WebRPCError) WithCause(cause error) WebRPCError { + err := e + err.cause = cause + err.Cause = cause.Error() + return err +} + +func (e WebRPCError) WithCausef(format string, args ...interface{}) WebRPCError { + cause := fmt.Errorf(format, args...) + err := e + err.cause = cause + err.Cause = cause.Error() + return err +} + +// Deprecated: Use .WithCause() method on WebRPCError. +func ErrorWithCause(rpcErr WebRPCError, cause error) WebRPCError { + return rpcErr.WithCause(cause) +} + +// Webrpc errors +var ( + ErrWebrpcEndpoint = WebRPCError{Code: 0, Name: "WebrpcEndpoint", Message: "endpoint error", HTTPStatus: 400} + ErrWebrpcRequestFailed = WebRPCError{Code: -1, Name: "WebrpcRequestFailed", Message: "request failed", HTTPStatus: 400} + ErrWebrpcBadRoute = WebRPCError{Code: -2, Name: "WebrpcBadRoute", Message: "bad route", HTTPStatus: 404} + ErrWebrpcBadMethod = WebRPCError{Code: -3, Name: "WebrpcBadMethod", Message: "bad method", HTTPStatus: 405} + ErrWebrpcBadRequest = WebRPCError{Code: -4, Name: "WebrpcBadRequest", Message: "bad request", HTTPStatus: 400} + ErrWebrpcBadResponse = WebRPCError{Code: -5, Name: "WebrpcBadResponse", Message: "bad response", HTTPStatus: 500} + ErrWebrpcServerPanic = WebRPCError{Code: -6, Name: "WebrpcServerPanic", Message: "server panic", HTTPStatus: 500} + ErrWebrpcInternalError = WebRPCError{Code: -7, Name: "WebrpcInternalError", Message: "internal error", HTTPStatus: 500} + ErrWebrpcClientDisconnected = WebRPCError{Code: -8, Name: "WebrpcClientDisconnected", Message: "client disconnected", HTTPStatus: 400} + ErrWebrpcStreamLost = WebRPCError{Code: -9, Name: "WebrpcStreamLost", Message: "stream lost", HTTPStatus: 400} + ErrWebrpcStreamFinished = WebRPCError{Code: -10, Name: "WebrpcStreamFinished", Message: "stream finished", HTTPStatus: 200} +) + +// Schema errors +var ( + ErrUnauthorized = WebRPCError{Code: 1, Name: "Unauthorized", Message: "unauthorized", HTTPStatus: 401} + ErrExpiredToken = WebRPCError{Code: 2, Name: "ExpiredToken", Message: "expired token", HTTPStatus: 401} + ErrInvalidToken = WebRPCError{Code: 3, Name: "InvalidToken", Message: "invalid token", HTTPStatus: 401} + ErrDeactivated = WebRPCError{Code: 4, Name: "Deactivated", Message: "account deactivated", HTTPStatus: 403} + ErrConfirmAccount = WebRPCError{Code: 5, Name: "ConfirmAccount", Message: "confirm your email", HTTPStatus: 403} + ErrAccessDenied = WebRPCError{Code: 6, Name: "AccessDenied", Message: "access denied", HTTPStatus: 403} + ErrMissingArgument = WebRPCError{Code: 7, Name: "MissingArgument", Message: "missing argument", HTTPStatus: 400} + ErrUnexpectedValue = WebRPCError{Code: 8, Name: "UnexpectedValue", Message: "unexpected value", HTTPStatus: 400} + ErrRateLimited = WebRPCError{Code: 100, Name: "RateLimited", Message: "too many requests", HTTPStatus: 429} + ErrDatabaseDown = WebRPCError{Code: 101, Name: "DatabaseDown", Message: "service outage", HTTPStatus: 503} + ErrElasticDown = WebRPCError{Code: 102, Name: "ElasticDown", Message: "search is degraded", HTTPStatus: 503} + ErrNotImplemented = WebRPCError{Code: 103, Name: "NotImplemented", Message: "not implemented", HTTPStatus: 501} + ErrUserNotFound = WebRPCError{Code: 200, Name: "UserNotFound", Message: "user not found", HTTPStatus: 400} + ErrUserBusy = WebRPCError{Code: 201, Name: "UserBusy", Message: "user busy", HTTPStatus: 400} + ErrInvalidUsername = WebRPCError{Code: 202, Name: "InvalidUsername", Message: "invalid username", HTTPStatus: 400} + ErrFileTooBig = WebRPCError{Code: 300, Name: "FileTooBig", Message: "file is too big (max 1GB)", HTTPStatus: 400} + ErrFileInfected = WebRPCError{Code: 301, Name: "FileInfected", Message: "file is infected", HTTPStatus: 400} + ErrFileType = WebRPCError{Code: 302, Name: "FileType", Message: "unsupported file type", HTTPStatus: 400} +) diff --git a/tests/client/client.go b/tests/client/client.go new file mode 100644 index 00000000..9ba54897 --- /dev/null +++ b/tests/client/client.go @@ -0,0 +1,159 @@ +package client + +import ( + "context" + "errors" + "fmt" + "net/http" + "strings" + "time" +) + +func Wait(serverURL string, timeout time.Duration) error { + testApi := NewTestApiClient(serverURL, &http.Client{}) + + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + for { + err := testApi.GetEmpty(ctx) + if err == nil { + return nil // Success. + } + + select { + case <-ctx.Done(): + return fmt.Errorf("wait: test server (%v) still not ready after %v", serverURL, timeout) + + case <-time.After(100 * time.Millisecond): + // Add a delay between retry attempts. + } + } +} + +func RunTests(ctx context.Context, serverURL string) error { + var errs []error // Note: We can't use Go 1.20's errors.Join() until we drop support for older Go versions. + + testApi := NewTestApiClient(serverURL, &http.Client{}) + + if err := testApi.GetEmpty(ctx); err != nil { + errs = append(errs, fmt.Errorf("GetEmpty(): %w", err)) + } + + if err := testApi.GetError(ctx); err == nil { + errs = append(errs, fmt.Errorf("GetError(): expected error, got nil")) + } + + one, err := testApi.GetOne(ctx) + if err != nil { + errs = append(errs, fmt.Errorf("GetOne(): %w", err)) + } + if err := testApi.SendOne(ctx, one); err != nil { + errs = append(errs, fmt.Errorf("SendOne(): %w", err)) + } + + one, two, three, err := testApi.GetMulti(ctx) + if err != nil { + errs = append(errs, fmt.Errorf("GetMulti(): %w", err)) + } + if err := testApi.SendMulti(ctx, one, two, three); err != nil { + errs = append(errs, fmt.Errorf("SendMulti(): %w", err)) + } + + complex, err := testApi.GetComplex(ctx) + if err != nil { + errs = append(errs, fmt.Errorf("GetComplex(): %w", err)) + } + if err := testApi.SendComplex(ctx, complex); err != nil { + errs = append(errs, fmt.Errorf("SendComplex(): %w", err)) + } + + enumList, err := testApi.GetEnumList(ctx) + if err != nil { + errs = append(errs, fmt.Errorf("GetEnumList(): %w", err)) + } + if len(enumList) != 2 { + errs = append(errs, fmt.Errorf("GetEnumList(): expected 2 items, got %v", len(enumList))) + } + + enumMap, err := testApi.GetEnumMap(ctx) + if err != nil { + errs = append(errs, fmt.Errorf("GetEnumMap(): %w", err)) + } + if len(enumMap) != 2 { + errs = append(errs, fmt.Errorf("GetEnumMap(): expected 2 items, got %v", len(enumMap))) + } + + schemaErrs := testSchemaErrors(ctx, testApi) + errs = append(errs, schemaErrs...) + + if len(errs) > 0 { + var b strings.Builder + fmt.Fprintf(&b, "Failed tests:\n") + for _, err := range errs { + fmt.Fprintf(&b, "%v\n", err) + } + return fmt.Errorf(b.String()) + } + + return nil +} + +func testSchemaErrors(ctx context.Context, testApi TestApi) []error { + tt := []struct { + code int + err WebRPCError + name string + msg string + httpStatusCode int + cause string + }{ + {code: 0, err: ErrWebrpcEndpoint, name: "WebrpcEndpoint", msg: "endpoint error", httpStatusCode: 400, cause: "failed to read file: unexpected EOF"}, + {code: 1, err: ErrUnauthorized, name: "Unauthorized", msg: "unauthorized", httpStatusCode: 401, cause: "failed to verify JWT token"}, + {code: 2, err: ErrExpiredToken, name: "ExpiredToken", msg: "expired token", httpStatusCode: 401}, + {code: 3, err: ErrInvalidToken, name: "InvalidToken", msg: "invalid token", httpStatusCode: 401}, + {code: 4, err: ErrDeactivated, name: "Deactivated", msg: "account deactivated", httpStatusCode: 403}, + {code: 5, err: ErrConfirmAccount, name: "ConfirmAccount", msg: "confirm your email", httpStatusCode: 403}, + {code: 6, err: ErrAccessDenied, name: "AccessDenied", msg: "access denied", httpStatusCode: 403}, + {code: 7, err: ErrMissingArgument, name: "MissingArgument", msg: "missing argument", httpStatusCode: 400}, + {code: 8, err: ErrUnexpectedValue, name: "UnexpectedValue", msg: "unexpected value", httpStatusCode: 400}, + {code: 100, err: ErrRateLimited, name: "RateLimited", msg: "too many requests", httpStatusCode: 429, cause: "1000 req/min exceeded"}, + {code: 101, err: ErrDatabaseDown, name: "DatabaseDown", msg: "service outage", httpStatusCode: 503}, + {code: 102, err: ErrElasticDown, name: "ElasticDown", msg: "search is degraded", httpStatusCode: 503}, + {code: 103, err: ErrNotImplemented, name: "NotImplemented", msg: "not implemented", httpStatusCode: 501}, + {code: 200, err: ErrUserNotFound, name: "UserNotFound", msg: "user not found", httpStatusCode: 400}, + {code: 201, err: ErrUserBusy, name: "UserBusy", msg: "user busy", httpStatusCode: 400}, + {code: 202, err: ErrInvalidUsername, name: "InvalidUsername", msg: "invalid username", httpStatusCode: 400}, + {code: 300, err: ErrFileTooBig, name: "FileTooBig", msg: "file is too big (max 1GB)", httpStatusCode: 400}, + {code: 301, err: ErrFileInfected, name: "FileInfected", msg: "file is infected", httpStatusCode: 400}, + {code: 302, err: ErrFileType, name: "FileType", msg: "unsupported file type", httpStatusCode: 400, cause: ".wav is not supported"}, + } + + var errs []error + + for _, tc := range tt { + err := testApi.GetSchemaError(ctx, tc.code) + if !errors.Is(err, tc.err) { + errs = append(errs, fmt.Errorf("unexpected error for code=%v:\nexpected: %#v,\ngot: %#v", tc.code, tc.err, err)) + } + + rpcErr, _ := err.(WebRPCError) + if rpcErr.Code != tc.code { + errs = append(errs, fmt.Errorf("unexpected error code: expected: %v, got: %v", tc.code, rpcErr.Code)) + } + if rpcErr.Name != tc.name { + errs = append(errs, fmt.Errorf("unexpected error name: expected: %q, got: %q", tc.name, rpcErr.Name)) + } + if rpcErr.Message != tc.msg { + errs = append(errs, fmt.Errorf("unexpected error message: expected: %q, got: %q", tc.msg, rpcErr.Message)) + } + if rpcErr.HTTPStatus != tc.httpStatusCode { + errs = append(errs, fmt.Errorf("unexpected error HTTP status code: expected: %v, got: %v", tc.httpStatusCode, rpcErr.HTTPStatus)) + } + if cause := rpcErr.Unwrap(); cause != nil && cause.Error() != tc.cause { + errs = append(errs, fmt.Errorf("unexpected error cause: expected %q, got %q", tc.cause, cause.Error())) + } + } + + return errs +} diff --git a/tests/embed.go b/tests/embed.go new file mode 100644 index 00000000..fc08e3ae --- /dev/null +++ b/tests/embed.go @@ -0,0 +1,21 @@ +package tests + +import ( + "embed" + + "github.com/webrpc/webrpc/schema/ridl" +) + +//go:embed schema/test.ridl +var fs embed.FS + +func GetRIDLSchema() string { + data, _ := fs.ReadFile("schema/test.ridl") + return string(data) +} + +func GetJSONSchema() string { + schema, _ := ridl.NewParser(fs, "schema/test.ridl").Parse() + data, _ := schema.ToJSON() + return data +} diff --git a/tests/interoperability_test.go b/tests/interoperability_test.go new file mode 100644 index 00000000..4beefd08 --- /dev/null +++ b/tests/interoperability_test.go @@ -0,0 +1,24 @@ +package tests + +//go:generate webrpc-gen -schema=./schema/test.ridl -target=json -out=./schema/test.gen.json +//go:generate webrpc-gen -schema=./schema/test.ridl -target=debug -out=./schema/test.debug.gen.txt +//go:generate webrpc-gen -schema=./schema/test.ridl -target=golang -pkg=client -client -out=./client/client.gen.go +//go:generate webrpc-gen -schema=./schema/test.ridl -target=golang -pkg=server -server -out=./server/server.gen.go + +import ( + "context" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/webrpc/webrpc/tests/client" + "github.com/webrpc/webrpc/tests/server" +) + +func TestInteroperability(t *testing.T) { + srv := httptest.NewServer(server.NewTestApiServer(&server.TestServer{})) + defer srv.Close() + + err := client.RunTests(context.Background(), srv.URL) + assert.NoError(t, err) +} diff --git a/tests/schema/test.debug.gen.txt b/tests/schema/test.debug.gen.txt new file mode 100644 index 00000000..0e8454dc --- /dev/null +++ b/tests/schema/test.debug.gen.txt @@ -0,0 +1,3746 @@ +(gen.TemplateVars) { + WebRPCSchema: (*schema.WebRPCSchema)({ + WebrpcVersion: (string) (len=2) "v1", + SchemaName: (string) (len=4) "Test", + SchemaVersion: (string) (len=7) "v0.10.0", + Types: ([]*schema.Type) (len=6 cap=8) { + (*schema.Type)({ + Kind: (string) (len=4) "enum", + Name: (string) (len=6) "Status", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "uint32", + Type: (schema.CoreType) 8, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + Fields: ([]*schema.TypeField) (len=2 cap=2) { + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=9) "AVAILABLE", + Type: (*schema.VarType)(), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) (len=1) "0", + Meta: ([]schema.TypeFieldMeta) + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=13) "NOT_AVAILABLE", + Type: (*schema.VarType)(), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) (len=1) "1", + Meta: ([]schema.TypeFieldMeta) + } + }) + }, + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + }, + Comments: ([]string) + }), + (*schema.Type)({ + Kind: (string) (len=4) "enum", + Name: (string) (len=6) "Access", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "uint32", + Type: (schema.CoreType) 8, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + Fields: ([]*schema.TypeField) (len=5 cap=8) { + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=4) "NONE", + Type: (*schema.VarType)(), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) (len=1) "0", + Meta: ([]schema.TypeFieldMeta) + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=4) "READ", + Type: (*schema.VarType)(), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) (len=1) "1", + Meta: ([]schema.TypeFieldMeta) + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=5) "WRITE", + Type: (*schema.VarType)(), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) (len=1) "2", + Meta: ([]schema.TypeFieldMeta) + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=5) "ADMIN", + Type: (*schema.VarType)(), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) (len=1) "3", + Meta: ([]schema.TypeFieldMeta) + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=5) "OWNER", + Type: (*schema.VarType)(), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) (len=1) "4", + Meta: ([]schema.TypeFieldMeta) + } + }) + }, + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + }, + Comments: ([]string) + }), + (*schema.Type)({ + Kind: (string) (len=6) "struct", + Name: (string) (len=6) "Simple", + Type: (*schema.VarType)(), + Fields: ([]*schema.TypeField) (len=2 cap=2) { + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=2) "id", + Type: (*schema.VarType)({ + Expr: (string) (len=3) "int", + Type: (schema.CoreType) 10, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=4) "name", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "string", + Type: (schema.CoreType) 17, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }) + }, + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + }, + Comments: ([]string) { + } + }), + (*schema.Type)({ + Kind: (string) (len=6) "struct", + Name: (string) (len=4) "User", + Type: (*schema.VarType)(), + Fields: ([]*schema.TypeField) (len=3 cap=4) { + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=2) "id", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "uint64", + Type: (schema.CoreType) 9, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) (len=3 cap=4) { + (schema.TypeFieldMeta) (len=1) { + (string) (len=4) "json": (string) (len=2) "id" + }, + (schema.TypeFieldMeta) (len=1) { + (string) (len=13) "go.field.name": (string) (len=2) "ID" + }, + (schema.TypeFieldMeta) (len=1) { + (string) (len=9) "go.tag.db": (string) (len=2) "id" + } + } + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=8) "username", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "string", + Type: (schema.CoreType) 17, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) (len=2 cap=2) { + (schema.TypeFieldMeta) (len=1) { + (string) (len=4) "json": (string) (len=8) "USERNAME" + }, + (schema.TypeFieldMeta) (len=1) { + (string) (len=9) "go.tag.db": (string) (len=8) "username" + } + } + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=4) "role", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "string", + Type: (schema.CoreType) 17, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) (len=1 cap=1) { + (schema.TypeFieldMeta) (len=1) { + (string) (len=9) "go.tag.db": (string) (len=1) "-" + } + } + } + }) + }, + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + }, + Comments: ([]string) { + } + }), + (*schema.Type)({ + Kind: (string) (len=6) "struct", + Name: (string) (len=7) "Complex", + Type: (*schema.VarType)(), + Fields: ([]*schema.TypeField) (len=10 cap=16) { + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=4) "meta", + Type: (*schema.VarType)({ + Expr: (string) (len=15) "map", + Type: (schema.CoreType) 20, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)({ + Key: (*schema.VarType)({ + Expr: (string) (len=6) "string", + Type: (schema.CoreType) 17, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + Value: (*schema.VarType)({ + Expr: (string) (len=3) "any", + Type: (schema.CoreType) 2, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }) + }), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=17) "metaNestedExample", + Type: (*schema.VarType)({ + Expr: (string) (len=30) "map>", + Type: (schema.CoreType) 20, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)({ + Key: (*schema.VarType)({ + Expr: (string) (len=6) "string", + Type: (schema.CoreType) 17, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + Value: (*schema.VarType)({ + Expr: (string) (len=18) "map", + Type: (schema.CoreType) 20, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)({ + Key: (*schema.VarType)({ + Expr: (string) (len=6) "string", + Type: (schema.CoreType) 17, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + Value: (*schema.VarType)({ + Expr: (string) (len=6) "uint32", + Type: (schema.CoreType) 8, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }) + }), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }) + }), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=9) "namesList", + Type: (*schema.VarType)({ + Expr: (string) (len=8) "[]string", + Type: (schema.CoreType) 19, + Comments: ([]string) , + List: (*schema.VarListType)({ + Elem: (*schema.VarType)({ + Expr: (string) (len=6) "string", + Type: (schema.CoreType) 17, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }) + }), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=8) "numsList", + Type: (*schema.VarType)({ + Expr: (string) (len=7) "[]int64", + Type: (schema.CoreType) 19, + Comments: ([]string) , + List: (*schema.VarListType)({ + Elem: (*schema.VarType)({ + Expr: (string) (len=5) "int64", + Type: (schema.CoreType) 14, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }) + }), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=11) "doubleArray", + Type: (*schema.VarType)({ + Expr: (string) (len=10) "[][]string", + Type: (schema.CoreType) 19, + Comments: ([]string) , + List: (*schema.VarListType)({ + Elem: (*schema.VarType)({ + Expr: (string) (len=8) "[]string", + Type: (schema.CoreType) 19, + Comments: ([]string) , + List: (*schema.VarListType)({ + Elem: (*schema.VarType)({ + Expr: (string) (len=6) "string", + Type: (schema.CoreType) 17, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }) + }), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }) + }), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=10) "listOfMaps", + Type: (*schema.VarType)({ + Expr: (string) (len=20) "[]map", + Type: (schema.CoreType) 19, + Comments: ([]string) , + List: (*schema.VarListType)({ + Elem: (*schema.VarType)({ + Expr: (string) (len=18) "map", + Type: (schema.CoreType) 20, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)({ + Key: (*schema.VarType)({ + Expr: (string) (len=6) "string", + Type: (schema.CoreType) 17, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + Value: (*schema.VarType)({ + Expr: (string) (len=6) "uint32", + Type: (schema.CoreType) 8, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }) + }), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }) + }), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=11) "listOfUsers", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "[]User", + Type: (schema.CoreType) 19, + Comments: ([]string) , + List: (*schema.VarListType)({ + Elem: (*schema.VarType)({ + Expr: (string) (len=4) "User", + Type: (schema.CoreType) 21, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)({ + Name: (string) (len=4) "User", + Type: (*schema.Type)({ + Kind: (string) (len=6) "struct", + Name: (string) (len=4) "User", + Type: (*schema.VarType)(), + Fields: ([]*schema.TypeField) (len=3 cap=4) { + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=2) "id", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "uint64", + Type: (schema.CoreType) 9, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) (len=3 cap=4) { + (schema.TypeFieldMeta) (len=1) { + (string) (len=4) "json": (string) (len=2) "id" + }, + (schema.TypeFieldMeta) (len=1) { + (string) (len=13) "go.field.name": (string) (len=2) "ID" + }, + (schema.TypeFieldMeta) (len=1) { + (string) (len=9) "go.tag.db": (string) (len=2) "id" + } + } + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=8) "username", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "string", + Type: (schema.CoreType) 17, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) (len=2 cap=2) { + (schema.TypeFieldMeta) (len=1) { + (string) (len=4) "json": (string) (len=8) "USERNAME" + }, + (schema.TypeFieldMeta) (len=1) { + (string) (len=9) "go.tag.db": (string) (len=8) "username" + } + } + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=4) "role", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "string", + Type: (schema.CoreType) 17, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) (len=1 cap=1) { + (schema.TypeFieldMeta) (len=1) { + (string) (len=9) "go.tag.db": (string) (len=1) "-" + } + } + } + }) + }, + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + }, + Comments: ([]string) { + } + }) + }), + Enum: (*schema.VarEnumType)() + }) + }), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=10) "mapOfUsers", + Type: (*schema.VarType)({ + Expr: (string) (len=16) "map", + Type: (schema.CoreType) 20, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)({ + Key: (*schema.VarType)({ + Expr: (string) (len=6) "string", + Type: (schema.CoreType) 17, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + Value: (*schema.VarType)({ + Expr: (string) (len=4) "User", + Type: (schema.CoreType) 21, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)({ + Name: (string) (len=4) "User", + Type: (*schema.Type)({ + Kind: (string) (len=6) "struct", + Name: (string) (len=4) "User", + Type: (*schema.VarType)(), + Fields: ([]*schema.TypeField) (len=3 cap=4) { + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=2) "id", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "uint64", + Type: (schema.CoreType) 9, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) (len=3 cap=4) { + (schema.TypeFieldMeta) (len=1) { + (string) (len=4) "json": (string) (len=2) "id" + }, + (schema.TypeFieldMeta) (len=1) { + (string) (len=13) "go.field.name": (string) (len=2) "ID" + }, + (schema.TypeFieldMeta) (len=1) { + (string) (len=9) "go.tag.db": (string) (len=2) "id" + } + } + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=8) "username", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "string", + Type: (schema.CoreType) 17, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) (len=2 cap=2) { + (schema.TypeFieldMeta) (len=1) { + (string) (len=4) "json": (string) (len=8) "USERNAME" + }, + (schema.TypeFieldMeta) (len=1) { + (string) (len=9) "go.tag.db": (string) (len=8) "username" + } + } + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=4) "role", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "string", + Type: (schema.CoreType) 17, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) (len=1 cap=1) { + (schema.TypeFieldMeta) (len=1) { + (string) (len=9) "go.tag.db": (string) (len=1) "-" + } + } + } + }) + }, + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + }, + Comments: ([]string) { + } + }) + }), + Enum: (*schema.VarEnumType)() + }) + }), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=4) "user", + Type: (*schema.VarType)({ + Expr: (string) (len=4) "User", + Type: (schema.CoreType) 21, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)({ + Name: (string) (len=4) "User", + Type: (*schema.Type)({ + Kind: (string) (len=6) "struct", + Name: (string) (len=4) "User", + Type: (*schema.VarType)(), + Fields: ([]*schema.TypeField) (len=3 cap=4) { + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=2) "id", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "uint64", + Type: (schema.CoreType) 9, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) (len=3 cap=4) { + (schema.TypeFieldMeta) (len=1) { + (string) (len=4) "json": (string) (len=2) "id" + }, + (schema.TypeFieldMeta) (len=1) { + (string) (len=13) "go.field.name": (string) (len=2) "ID" + }, + (schema.TypeFieldMeta) (len=1) { + (string) (len=9) "go.tag.db": (string) (len=2) "id" + } + } + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=8) "username", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "string", + Type: (schema.CoreType) 17, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) (len=2 cap=2) { + (schema.TypeFieldMeta) (len=1) { + (string) (len=4) "json": (string) (len=8) "USERNAME" + }, + (schema.TypeFieldMeta) (len=1) { + (string) (len=9) "go.tag.db": (string) (len=8) "username" + } + } + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=4) "role", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "string", + Type: (schema.CoreType) 17, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) (len=1 cap=1) { + (schema.TypeFieldMeta) (len=1) { + (string) (len=9) "go.tag.db": (string) (len=1) "-" + } + } + } + }) + }, + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + }, + Comments: ([]string) { + } + }) + }), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=6) "status", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "Status", + Type: (schema.CoreType) 22, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)({ + Name: (string) (len=6) "Status", + Type: (*schema.Type)({ + Kind: (string) (len=4) "enum", + Name: (string) (len=6) "Status", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "uint32", + Type: (schema.CoreType) 8, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + Fields: ([]*schema.TypeField) (len=2 cap=2) { + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=9) "AVAILABLE", + Type: (*schema.VarType)(), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) (len=1) "0", + Meta: ([]schema.TypeFieldMeta) + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=13) "NOT_AVAILABLE", + Type: (*schema.VarType)(), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) (len=1) "1", + Meta: ([]schema.TypeFieldMeta) + } + }) + }, + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + }, + Comments: ([]string) + }) + }) + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }) + }, + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + }, + Comments: ([]string) { + } + }), + (*schema.Type)({ + Kind: (string) (len=6) "struct", + Name: (string) (len=8) "EnumData", + Type: (*schema.VarType)(), + Fields: ([]*schema.TypeField) (len=2 cap=2) { + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=4) "dict", + Type: (*schema.VarType)({ + Expr: (string) (len=18) "map", + Type: (schema.CoreType) 20, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)({ + Key: (*schema.VarType)({ + Expr: (string) (len=6) "Access", + Type: (schema.CoreType) 22, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)({ + Name: (string) (len=6) "Access", + Type: (*schema.Type)({ + Kind: (string) (len=4) "enum", + Name: (string) (len=6) "Access", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "uint32", + Type: (schema.CoreType) 8, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + Fields: ([]*schema.TypeField) (len=5 cap=8) { + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=4) "NONE", + Type: (*schema.VarType)(), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) (len=1) "0", + Meta: ([]schema.TypeFieldMeta) + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=4) "READ", + Type: (*schema.VarType)(), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) (len=1) "1", + Meta: ([]schema.TypeFieldMeta) + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=5) "WRITE", + Type: (*schema.VarType)(), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) (len=1) "2", + Meta: ([]schema.TypeFieldMeta) + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=5) "ADMIN", + Type: (*schema.VarType)(), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) (len=1) "3", + Meta: ([]schema.TypeFieldMeta) + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=5) "OWNER", + Type: (*schema.VarType)(), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) (len=1) "4", + Meta: ([]schema.TypeFieldMeta) + } + }) + }, + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + }, + Comments: ([]string) + }) + }) + }), + Value: (*schema.VarType)({ + Expr: (string) (len=6) "uint64", + Type: (schema.CoreType) 9, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }) + }), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=4) "list", + Type: (*schema.VarType)({ + Expr: (string) (len=8) "[]Status", + Type: (schema.CoreType) 19, + Comments: ([]string) , + List: (*schema.VarListType)({ + Elem: (*schema.VarType)({ + Expr: (string) (len=6) "Status", + Type: (schema.CoreType) 22, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)({ + Name: (string) (len=6) "Status", + Type: (*schema.Type)({ + Kind: (string) (len=4) "enum", + Name: (string) (len=6) "Status", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "uint32", + Type: (schema.CoreType) 8, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + Fields: ([]*schema.TypeField) (len=2 cap=2) { + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=9) "AVAILABLE", + Type: (*schema.VarType)(), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) (len=1) "0", + Meta: ([]schema.TypeFieldMeta) + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=13) "NOT_AVAILABLE", + Type: (*schema.VarType)(), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) (len=1) "1", + Meta: ([]schema.TypeFieldMeta) + } + }) + }, + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + }, + Comments: ([]string) + }) + }) + }) + }), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }) + }, + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + }, + Comments: ([]string) { + } + }) + }, + Errors: ([]*schema.Error) (len=18 cap=32) { + (*schema.Error)({ + Code: (int) 1, + Name: (string) (len=12) "Unauthorized", + Message: (string) (len=12) "unauthorized", + HTTPStatus: (int) 401 + }), + (*schema.Error)({ + Code: (int) 2, + Name: (string) (len=12) "ExpiredToken", + Message: (string) (len=13) "expired token", + HTTPStatus: (int) 401 + }), + (*schema.Error)({ + Code: (int) 3, + Name: (string) (len=12) "InvalidToken", + Message: (string) (len=13) "invalid token", + HTTPStatus: (int) 401 + }), + (*schema.Error)({ + Code: (int) 4, + Name: (string) (len=11) "Deactivated", + Message: (string) (len=19) "account deactivated", + HTTPStatus: (int) 403 + }), + (*schema.Error)({ + Code: (int) 5, + Name: (string) (len=14) "ConfirmAccount", + Message: (string) (len=18) "confirm your email", + HTTPStatus: (int) 403 + }), + (*schema.Error)({ + Code: (int) 6, + Name: (string) (len=12) "AccessDenied", + Message: (string) (len=13) "access denied", + HTTPStatus: (int) 403 + }), + (*schema.Error)({ + Code: (int) 7, + Name: (string) (len=15) "MissingArgument", + Message: (string) (len=16) "missing argument", + HTTPStatus: (int) 400 + }), + (*schema.Error)({ + Code: (int) 8, + Name: (string) (len=15) "UnexpectedValue", + Message: (string) (len=16) "unexpected value", + HTTPStatus: (int) 400 + }), + (*schema.Error)({ + Code: (int) 100, + Name: (string) (len=11) "RateLimited", + Message: (string) (len=17) "too many requests", + HTTPStatus: (int) 429 + }), + (*schema.Error)({ + Code: (int) 101, + Name: (string) (len=12) "DatabaseDown", + Message: (string) (len=14) "service outage", + HTTPStatus: (int) 503 + }), + (*schema.Error)({ + Code: (int) 102, + Name: (string) (len=11) "ElasticDown", + Message: (string) (len=18) "search is degraded", + HTTPStatus: (int) 503 + }), + (*schema.Error)({ + Code: (int) 103, + Name: (string) (len=14) "NotImplemented", + Message: (string) (len=15) "not implemented", + HTTPStatus: (int) 501 + }), + (*schema.Error)({ + Code: (int) 200, + Name: (string) (len=12) "UserNotFound", + Message: (string) (len=14) "user not found", + HTTPStatus: (int) 400 + }), + (*schema.Error)({ + Code: (int) 201, + Name: (string) (len=8) "UserBusy", + Message: (string) (len=9) "user busy", + HTTPStatus: (int) 400 + }), + (*schema.Error)({ + Code: (int) 202, + Name: (string) (len=15) "InvalidUsername", + Message: (string) (len=16) "invalid username", + HTTPStatus: (int) 400 + }), + (*schema.Error)({ + Code: (int) 300, + Name: (string) (len=10) "FileTooBig", + Message: (string) (len=25) "file is too big (max 1GB)", + HTTPStatus: (int) 400 + }), + (*schema.Error)({ + Code: (int) 301, + Name: (string) (len=12) "FileInfected", + Message: (string) (len=16) "file is infected", + HTTPStatus: (int) 400 + }), + (*schema.Error)({ + Code: (int) 302, + Name: (string) (len=8) "FileType", + Message: (string) (len=21) "unsupported file type", + HTTPStatus: (int) 400 + }) + }, + Services: ([]*schema.Service) (len=1 cap=1) { + (*schema.Service)({ + Name: (string) (len=7) "TestApi", + Methods: ([]*schema.Method) (len=11 cap=16) { + (*schema.Method)({ + Name: (string) (len=8) "GetEmpty", + Annotations: (schema.Annotations) { + }, + Comments: ([]string) { + }, + StreamInput: (bool) false, + StreamOutput: (bool) false, + Proxy: (bool) false, + Inputs: ([]*schema.MethodArgument) { + }, + Outputs: ([]*schema.MethodArgument) { + }, + Service: (*schema.Service)() + }), + (*schema.Method)({ + Name: (string) (len=8) "GetError", + Annotations: (schema.Annotations) { + }, + Comments: ([]string) { + }, + StreamInput: (bool) false, + StreamOutput: (bool) false, + Proxy: (bool) false, + Inputs: ([]*schema.MethodArgument) { + }, + Outputs: ([]*schema.MethodArgument) { + }, + Service: (*schema.Service)() + }), + (*schema.Method)({ + Name: (string) (len=6) "GetOne", + Annotations: (schema.Annotations) { + }, + Comments: ([]string) { + }, + StreamInput: (bool) false, + StreamOutput: (bool) false, + Proxy: (bool) false, + Inputs: ([]*schema.MethodArgument) { + }, + Outputs: ([]*schema.MethodArgument) (len=1 cap=1) { + (*schema.MethodArgument)({ + Name: (string) (len=3) "one", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "Simple", + Type: (schema.CoreType) 21, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)({ + Name: (string) (len=6) "Simple", + Type: (*schema.Type)({ + Kind: (string) (len=6) "struct", + Name: (string) (len=6) "Simple", + Type: (*schema.VarType)(), + Fields: ([]*schema.TypeField) (len=2 cap=2) { + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=2) "id", + Type: (*schema.VarType)({ + Expr: (string) (len=3) "int", + Type: (schema.CoreType) 10, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=4) "name", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "string", + Type: (schema.CoreType) 17, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }) + }, + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + }, + Comments: ([]string) { + } + }) + }), + Enum: (*schema.VarEnumType)() + }), + Optional: (bool) false, + InputArg: (bool) false, + OutputArg: (bool) true, + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }) + }, + Service: (*schema.Service)() + }), + (*schema.Method)({ + Name: (string) (len=7) "SendOne", + Annotations: (schema.Annotations) { + }, + Comments: ([]string) { + }, + StreamInput: (bool) false, + StreamOutput: (bool) false, + Proxy: (bool) false, + Inputs: ([]*schema.MethodArgument) (len=1 cap=1) { + (*schema.MethodArgument)({ + Name: (string) (len=3) "one", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "Simple", + Type: (schema.CoreType) 21, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)({ + Name: (string) (len=6) "Simple", + Type: (*schema.Type)({ + Kind: (string) (len=6) "struct", + Name: (string) (len=6) "Simple", + Type: (*schema.VarType)(), + Fields: ([]*schema.TypeField) (len=2 cap=2) { + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=2) "id", + Type: (*schema.VarType)({ + Expr: (string) (len=3) "int", + Type: (schema.CoreType) 10, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=4) "name", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "string", + Type: (schema.CoreType) 17, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }) + }, + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + }, + Comments: ([]string) { + } + }) + }), + Enum: (*schema.VarEnumType)() + }), + Optional: (bool) false, + InputArg: (bool) true, + OutputArg: (bool) false, + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }) + }, + Outputs: ([]*schema.MethodArgument) { + }, + Service: (*schema.Service)() + }), + (*schema.Method)({ + Name: (string) (len=8) "GetMulti", + Annotations: (schema.Annotations) { + }, + Comments: ([]string) { + }, + StreamInput: (bool) false, + StreamOutput: (bool) false, + Proxy: (bool) false, + Inputs: ([]*schema.MethodArgument) { + }, + Outputs: ([]*schema.MethodArgument) (len=3 cap=4) { + (*schema.MethodArgument)({ + Name: (string) (len=3) "one", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "Simple", + Type: (schema.CoreType) 21, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)({ + Name: (string) (len=6) "Simple", + Type: (*schema.Type)({ + Kind: (string) (len=6) "struct", + Name: (string) (len=6) "Simple", + Type: (*schema.VarType)(), + Fields: ([]*schema.TypeField) (len=2 cap=2) { + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=2) "id", + Type: (*schema.VarType)({ + Expr: (string) (len=3) "int", + Type: (schema.CoreType) 10, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=4) "name", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "string", + Type: (schema.CoreType) 17, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }) + }, + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + }, + Comments: ([]string) { + } + }) + }), + Enum: (*schema.VarEnumType)() + }), + Optional: (bool) false, + InputArg: (bool) false, + OutputArg: (bool) true, + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }), + (*schema.MethodArgument)({ + Name: (string) (len=3) "two", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "Simple", + Type: (schema.CoreType) 21, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)({ + Name: (string) (len=6) "Simple", + Type: (*schema.Type)({ + Kind: (string) (len=6) "struct", + Name: (string) (len=6) "Simple", + Type: (*schema.VarType)(), + Fields: ([]*schema.TypeField) (len=2 cap=2) { + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=2) "id", + Type: (*schema.VarType)({ + Expr: (string) (len=3) "int", + Type: (schema.CoreType) 10, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=4) "name", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "string", + Type: (schema.CoreType) 17, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }) + }, + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + }, + Comments: ([]string) { + } + }) + }), + Enum: (*schema.VarEnumType)() + }), + Optional: (bool) false, + InputArg: (bool) false, + OutputArg: (bool) true, + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }), + (*schema.MethodArgument)({ + Name: (string) (len=5) "three", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "Simple", + Type: (schema.CoreType) 21, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)({ + Name: (string) (len=6) "Simple", + Type: (*schema.Type)({ + Kind: (string) (len=6) "struct", + Name: (string) (len=6) "Simple", + Type: (*schema.VarType)(), + Fields: ([]*schema.TypeField) (len=2 cap=2) { + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=2) "id", + Type: (*schema.VarType)({ + Expr: (string) (len=3) "int", + Type: (schema.CoreType) 10, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=4) "name", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "string", + Type: (schema.CoreType) 17, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }) + }, + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + }, + Comments: ([]string) { + } + }) + }), + Enum: (*schema.VarEnumType)() + }), + Optional: (bool) false, + InputArg: (bool) false, + OutputArg: (bool) true, + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }) + }, + Service: (*schema.Service)() + }), + (*schema.Method)({ + Name: (string) (len=9) "SendMulti", + Annotations: (schema.Annotations) { + }, + Comments: ([]string) { + }, + StreamInput: (bool) false, + StreamOutput: (bool) false, + Proxy: (bool) false, + Inputs: ([]*schema.MethodArgument) (len=3 cap=4) { + (*schema.MethodArgument)({ + Name: (string) (len=3) "one", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "Simple", + Type: (schema.CoreType) 21, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)({ + Name: (string) (len=6) "Simple", + Type: (*schema.Type)({ + Kind: (string) (len=6) "struct", + Name: (string) (len=6) "Simple", + Type: (*schema.VarType)(), + Fields: ([]*schema.TypeField) (len=2 cap=2) { + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=2) "id", + Type: (*schema.VarType)({ + Expr: (string) (len=3) "int", + Type: (schema.CoreType) 10, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=4) "name", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "string", + Type: (schema.CoreType) 17, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }) + }, + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + }, + Comments: ([]string) { + } + }) + }), + Enum: (*schema.VarEnumType)() + }), + Optional: (bool) false, + InputArg: (bool) true, + OutputArg: (bool) false, + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }), + (*schema.MethodArgument)({ + Name: (string) (len=3) "two", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "Simple", + Type: (schema.CoreType) 21, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)({ + Name: (string) (len=6) "Simple", + Type: (*schema.Type)({ + Kind: (string) (len=6) "struct", + Name: (string) (len=6) "Simple", + Type: (*schema.VarType)(), + Fields: ([]*schema.TypeField) (len=2 cap=2) { + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=2) "id", + Type: (*schema.VarType)({ + Expr: (string) (len=3) "int", + Type: (schema.CoreType) 10, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=4) "name", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "string", + Type: (schema.CoreType) 17, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }) + }, + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + }, + Comments: ([]string) { + } + }) + }), + Enum: (*schema.VarEnumType)() + }), + Optional: (bool) false, + InputArg: (bool) true, + OutputArg: (bool) false, + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }), + (*schema.MethodArgument)({ + Name: (string) (len=5) "three", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "Simple", + Type: (schema.CoreType) 21, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)({ + Name: (string) (len=6) "Simple", + Type: (*schema.Type)({ + Kind: (string) (len=6) "struct", + Name: (string) (len=6) "Simple", + Type: (*schema.VarType)(), + Fields: ([]*schema.TypeField) (len=2 cap=2) { + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=2) "id", + Type: (*schema.VarType)({ + Expr: (string) (len=3) "int", + Type: (schema.CoreType) 10, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=4) "name", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "string", + Type: (schema.CoreType) 17, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }) + }, + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + }, + Comments: ([]string) { + } + }) + }), + Enum: (*schema.VarEnumType)() + }), + Optional: (bool) false, + InputArg: (bool) true, + OutputArg: (bool) false, + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }) + }, + Outputs: ([]*schema.MethodArgument) { + }, + Service: (*schema.Service)() + }), + (*schema.Method)({ + Name: (string) (len=10) "GetComplex", + Annotations: (schema.Annotations) { + }, + Comments: ([]string) { + }, + StreamInput: (bool) false, + StreamOutput: (bool) false, + Proxy: (bool) false, + Inputs: ([]*schema.MethodArgument) { + }, + Outputs: ([]*schema.MethodArgument) (len=1 cap=1) { + (*schema.MethodArgument)({ + Name: (string) (len=7) "complex", + Type: (*schema.VarType)({ + Expr: (string) (len=7) "Complex", + Type: (schema.CoreType) 21, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)({ + Name: (string) (len=7) "Complex", + Type: (*schema.Type)({ + Kind: (string) (len=6) "struct", + Name: (string) (len=7) "Complex", + Type: (*schema.VarType)(), + Fields: ([]*schema.TypeField) (len=10 cap=16) { + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=4) "meta", + Type: (*schema.VarType)({ + Expr: (string) (len=15) "map", + Type: (schema.CoreType) 20, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)({ + Key: (*schema.VarType)({ + Expr: (string) (len=6) "string", + Type: (schema.CoreType) 17, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + Value: (*schema.VarType)({ + Expr: (string) (len=3) "any", + Type: (schema.CoreType) 2, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }) + }), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=17) "metaNestedExample", + Type: (*schema.VarType)({ + Expr: (string) (len=30) "map>", + Type: (schema.CoreType) 20, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)({ + Key: (*schema.VarType)({ + Expr: (string) (len=6) "string", + Type: (schema.CoreType) 17, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + Value: (*schema.VarType)({ + Expr: (string) (len=18) "map", + Type: (schema.CoreType) 20, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)({ + Key: (*schema.VarType)({ + Expr: (string) (len=6) "string", + Type: (schema.CoreType) 17, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + Value: (*schema.VarType)({ + Expr: (string) (len=6) "uint32", + Type: (schema.CoreType) 8, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }) + }), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }) + }), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=9) "namesList", + Type: (*schema.VarType)({ + Expr: (string) (len=8) "[]string", + Type: (schema.CoreType) 19, + Comments: ([]string) , + List: (*schema.VarListType)({ + Elem: (*schema.VarType)({ + Expr: (string) (len=6) "string", + Type: (schema.CoreType) 17, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }) + }), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=8) "numsList", + Type: (*schema.VarType)({ + Expr: (string) (len=7) "[]int64", + Type: (schema.CoreType) 19, + Comments: ([]string) , + List: (*schema.VarListType)({ + Elem: (*schema.VarType)({ + Expr: (string) (len=5) "int64", + Type: (schema.CoreType) 14, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }) + }), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=11) "doubleArray", + Type: (*schema.VarType)({ + Expr: (string) (len=10) "[][]string", + Type: (schema.CoreType) 19, + Comments: ([]string) , + List: (*schema.VarListType)({ + Elem: (*schema.VarType)({ + Expr: (string) (len=8) "[]string", + Type: (schema.CoreType) 19, + Comments: ([]string) , + List: (*schema.VarListType)({ + Elem: (*schema.VarType)({ + Expr: (string) (len=6) "string", + Type: (schema.CoreType) 17, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }) + }), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }) + }), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=10) "listOfMaps", + Type: (*schema.VarType)({ + Expr: (string) (len=20) "[]map", + Type: (schema.CoreType) 19, + Comments: ([]string) , + List: (*schema.VarListType)({ + Elem: (*schema.VarType)({ + Expr: (string) (len=18) "map", + Type: (schema.CoreType) 20, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)({ + Key: (*schema.VarType)({ + Expr: (string) (len=6) "string", + Type: (schema.CoreType) 17, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + Value: (*schema.VarType)({ + Expr: (string) (len=6) "uint32", + Type: (schema.CoreType) 8, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }) + }), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }) + }), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=11) "listOfUsers", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "[]User", + Type: (schema.CoreType) 19, + Comments: ([]string) , + List: (*schema.VarListType)({ + Elem: (*schema.VarType)({ + Expr: (string) (len=4) "User", + Type: (schema.CoreType) 21, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)({ + Name: (string) (len=4) "User", + Type: (*schema.Type)({ + Kind: (string) (len=6) "struct", + Name: (string) (len=4) "User", + Type: (*schema.VarType)(), + Fields: ([]*schema.TypeField) (len=3 cap=4) { + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=2) "id", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "uint64", + Type: (schema.CoreType) 9, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) (len=3 cap=4) { + (schema.TypeFieldMeta) (len=1) { + (string) (len=4) "json": (string) (len=2) "id" + }, + (schema.TypeFieldMeta) (len=1) { + (string) (len=13) "go.field.name": (string) (len=2) "ID" + }, + (schema.TypeFieldMeta) (len=1) { + (string) (len=9) "go.tag.db": (string) (len=2) "id" + } + } + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=8) "username", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "string", + Type: (schema.CoreType) 17, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) (len=2 cap=2) { + (schema.TypeFieldMeta) (len=1) { + (string) (len=4) "json": (string) (len=8) "USERNAME" + }, + (schema.TypeFieldMeta) (len=1) { + (string) (len=9) "go.tag.db": (string) (len=8) "username" + } + } + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=4) "role", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "string", + Type: (schema.CoreType) 17, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) (len=1 cap=1) { + (schema.TypeFieldMeta) (len=1) { + (string) (len=9) "go.tag.db": (string) (len=1) "-" + } + } + } + }) + }, + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + }, + Comments: ([]string) { + } + }) + }), + Enum: (*schema.VarEnumType)() + }) + }), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=10) "mapOfUsers", + Type: (*schema.VarType)({ + Expr: (string) (len=16) "map", + Type: (schema.CoreType) 20, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)({ + Key: (*schema.VarType)({ + Expr: (string) (len=6) "string", + Type: (schema.CoreType) 17, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + Value: (*schema.VarType)({ + Expr: (string) (len=4) "User", + Type: (schema.CoreType) 21, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)({ + Name: (string) (len=4) "User", + Type: (*schema.Type)({ + Kind: (string) (len=6) "struct", + Name: (string) (len=4) "User", + Type: (*schema.VarType)(), + Fields: ([]*schema.TypeField) (len=3 cap=4) { + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=2) "id", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "uint64", + Type: (schema.CoreType) 9, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) (len=3 cap=4) { + (schema.TypeFieldMeta) (len=1) { + (string) (len=4) "json": (string) (len=2) "id" + }, + (schema.TypeFieldMeta) (len=1) { + (string) (len=13) "go.field.name": (string) (len=2) "ID" + }, + (schema.TypeFieldMeta) (len=1) { + (string) (len=9) "go.tag.db": (string) (len=2) "id" + } + } + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=8) "username", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "string", + Type: (schema.CoreType) 17, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) (len=2 cap=2) { + (schema.TypeFieldMeta) (len=1) { + (string) (len=4) "json": (string) (len=8) "USERNAME" + }, + (schema.TypeFieldMeta) (len=1) { + (string) (len=9) "go.tag.db": (string) (len=8) "username" + } + } + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=4) "role", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "string", + Type: (schema.CoreType) 17, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) (len=1 cap=1) { + (schema.TypeFieldMeta) (len=1) { + (string) (len=9) "go.tag.db": (string) (len=1) "-" + } + } + } + }) + }, + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + }, + Comments: ([]string) { + } + }) + }), + Enum: (*schema.VarEnumType)() + }) + }), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=4) "user", + Type: (*schema.VarType)({ + Expr: (string) (len=4) "User", + Type: (schema.CoreType) 21, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)({ + Name: (string) (len=4) "User", + Type: (*schema.Type)({ + Kind: (string) (len=6) "struct", + Name: (string) (len=4) "User", + Type: (*schema.VarType)(), + Fields: ([]*schema.TypeField) (len=3 cap=4) { + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=2) "id", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "uint64", + Type: (schema.CoreType) 9, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) (len=3 cap=4) { + (schema.TypeFieldMeta) (len=1) { + (string) (len=4) "json": (string) (len=2) "id" + }, + (schema.TypeFieldMeta) (len=1) { + (string) (len=13) "go.field.name": (string) (len=2) "ID" + }, + (schema.TypeFieldMeta) (len=1) { + (string) (len=9) "go.tag.db": (string) (len=2) "id" + } + } + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=8) "username", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "string", + Type: (schema.CoreType) 17, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) (len=2 cap=2) { + (schema.TypeFieldMeta) (len=1) { + (string) (len=4) "json": (string) (len=8) "USERNAME" + }, + (schema.TypeFieldMeta) (len=1) { + (string) (len=9) "go.tag.db": (string) (len=8) "username" + } + } + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=4) "role", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "string", + Type: (schema.CoreType) 17, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) (len=1 cap=1) { + (schema.TypeFieldMeta) (len=1) { + (string) (len=9) "go.tag.db": (string) (len=1) "-" + } + } + } + }) + }, + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + }, + Comments: ([]string) { + } + }) + }), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=6) "status", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "Status", + Type: (schema.CoreType) 22, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)({ + Name: (string) (len=6) "Status", + Type: (*schema.Type)({ + Kind: (string) (len=4) "enum", + Name: (string) (len=6) "Status", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "uint32", + Type: (schema.CoreType) 8, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + Fields: ([]*schema.TypeField) (len=2 cap=2) { + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=9) "AVAILABLE", + Type: (*schema.VarType)(), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) (len=1) "0", + Meta: ([]schema.TypeFieldMeta) + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=13) "NOT_AVAILABLE", + Type: (*schema.VarType)(), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) (len=1) "1", + Meta: ([]schema.TypeFieldMeta) + } + }) + }, + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + }, + Comments: ([]string) + }) + }) + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }) + }, + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + }, + Comments: ([]string) { + } + }) + }), + Enum: (*schema.VarEnumType)() + }), + Optional: (bool) false, + InputArg: (bool) false, + OutputArg: (bool) true, + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }) + }, + Service: (*schema.Service)() + }), + (*schema.Method)({ + Name: (string) (len=11) "SendComplex", + Annotations: (schema.Annotations) { + }, + Comments: ([]string) { + }, + StreamInput: (bool) false, + StreamOutput: (bool) false, + Proxy: (bool) false, + Inputs: ([]*schema.MethodArgument) (len=1 cap=1) { + (*schema.MethodArgument)({ + Name: (string) (len=7) "complex", + Type: (*schema.VarType)({ + Expr: (string) (len=7) "Complex", + Type: (schema.CoreType) 21, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)({ + Name: (string) (len=7) "Complex", + Type: (*schema.Type)({ + Kind: (string) (len=6) "struct", + Name: (string) (len=7) "Complex", + Type: (*schema.VarType)(), + Fields: ([]*schema.TypeField) (len=10 cap=16) { + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=4) "meta", + Type: (*schema.VarType)({ + Expr: (string) (len=15) "map", + Type: (schema.CoreType) 20, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)({ + Key: (*schema.VarType)({ + Expr: (string) (len=6) "string", + Type: (schema.CoreType) 17, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + Value: (*schema.VarType)({ + Expr: (string) (len=3) "any", + Type: (schema.CoreType) 2, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }) + }), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=17) "metaNestedExample", + Type: (*schema.VarType)({ + Expr: (string) (len=30) "map>", + Type: (schema.CoreType) 20, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)({ + Key: (*schema.VarType)({ + Expr: (string) (len=6) "string", + Type: (schema.CoreType) 17, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + Value: (*schema.VarType)({ + Expr: (string) (len=18) "map", + Type: (schema.CoreType) 20, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)({ + Key: (*schema.VarType)({ + Expr: (string) (len=6) "string", + Type: (schema.CoreType) 17, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + Value: (*schema.VarType)({ + Expr: (string) (len=6) "uint32", + Type: (schema.CoreType) 8, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }) + }), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }) + }), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=9) "namesList", + Type: (*schema.VarType)({ + Expr: (string) (len=8) "[]string", + Type: (schema.CoreType) 19, + Comments: ([]string) , + List: (*schema.VarListType)({ + Elem: (*schema.VarType)({ + Expr: (string) (len=6) "string", + Type: (schema.CoreType) 17, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }) + }), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=8) "numsList", + Type: (*schema.VarType)({ + Expr: (string) (len=7) "[]int64", + Type: (schema.CoreType) 19, + Comments: ([]string) , + List: (*schema.VarListType)({ + Elem: (*schema.VarType)({ + Expr: (string) (len=5) "int64", + Type: (schema.CoreType) 14, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }) + }), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=11) "doubleArray", + Type: (*schema.VarType)({ + Expr: (string) (len=10) "[][]string", + Type: (schema.CoreType) 19, + Comments: ([]string) , + List: (*schema.VarListType)({ + Elem: (*schema.VarType)({ + Expr: (string) (len=8) "[]string", + Type: (schema.CoreType) 19, + Comments: ([]string) , + List: (*schema.VarListType)({ + Elem: (*schema.VarType)({ + Expr: (string) (len=6) "string", + Type: (schema.CoreType) 17, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }) + }), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }) + }), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=10) "listOfMaps", + Type: (*schema.VarType)({ + Expr: (string) (len=20) "[]map", + Type: (schema.CoreType) 19, + Comments: ([]string) , + List: (*schema.VarListType)({ + Elem: (*schema.VarType)({ + Expr: (string) (len=18) "map", + Type: (schema.CoreType) 20, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)({ + Key: (*schema.VarType)({ + Expr: (string) (len=6) "string", + Type: (schema.CoreType) 17, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + Value: (*schema.VarType)({ + Expr: (string) (len=6) "uint32", + Type: (schema.CoreType) 8, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }) + }), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }) + }), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=11) "listOfUsers", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "[]User", + Type: (schema.CoreType) 19, + Comments: ([]string) , + List: (*schema.VarListType)({ + Elem: (*schema.VarType)({ + Expr: (string) (len=4) "User", + Type: (schema.CoreType) 21, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)({ + Name: (string) (len=4) "User", + Type: (*schema.Type)({ + Kind: (string) (len=6) "struct", + Name: (string) (len=4) "User", + Type: (*schema.VarType)(), + Fields: ([]*schema.TypeField) (len=3 cap=4) { + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=2) "id", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "uint64", + Type: (schema.CoreType) 9, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) (len=3 cap=4) { + (schema.TypeFieldMeta) (len=1) { + (string) (len=4) "json": (string) (len=2) "id" + }, + (schema.TypeFieldMeta) (len=1) { + (string) (len=13) "go.field.name": (string) (len=2) "ID" + }, + (schema.TypeFieldMeta) (len=1) { + (string) (len=9) "go.tag.db": (string) (len=2) "id" + } + } + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=8) "username", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "string", + Type: (schema.CoreType) 17, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) (len=2 cap=2) { + (schema.TypeFieldMeta) (len=1) { + (string) (len=4) "json": (string) (len=8) "USERNAME" + }, + (schema.TypeFieldMeta) (len=1) { + (string) (len=9) "go.tag.db": (string) (len=8) "username" + } + } + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=4) "role", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "string", + Type: (schema.CoreType) 17, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) (len=1 cap=1) { + (schema.TypeFieldMeta) (len=1) { + (string) (len=9) "go.tag.db": (string) (len=1) "-" + } + } + } + }) + }, + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + }, + Comments: ([]string) { + } + }) + }), + Enum: (*schema.VarEnumType)() + }) + }), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=10) "mapOfUsers", + Type: (*schema.VarType)({ + Expr: (string) (len=16) "map", + Type: (schema.CoreType) 20, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)({ + Key: (*schema.VarType)({ + Expr: (string) (len=6) "string", + Type: (schema.CoreType) 17, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + Value: (*schema.VarType)({ + Expr: (string) (len=4) "User", + Type: (schema.CoreType) 21, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)({ + Name: (string) (len=4) "User", + Type: (*schema.Type)({ + Kind: (string) (len=6) "struct", + Name: (string) (len=4) "User", + Type: (*schema.VarType)(), + Fields: ([]*schema.TypeField) (len=3 cap=4) { + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=2) "id", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "uint64", + Type: (schema.CoreType) 9, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) (len=3 cap=4) { + (schema.TypeFieldMeta) (len=1) { + (string) (len=4) "json": (string) (len=2) "id" + }, + (schema.TypeFieldMeta) (len=1) { + (string) (len=13) "go.field.name": (string) (len=2) "ID" + }, + (schema.TypeFieldMeta) (len=1) { + (string) (len=9) "go.tag.db": (string) (len=2) "id" + } + } + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=8) "username", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "string", + Type: (schema.CoreType) 17, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) (len=2 cap=2) { + (schema.TypeFieldMeta) (len=1) { + (string) (len=4) "json": (string) (len=8) "USERNAME" + }, + (schema.TypeFieldMeta) (len=1) { + (string) (len=9) "go.tag.db": (string) (len=8) "username" + } + } + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=4) "role", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "string", + Type: (schema.CoreType) 17, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) (len=1 cap=1) { + (schema.TypeFieldMeta) (len=1) { + (string) (len=9) "go.tag.db": (string) (len=1) "-" + } + } + } + }) + }, + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + }, + Comments: ([]string) { + } + }) + }), + Enum: (*schema.VarEnumType)() + }) + }), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=4) "user", + Type: (*schema.VarType)({ + Expr: (string) (len=4) "User", + Type: (schema.CoreType) 21, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)({ + Name: (string) (len=4) "User", + Type: (*schema.Type)({ + Kind: (string) (len=6) "struct", + Name: (string) (len=4) "User", + Type: (*schema.VarType)(), + Fields: ([]*schema.TypeField) (len=3 cap=4) { + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=2) "id", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "uint64", + Type: (schema.CoreType) 9, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) (len=3 cap=4) { + (schema.TypeFieldMeta) (len=1) { + (string) (len=4) "json": (string) (len=2) "id" + }, + (schema.TypeFieldMeta) (len=1) { + (string) (len=13) "go.field.name": (string) (len=2) "ID" + }, + (schema.TypeFieldMeta) (len=1) { + (string) (len=9) "go.tag.db": (string) (len=2) "id" + } + } + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=8) "username", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "string", + Type: (schema.CoreType) 17, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) (len=2 cap=2) { + (schema.TypeFieldMeta) (len=1) { + (string) (len=4) "json": (string) (len=8) "USERNAME" + }, + (schema.TypeFieldMeta) (len=1) { + (string) (len=9) "go.tag.db": (string) (len=8) "username" + } + } + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=4) "role", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "string", + Type: (schema.CoreType) 17, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) (len=1 cap=1) { + (schema.TypeFieldMeta) (len=1) { + (string) (len=9) "go.tag.db": (string) (len=1) "-" + } + } + } + }) + }, + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + }, + Comments: ([]string) { + } + }) + }), + Enum: (*schema.VarEnumType)() + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=6) "status", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "Status", + Type: (schema.CoreType) 22, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)({ + Name: (string) (len=6) "Status", + Type: (*schema.Type)({ + Kind: (string) (len=4) "enum", + Name: (string) (len=6) "Status", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "uint32", + Type: (schema.CoreType) 8, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + Fields: ([]*schema.TypeField) (len=2 cap=2) { + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=9) "AVAILABLE", + Type: (*schema.VarType)(), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) (len=1) "0", + Meta: ([]schema.TypeFieldMeta) + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=13) "NOT_AVAILABLE", + Type: (*schema.VarType)(), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) (len=1) "1", + Meta: ([]schema.TypeFieldMeta) + } + }) + }, + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + }, + Comments: ([]string) + }) + }) + }), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }) + }, + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + }, + Comments: ([]string) { + } + }) + }), + Enum: (*schema.VarEnumType)() + }), + Optional: (bool) false, + InputArg: (bool) true, + OutputArg: (bool) false, + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }) + }, + Outputs: ([]*schema.MethodArgument) { + }, + Service: (*schema.Service)() + }), + (*schema.Method)({ + Name: (string) (len=11) "GetEnumList", + Annotations: (schema.Annotations) { + }, + Comments: ([]string) { + }, + StreamInput: (bool) false, + StreamOutput: (bool) false, + Proxy: (bool) false, + Inputs: ([]*schema.MethodArgument) { + }, + Outputs: ([]*schema.MethodArgument) (len=1 cap=1) { + (*schema.MethodArgument)({ + Name: (string) (len=4) "list", + Type: (*schema.VarType)({ + Expr: (string) (len=8) "[]Status", + Type: (schema.CoreType) 19, + Comments: ([]string) , + List: (*schema.VarListType)({ + Elem: (*schema.VarType)({ + Expr: (string) (len=6) "Status", + Type: (schema.CoreType) 22, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)({ + Name: (string) (len=6) "Status", + Type: (*schema.Type)({ + Kind: (string) (len=4) "enum", + Name: (string) (len=6) "Status", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "uint32", + Type: (schema.CoreType) 8, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + Fields: ([]*schema.TypeField) (len=2 cap=2) { + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=9) "AVAILABLE", + Type: (*schema.VarType)(), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) (len=1) "0", + Meta: ([]schema.TypeFieldMeta) + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=13) "NOT_AVAILABLE", + Type: (*schema.VarType)(), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) (len=1) "1", + Meta: ([]schema.TypeFieldMeta) + } + }) + }, + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + }, + Comments: ([]string) + }) + }) + }) + }), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + Optional: (bool) false, + InputArg: (bool) false, + OutputArg: (bool) true, + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }) + }, + Service: (*schema.Service)() + }), + (*schema.Method)({ + Name: (string) (len=10) "GetEnumMap", + Annotations: (schema.Annotations) { + }, + Comments: ([]string) { + }, + StreamInput: (bool) false, + StreamOutput: (bool) false, + Proxy: (bool) false, + Inputs: ([]*schema.MethodArgument) { + }, + Outputs: ([]*schema.MethodArgument) (len=1 cap=1) { + (*schema.MethodArgument)({ + Name: (string) (len=3) "map", + Type: (*schema.VarType)({ + Expr: (string) (len=18) "map", + Type: (schema.CoreType) 20, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)({ + Key: (*schema.VarType)({ + Expr: (string) (len=6) "Access", + Type: (schema.CoreType) 22, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)({ + Name: (string) (len=6) "Access", + Type: (*schema.Type)({ + Kind: (string) (len=4) "enum", + Name: (string) (len=6) "Access", + Type: (*schema.VarType)({ + Expr: (string) (len=6) "uint32", + Type: (schema.CoreType) 8, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + Fields: ([]*schema.TypeField) (len=5 cap=8) { + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=4) "NONE", + Type: (*schema.VarType)(), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) (len=1) "0", + Meta: ([]schema.TypeFieldMeta) + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=4) "READ", + Type: (*schema.VarType)(), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) (len=1) "1", + Meta: ([]schema.TypeFieldMeta) + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=5) "WRITE", + Type: (*schema.VarType)(), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) (len=1) "2", + Meta: ([]schema.TypeFieldMeta) + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=5) "ADMIN", + Type: (*schema.VarType)(), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) (len=1) "3", + Meta: ([]schema.TypeFieldMeta) + } + }), + (*schema.TypeField)({ + Comments: ([]string) { + }, + Name: (string) (len=5) "OWNER", + Type: (*schema.VarType)(), + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) (len=1) "4", + Meta: ([]schema.TypeFieldMeta) + } + }) + }, + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + }, + Comments: ([]string) + }) + }) + }), + Value: (*schema.VarType)({ + Expr: (string) (len=6) "uint64", + Type: (schema.CoreType) 9, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }) + }), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + Optional: (bool) false, + InputArg: (bool) false, + OutputArg: (bool) true, + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }) + }, + Service: (*schema.Service)() + }), + (*schema.Method)({ + Name: (string) (len=14) "GetSchemaError", + Annotations: (schema.Annotations) { + }, + Comments: ([]string) (len=1 cap=1) { + (string) (len=16) "added in v0.11.0" + }, + StreamInput: (bool) false, + StreamOutput: (bool) false, + Proxy: (bool) false, + Inputs: ([]*schema.MethodArgument) (len=1 cap=1) { + (*schema.MethodArgument)({ + Name: (string) (len=4) "code", + Type: (*schema.VarType)({ + Expr: (string) (len=3) "int", + Type: (schema.CoreType) 10, + Comments: ([]string) , + List: (*schema.VarListType)(), + Map: (*schema.VarMapType)(), + Struct: (*schema.VarStructType)(), + Enum: (*schema.VarEnumType)() + }), + Optional: (bool) false, + InputArg: (bool) true, + OutputArg: (bool) false, + TypeExtra: (schema.TypeExtra) { + Optional: (bool) false, + Value: (string) "", + Meta: ([]schema.TypeFieldMeta) + } + }) + }, + Outputs: ([]*schema.MethodArgument) { + }, + Service: (*schema.Service)() + }) + }, + Comments: ([]string) { + }, + Schema: (*schema.WebRPCSchema)() + }) + }, + Deprecated_Messages: ([]interface {}) + }), + SchemaHash: (string) (len=40) "61c9c8c942e3f6bdffd9929e78ebb3631d924993", + WebrpcGenCommand: (string) (len=84) "webrpc-gen -schema=./schema/test.ridl -target=debug -out=./schema/test.debug.gen.txt", + WebrpcHeader: (string) "", + WebrpcTarget: (string) (len=5) "debug", + WebrpcErrors: ([]*schema.Error) (len=11 cap=11) { + (*schema.Error)({ + Code: (int) 0, + Name: (string) (len=14) "WebrpcEndpoint", + Message: (string) (len=14) "endpoint error", + HTTPStatus: (int) 400 + }), + (*schema.Error)({ + Code: (int) -1, + Name: (string) (len=19) "WebrpcRequestFailed", + Message: (string) (len=14) "request failed", + HTTPStatus: (int) 400 + }), + (*schema.Error)({ + Code: (int) -2, + Name: (string) (len=14) "WebrpcBadRoute", + Message: (string) (len=9) "bad route", + HTTPStatus: (int) 404 + }), + (*schema.Error)({ + Code: (int) -3, + Name: (string) (len=15) "WebrpcBadMethod", + Message: (string) (len=10) "bad method", + HTTPStatus: (int) 405 + }), + (*schema.Error)({ + Code: (int) -4, + Name: (string) (len=16) "WebrpcBadRequest", + Message: (string) (len=11) "bad request", + HTTPStatus: (int) 400 + }), + (*schema.Error)({ + Code: (int) -5, + Name: (string) (len=17) "WebrpcBadResponse", + Message: (string) (len=12) "bad response", + HTTPStatus: (int) 500 + }), + (*schema.Error)({ + Code: (int) -6, + Name: (string) (len=17) "WebrpcServerPanic", + Message: (string) (len=12) "server panic", + HTTPStatus: (int) 500 + }), + (*schema.Error)({ + Code: (int) -7, + Name: (string) (len=19) "WebrpcInternalError", + Message: (string) (len=14) "internal error", + HTTPStatus: (int) 500 + }), + (*schema.Error)({ + Code: (int) -8, + Name: (string) (len=24) "WebrpcClientDisconnected", + Message: (string) (len=19) "client disconnected", + HTTPStatus: (int) 400 + }), + (*schema.Error)({ + Code: (int) -9, + Name: (string) (len=16) "WebrpcStreamLost", + Message: (string) (len=11) "stream lost", + HTTPStatus: (int) 400 + }), + (*schema.Error)({ + Code: (int) -10, + Name: (string) (len=20) "WebrpcStreamFinished", + Message: (string) (len=15) "stream finished", + HTTPStatus: (int) 200 + }) + }, + Opts: (map[string]interface {}) { + }, + CodeGen: (string) "", + CodeGenVersion: (string) "", + CodeGenName: (string) "" +} diff --git a/tests/schema/test.gen.json b/tests/schema/test.gen.json new file mode 100644 index 00000000..c2203601 --- /dev/null +++ b/tests/schema/test.gen.json @@ -0,0 +1,436 @@ +{ + "webrpc": "v1", + "name": "Test", + "version": "v0.10.0", + "types": [ + { + "kind": "enum", + "name": "Status", + "type": "uint32", + "fields": [ + { + "name": "AVAILABLE", + "value": "0" + }, + { + "name": "NOT_AVAILABLE", + "value": "1" + } + ] + }, + { + "kind": "enum", + "name": "Access", + "type": "uint32", + "fields": [ + { + "name": "NONE", + "value": "0" + }, + { + "name": "READ", + "value": "1" + }, + { + "name": "WRITE", + "value": "2" + }, + { + "name": "ADMIN", + "value": "3" + }, + { + "name": "OWNER", + "value": "4" + } + ] + }, + { + "kind": "struct", + "name": "Simple", + "fields": [ + { + "name": "id", + "type": "int" + }, + { + "name": "name", + "type": "string" + } + ] + }, + { + "kind": "struct", + "name": "User", + "fields": [ + { + "name": "id", + "type": "uint64", + "meta": [ + { + "json": "id" + }, + { + "go.field.name": "ID" + }, + { + "go.tag.db": "id" + } + ] + }, + { + "name": "username", + "type": "string", + "meta": [ + { + "json": "USERNAME" + }, + { + "go.tag.db": "username" + } + ] + }, + { + "name": "role", + "type": "string", + "meta": [ + { + "go.tag.db": "-" + } + ] + } + ] + }, + { + "kind": "struct", + "name": "Complex", + "fields": [ + { + "name": "meta", + "type": "map" + }, + { + "name": "metaNestedExample", + "type": "map>" + }, + { + "name": "namesList", + "type": "[]string" + }, + { + "name": "numsList", + "type": "[]int64" + }, + { + "name": "doubleArray", + "type": "[][]string" + }, + { + "name": "listOfMaps", + "type": "[]map" + }, + { + "name": "listOfUsers", + "type": "[]User" + }, + { + "name": "mapOfUsers", + "type": "map" + }, + { + "name": "user", + "type": "User" + }, + { + "name": "status", + "type": "Status" + } + ] + }, + { + "kind": "struct", + "name": "EnumData", + "fields": [ + { + "name": "dict", + "type": "map" + }, + { + "name": "list", + "type": "[]Status" + } + ] + } + ], + "errors": [ + { + "code": 1, + "name": "Unauthorized", + "message": "unauthorized", + "httpStatus": 401 + }, + { + "code": 2, + "name": "ExpiredToken", + "message": "expired token", + "httpStatus": 401 + }, + { + "code": 3, + "name": "InvalidToken", + "message": "invalid token", + "httpStatus": 401 + }, + { + "code": 4, + "name": "Deactivated", + "message": "account deactivated", + "httpStatus": 403 + }, + { + "code": 5, + "name": "ConfirmAccount", + "message": "confirm your email", + "httpStatus": 403 + }, + { + "code": 6, + "name": "AccessDenied", + "message": "access denied", + "httpStatus": 403 + }, + { + "code": 7, + "name": "MissingArgument", + "message": "missing argument", + "httpStatus": 400 + }, + { + "code": 8, + "name": "UnexpectedValue", + "message": "unexpected value", + "httpStatus": 400 + }, + { + "code": 100, + "name": "RateLimited", + "message": "too many requests", + "httpStatus": 429 + }, + { + "code": 101, + "name": "DatabaseDown", + "message": "service outage", + "httpStatus": 503 + }, + { + "code": 102, + "name": "ElasticDown", + "message": "search is degraded", + "httpStatus": 503 + }, + { + "code": 103, + "name": "NotImplemented", + "message": "not implemented", + "httpStatus": 501 + }, + { + "code": 200, + "name": "UserNotFound", + "message": "user not found", + "httpStatus": 400 + }, + { + "code": 201, + "name": "UserBusy", + "message": "user busy", + "httpStatus": 400 + }, + { + "code": 202, + "name": "InvalidUsername", + "message": "invalid username", + "httpStatus": 400 + }, + { + "code": 300, + "name": "FileTooBig", + "message": "file is too big (max 1GB)", + "httpStatus": 400 + }, + { + "code": 301, + "name": "FileInfected", + "message": "file is infected", + "httpStatus": 400 + }, + { + "code": 302, + "name": "FileType", + "message": "unsupported file type", + "httpStatus": 400 + } + ], + "services": [ + { + "name": "TestApi", + "methods": [ + { + "name": "GetEmpty", + "annotations": {}, + "comments": [], + "inputs": [], + "outputs": [] + }, + { + "name": "GetError", + "annotations": {}, + "comments": [], + "inputs": [], + "outputs": [] + }, + { + "name": "GetOne", + "annotations": {}, + "comments": [], + "inputs": [], + "outputs": [ + { + "name": "one", + "type": "Simple", + "optional": false + } + ] + }, + { + "name": "SendOne", + "annotations": {}, + "comments": [], + "inputs": [ + { + "name": "one", + "type": "Simple", + "optional": false + } + ], + "outputs": [] + }, + { + "name": "GetMulti", + "annotations": {}, + "comments": [], + "inputs": [], + "outputs": [ + { + "name": "one", + "type": "Simple", + "optional": false + }, + { + "name": "two", + "type": "Simple", + "optional": false + }, + { + "name": "three", + "type": "Simple", + "optional": false + } + ] + }, + { + "name": "SendMulti", + "annotations": {}, + "comments": [], + "inputs": [ + { + "name": "one", + "type": "Simple", + "optional": false + }, + { + "name": "two", + "type": "Simple", + "optional": false + }, + { + "name": "three", + "type": "Simple", + "optional": false + } + ], + "outputs": [] + }, + { + "name": "GetComplex", + "annotations": {}, + "comments": [], + "inputs": [], + "outputs": [ + { + "name": "complex", + "type": "Complex", + "optional": false + } + ] + }, + { + "name": "SendComplex", + "annotations": {}, + "comments": [], + "inputs": [ + { + "name": "complex", + "type": "Complex", + "optional": false + } + ], + "outputs": [] + }, + { + "name": "GetEnumList", + "annotations": {}, + "comments": [], + "inputs": [], + "outputs": [ + { + "name": "list", + "type": "[]Status", + "optional": false + } + ] + }, + { + "name": "GetEnumMap", + "annotations": {}, + "comments": [], + "inputs": [], + "outputs": [ + { + "name": "map", + "type": "map", + "optional": false + } + ] + }, + { + "name": "GetSchemaError", + "annotations": {}, + "comments": [ + "added in v0.11.0" + ], + "inputs": [ + { + "name": "code", + "type": "int", + "optional": false + } + ], + "outputs": [] + } + ], + "comments": [] + } + ] +} diff --git a/tests/schema/test.ridl b/tests/schema/test.ridl new file mode 100644 index 00000000..57da547c --- /dev/null +++ b/tests/schema/test.ridl @@ -0,0 +1,86 @@ +webrpc = v1 + +name = Test +version = v0.10.0 + +service TestApi + - GetEmpty() + - GetError() + + - GetOne() => (one: Simple) + - SendOne(one: Simple) + + - GetMulti() => (one: Simple, two: Simple, three: Simple) + - SendMulti(one: Simple, two: Simple, three: Simple) + + - GetComplex() => (complex: Complex) + - SendComplex(complex: Complex) + + - GetEnumList() => (list: []Status) + - GetEnumMap() => (map: map) + + # added in v0.11.0 + - GetSchemaError(code: int) + +struct Simple + - id: int + - name: string + +struct User + - id: uint64 + + json = id + + go.field.name = ID + + go.tag.db = id + + - username: string + + json = USERNAME + + go.tag.db = username + + - role: string + + go.tag.db = - + +enum Status: uint32 + - AVAILABLE + - NOT_AVAILABLE + +enum Access: uint32 + - NONE + - READ + - WRITE + - ADMIN + - OWNER + +struct Complex + - meta: map + - metaNestedExample: map> + - namesList: []string + - numsList: []int64 + - doubleArray: [][]string + - listOfMaps: []map + - listOfUsers: []User + - mapOfUsers: map + - user: User + - status: Status + +struct EnumData + - dict: map + - list: []Status + +error 1 Unauthorized "unauthorized" HTTP 401 +error 2 ExpiredToken "expired token" HTTP 401 +error 3 InvalidToken "invalid token" HTTP 401 +error 4 Deactivated "account deactivated" HTTP 403 +error 5 ConfirmAccount "confirm your email" HTTP 403 +error 6 AccessDenied "access denied" HTTP 403 +error 7 MissingArgument "missing argument" HTTP 400 +error 8 UnexpectedValue "unexpected value" HTTP 400 +error 100 RateLimited "too many requests" HTTP 429 +error 101 DatabaseDown "service outage" HTTP 503 +error 102 ElasticDown "search is degraded" HTTP 503 +error 103 NotImplemented "not implemented" HTTP 501 +error 200 UserNotFound "user not found" +error 201 UserBusy "user busy" +error 202 InvalidUsername "invalid username" +error 300 FileTooBig "file is too big (max 1GB)" +error 301 FileInfected "file is infected" +error 302 FileType "unsupported file type" diff --git a/tests/schema_test.go b/tests/schema_test.go new file mode 100644 index 00000000..c64c3135 --- /dev/null +++ b/tests/schema_test.go @@ -0,0 +1,38 @@ +package tests + +import ( + "flag" + "os" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/assert" + "github.com/webrpc/webrpc/schema/ridl" +) + +var updateFlag = flag.String("update", "", "update golden file to match tests' current behavior") + +func TestRIDLSchemaAgainstJSON(t *testing.T) { + exampleDirFS := os.DirFS("./schema") + + schema, err := ridl.NewParser(exampleDirFS, "test.ridl").Parse() + assert.NoError(t, err) + + jsonSchema, err := schema.ToJSON() + assert.NoError(t, err) + + current := []byte(jsonSchema) + + golden, err := os.ReadFile("./_testdata/test.golden.json") + assert.NoError(t, err) + + if *updateFlag == "./_testdata/test.golden.json" { + assert.NoError(t, os.WriteFile("./_testdata/test.golden.json", current, 0644)) + return + } + + if !cmp.Equal(golden, current) { + t.Error(cmp.Diff(golden, current)) + t.Log("To update the golden file, run `go test ./tests -update=./_testdata/test.golden.json'") + } +} diff --git a/tests/server/server.gen.go b/tests/server/server.gen.go new file mode 100644 index 00000000..3981227b --- /dev/null +++ b/tests/server/server.gen.go @@ -0,0 +1,932 @@ +// Test v0.10.0 61c9c8c942e3f6bdffd9929e78ebb3631d924993 +// -- +// Code generated by webrpc-gen with golang generator. DO NOT EDIT. +// +// webrpc-gen -schema=./schema/test.ridl -target=golang -pkg=server -server -out=./server/server.gen.go +package server + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strings" +) + +const WebrpcHeader = "Webrpc" + +const WebrpcHeaderValue = "webrpc;gen-golang@v0.17.0;Test@v0.10.0" + +// WebRPC description and code-gen version +func WebRPCVersion() string { + return "v1" +} + +// Schema version of your RIDL schema +func WebRPCSchemaVersion() string { + return "v0.10.0" +} + +// Schema hash generated from your RIDL schema +func WebRPCSchemaHash() string { + return "61c9c8c942e3f6bdffd9929e78ebb3631d924993" +} + +type WebrpcGenVersions struct { + WebrpcGenVersion string + CodeGenName string + CodeGenVersion string + SchemaName string + SchemaVersion string +} + +func VersionFromHeader(h http.Header) (*WebrpcGenVersions, error) { + if h.Get(WebrpcHeader) == "" { + return nil, fmt.Errorf("header is empty or missing") + } + + versions, err := parseWebrpcGenVersions(h.Get(WebrpcHeader)) + if err != nil { + return nil, fmt.Errorf("webrpc header is invalid: %w", err) + } + + return versions, nil +} + +func parseWebrpcGenVersions(header string) (*WebrpcGenVersions, error) { + versions := strings.Split(header, ";") + if len(versions) < 3 { + return nil, fmt.Errorf("expected at least 3 parts while parsing webrpc header: %v", header) + } + + _, webrpcGenVersion, ok := strings.Cut(versions[0], "@") + if !ok { + return nil, fmt.Errorf("webrpc gen version could not be parsed from: %s", versions[0]) + } + + tmplTarget, tmplVersion, ok := strings.Cut(versions[1], "@") + if !ok { + return nil, fmt.Errorf("tmplTarget and tmplVersion could not be parsed from: %s", versions[1]) + } + + schemaName, schemaVersion, ok := strings.Cut(versions[2], "@") + if !ok { + return nil, fmt.Errorf("schema name and schema version could not be parsed from: %s", versions[2]) + } + + return &WebrpcGenVersions{ + WebrpcGenVersion: webrpcGenVersion, + CodeGenName: tmplTarget, + CodeGenVersion: tmplVersion, + SchemaName: schemaName, + SchemaVersion: schemaVersion, + }, nil +} + +// +// Common types +// + +type Status uint32 + +const ( + Status_AVAILABLE Status = 0 + Status_NOT_AVAILABLE Status = 1 +) + +var Status_name = map[uint32]string{ + 0: "AVAILABLE", + 1: "NOT_AVAILABLE", +} + +var Status_value = map[string]uint32{ + "AVAILABLE": 0, + "NOT_AVAILABLE": 1, +} + +func (x Status) String() string { + return Status_name[uint32(x)] +} + +func (x Status) MarshalText() ([]byte, error) { + return []byte(Status_name[uint32(x)]), nil +} + +func (x *Status) UnmarshalText(b []byte) error { + *x = Status(Status_value[string(b)]) + return nil +} + +func (x *Status) Is(values ...Status) bool { + if x == nil { + return false + } + for _, v := range values { + if *x == v { + return true + } + } + return false +} + +type Access uint32 + +const ( + Access_NONE Access = 0 + Access_READ Access = 1 + Access_WRITE Access = 2 + Access_ADMIN Access = 3 + Access_OWNER Access = 4 +) + +var Access_name = map[uint32]string{ + 0: "NONE", + 1: "READ", + 2: "WRITE", + 3: "ADMIN", + 4: "OWNER", +} + +var Access_value = map[string]uint32{ + "NONE": 0, + "READ": 1, + "WRITE": 2, + "ADMIN": 3, + "OWNER": 4, +} + +func (x Access) String() string { + return Access_name[uint32(x)] +} + +func (x Access) MarshalText() ([]byte, error) { + return []byte(Access_name[uint32(x)]), nil +} + +func (x *Access) UnmarshalText(b []byte) error { + *x = Access(Access_value[string(b)]) + return nil +} + +func (x *Access) Is(values ...Access) bool { + if x == nil { + return false + } + for _, v := range values { + if *x == v { + return true + } + } + return false +} + +type Simple struct { + Id int `json:"id"` + Name string `json:"name"` +} + +type User struct { + ID uint64 `json:"id" db:"id"` + Username string `json:"USERNAME" db:"username"` + Role string `json:"role" db:"-"` +} + +type Complex struct { + Meta map[string]interface{} `json:"meta"` + MetaNestedExample map[string]map[string]uint32 `json:"metaNestedExample"` + NamesList []string `json:"namesList"` + NumsList []int64 `json:"numsList"` + DoubleArray [][]string `json:"doubleArray"` + ListOfMaps []map[string]uint32 `json:"listOfMaps"` + ListOfUsers []*User `json:"listOfUsers"` + MapOfUsers map[string]*User `json:"mapOfUsers"` + User *User `json:"user"` + Status Status `json:"status"` +} + +type EnumData struct { + Dict map[Access]uint64 `json:"dict"` + List []Status `json:"list"` +} + +var methods = map[string]method{ + "/rpc/TestApi/GetEmpty": { + Name: "GetEmpty", + Service: "TestApi", + Annotations: map[string]string{}, + }, + "/rpc/TestApi/GetError": { + Name: "GetError", + Service: "TestApi", + Annotations: map[string]string{}, + }, + "/rpc/TestApi/GetOne": { + Name: "GetOne", + Service: "TestApi", + Annotations: map[string]string{}, + }, + "/rpc/TestApi/SendOne": { + Name: "SendOne", + Service: "TestApi", + Annotations: map[string]string{}, + }, + "/rpc/TestApi/GetMulti": { + Name: "GetMulti", + Service: "TestApi", + Annotations: map[string]string{}, + }, + "/rpc/TestApi/SendMulti": { + Name: "SendMulti", + Service: "TestApi", + Annotations: map[string]string{}, + }, + "/rpc/TestApi/GetComplex": { + Name: "GetComplex", + Service: "TestApi", + Annotations: map[string]string{}, + }, + "/rpc/TestApi/SendComplex": { + Name: "SendComplex", + Service: "TestApi", + Annotations: map[string]string{}, + }, + "/rpc/TestApi/GetEnumList": { + Name: "GetEnumList", + Service: "TestApi", + Annotations: map[string]string{}, + }, + "/rpc/TestApi/GetEnumMap": { + Name: "GetEnumMap", + Service: "TestApi", + Annotations: map[string]string{}, + }, + "/rpc/TestApi/GetSchemaError": { + Name: "GetSchemaError", + Service: "TestApi", + Annotations: map[string]string{}, + }, +} + +func WebrpcMethods() map[string]method { + res := make(map[string]method, len(methods)) + for k, v := range methods { + res[k] = v + } + + return res +} + +var WebRPCServices = map[string][]string{ + "TestApi": { + "GetEmpty", + "GetError", + "GetOne", + "SendOne", + "GetMulti", + "SendMulti", + "GetComplex", + "SendComplex", + "GetEnumList", + "GetEnumMap", + "GetSchemaError", + }, +} + +// +// Server types +// + +type TestApi interface { + GetEmpty(ctx context.Context) error + GetError(ctx context.Context) error + GetOne(ctx context.Context) (*Simple, error) + SendOne(ctx context.Context, one *Simple) error + GetMulti(ctx context.Context) (*Simple, *Simple, *Simple, error) + SendMulti(ctx context.Context, one *Simple, two *Simple, three *Simple) error + GetComplex(ctx context.Context) (*Complex, error) + SendComplex(ctx context.Context, complex *Complex) error + GetEnumList(ctx context.Context) ([]Status, error) + GetEnumMap(ctx context.Context) (map[Access]uint64, error) + // added in v0.11.0 + GetSchemaError(ctx context.Context, code int) error +} + +// +// Client types +// + +type TestApiClient interface { + GetEmpty(ctx context.Context) error + GetError(ctx context.Context) error + GetOne(ctx context.Context) (*Simple, error) + SendOne(ctx context.Context, one *Simple) error + GetMulti(ctx context.Context) (*Simple, *Simple, *Simple, error) + SendMulti(ctx context.Context, one *Simple, two *Simple, three *Simple) error + GetComplex(ctx context.Context) (*Complex, error) + SendComplex(ctx context.Context, complex *Complex) error + GetEnumList(ctx context.Context) ([]Status, error) + GetEnumMap(ctx context.Context) (map[Access]uint64, error) + // added in v0.11.0 + GetSchemaError(ctx context.Context, code int) error +} + +// +// Server +// + +type WebRPCServer interface { + http.Handler +} + +type testApiServer struct { + TestApi + OnError func(r *http.Request, rpcErr *WebRPCError) + OnRequest func(w http.ResponseWriter, r *http.Request) error +} + +func NewTestApiServer(svc TestApi) *testApiServer { + return &testApiServer{ + TestApi: svc, + } +} + +func (s *testApiServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { + defer func() { + // In case of a panic, serve a HTTP 500 error and then panic. + if rr := recover(); rr != nil { + s.sendErrorJSON(w, r, ErrWebrpcServerPanic.WithCausef("%v", rr)) + panic(rr) + } + }() + + w.Header().Set(WebrpcHeader, WebrpcHeaderValue) + + ctx := r.Context() + ctx = context.WithValue(ctx, HTTPResponseWriterCtxKey, w) + ctx = context.WithValue(ctx, HTTPRequestCtxKey, r) + ctx = context.WithValue(ctx, ServiceNameCtxKey, "TestApi") + + r = r.WithContext(ctx) + + var handler func(ctx context.Context, w http.ResponseWriter, r *http.Request) + switch r.URL.Path { + case "/rpc/TestApi/GetEmpty": + handler = s.serveGetEmptyJSON + case "/rpc/TestApi/GetError": + handler = s.serveGetErrorJSON + case "/rpc/TestApi/GetOne": + handler = s.serveGetOneJSON + case "/rpc/TestApi/SendOne": + handler = s.serveSendOneJSON + case "/rpc/TestApi/GetMulti": + handler = s.serveGetMultiJSON + case "/rpc/TestApi/SendMulti": + handler = s.serveSendMultiJSON + case "/rpc/TestApi/GetComplex": + handler = s.serveGetComplexJSON + case "/rpc/TestApi/SendComplex": + handler = s.serveSendComplexJSON + case "/rpc/TestApi/GetEnumList": + handler = s.serveGetEnumListJSON + case "/rpc/TestApi/GetEnumMap": + handler = s.serveGetEnumMapJSON + case "/rpc/TestApi/GetSchemaError": + handler = s.serveGetSchemaErrorJSON + default: + err := ErrWebrpcBadRoute.WithCausef("no webrpc method defined for path %v", r.URL.Path) + s.sendErrorJSON(w, r, err) + return + } + + if r.Method != "POST" { + w.Header().Add("Allow", "POST") // RFC 9110. + err := ErrWebrpcBadMethod.WithCausef("unsupported HTTP method %v (only POST is allowed)", r.Method) + s.sendErrorJSON(w, r, err) + return + } + + contentType := r.Header.Get("Content-Type") + if i := strings.Index(contentType, ";"); i >= 0 { + contentType = contentType[:i] + } + contentType = strings.TrimSpace(strings.ToLower(contentType)) + + switch contentType { + case "application/json": + if s.OnRequest != nil { + if err := s.OnRequest(w, r); err != nil { + rpcErr, ok := err.(WebRPCError) + if !ok { + rpcErr = ErrWebrpcEndpoint.WithCause(err) + } + s.sendErrorJSON(w, r, rpcErr) + return + } + } + + handler(ctx, w, r) + default: + err := ErrWebrpcBadRequest.WithCausef("unsupported Content-Type %q (only application/json is allowed)", r.Header.Get("Content-Type")) + s.sendErrorJSON(w, r, err) + } +} + +func (s *testApiServer) serveGetEmptyJSON(ctx context.Context, w http.ResponseWriter, r *http.Request) { + ctx = context.WithValue(ctx, MethodNameCtxKey, "GetEmpty") + + // Call service method implementation. + err := s.TestApi.GetEmpty(ctx) + if err != nil { + rpcErr, ok := err.(WebRPCError) + if !ok { + rpcErr = ErrWebrpcEndpoint.WithCause(err) + } + s.sendErrorJSON(w, r, rpcErr) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte("{}")) +} + +func (s *testApiServer) serveGetErrorJSON(ctx context.Context, w http.ResponseWriter, r *http.Request) { + ctx = context.WithValue(ctx, MethodNameCtxKey, "GetError") + + // Call service method implementation. + err := s.TestApi.GetError(ctx) + if err != nil { + rpcErr, ok := err.(WebRPCError) + if !ok { + rpcErr = ErrWebrpcEndpoint.WithCause(err) + } + s.sendErrorJSON(w, r, rpcErr) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte("{}")) +} + +func (s *testApiServer) serveGetOneJSON(ctx context.Context, w http.ResponseWriter, r *http.Request) { + ctx = context.WithValue(ctx, MethodNameCtxKey, "GetOne") + + // Call service method implementation. + ret0, err := s.TestApi.GetOne(ctx) + if err != nil { + rpcErr, ok := err.(WebRPCError) + if !ok { + rpcErr = ErrWebrpcEndpoint.WithCause(err) + } + s.sendErrorJSON(w, r, rpcErr) + return + } + + respPayload := struct { + Ret0 *Simple `json:"one"` + }{ret0} + respBody, err := json.Marshal(respPayload) + if err != nil { + s.sendErrorJSON(w, r, ErrWebrpcBadResponse.WithCausef("failed to marshal json response: %w", err)) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write(respBody) +} + +func (s *testApiServer) serveSendOneJSON(ctx context.Context, w http.ResponseWriter, r *http.Request) { + ctx = context.WithValue(ctx, MethodNameCtxKey, "SendOne") + + reqBody, err := io.ReadAll(r.Body) + if err != nil { + s.sendErrorJSON(w, r, ErrWebrpcBadRequest.WithCausef("failed to read request data: %w", err)) + return + } + defer r.Body.Close() + + reqPayload := struct { + Arg0 *Simple `json:"one"` + }{} + if err := json.Unmarshal(reqBody, &reqPayload); err != nil { + s.sendErrorJSON(w, r, ErrWebrpcBadRequest.WithCausef("failed to unmarshal request data: %w", err)) + return + } + + // Call service method implementation. + err = s.TestApi.SendOne(ctx, reqPayload.Arg0) + if err != nil { + rpcErr, ok := err.(WebRPCError) + if !ok { + rpcErr = ErrWebrpcEndpoint.WithCause(err) + } + s.sendErrorJSON(w, r, rpcErr) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte("{}")) +} + +func (s *testApiServer) serveGetMultiJSON(ctx context.Context, w http.ResponseWriter, r *http.Request) { + ctx = context.WithValue(ctx, MethodNameCtxKey, "GetMulti") + + // Call service method implementation. + ret0, ret1, ret2, err := s.TestApi.GetMulti(ctx) + if err != nil { + rpcErr, ok := err.(WebRPCError) + if !ok { + rpcErr = ErrWebrpcEndpoint.WithCause(err) + } + s.sendErrorJSON(w, r, rpcErr) + return + } + + respPayload := struct { + Ret0 *Simple `json:"one"` + Ret1 *Simple `json:"two"` + Ret2 *Simple `json:"three"` + }{ret0, ret1, ret2} + respBody, err := json.Marshal(respPayload) + if err != nil { + s.sendErrorJSON(w, r, ErrWebrpcBadResponse.WithCausef("failed to marshal json response: %w", err)) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write(respBody) +} + +func (s *testApiServer) serveSendMultiJSON(ctx context.Context, w http.ResponseWriter, r *http.Request) { + ctx = context.WithValue(ctx, MethodNameCtxKey, "SendMulti") + + reqBody, err := io.ReadAll(r.Body) + if err != nil { + s.sendErrorJSON(w, r, ErrWebrpcBadRequest.WithCausef("failed to read request data: %w", err)) + return + } + defer r.Body.Close() + + reqPayload := struct { + Arg0 *Simple `json:"one"` + Arg1 *Simple `json:"two"` + Arg2 *Simple `json:"three"` + }{} + if err := json.Unmarshal(reqBody, &reqPayload); err != nil { + s.sendErrorJSON(w, r, ErrWebrpcBadRequest.WithCausef("failed to unmarshal request data: %w", err)) + return + } + + // Call service method implementation. + err = s.TestApi.SendMulti(ctx, reqPayload.Arg0, reqPayload.Arg1, reqPayload.Arg2) + if err != nil { + rpcErr, ok := err.(WebRPCError) + if !ok { + rpcErr = ErrWebrpcEndpoint.WithCause(err) + } + s.sendErrorJSON(w, r, rpcErr) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte("{}")) +} + +func (s *testApiServer) serveGetComplexJSON(ctx context.Context, w http.ResponseWriter, r *http.Request) { + ctx = context.WithValue(ctx, MethodNameCtxKey, "GetComplex") + + // Call service method implementation. + ret0, err := s.TestApi.GetComplex(ctx) + if err != nil { + rpcErr, ok := err.(WebRPCError) + if !ok { + rpcErr = ErrWebrpcEndpoint.WithCause(err) + } + s.sendErrorJSON(w, r, rpcErr) + return + } + + respPayload := struct { + Ret0 *Complex `json:"complex"` + }{ret0} + respBody, err := json.Marshal(respPayload) + if err != nil { + s.sendErrorJSON(w, r, ErrWebrpcBadResponse.WithCausef("failed to marshal json response: %w", err)) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write(respBody) +} + +func (s *testApiServer) serveSendComplexJSON(ctx context.Context, w http.ResponseWriter, r *http.Request) { + ctx = context.WithValue(ctx, MethodNameCtxKey, "SendComplex") + + reqBody, err := io.ReadAll(r.Body) + if err != nil { + s.sendErrorJSON(w, r, ErrWebrpcBadRequest.WithCausef("failed to read request data: %w", err)) + return + } + defer r.Body.Close() + + reqPayload := struct { + Arg0 *Complex `json:"complex"` + }{} + if err := json.Unmarshal(reqBody, &reqPayload); err != nil { + s.sendErrorJSON(w, r, ErrWebrpcBadRequest.WithCausef("failed to unmarshal request data: %w", err)) + return + } + + // Call service method implementation. + err = s.TestApi.SendComplex(ctx, reqPayload.Arg0) + if err != nil { + rpcErr, ok := err.(WebRPCError) + if !ok { + rpcErr = ErrWebrpcEndpoint.WithCause(err) + } + s.sendErrorJSON(w, r, rpcErr) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte("{}")) +} + +func (s *testApiServer) serveGetEnumListJSON(ctx context.Context, w http.ResponseWriter, r *http.Request) { + ctx = context.WithValue(ctx, MethodNameCtxKey, "GetEnumList") + + // Call service method implementation. + ret0, err := s.TestApi.GetEnumList(ctx) + if err != nil { + rpcErr, ok := err.(WebRPCError) + if !ok { + rpcErr = ErrWebrpcEndpoint.WithCause(err) + } + s.sendErrorJSON(w, r, rpcErr) + return + } + + respPayload := struct { + Ret0 []Status `json:"list"` + }{ret0} + respBody, err := json.Marshal(respPayload) + if err != nil { + s.sendErrorJSON(w, r, ErrWebrpcBadResponse.WithCausef("failed to marshal json response: %w", err)) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write(respBody) +} + +func (s *testApiServer) serveGetEnumMapJSON(ctx context.Context, w http.ResponseWriter, r *http.Request) { + ctx = context.WithValue(ctx, MethodNameCtxKey, "GetEnumMap") + + // Call service method implementation. + ret0, err := s.TestApi.GetEnumMap(ctx) + if err != nil { + rpcErr, ok := err.(WebRPCError) + if !ok { + rpcErr = ErrWebrpcEndpoint.WithCause(err) + } + s.sendErrorJSON(w, r, rpcErr) + return + } + + respPayload := struct { + Ret0 map[Access]uint64 `json:"map"` + }{ret0} + respBody, err := json.Marshal(respPayload) + if err != nil { + s.sendErrorJSON(w, r, ErrWebrpcBadResponse.WithCausef("failed to marshal json response: %w", err)) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write(respBody) +} + +func (s *testApiServer) serveGetSchemaErrorJSON(ctx context.Context, w http.ResponseWriter, r *http.Request) { + ctx = context.WithValue(ctx, MethodNameCtxKey, "GetSchemaError") + + reqBody, err := io.ReadAll(r.Body) + if err != nil { + s.sendErrorJSON(w, r, ErrWebrpcBadRequest.WithCausef("failed to read request data: %w", err)) + return + } + defer r.Body.Close() + + reqPayload := struct { + Arg0 int `json:"code"` + }{} + if err := json.Unmarshal(reqBody, &reqPayload); err != nil { + s.sendErrorJSON(w, r, ErrWebrpcBadRequest.WithCausef("failed to unmarshal request data: %w", err)) + return + } + + // Call service method implementation. + err = s.TestApi.GetSchemaError(ctx, reqPayload.Arg0) + if err != nil { + rpcErr, ok := err.(WebRPCError) + if !ok { + rpcErr = ErrWebrpcEndpoint.WithCause(err) + } + s.sendErrorJSON(w, r, rpcErr) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte("{}")) +} + +func (s *testApiServer) sendErrorJSON(w http.ResponseWriter, r *http.Request, rpcErr WebRPCError) { + if s.OnError != nil { + s.OnError(r, &rpcErr) + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(rpcErr.HTTPStatus) + + respBody, _ := json.Marshal(rpcErr) + w.Write(respBody) +} + +func RespondWithError(w http.ResponseWriter, err error) { + rpcErr, ok := err.(WebRPCError) + if !ok { + rpcErr = ErrWebrpcEndpoint.WithCause(err) + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(rpcErr.HTTPStatus) + + respBody, _ := json.Marshal(rpcErr) + w.Write(respBody) +} + +// +// Helpers +// + +type method struct { + Name string + Service string + Annotations map[string]string +} + +type contextKey struct { + name string +} + +func (k *contextKey) String() string { + return "webrpc context value " + k.name +} + +var ( + HTTPResponseWriterCtxKey = &contextKey{"HTTPResponseWriter"} + + HTTPRequestCtxKey = &contextKey{"HTTPRequest"} + + ServiceNameCtxKey = &contextKey{"ServiceName"} + + MethodNameCtxKey = &contextKey{"MethodName"} +) + +func ServiceNameFromContext(ctx context.Context) string { + service, _ := ctx.Value(ServiceNameCtxKey).(string) + return service +} + +func MethodNameFromContext(ctx context.Context) string { + method, _ := ctx.Value(MethodNameCtxKey).(string) + return method +} + +func RequestFromContext(ctx context.Context) *http.Request { + r, _ := ctx.Value(HTTPRequestCtxKey).(*http.Request) + return r +} + +func MethodCtx(ctx context.Context) (method, bool) { + req := RequestFromContext(ctx) + if req == nil { + return method{}, false + } + + m, ok := methods[req.URL.Path] + if !ok { + return method{}, false + } + + return m, true +} + +func ResponseWriterFromContext(ctx context.Context) http.ResponseWriter { + w, _ := ctx.Value(HTTPResponseWriterCtxKey).(http.ResponseWriter) + return w +} + +// +// Errors +// + +type WebRPCError struct { + Name string `json:"error"` + Code int `json:"code"` + Message string `json:"msg"` + Cause string `json:"cause,omitempty"` + HTTPStatus int `json:"status"` + cause error +} + +var _ error = WebRPCError{} + +func (e WebRPCError) Error() string { + if e.cause != nil { + return fmt.Sprintf("%s %d: %s: %v", e.Name, e.Code, e.Message, e.cause) + } + return fmt.Sprintf("%s %d: %s", e.Name, e.Code, e.Message) +} + +func (e WebRPCError) Is(target error) bool { + if target == nil { + return false + } + if rpcErr, ok := target.(WebRPCError); ok { + return rpcErr.Code == e.Code + } + return errors.Is(e.cause, target) +} + +func (e WebRPCError) Unwrap() error { + return e.cause +} + +func (e WebRPCError) WithCause(cause error) WebRPCError { + err := e + err.cause = cause + err.Cause = cause.Error() + return err +} + +func (e WebRPCError) WithCausef(format string, args ...interface{}) WebRPCError { + cause := fmt.Errorf(format, args...) + err := e + err.cause = cause + err.Cause = cause.Error() + return err +} + +// Deprecated: Use .WithCause() method on WebRPCError. +func ErrorWithCause(rpcErr WebRPCError, cause error) WebRPCError { + return rpcErr.WithCause(cause) +} + +// Webrpc errors +var ( + ErrWebrpcEndpoint = WebRPCError{Code: 0, Name: "WebrpcEndpoint", Message: "endpoint error", HTTPStatus: 400} + ErrWebrpcRequestFailed = WebRPCError{Code: -1, Name: "WebrpcRequestFailed", Message: "request failed", HTTPStatus: 400} + ErrWebrpcBadRoute = WebRPCError{Code: -2, Name: "WebrpcBadRoute", Message: "bad route", HTTPStatus: 404} + ErrWebrpcBadMethod = WebRPCError{Code: -3, Name: "WebrpcBadMethod", Message: "bad method", HTTPStatus: 405} + ErrWebrpcBadRequest = WebRPCError{Code: -4, Name: "WebrpcBadRequest", Message: "bad request", HTTPStatus: 400} + ErrWebrpcBadResponse = WebRPCError{Code: -5, Name: "WebrpcBadResponse", Message: "bad response", HTTPStatus: 500} + ErrWebrpcServerPanic = WebRPCError{Code: -6, Name: "WebrpcServerPanic", Message: "server panic", HTTPStatus: 500} + ErrWebrpcInternalError = WebRPCError{Code: -7, Name: "WebrpcInternalError", Message: "internal error", HTTPStatus: 500} + ErrWebrpcClientDisconnected = WebRPCError{Code: -8, Name: "WebrpcClientDisconnected", Message: "client disconnected", HTTPStatus: 400} + ErrWebrpcStreamLost = WebRPCError{Code: -9, Name: "WebrpcStreamLost", Message: "stream lost", HTTPStatus: 400} + ErrWebrpcStreamFinished = WebRPCError{Code: -10, Name: "WebrpcStreamFinished", Message: "stream finished", HTTPStatus: 200} +) + +// Schema errors +var ( + ErrUnauthorized = WebRPCError{Code: 1, Name: "Unauthorized", Message: "unauthorized", HTTPStatus: 401} + ErrExpiredToken = WebRPCError{Code: 2, Name: "ExpiredToken", Message: "expired token", HTTPStatus: 401} + ErrInvalidToken = WebRPCError{Code: 3, Name: "InvalidToken", Message: "invalid token", HTTPStatus: 401} + ErrDeactivated = WebRPCError{Code: 4, Name: "Deactivated", Message: "account deactivated", HTTPStatus: 403} + ErrConfirmAccount = WebRPCError{Code: 5, Name: "ConfirmAccount", Message: "confirm your email", HTTPStatus: 403} + ErrAccessDenied = WebRPCError{Code: 6, Name: "AccessDenied", Message: "access denied", HTTPStatus: 403} + ErrMissingArgument = WebRPCError{Code: 7, Name: "MissingArgument", Message: "missing argument", HTTPStatus: 400} + ErrUnexpectedValue = WebRPCError{Code: 8, Name: "UnexpectedValue", Message: "unexpected value", HTTPStatus: 400} + ErrRateLimited = WebRPCError{Code: 100, Name: "RateLimited", Message: "too many requests", HTTPStatus: 429} + ErrDatabaseDown = WebRPCError{Code: 101, Name: "DatabaseDown", Message: "service outage", HTTPStatus: 503} + ErrElasticDown = WebRPCError{Code: 102, Name: "ElasticDown", Message: "search is degraded", HTTPStatus: 503} + ErrNotImplemented = WebRPCError{Code: 103, Name: "NotImplemented", Message: "not implemented", HTTPStatus: 501} + ErrUserNotFound = WebRPCError{Code: 200, Name: "UserNotFound", Message: "user not found", HTTPStatus: 400} + ErrUserBusy = WebRPCError{Code: 201, Name: "UserBusy", Message: "user busy", HTTPStatus: 400} + ErrInvalidUsername = WebRPCError{Code: 202, Name: "InvalidUsername", Message: "invalid username", HTTPStatus: 400} + ErrFileTooBig = WebRPCError{Code: 300, Name: "FileTooBig", Message: "file is too big (max 1GB)", HTTPStatus: 400} + ErrFileInfected = WebRPCError{Code: 301, Name: "FileInfected", Message: "file is infected", HTTPStatus: 400} + ErrFileType = WebRPCError{Code: 302, Name: "FileType", Message: "unsupported file type", HTTPStatus: 400} +) diff --git a/tests/server/server.go b/tests/server/server.go new file mode 100644 index 00000000..97ba2863 --- /dev/null +++ b/tests/server/server.go @@ -0,0 +1,244 @@ +package server + +import ( + "context" + "fmt" + "io" + "net" + "net/http" + "time" + + "github.com/google/go-cmp/cmp" +) + +type TestServer struct{} + +func (c *TestServer) GetEmpty(ctx context.Context) error { + return nil +} + +func (c *TestServer) GetError(ctx context.Context) error { + return fmt.Errorf("internal error") +} + +func (c *TestServer) GetOne(ctx context.Context) (*Simple, error) { + return &fixtureOne, nil +} + +func (c *TestServer) SendOne(ctx context.Context, one *Simple) error { + if !cmp.Equal(&fixtureOne, one) { + return ErrorWithCause(ErrUnexpectedValue, fmt.Errorf("%q:\n%s", "one", cmp.Diff(&fixtureOne, one))) + } + + return nil +} + +func (c *TestServer) GetMulti(ctx context.Context) (*Simple, *Simple, *Simple, error) { + return &fixtureOne, &fixtureTwo, &fixtureThree, nil +} + +func (c *TestServer) SendMulti(ctx context.Context, one, two, three *Simple) error { + if !cmp.Equal(&fixtureOne, one) { + return ErrorWithCause(ErrUnexpectedValue, fmt.Errorf("%q:\n%s", "one", cmp.Diff(&fixtureOne, one))) + } + if !cmp.Equal(&fixtureTwo, two) { + return ErrorWithCause(ErrUnexpectedValue, fmt.Errorf("%q:\n%s", "two", cmp.Diff(&fixtureTwo, two))) + } + if !cmp.Equal(&fixtureThree, three) { + return ErrorWithCause(ErrUnexpectedValue, fmt.Errorf("%q:\n%s", "three", cmp.Diff(&fixtureThree, three))) + } + + return nil +} + +func (c *TestServer) GetComplex(ctx context.Context) (*Complex, error) { + return &fixtureComplex, nil +} + +func (c *TestServer) SendComplex(ctx context.Context, complex *Complex) error { + if !cmp.Equal(&fixtureComplex, complex) { + return ErrorWithCause(ErrUnexpectedValue, fmt.Errorf("%q:\n%s", "complex", cmp.Diff(&fixtureComplex, complex))) + } + + return nil +} + +func (c *TestServer) GetEnumList(ctx context.Context) ([]Status, error) { + return fixtureEnums.List, nil +} + +func (c *TestServer) GetEnumMap(ctx context.Context) (map[Access]uint64, error) { + return fixtureEnums.Dict, nil +} + +func (c *TestServer) GetSchemaError(ctx context.Context, code int) error { + switch code { + case 0: + return fmt.Errorf("failed to read file: %w", io.ErrUnexpectedEOF) + case 1: + return ErrorWithCause(ErrUnauthorized, fmt.Errorf("failed to verify JWT token")) + case 2: + return ErrExpiredToken + case 3: + return ErrInvalidToken + case 4: + return ErrDeactivated + case 5: + return ErrConfirmAccount + case 6: + return ErrAccessDenied + case 7: + return ErrMissingArgument + case 8: + return ErrUnexpectedValue + case 100: + return ErrorWithCause(ErrRateLimited, fmt.Errorf("1000 req/min exceeded")) + case 101: + return ErrDatabaseDown + case 102: + return ErrElasticDown + case 103: + return ErrNotImplemented + case 200: + return ErrUserNotFound + case 201: + return ErrUserBusy + case 202: + return ErrInvalidUsername + case 300: + return ErrFileTooBig + case 301: + return ErrFileInfected + case 302: + return ErrorWithCause(ErrFileType, fmt.Errorf(".wav is not supported")) + } + + return nil +} + +// Fixtures +var ( + fixtureOne = Simple{ + Id: 1, + Name: "one", + } + fixtureTwo = Simple{ + Id: 2, + Name: "two", + } + fixtureThree = Simple{ + Id: 3, + Name: "three", + } + + meta = map[string]interface{}{ + "1": "23", + "2": float64(24), // Go JSON unmarshaler uses float64 for numbers by default. + } + metaNested = map[string]map[string]uint32{ + "1": { + "2": 1, + }, + } + namesList = []string{"John", "Alice", "Jakob"} + numsList = []int64{1, 2, 3, 4534643543} + doubleArray = [][]string{{"testing"}, {"api"}} + listOfMaps = []map[string]uint32{ + { + "john": 1, + "alice": 2, + "Jakob": 251, + }, + } + listOfUsers = []*User{ + { + ID: 1, + Username: "John-Doe", + Role: "admin", + }, + } + mapOfUsers = map[string]*User{ + "admin": { + ID: 1, + Username: "John-Doe", + Role: "admin", + }, + } + user = &User{ + ID: 1, + Username: "John-Doe", + Role: "admin", + } + + fixtureComplex = Complex{ + Meta: meta, + MetaNestedExample: metaNested, + NamesList: namesList, + NumsList: numsList, + DoubleArray: doubleArray, + ListOfMaps: listOfMaps, + ListOfUsers: listOfUsers, + MapOfUsers: mapOfUsers, + User: user, + Status: Status_AVAILABLE, + } + + fixtureEnums = EnumData{ + Dict: map[Access]uint64{Access_READ: 1, Access_WRITE: 2}, + List: []Status{Status_AVAILABLE, Status_NOT_AVAILABLE}, + } +) + +func RunTestServer(addr string, timeout time.Duration) (*testServer, error) { + srv := &testServer{ + Server: &http.Server{ + Addr: addr, + Handler: NewTestApiServer(&TestServer{}), + }, + closed: make(chan struct{}), + } + + go func() { + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + select { + case <-srv.closed: + + case <-ctx.Done(): + // 1s graceful shutdown + gracefulShutdownCtx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + srv.err = srv.Shutdown(gracefulShutdownCtx) + close(srv.closed) + } + }() + + l, err := net.Listen("tcp", addr) + if err != nil { + return nil, fmt.Errorf("failed to bind %v: %w", addr, err) + } + + go srv.Serve(l) + + return srv, nil +} + +type testServer struct { + *http.Server + closed chan struct{} + err error +} + +func (srv *testServer) Wait() error { + <-srv.closed + return srv.err +} + +func (srv *testServer) Close() error { + gracefulShutdownCtx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + return srv.Shutdown(gracefulShutdownCtx) +} diff --git a/version.go b/version.go new file mode 100644 index 00000000..42375be2 --- /dev/null +++ b/version.go @@ -0,0 +1,45 @@ +package webrpc + +import ( + _ "embed" + "os/exec" + "strings" +) + +// Version of webrpc-gen tooling & Template Functions API. +// Available as {{.WebrpcGenVersion}} variable in Go templates. +// +// The value is injected during `go build' in the release CI step. +var VERSION = "" + +//go:embed go.mod +var GoModFile string + +func init() { + if VERSION == "" { + VERSION = getRuntimeVersion() + } +} + +// getRuntimeVersion tries to infer webrpc version +// 1. from the current go.mod file, which is useful when running webrpc-gen from +// another Go module using `go run github.com/webrpc/webrpc/cmd/webrpc-gen'. +// 2. from the current git history. +func getRuntimeVersion() string { + // $ go list -m github.com/webrpc/webrpc + // github.com/webrpc/webrpc v0.15.1\n + if out, _ := exec.Command("go", "list", "-m", "github.com/webrpc/webrpc").Output(); len(out) > 0 { + parts := strings.Split(strings.TrimSpace(string(out)), " ") + if len(parts) >= 2 { + return parts[1] + } + } + + // $ git describe --tags + // v0.15.1-6-g550333d\n + if out, _ := exec.Command("git", "describe", "--tags").Output(); len(out) > 0 { + return strings.TrimSpace(string(out)) + } + + return "unknown" +} diff --git a/webrpc.go b/webrpc.go index 43699122..c3b9b50d 100644 --- a/webrpc.go +++ b/webrpc.go @@ -1,61 +1,49 @@ package webrpc import ( - "io/ioutil" + "fmt" "os" "path/filepath" - "github.com/pkg/errors" "github.com/webrpc/webrpc/schema" "github.com/webrpc/webrpc/schema/ridl" ) -const VERSION = "v0.5.0" - -func ParseSchemaFile(schemaFilePath string) (*schema.WebRPCSchema, error) { - cwd, err := os.Getwd() +func ParseSchemaFile(path string) (*schema.WebRPCSchema, error) { + absolutePath, err := filepath.Abs(path) if err != nil { - return nil, err + return nil, fmt.Errorf("invalid path %q: %w", path, err) } - var path string - if schemaFilePath[0:1] == "/" { - path = schemaFilePath - } else { - path = filepath.Join(cwd, schemaFilePath) - } + ext := filepath.Ext(absolutePath) + switch ext { + case ".json": + json, err := os.ReadFile(absolutePath) + if err != nil { + return nil, fmt.Errorf("failed to read %q: %w", path, err) + } - // ensure schema file exists - if _, err := os.Stat(path); os.IsNotExist(err) { - return nil, err - } + return schema.ParseSchemaJSON(json) - // open file - fp, err := os.Open(path) - if err != nil { - return nil, err - } - defer fp.Close() + case ".ridl": + // Use root FS to allow RIDL file imports from parent directories, + // ie. import ../../common.ridl. - ext := filepath.Ext(path) - if ext == ".json" { - // TODO: implement ParseSchemaJSON with io.Reader or read contents - // before passing them. - contents, err := ioutil.ReadAll(fp) - if err != nil { - return nil, err - } + root := "/" - return schema.ParseSchemaJSON(contents) - } else if ext == ".ridl" { - rdr := ridl.NewParser(schema.NewReader(fp, path)) - s, err := rdr.Parse() - if err != nil { - return nil, err + // Support Windows paths. Currently only supports paths on the same volume. + if volume := filepath.VolumeName(absolutePath); volume != "" { + root = volume + "/" } - return s, nil - } else { - return nil, errors.Errorf("error! invalid extension, %s", ext) + path := filepath.ToSlash(absolutePath[len(root):]) + + rootFS := os.DirFS(root) + + r := ridl.NewParser(rootFS, path) + return r.Parse() + + default: + return nil, fmt.Errorf("invalid schema file extension %q", ext) } }