├── .github ├── FUNDING.yml ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE.yml └── workflows │ └── test.yml ├── .gitignore ├── generators ├── mddocs │ ├── mddocs_test.go │ └── mddocs.go ├── dotnetclient │ ├── dotnetclient_test.go │ ├── testdata │ │ └── todo_client.cs │ └── dotnetclient.go ├── tstypes │ ├── tstypes_test.go │ ├── testdata │ │ └── todo_types.ts │ └── tstypes.go ├── goclient │ ├── goclient_test.go │ ├── testdata │ │ └── todo_client.go │ └── goclient.go ├── elmclient │ ├── elmclient_test.go │ ├── testdata │ │ └── todo_client.elm │ └── elmclient.go ├── phpclient │ ├── phpclient_test.go │ ├── testdata │ │ └── todo_client.php │ └── phpclient.go ├── tsclient │ ├── tsclient_test.go │ ├── testdata │ │ └── todo_client.ts │ └── tsclient.go ├── rubyclient │ ├── rubyclient_test.go │ ├── testdata │ │ └── todo_client.rb │ └── rubyclient.go ├── goserver │ ├── goserver_test.go │ ├── testdata │ │ ├── todo_server_no_types.go │ │ └── todo_server_types.go │ └── goserver.go └── gotypes │ ├── gotypes_test.go │ ├── testdata │ ├── todo_types_no_validate.go │ └── todo_types_validate.go │ └── gotypes.go ├── Makefile ├── doc.go ├── go.mod ├── validate.go ├── response.go ├── context.go ├── internal ├── format │ ├── format_test.go │ └── format.go └── schemautil │ └── schemautil.go ├── cmd ├── rpc-md-docs │ └── main.go ├── rpc-elm-client │ └── main.go ├── rpc-php-client │ └── main.go ├── rpc-ruby-client │ └── main.go ├── rpc-dotnet-client │ └── main.go ├── rpc-go-types │ └── main.go ├── rpc-ts-client │ └── main.go ├── rpc-go-client │ └── main.go └── rpc-go-server │ └── main.go ├── health.go ├── request.go ├── LICENSE ├── health_test.go ├── Contributing.md ├── error_test.go ├── History.md ├── response_test.go ├── examples └── todo │ └── schema.json ├── request_test.go ├── error.go ├── go.sum ├── schema ├── schema.go ├── schema.json └── schema_json.go └── Readme.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: tj -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .envrc 2 | test 3 | -------------------------------------------------------------------------------- /generators/mddocs/mddocs_test.go: -------------------------------------------------------------------------------- 1 | package mddocs_test 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | # Run all tests. 3 | test: 4 | @go test ./... 5 | .PHONY: test -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package rpc provides simple RPC style APIs with generated clients & servers. 2 | package rpc 3 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Please open an issue and discuss changes before spending time on them, unless the change is trivial or an issue already exists. 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.yml: -------------------------------------------------------------------------------- 1 | ## Prerequisites 2 | 3 | * [ ] I searched to see if the issue already exists. 4 | 5 | ## Description 6 | 7 | Describe the bug or feature. 8 | 9 | ## Steps to Reproduce 10 | 11 | Describe the steps required to reproduce the issue if applicable. 12 | 13 | ## Slack 14 | 15 | Join us on Slack https://chat.apex.sh/ 16 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/apex/rpc 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/gookit/color v1.2.6 // indirect 7 | github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334 8 | github.com/json-iterator/go v1.1.9 9 | github.com/sergi/go-diff v1.1.0 // indirect 10 | github.com/tj/assert v0.0.0-20190920132354-ee03d75cd160 11 | github.com/tj/go-fixture v1.0.0 12 | github.com/xeipuuv/gojsonschema v1.2.0 13 | ) 14 | -------------------------------------------------------------------------------- /validate.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import "fmt" 4 | 5 | // Validator is the interface used for validating input. 6 | type Validator interface { 7 | Validate() error 8 | } 9 | 10 | // ValidationError is a field validation error. 11 | type ValidationError struct { 12 | Field string `json:"field"` 13 | Message string `json:"message"` 14 | } 15 | 16 | // Error implementation. 17 | func (e ValidationError) Error() string { 18 | return fmt.Sprintf("%s %s", e.Field, e.Message) 19 | } 20 | -------------------------------------------------------------------------------- /response.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | // WriteResponse writes a JSON response, or 204 if the value is nil 8 | // to indicate there is no content. 9 | func WriteResponse(w http.ResponseWriter, value interface{}) { 10 | if value == nil { 11 | w.WriteHeader(http.StatusNoContent) 12 | return 13 | } 14 | 15 | w.Header().Set("Content-Type", "application/json") 16 | enc := json.NewEncoder(w) 17 | enc.SetIndent("", " ") 18 | enc.Encode(value) 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: Tests 3 | jobs: 4 | test: 5 | strategy: 6 | matrix: 7 | go-version: [1.14.x] 8 | platform: [ubuntu-latest, macos-latest] 9 | runs-on: ${{ matrix.platform }} 10 | steps: 11 | - name: Install Go 12 | uses: actions/setup-go@v1 13 | with: 14 | go-version: ${{ matrix.go-version }} 15 | - name: Checkout code 16 | uses: actions/checkout@v1 17 | - name: Test 18 | run: go test ./... -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | ) 7 | 8 | // ctxKey is a private context key. 9 | type ctxKey struct{} 10 | 11 | // NewRequestContext returns a new context with ctx. 12 | func NewRequestContext(ctx context.Context, v *http.Request) context.Context { 13 | return context.WithValue(ctx, ctxKey{}, v) 14 | } 15 | 16 | // RequestFromContext returns ctx from context. 17 | func RequestFromContext(ctx context.Context) (*http.Request, bool) { 18 | v, ok := ctx.Value(ctxKey{}).(*http.Request) 19 | return v, ok 20 | } 21 | -------------------------------------------------------------------------------- /generators/dotnetclient/dotnetclient_test.go: -------------------------------------------------------------------------------- 1 | package dotnetclient 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/tj/assert" 8 | "github.com/tj/go-fixture" 9 | 10 | "github.com/apex/rpc/schema" 11 | ) 12 | 13 | func TestGenerate(t *testing.T) { 14 | schema, err := schema.Load("../../examples/todo/schema.json") 15 | assert.NoError(t, err, "loading schema") 16 | 17 | var act bytes.Buffer 18 | err = Generate(&act, schema, "ApexLogs", "Client") 19 | assert.NoError(t, err, "generating") 20 | 21 | fixture.Assert(t, "todo_client.cs", act.Bytes()) 22 | } 23 | -------------------------------------------------------------------------------- /generators/tstypes/tstypes_test.go: -------------------------------------------------------------------------------- 1 | package tstypes_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/tj/assert" 8 | "github.com/tj/go-fixture" 9 | 10 | "github.com/apex/rpc/generators/tstypes" 11 | "github.com/apex/rpc/schema" 12 | ) 13 | 14 | func TestGenerate(t *testing.T) { 15 | schema, err := schema.Load("../../examples/todo/schema.json") 16 | assert.NoError(t, err, "loading schema") 17 | 18 | var act bytes.Buffer 19 | err = tstypes.Generate(&act, schema) 20 | assert.NoError(t, err, "generating") 21 | 22 | fixture.Assert(t, "todo_types.ts", act.Bytes()) 23 | } 24 | -------------------------------------------------------------------------------- /generators/goclient/goclient_test.go: -------------------------------------------------------------------------------- 1 | package goclient_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/tj/assert" 8 | "github.com/tj/go-fixture" 9 | 10 | "github.com/apex/rpc/generators/goclient" 11 | "github.com/apex/rpc/schema" 12 | ) 13 | 14 | func TestGenerate(t *testing.T) { 15 | schema, err := schema.Load("../../examples/todo/schema.json") 16 | assert.NoError(t, err, "loading schema") 17 | 18 | var act bytes.Buffer 19 | err = goclient.Generate(&act, schema) 20 | assert.NoError(t, err, "generating") 21 | 22 | fixture.Assert(t, "todo_client.go", act.Bytes()) 23 | } 24 | -------------------------------------------------------------------------------- /generators/elmclient/elmclient_test.go: -------------------------------------------------------------------------------- 1 | package elmclient_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/tj/assert" 8 | "github.com/tj/go-fixture" 9 | 10 | "github.com/apex/rpc/generators/elmclient" 11 | "github.com/apex/rpc/schema" 12 | ) 13 | 14 | func TestGenerate(t *testing.T) { 15 | schema, err := schema.Load("../../examples/todo/schema.json") 16 | assert.NoError(t, err, "loading schema") 17 | 18 | var act bytes.Buffer 19 | err = elmclient.Generate(&act, schema) 20 | assert.NoError(t, err, "generating") 21 | 22 | fixture.Assert(t, "todo_client.elm", act.Bytes()) 23 | } 24 | -------------------------------------------------------------------------------- /generators/phpclient/phpclient_test.go: -------------------------------------------------------------------------------- 1 | package phpclient_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/tj/assert" 8 | "github.com/tj/go-fixture" 9 | 10 | "github.com/apex/rpc/generators/phpclient" 11 | "github.com/apex/rpc/schema" 12 | ) 13 | 14 | func TestGenerate(t *testing.T) { 15 | schema, err := schema.Load("../../examples/todo/schema.json") 16 | assert.NoError(t, err, "loading schema") 17 | 18 | var act bytes.Buffer 19 | err = phpclient.Generate(&act, schema, "Client") 20 | assert.NoError(t, err, "generating") 21 | 22 | fixture.Assert(t, "todo_client.php", act.Bytes()) 23 | } 24 | -------------------------------------------------------------------------------- /generators/tsclient/tsclient_test.go: -------------------------------------------------------------------------------- 1 | package tsclient_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/tj/assert" 8 | "github.com/tj/go-fixture" 9 | 10 | "github.com/apex/rpc/generators/tsclient" 11 | "github.com/apex/rpc/schema" 12 | ) 13 | 14 | func TestGenerate(t *testing.T) { 15 | schema, err := schema.Load("../../examples/todo/schema.json") 16 | assert.NoError(t, err, "loading schema") 17 | 18 | var act bytes.Buffer 19 | err = tsclient.Generate(&act, schema, "node-fetch") 20 | assert.NoError(t, err, "generating") 21 | 22 | fixture.Assert(t, "todo_client.ts", act.Bytes()) 23 | } 24 | -------------------------------------------------------------------------------- /generators/rubyclient/rubyclient_test.go: -------------------------------------------------------------------------------- 1 | package rubyclient_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/tj/assert" 8 | "github.com/tj/go-fixture" 9 | 10 | "github.com/apex/rpc/generators/rubyclient" 11 | "github.com/apex/rpc/schema" 12 | ) 13 | 14 | func TestGenerate(t *testing.T) { 15 | schema, err := schema.Load("../../examples/todo/schema.json") 16 | assert.NoError(t, err, "loading schema") 17 | 18 | var act bytes.Buffer 19 | err = rubyclient.Generate(&act, schema, "Todo", "Client") 20 | assert.NoError(t, err, "generating") 21 | 22 | fixture.Assert(t, "todo_client.rb", act.Bytes()) 23 | } 24 | -------------------------------------------------------------------------------- /internal/format/format_test.go: -------------------------------------------------------------------------------- 1 | package format_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/apex/rpc/internal/format" 7 | "github.com/tj/assert" 8 | ) 9 | 10 | // Test go name formatting. 11 | func TestGoName(t *testing.T) { 12 | assert.Equal(t, "UpdateUser", format.GoName("update_user")) 13 | assert.Equal(t, "UserID", format.GoName("user_id")) 14 | assert.Equal(t, "IP", format.GoName("ip")) 15 | } 16 | 17 | // Test js name formatting. 18 | func TestJsName(t *testing.T) { 19 | assert.Equal(t, "updateUserSettings", format.JsName("update_user_settings")) 20 | assert.Equal(t, "userId", format.JsName("user_id")) 21 | } 22 | -------------------------------------------------------------------------------- /cmd/rpc-md-docs/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | 8 | "github.com/apex/rpc/generators/mddocs" 9 | "github.com/apex/rpc/schema" 10 | ) 11 | 12 | func main() { 13 | path := flag.String("schema", "schema.json", "Path to the schema file") 14 | out := flag.String("output", "docs", "Output directory") 15 | flag.Parse() 16 | 17 | s, err := schema.Load(*path) 18 | if err != nil { 19 | log.Fatalf("error: %s", err) 20 | } 21 | 22 | println() 23 | defer println() 24 | 25 | err = mddocs.Generate(s, *out) 26 | if err != nil { 27 | log.Fatalf("error: %s", err) 28 | } 29 | 30 | fmt.Printf(" ==> Complete\n") 31 | } 32 | -------------------------------------------------------------------------------- /health.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | ) 7 | 8 | // TODO: remove health checking, this can be provided 9 | // in middleware and has nothing to do with RPC :D 10 | 11 | // HealthChecker is the interface used for servers providing a health check. 12 | type HealthChecker interface { 13 | Health() error 14 | } 15 | 16 | // WriteHealth responds with 200 OK or invokes the Health() method on the server 17 | // if it implements the HealthChecker interface. 18 | func WriteHealth(w http.ResponseWriter, s interface{}) { 19 | h, ok := s.(HealthChecker) 20 | if ok { 21 | err := h.Health() 22 | if err != nil { 23 | http.Error(w, "Health check failed", http.StatusInternalServerError) 24 | return 25 | } 26 | } 27 | 28 | fmt.Fprintln(w, "OK") 29 | } 30 | -------------------------------------------------------------------------------- /cmd/rpc-elm-client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io" 7 | "log" 8 | "os" 9 | 10 | "github.com/apex/rpc/generators/elmclient" 11 | "github.com/apex/rpc/schema" 12 | ) 13 | 14 | func main() { 15 | path := flag.String("schema", "schema.json", "Path to the schema file") 16 | flag.Parse() 17 | 18 | s, err := schema.Load(*path) 19 | if err != nil { 20 | log.Fatalf("error: %s", err) 21 | } 22 | 23 | err = generate(os.Stdout, s) 24 | if err != nil { 25 | log.Fatalf("error: %s", err) 26 | } 27 | } 28 | 29 | // generate implementation. 30 | func generate(w io.Writer, s *schema.Schema) error { 31 | err := elmclient.Generate(w, s) 32 | if err != nil { 33 | return fmt.Errorf("generating client: %w", err) 34 | } 35 | 36 | return nil 37 | } 38 | -------------------------------------------------------------------------------- /cmd/rpc-php-client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io" 7 | "log" 8 | "os" 9 | 10 | "github.com/apex/rpc/generators/phpclient" 11 | "github.com/apex/rpc/schema" 12 | ) 13 | 14 | func main() { 15 | path := flag.String("schema", "schema.json", "Path to the schema file") 16 | className := flag.String("class", "Client", "Name of the client class") 17 | flag.Parse() 18 | 19 | s, err := schema.Load(*path) 20 | if err != nil { 21 | log.Fatalf("error: %s", err) 22 | } 23 | 24 | err = generate(os.Stdout, s, *className) 25 | if err != nil { 26 | log.Fatalf("error: %s", err) 27 | } 28 | } 29 | 30 | // generate implementation. 31 | func generate(w io.Writer, s *schema.Schema, className string) error { 32 | err := phpclient.Generate(w, s, className) 33 | if err != nil { 34 | return fmt.Errorf("generating client: %w", err) 35 | } 36 | 37 | return nil 38 | } 39 | -------------------------------------------------------------------------------- /generators/tstypes/testdata/todo_types.ts: -------------------------------------------------------------------------------- 1 | // Item is a to-do item. 2 | export interface Item { 3 | // created_at is the time the to-do item was created. 4 | created_at?: Date 5 | 6 | // id is the id of the item. This field is read-only. 7 | id?: number 8 | 9 | // text is the to-do item text. This field is required. 10 | text: string 11 | } 12 | 13 | // AddItemInput params. 14 | interface AddItemInput { 15 | // item is the item to add. This field is required. 16 | item: string 17 | } 18 | 19 | // GetItemsOutput params. 20 | interface GetItemsOutput { 21 | // items is the list of to-do items. 22 | items?: Item[] 23 | } 24 | 25 | // RemoveItemInput params. 26 | interface RemoveItemInput { 27 | // id is the id of the item to remove. 28 | id?: number 29 | } 30 | 31 | // RemoveItemOutput params. 32 | interface RemoveItemOutput { 33 | // item is the item removed. 34 | item?: Item 35 | } 36 | 37 | -------------------------------------------------------------------------------- /request.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "net/http" 5 | 6 | jsoniter "github.com/json-iterator/go" 7 | ) 8 | 9 | var json = jsoniter.ConfigCompatibleWithStandardLibrary 10 | 11 | // ReadRequest parses application/json request bodies into value, or returns an error. 12 | func ReadRequest(r *http.Request, value interface{}) error { 13 | switch r.Header.Get("Content-Type") { 14 | case "application/json": 15 | // decode 16 | err := json.NewDecoder(r.Body).Decode(value) 17 | if err != nil { 18 | return BadRequest("Failed to parse malformed request body, must be a valid JSON object") 19 | } 20 | 21 | // validate 22 | if v, ok := value.(Validator); ok { 23 | err := v.Validate() 24 | if err != nil { 25 | return Invalid(err.Error()) 26 | } 27 | } 28 | 29 | return nil 30 | default: 31 | return BadRequest("Unsupported request Content-Type, must be application/json") 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /generators/goserver/goserver_test.go: -------------------------------------------------------------------------------- 1 | package goserver_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/tj/assert" 8 | "github.com/tj/go-fixture" 9 | 10 | "github.com/apex/rpc/generators/goserver" 11 | "github.com/apex/rpc/schema" 12 | ) 13 | 14 | func TestGenerate_noTypes(t *testing.T) { 15 | schema, err := schema.Load("../../examples/todo/schema.json") 16 | assert.NoError(t, err, "loading schema") 17 | 18 | var act bytes.Buffer 19 | err = goserver.Generate(&act, schema, false, "") 20 | assert.NoError(t, err, "generating") 21 | 22 | fixture.Assert(t, "todo_server_no_types.go", act.Bytes()) 23 | } 24 | func TestGenerate_types(t *testing.T) { 25 | schema, err := schema.Load("../../examples/todo/schema.json") 26 | assert.NoError(t, err, "loading schema") 27 | 28 | var act bytes.Buffer 29 | err = goserver.Generate(&act, schema, false, "api") 30 | assert.NoError(t, err, "generating") 31 | 32 | fixture.Assert(t, "todo_server_types.go", act.Bytes()) 33 | } 34 | -------------------------------------------------------------------------------- /generators/gotypes/gotypes_test.go: -------------------------------------------------------------------------------- 1 | package gotypes_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/tj/assert" 8 | "github.com/tj/go-fixture" 9 | 10 | "github.com/apex/rpc/generators/gotypes" 11 | "github.com/apex/rpc/schema" 12 | ) 13 | 14 | func TestGenerate_validate(t *testing.T) { 15 | schema, err := schema.Load("../../examples/todo/schema.json") 16 | assert.NoError(t, err, "loading schema") 17 | 18 | var act bytes.Buffer 19 | err = gotypes.Generate(&act, schema, true) 20 | assert.NoError(t, err, "generating") 21 | 22 | fixture.Assert(t, "todo_types_validate.go", act.Bytes()) 23 | } 24 | 25 | func TestGenerate_noValidate(t *testing.T) { 26 | schema, err := schema.Load("../../examples/todo/schema.json") 27 | assert.NoError(t, err, "loading schema") 28 | 29 | var act bytes.Buffer 30 | err = gotypes.Generate(&act, schema, false) 31 | assert.NoError(t, err, "generating") 32 | 33 | fixture.Assert(t, "todo_types_no_validate.go", act.Bytes()) 34 | } 35 | -------------------------------------------------------------------------------- /generators/gotypes/testdata/todo_types_no_validate.go: -------------------------------------------------------------------------------- 1 | // Item is a to-do item. 2 | type Item struct { 3 | // CreatedAt is the time the to-do item was created. 4 | CreatedAt time.Time `json:"created_at"` 5 | 6 | // ID is the id of the item. This field is read-only. 7 | ID int `json:"id"` 8 | 9 | // Text is the to-do item text. This field is required. 10 | Text string `json:"text"` 11 | } 12 | 13 | // AddItemInput params. 14 | type AddItemInput struct { 15 | // Item is the item to add. This field is required. 16 | Item string `json:"item"` 17 | } 18 | 19 | // GetItemsOutput params. 20 | type GetItemsOutput struct { 21 | // Items is the list of to-do items. 22 | Items []Item `json:"items"` 23 | } 24 | 25 | // RemoveItemInput params. 26 | type RemoveItemInput struct { 27 | // ID is the id of the item to remove. 28 | ID int `json:"id"` 29 | } 30 | 31 | // RemoveItemOutput params. 32 | type RemoveItemOutput struct { 33 | // Item is the item removed. 34 | Item Item `json:"item"` 35 | } 36 | 37 | -------------------------------------------------------------------------------- /cmd/rpc-ruby-client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io" 7 | "log" 8 | "os" 9 | 10 | "github.com/apex/rpc/generators/rubyclient" 11 | "github.com/apex/rpc/schema" 12 | ) 13 | 14 | func main() { 15 | path := flag.String("schema", "schema.json", "Path to the schema file") 16 | moduleName := flag.String("module", "MyModule", "Name of the module") 17 | className := flag.String("class", "Client", "Name of the client class") 18 | flag.Parse() 19 | 20 | s, err := schema.Load(*path) 21 | if err != nil { 22 | log.Fatalf("error: %s", err) 23 | } 24 | 25 | err = generate(os.Stdout, s, *moduleName, *className) 26 | if err != nil { 27 | log.Fatalf("error: %s", err) 28 | } 29 | } 30 | 31 | // generate implementation. 32 | func generate(w io.Writer, s *schema.Schema, moduleName, className string) error { 33 | err := rubyclient.Generate(w, s, moduleName, className) 34 | if err != nil { 35 | return fmt.Errorf("generating client: %w", err) 36 | } 37 | 38 | return nil 39 | } 40 | -------------------------------------------------------------------------------- /cmd/rpc-dotnet-client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io" 7 | "log" 8 | "os" 9 | 10 | "github.com/apex/rpc/generators/dotnetclient" 11 | "github.com/apex/rpc/schema" 12 | ) 13 | 14 | func main() { 15 | path := flag.String("schema", "schema.json", "Path to the schema file") 16 | namespaceName := flag.String("namespace", "MyNamespace", "Name of the namespace") 17 | className := flag.String("class", "Client", "Name of the client class") 18 | flag.Parse() 19 | 20 | s, err := schema.Load(*path) 21 | if err != nil { 22 | log.Fatalf("error: %s", err) 23 | } 24 | 25 | err = generate(os.Stdout, s, *namespaceName, *className) 26 | if err != nil { 27 | log.Fatalf("error: %s", err) 28 | } 29 | } 30 | 31 | // generate implementation. 32 | func generate(w io.Writer, s *schema.Schema, namespace, className string) error { 33 | err := dotnetclient.Generate(w, s, namespace, className) 34 | if err != nil { 35 | return fmt.Errorf("generating client: %w", err) 36 | } 37 | 38 | return nil 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2019 TJ Holowaychuk tj@tjholowaychuk.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | -------------------------------------------------------------------------------- /health_test.go: -------------------------------------------------------------------------------- 1 | package rpc_test 2 | 3 | import ( 4 | "errors" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | "github.com/tj/assert" 9 | 10 | "github.com/apex/rpc" 11 | ) 12 | 13 | // healthChecker implementation. 14 | type healthChecker struct { 15 | err error 16 | } 17 | 18 | // Health implementation. 19 | func (h healthChecker) Health() error { 20 | return h.err 21 | } 22 | 23 | // Test health checks. 24 | func TestWriteHealth(t *testing.T) { 25 | t.Run("without a HealthChecker", func(t *testing.T) { 26 | w := httptest.NewRecorder() 27 | rpc.WriteHealth(w, nil) 28 | assert.Equal(t, 200, w.Code) 29 | assert.Equal(t, "OK\n", w.Body.String()) 30 | }) 31 | 32 | t.Run("with a HealthChecker passing", func(t *testing.T) { 33 | w := httptest.NewRecorder() 34 | rpc.WriteHealth(w, healthChecker{}) 35 | assert.Equal(t, 200, w.Code) 36 | assert.Equal(t, "OK\n", w.Body.String()) 37 | }) 38 | 39 | t.Run("with a HealthChecker failing", func(t *testing.T) { 40 | w := httptest.NewRecorder() 41 | rpc.WriteHealth(w, healthChecker{errors.New("boom")}) 42 | assert.Equal(t, 500, w.Code) 43 | assert.Equal(t, "Health check failed\n", w.Body.String()) 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /Contributing.md: -------------------------------------------------------------------------------- 1 | 2 | ## Contributing a language client 3 | 4 | Contributing a client starts with creating a new [generator](./generators), a Go package which is tasked with creating the output for the new client, then a [command](./cmd) which wraps this package for use as a command-line tool. 5 | 6 | Generated clients __MUST__: 7 | 8 | - Set the Content-Type header field to `application/json` 9 | - Support Authorization Bearer tokens 10 | - Handle HTTP status errors reporting `>= 300` appropriately 11 | - Support decoding of application/json error responses and exposing this information 12 | - Support 204 "No Content" responses for methods which return no data 13 | 14 | Generated clients __SHOULD__: 15 | 16 | - Set the `User-Agent` header field with the name and version of the library 17 | - Support compressed gzip requests 18 | - Support compressed gzip responses 19 | 20 | ## Testing 21 | 22 | Clients use [tj/go-fixture](https://github.com/tj/go-fixture) for testing, use `go test -update` to generate or update any test fixtures. 23 | 24 | ## Schema 25 | 26 | The JSON Schema used to validate Apex RPC's schema is located in the ./schema directory. Any changes made to this schema must be re-generated with `go generate`. -------------------------------------------------------------------------------- /cmd/rpc-go-types/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io" 7 | "log" 8 | "os" 9 | 10 | "github.com/apex/rpc/generators/gotypes" 11 | "github.com/apex/rpc/schema" 12 | ) 13 | 14 | func main() { 15 | path := flag.String("schema", "schema.json", "Path to the schema file") 16 | pkg := flag.String("package", "api", "Name of the package") 17 | flag.Parse() 18 | 19 | s, err := schema.Load(*path) 20 | if err != nil { 21 | log.Fatalf("error: %s", err) 22 | } 23 | 24 | err = generate(os.Stdout, s, *pkg) 25 | if err != nil { 26 | log.Fatalf("error: %s", err) 27 | } 28 | } 29 | 30 | // generate implementation. 31 | func generate(w io.Writer, s *schema.Schema, pkg string) error { 32 | out := fmt.Fprintf 33 | 34 | // TODO: move these to generator 35 | out(w, "// Do not edit, this file was generated by github.com/apex/rpc.\n\n") 36 | out(w, "package %s\n\n", pkg) 37 | 38 | out(w, "import (\n") 39 | out(w, " \"fmt\"\n") 40 | out(w, " \"time\"\n") 41 | out(w, "\n") 42 | out(w, " \"github.com/apex/rpc\"\n") 43 | out(w, ")\n\n") 44 | 45 | err := gotypes.Generate(w, s, true) 46 | if err != nil { 47 | return fmt.Errorf("generating types: %w", err) 48 | } 49 | 50 | return nil 51 | } 52 | -------------------------------------------------------------------------------- /cmd/rpc-ts-client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io" 7 | "log" 8 | "os" 9 | 10 | "github.com/apex/rpc/generators/tsclient" 11 | "github.com/apex/rpc/generators/tstypes" 12 | "github.com/apex/rpc/schema" 13 | ) 14 | 15 | func main() { 16 | path := flag.String("schema", "schema.json", "Path to the schema file") 17 | fetchLibrary := flag.String("fetch-library", "node-fetch", "Module import for the fetch library") 18 | flag.Parse() 19 | 20 | s, err := schema.Load(*path) 21 | if err != nil { 22 | log.Fatalf("error: %s", err) 23 | } 24 | 25 | err = generate(os.Stdout, s, "client", *fetchLibrary) 26 | if err != nil { 27 | log.Fatalf("error: %s", err) 28 | } 29 | } 30 | 31 | // generate implementation. 32 | func generate(w io.Writer, s *schema.Schema, pkg, fetchLibrary string) error { 33 | out := fmt.Fprintf 34 | 35 | out(w, "// Do not edit, this file was generated by github.com/apex/rpc.\n\n") 36 | 37 | err := tstypes.Generate(w, s) 38 | if err != nil { 39 | return fmt.Errorf("generating types: %w", err) 40 | } 41 | 42 | err = tsclient.Generate(w, s, fetchLibrary) 43 | if err != nil { 44 | return fmt.Errorf("generating client: %w", err) 45 | } 46 | 47 | return nil 48 | } 49 | -------------------------------------------------------------------------------- /internal/format/format.go: -------------------------------------------------------------------------------- 1 | package format 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/iancoleman/strcase" 8 | ) 9 | 10 | // TODO: rename these functions, no need to be language specific 11 | 12 | // TODO: list of these somewhere? 13 | var cases = []string{ 14 | "Api", 15 | "Id", 16 | "Http", 17 | "Https", 18 | "Pdf", 19 | "Ip", 20 | "Json", 21 | "Sql", 22 | "Vat", 23 | "Tcp", 24 | "Tls", 25 | "Udp", 26 | "Ui", 27 | "Uid", 28 | "Uuid", 29 | "Uri", 30 | "Url", 31 | "Utf8", 32 | } 33 | 34 | // GoName returns a name formatted for Go. 35 | func GoName(s string) string { 36 | s = strcase.ToCamel(s) 37 | for _, c := range cases { 38 | if strings.HasSuffix(s, c) { 39 | s = strings.Replace(s, c, strings.ToUpper(c), 1) 40 | } 41 | } 42 | return s 43 | } 44 | 45 | // GoInputType returns the name of a method input type 46 | func GoInputType(types, method string) string { 47 | if len(types) == 0 { 48 | return fmt.Sprintf("%sInput", GoName(method)) 49 | } 50 | return fmt.Sprintf("%s.%sInput", types, GoName(method)) 51 | } 52 | 53 | // JsName returns a name formatted for JS. 54 | func JsName(s string) string { 55 | return strcase.ToLowerCamel(s) 56 | } 57 | 58 | // ID returns the id case for anchors. 59 | func ID(s string) string { 60 | return strcase.ToSnake(s) 61 | } 62 | -------------------------------------------------------------------------------- /error_test.go: -------------------------------------------------------------------------------- 1 | package rpc_test 2 | 3 | import ( 4 | "errors" 5 | "net/http/httptest" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/tj/assert" 10 | 11 | "github.com/apex/rpc" 12 | ) 13 | 14 | // Test error reporting. 15 | func TestWriteError(t *testing.T) { 16 | t.Run("with a regular error", func(t *testing.T) { 17 | w := httptest.NewRecorder() 18 | rpc.WriteError(w, errors.New("boom")) 19 | assert.Equal(t, 500, w.Code) 20 | assert.Equal(t, "application/json", w.Header().Get("Content-Type")) 21 | assert.Equal(t, "{\n \"type\": \"internal\",\n \"message\": \"boom\"\n}", strings.TrimSpace(w.Body.String())) 22 | }) 23 | 24 | t.Run("with a TypeProvider", func(t *testing.T) { 25 | w := httptest.NewRecorder() 26 | rpc.WriteError(w, rpc.Error(400, "invalid_slug", "Invalid team slug")) 27 | assert.Equal(t, "application/json", w.Header().Get("Content-Type")) 28 | assert.Equal(t, "{\n \"type\": \"invalid_slug\",\n \"message\": \"Invalid team slug\"\n}", strings.TrimSpace(w.Body.String())) 29 | }) 30 | 31 | t.Run("with a StatusProvider", func(t *testing.T) { 32 | w := httptest.NewRecorder() 33 | rpc.WriteError(w, rpc.Error(400, "invalid_slug", "Invalid team slug")) 34 | assert.Equal(t, "application/json", w.Header().Get("Content-Type")) 35 | assert.Equal(t, 400, w.Code) 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /cmd/rpc-go-client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io" 7 | "log" 8 | "os" 9 | 10 | "github.com/apex/rpc/generators/goclient" 11 | "github.com/apex/rpc/generators/gotypes" 12 | "github.com/apex/rpc/schema" 13 | ) 14 | 15 | func main() { 16 | path := flag.String("schema", "schema.json", "Path to the schema file") 17 | pkg := flag.String("package", "client", "Name of the package") 18 | flag.Parse() 19 | 20 | s, err := schema.Load(*path) 21 | if err != nil { 22 | log.Fatalf("error: %s", err) 23 | } 24 | 25 | err = generate(os.Stdout, s, *pkg) 26 | if err != nil { 27 | log.Fatalf("error: %s", err) 28 | } 29 | } 30 | 31 | // generate implementation. 32 | func generate(w io.Writer, s *schema.Schema, pkg string) error { 33 | out := fmt.Fprintf 34 | 35 | // force tags to be json only 36 | s.Go.Tags = []string{"json"} 37 | 38 | out(w, "// Do not edit, this file was generated by github.com/apex/rpc.\n\n") 39 | out(w, "package %s\n\n", pkg) 40 | 41 | out(w, "import (\n") 42 | out(w, " \"bytes\"\n") 43 | out(w, " \"encoding/json\"\n") 44 | out(w, " \"fmt\"\n") 45 | out(w, " \"io\"\n") 46 | out(w, " \"net/http\"\n") 47 | out(w, " \"time\"\n") 48 | out(w, ")\n\n") 49 | 50 | err := gotypes.Generate(w, s, false) 51 | if err != nil { 52 | return fmt.Errorf("generating types: %w", err) 53 | } 54 | 55 | err = goclient.Generate(w, s) 56 | if err != nil { 57 | return fmt.Errorf("generating client: %w", err) 58 | } 59 | 60 | return nil 61 | } 62 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 2 | v0.3.2 / 2020-09-01 3 | =================== 4 | 5 | * Revert "fix camel-casing of TS type field names" (forgot this will serialize wrong) 6 | 7 | v0.3.1 / 2020-09-01 8 | =================== 9 | 10 | * fix camel-casing of TS type field names 11 | 12 | v0.3.0 / 2020-09-01 13 | =================== 14 | 15 | * add dotnet client (#35) 16 | * add schema/schema.json to the binary. Closes #1 17 | * refactor ReadRequest() error response to mention a valid JSON object 18 | * update github workflow to use Go 1.14.x 19 | * remove Any type 20 | 21 | v0.2.0 / 2020-08-06 22 | =================== 23 | 24 | * add exporting of TypeScript types 25 | * add start of Elm client (not usable yet) 26 | * add start of Ruby client. Closes #4 27 | * add start of PHP client. Closes #6 28 | * fix: remove inclusion of oneOf() util when no validation is present in gotypes 29 | * refactor test fixtures using tj/go-fixture 30 | 31 | v0.1.2 / 2020-07-13 32 | =================== 33 | 34 | * revert camel-casing 35 | 36 | v0.1.1 / 2020-07-13 37 | =================== 38 | 39 | * fix js field camel-casing 40 | 41 | v0.1.0 / 2020-07-13 42 | =================== 43 | 44 | * add sorting of type fields 45 | * add support for custom fetch imports 46 | * fix TypeScript errors to support Deno 47 | * fix TypeScript output types, use interface not class 48 | * fix timestamp fields, now providing Date conversion 49 | * fix rpc-go-server to not use hard coded types package 50 | * fix enum support for optional fields 51 | * remove rpc-apex-docs, not useful to other people. Closes #16 52 | -------------------------------------------------------------------------------- /cmd/rpc-go-server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io" 7 | "log" 8 | "os" 9 | "path" 10 | 11 | "github.com/apex/rpc/generators/goserver" 12 | "github.com/apex/rpc/schema" 13 | ) 14 | 15 | func main() { 16 | path := flag.String("schema", "schema.json", "Path to the schema file") 17 | pkg := flag.String("package", "server", "Name of the package") 18 | types := flag.String("types", "", "Types package to import") 19 | logging := flag.Bool("logging", true, "Enable logging generation") 20 | flag.Parse() 21 | 22 | s, err := schema.Load(*path) 23 | if err != nil { 24 | log.Fatalf("error: %s", err) 25 | } 26 | 27 | err = generate(os.Stdout, s, *pkg, *types, *logging) 28 | if err != nil { 29 | log.Fatalf("error: %s", err) 30 | } 31 | } 32 | 33 | // generate implementation. 34 | func generate(w io.Writer, s *schema.Schema, pkg, types string, logging bool) error { 35 | out := fmt.Fprintf 36 | 37 | // TODO: move these to generator 38 | out(w, "// Do not edit, this file was generated by github.com/apex/rpc.\n\n") 39 | out(w, "package %s\n\n", pkg) 40 | 41 | out(w, "import (\n") 42 | out(w, " \"context\"\n") 43 | out(w, " \"net/http\"\n") 44 | out(w, "\n") 45 | out(w, " \"github.com/apex/rpc\"\n") 46 | out(w, " \"github.com/apex/log\"\n") 47 | if len(types) > 0 { 48 | out(w, "\n") 49 | out(w, " \"%s\"\n", types) 50 | } 51 | out(w, ")\n\n") 52 | 53 | if len(types) > 0 { 54 | types = path.Base(types) 55 | } 56 | err := goserver.Generate(w, s, logging, types) 57 | if err != nil { 58 | return fmt.Errorf("generating client: %w", err) 59 | } 60 | 61 | return nil 62 | } 63 | -------------------------------------------------------------------------------- /response_test.go: -------------------------------------------------------------------------------- 1 | package rpc_test 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/tj/assert" 10 | 11 | "github.com/apex/rpc" 12 | ) 13 | 14 | // DiscardResponseWriter . 15 | type DiscardResponseWriter struct { 16 | } 17 | 18 | // Header implementation. 19 | func (d DiscardResponseWriter) Header() http.Header { 20 | return http.Header{} 21 | } 22 | 23 | // Write implementation. 24 | func (d DiscardResponseWriter) Write([]byte) (int, error) { 25 | return 0, nil 26 | } 27 | 28 | // WriteHeader implementation. 29 | func (d DiscardResponseWriter) WriteHeader(int) {} 30 | 31 | // Test responses. 32 | func TestWriteResponse(t *testing.T) { 33 | t.Run("with no content", func(t *testing.T) { 34 | w := httptest.NewRecorder() 35 | rpc.WriteResponse(w, nil) 36 | assert.Equal(t, http.StatusNoContent, w.Code) 37 | assert.Equal(t, ``, strings.TrimSpace(w.Body.String())) 38 | }) 39 | 40 | t.Run("with no content", func(t *testing.T) { 41 | w := httptest.NewRecorder() 42 | rpc.WriteResponse(w, struct { 43 | Name string `json:"name"` 44 | }{ 45 | Name: "Tobi", 46 | }) 47 | assert.Equal(t, 200, w.Code) 48 | assert.Equal(t, "{\n \"name\": \"Tobi\"\n}", strings.TrimSpace(w.Body.String())) 49 | }) 50 | } 51 | 52 | // Benchmark responses. 53 | func BenchmarkWriteResponse(b *testing.B) { 54 | b.ReportAllocs() 55 | b.SetBytes(1) 56 | 57 | out := struct{ Name, Species, Email string }{ 58 | Name: "Tobi", 59 | Species: "Ferret", 60 | Email: "tobi@ferret.com", 61 | } 62 | 63 | var w DiscardResponseWriter 64 | for i := 0; i < b.N; i++ { 65 | rpc.WriteResponse(w, out) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /generators/gotypes/testdata/todo_types_validate.go: -------------------------------------------------------------------------------- 1 | // Item is a to-do item. 2 | type Item struct { 3 | // CreatedAt is the time the to-do item was created. 4 | CreatedAt time.Time `json:"created_at"` 5 | 6 | // ID is the id of the item. This field is read-only. 7 | ID int `json:"id"` 8 | 9 | // Text is the to-do item text. This field is required. 10 | Text string `json:"text"` 11 | } 12 | 13 | // Validate implementation. 14 | func (i *Item) Validate() error { 15 | if i.Text == "" { 16 | return rpc.ValidationError{ Field: "text", Message: "is required" } 17 | } 18 | 19 | return nil 20 | } 21 | 22 | // AddItemInput params. 23 | type AddItemInput struct { 24 | // Item is the item to add. This field is required. 25 | Item string `json:"item"` 26 | } 27 | 28 | // Validate implementation. 29 | func (a *AddItemInput) Validate() error { 30 | if a.Item == "" { 31 | return rpc.ValidationError{ Field: "item", Message: "is required" } 32 | } 33 | 34 | return nil 35 | } 36 | 37 | // GetItemsOutput params. 38 | type GetItemsOutput struct { 39 | // Items is the list of to-do items. 40 | Items []Item `json:"items"` 41 | } 42 | 43 | // RemoveItemInput params. 44 | type RemoveItemInput struct { 45 | // ID is the id of the item to remove. 46 | ID int `json:"id"` 47 | } 48 | 49 | // Validate implementation. 50 | func (r *RemoveItemInput) Validate() error { 51 | return nil 52 | } 53 | 54 | // RemoveItemOutput params. 55 | type RemoveItemOutput struct { 56 | // Item is the item removed. 57 | Item Item `json:"item"` 58 | } 59 | 60 | 61 | // oneOf returns true if s is in the values. 62 | func oneOf(s string, values []string) bool { 63 | for _, v := range values { 64 | if s == v { 65 | return true 66 | } 67 | } 68 | return false 69 | } 70 | -------------------------------------------------------------------------------- /generators/elmclient/testdata/todo_client.elm: -------------------------------------------------------------------------------- 1 | 2 | -- Do not edit, this file was generated by github.com/apex/rpc. 3 | 4 | -- TYPES 5 | 6 | {-| Item is a to-do item. -} 7 | type alias Item = 8 | { createdAt : String 9 | , id : Int 10 | , text : String 11 | } 12 | 13 | -- METHOD PARAMS 14 | 15 | {-| AddItemInput params. -} 16 | type alias AddItemInput = 17 | { item : String 18 | } 19 | 20 | {-| GetItemsOutput params. -} 21 | type alias GetItemsOutput = 22 | { items : List Item 23 | } 24 | 25 | {-| RemoveItemInput params. -} 26 | type alias RemoveItemInput = 27 | { id : Int 28 | } 29 | 30 | {-| RemoveItemOutput params. -} 31 | type alias RemoveItemOutput = 32 | { item : Item 33 | } 34 | 35 | -- METHODS 36 | 37 | addItem : AddItemInput 38 | addItem = 39 | ... 40 | 41 | getItems : GetItemsInput 42 | getItems = 43 | ... 44 | 45 | removeItem : RemoveItemInput 46 | removeItem = 47 | ... 48 | 49 | -- DECODERS 50 | 51 | itemDecoder : Decoder Item 52 | itemDecoder = 53 | Decode.success Item 54 | |> required "created_at" string 55 | |> required "id" int 56 | |> required "text" string 57 | 58 | 59 | addItemInputDecoder : Decoder AddItemInput 60 | addItemInputDecoder = 61 | Decode.success AddItemInput 62 | |> required "item" string 63 | 64 | 65 | getItemsOutputDecoder : Decoder GetItemsOutput 66 | getItemsOutputDecoder = 67 | Decode.success GetItemsOutput 68 | |> required "items" (list itemDecoder) 69 | 70 | 71 | removeItemInputDecoder : Decoder RemoveItemInput 72 | removeItemInputDecoder = 73 | Decode.success RemoveItemInput 74 | |> required "id" int 75 | 76 | 77 | removeItemOutputDecoder : Decoder RemoveItemOutput 78 | removeItemOutputDecoder = 79 | Decode.success RemoveItemOutput 80 | |> required "item" itemDecoder 81 | 82 | 83 | -------------------------------------------------------------------------------- /generators/goserver/testdata/todo_server_no_types.go: -------------------------------------------------------------------------------- 1 | // ServeHTTP implementation. 2 | func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { 3 | if r.Method == "GET" { 4 | switch r.URL.Path { 5 | case "/_health": 6 | rpc.WriteHealth(w, s) 7 | default: 8 | rpc.WriteError(w, rpc.BadRequest("Invalid method")) 9 | } 10 | return 11 | } 12 | 13 | if r.Method == "POST" { 14 | ctx := rpc.NewRequestContext(r.Context(), r) 15 | var res interface{} 16 | var err error 17 | switch r.URL.Path { 18 | case "/add_item": 19 | var in AddItemInput 20 | err = rpc.ReadRequest(r, &in) 21 | if err != nil { 22 | break 23 | } 24 | res, err = s.addItem(ctx, in) 25 | case "/get_items": 26 | res, err = s.getItems(ctx) 27 | case "/remove_item": 28 | var in RemoveItemInput 29 | err = rpc.ReadRequest(r, &in) 30 | if err != nil { 31 | break 32 | } 33 | res, err = s.removeItem(ctx, in) 34 | default: 35 | err = rpc.BadRequest("Invalid method") 36 | } 37 | 38 | if err != nil { 39 | rpc.WriteError(w, err) 40 | return 41 | } 42 | 43 | rpc.WriteResponse(w, res) 44 | return 45 | } 46 | } 47 | 48 | // addItem adds an item to the list. 49 | func (s *Server) addItem(ctx context.Context, in AddItemInput) (interface{}, error) { 50 | err := s.AddItem(ctx, in) 51 | return nil, err 52 | } 53 | 54 | // getItems returns all items in the list. 55 | func (s *Server) getItems(ctx context.Context) (interface{}, error) { 56 | res, err := s.GetItems(ctx) 57 | return res, err 58 | } 59 | 60 | // removeItem removes an item from the to-do list. 61 | func (s *Server) removeItem(ctx context.Context, in RemoveItemInput) (interface{}, error) { 62 | res, err := s.RemoveItem(ctx, in) 63 | return res, err 64 | } 65 | 66 | -------------------------------------------------------------------------------- /generators/goserver/testdata/todo_server_types.go: -------------------------------------------------------------------------------- 1 | // ServeHTTP implementation. 2 | func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { 3 | if r.Method == "GET" { 4 | switch r.URL.Path { 5 | case "/_health": 6 | rpc.WriteHealth(w, s) 7 | default: 8 | rpc.WriteError(w, rpc.BadRequest("Invalid method")) 9 | } 10 | return 11 | } 12 | 13 | if r.Method == "POST" { 14 | ctx := rpc.NewRequestContext(r.Context(), r) 15 | var res interface{} 16 | var err error 17 | switch r.URL.Path { 18 | case "/add_item": 19 | var in api.AddItemInput 20 | err = rpc.ReadRequest(r, &in) 21 | if err != nil { 22 | break 23 | } 24 | res, err = s.addItem(ctx, in) 25 | case "/get_items": 26 | res, err = s.getItems(ctx) 27 | case "/remove_item": 28 | var in api.RemoveItemInput 29 | err = rpc.ReadRequest(r, &in) 30 | if err != nil { 31 | break 32 | } 33 | res, err = s.removeItem(ctx, in) 34 | default: 35 | err = rpc.BadRequest("Invalid method") 36 | } 37 | 38 | if err != nil { 39 | rpc.WriteError(w, err) 40 | return 41 | } 42 | 43 | rpc.WriteResponse(w, res) 44 | return 45 | } 46 | } 47 | 48 | // addItem adds an item to the list. 49 | func (s *Server) addItem(ctx context.Context, in api.AddItemInput) (interface{}, error) { 50 | err := s.AddItem(ctx, in) 51 | return nil, err 52 | } 53 | 54 | // getItems returns all items in the list. 55 | func (s *Server) getItems(ctx context.Context) (interface{}, error) { 56 | res, err := s.GetItems(ctx) 57 | return res, err 58 | } 59 | 60 | // removeItem removes an item from the to-do list. 61 | func (s *Server) removeItem(ctx context.Context, in api.RemoveItemInput) (interface{}, error) { 62 | res, err := s.RemoveItem(ctx, in) 63 | return res, err 64 | } 65 | 66 | -------------------------------------------------------------------------------- /internal/schemautil/schemautil.go: -------------------------------------------------------------------------------- 1 | package schemautil 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/apex/rpc/schema" 8 | ) 9 | 10 | // ResolveRef returns a resolved reference, or panics. 11 | func ResolveRef(s *schema.Schema, ref schema.Ref) schema.Type { 12 | name := strings.Replace(ref.Value, "#/types/", "", 1) 13 | 14 | for _, t := range s.Types { 15 | if t.Name == name { 16 | return t 17 | } 18 | } 19 | 20 | panic(fmt.Sprintf("reference to undefined type %q", ref.Value)) 21 | } 22 | 23 | // FormatExtra . 24 | func FormatExtra(f schema.Field) string { 25 | return FormatAttributes(f) + FormatEnum(f) 26 | } 27 | 28 | // FormatEnum returns a formatted enum description. 29 | func FormatEnum(f schema.Field) string { 30 | if f.Enum == nil { 31 | return "" 32 | } 33 | 34 | var values []string 35 | for _, v := range f.Enum { 36 | values = append(values, `"`+v+`"`) 37 | } 38 | 39 | return " Must be one of: " + strings.Join(values, ", ") + "." 40 | } 41 | 42 | // FormatAttributes returns a formatted field attributes. 43 | func FormatAttributes(f schema.Field) string { 44 | var attrs []string 45 | 46 | if f.Required { 47 | attrs = append(attrs, "required") 48 | } 49 | 50 | if f.ReadOnly { 51 | attrs = append(attrs, "read-only") 52 | } 53 | 54 | if len(attrs) == 0 { 55 | return "" 56 | } 57 | 58 | return " This field is " + join(attrs, ", ", " and ") + "." 59 | } 60 | 61 | // join list. 62 | func join(list []string, delim, separator string) string { 63 | if len(list) == 0 { 64 | return "" 65 | } 66 | 67 | if len(list) == 1 { 68 | return list[0] 69 | } 70 | 71 | switch len(list) { 72 | case 0: 73 | return "" 74 | case 1: 75 | return list[0] 76 | case 2: 77 | return list[0] + " " + separator + " " + list[1] 78 | default: 79 | last := list[len(list)-1] 80 | list = list[:len(list)-1] 81 | return strings.Join(list, delim) + delim + separator + " " + last 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /generators/phpclient/testdata/todo_client.php: -------------------------------------------------------------------------------- 1 | url = $url; 17 | $this->authToken = $authToken; 18 | } 19 | 20 | /** 21 | * addItem adds an item to the list. 22 | * 23 | * @param array $params The input parameters. 24 | */ 25 | public function addItem(array $params) { 26 | return $this->call("add_item", $params); 27 | } 28 | 29 | /** 30 | * getItems returns all items in the list. 31 | * 32 | * @return array 33 | */ 34 | public function getItems() { 35 | return $this->call("get_items", null); 36 | } 37 | 38 | /** 39 | * removeItem removes an item from the to-do list. 40 | * 41 | * @param array $params The input parameters. 42 | * @return array 43 | */ 44 | public function removeItem(array $params) { 45 | return $this->call("remove_item", $params); 46 | } 47 | 48 | private function call($method, $body) { 49 | $header = "Content-type: application/json\r\n"; 50 | 51 | if (isset($this->authToken)) { 52 | $header .= "Authorization: Bearer $this->authToken\r\n"; 53 | } 54 | 55 | $options = array( 56 | 'http' => array( 57 | 'header' => $header, 58 | 'method' => 'POST', 59 | 'content' => json_encode($body) 60 | ) 61 | ); 62 | 63 | $url = $this->url . "/" . $method; 64 | $context = stream_context_create($options); 65 | $result = file_get_contents($url, false, $context); 66 | 67 | // TODO: how to check the status code and parse error from the body? 68 | 69 | return json_decode($result); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /examples/todo/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todo", 3 | "version": "1.0.0", 4 | "description": "A to-do list example.", 5 | "methods": [ 6 | { 7 | "name": "add_item", 8 | "description": "adds an item to the list.", 9 | "inputs": [ 10 | { 11 | "name": "item", 12 | "description": "the item to add.", 13 | "required": true, 14 | "type": "string" 15 | } 16 | ] 17 | }, 18 | { 19 | "name": "get_items", 20 | "description": "returns all items in the list.", 21 | "outputs": [ 22 | { 23 | "name": "items", 24 | "description": "the list of to-do items.", 25 | "type": "array", 26 | "items": { 27 | "$ref": "#/types/item" 28 | } 29 | } 30 | ] 31 | }, 32 | { 33 | "name": "remove_item", 34 | "description": "removes an item from the to-do list.", 35 | "inputs": [ 36 | { 37 | "name": "id", 38 | "description": "the id of the item to remove.", 39 | "type": "integer" 40 | } 41 | ], 42 | "outputs": [ 43 | { 44 | "name": "item", 45 | "description": "the item removed.", 46 | "type": { 47 | "$ref": "#/types/item" 48 | } 49 | } 50 | ] 51 | } 52 | ], 53 | "types": { 54 | "item": { 55 | "description": "is a to-do item.", 56 | "properties": [ 57 | { 58 | "name": "id", 59 | "description": "the id of the item.", 60 | "type": "integer", 61 | "readonly": true 62 | }, 63 | { 64 | "name": "text", 65 | "description": "the to-do item text.", 66 | "required": true, 67 | "type": "string" 68 | }, 69 | { 70 | "name": "created_at", 71 | "description": "the time the to-do item was created.", 72 | "type": "timestamp" 73 | } 74 | ] 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /request_test.go: -------------------------------------------------------------------------------- 1 | package rpc_test 2 | 3 | import ( 4 | "net/http/httptest" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/tj/assert" 9 | 10 | "github.com/apex/rpc" 11 | ) 12 | 13 | // Test requests. 14 | func TestReadRequest(t *testing.T) { 15 | t.Run("with a no content-type", func(t *testing.T) { 16 | r := httptest.NewRequest("GET", "/", strings.NewReader(`{ "name": "Tobi" }`)) 17 | var in struct{ Name string } 18 | err := rpc.ReadRequest(r, &in) 19 | assert.EqualError(t, err, `Unsupported request Content-Type, must be application/json`) 20 | }) 21 | 22 | t.Run("with malformed JSON", func(t *testing.T) { 23 | r := httptest.NewRequest("GET", "/", strings.NewReader(`{ "name": "Tobi`)) 24 | r.Header.Set("Content-Type", "application/json") 25 | var in struct{ Name string } 26 | err := rpc.ReadRequest(r, &in) 27 | assert.EqualError(t, err, `Failed to parse malformed request body, must be a valid JSON object`) 28 | }) 29 | 30 | t.Run("with JSON array", func(t *testing.T) { 31 | r := httptest.NewRequest("GET", "/", strings.NewReader(`[{}]`)) 32 | r.Header.Set("Content-Type", "application/json") 33 | var in struct{ Name string } 34 | err := rpc.ReadRequest(r, &in) 35 | assert.EqualError(t, err, `Failed to parse malformed request body, must be a valid JSON object`) 36 | }) 37 | 38 | t.Run("with a json body", func(t *testing.T) { 39 | r := httptest.NewRequest("GET", "/", strings.NewReader(`{ "name": "Tobi" }`)) 40 | r.Header.Set("Content-Type", "application/json") 41 | var in struct{ Name string } 42 | err := rpc.ReadRequest(r, &in) 43 | assert.NoError(t, err, "parsing") 44 | assert.Equal(t, "Tobi", in.Name) 45 | }) 46 | } 47 | 48 | // Benchmark requests. 49 | func BenchmarkReadRequest(b *testing.B) { 50 | b.ReportAllocs() 51 | b.SetBytes(1) 52 | for i := 0; i < b.N; i++ { 53 | r := httptest.NewRequest("GET", "/", strings.NewReader(`{ "name": "Tobi", "species": "ferret", "email": "tobi@apex.sh" }`)) 54 | r.Header.Set("Content-Type", "application/json") 55 | var in struct{ Name, Species, Email string } 56 | err := rpc.ReadRequest(r, &in) 57 | if err != nil { 58 | b.Fatal(err) 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /generators/rubyclient/testdata/todo_client.rb: -------------------------------------------------------------------------------- 1 | 2 | # Do not edit, this file was generated by github.com/apex/rpc. 3 | 4 | require 'net/http' 5 | require 'net/https' 6 | require 'json' 7 | 8 | module Todo 9 | class Client 10 | # Error is raised when an API call fails due to a 4xx or 5xx HTTP error. 11 | class Error < StandardError 12 | attr_reader :type 13 | attr_reader :message 14 | attr_reader :status 15 | 16 | def initialize(status, type = nil, message = nil) 17 | @status = status 18 | @type = type 19 | @message = message 20 | end 21 | 22 | def to_s 23 | if @type 24 | "#{@status} response: #{@type}: #{@message}" 25 | else 26 | "#{@status} response" 27 | end 28 | end 29 | end 30 | 31 | # Initialize the client with API endpoint URL and optional authentication token. 32 | def initialize(url, auth_token = nil) 33 | @url = url 34 | @auth_token = auth_token 35 | end 36 | 37 | # Adds an item to the list. 38 | # 39 | # @param [Hash] params the input for this method. 40 | # @param params [String] :item The item to add. 41 | def add_item(params) 42 | call "add_item", params 43 | end 44 | 45 | # Returns all items in the list. 46 | def get_items 47 | call "get_items" 48 | end 49 | 50 | # Removes an item from the to-do list. 51 | # 52 | # @param [Hash] params the input for this method. 53 | # @param params [Number] :id The id of the item to remove. 54 | def remove_item(params) 55 | call "remove_item", params 56 | end 57 | 58 | private 59 | 60 | # call an API method with optional input parameters. 61 | def call(method, params = nil) 62 | url = @url + "/" + method 63 | header = { "Content-Type" => "application/json" } 64 | 65 | if @auth_token 66 | header["Authorization"] = "Bearer #{@auth_token}" 67 | end 68 | 69 | res = Net::HTTP.post URI(url), params.to_json, header 70 | status = res.code.to_i 71 | 72 | if status >= 400 73 | begin 74 | body = JSON.parse(res.body) 75 | rescue 76 | raise Error.new(status) 77 | end 78 | raise Error.new(status, body["type"], body["message"]) 79 | end 80 | 81 | res.body 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | // StatusProvider is the interface used for providing an HTTP status code. 8 | type StatusProvider interface { 9 | StatusCode() int 10 | } 11 | 12 | // TypeProvider is the interface used for providing an error type. 13 | type TypeProvider interface { 14 | Type() string 15 | } 16 | 17 | // ServerError is a server error which implements StatusProvider and TypeProvider. 18 | type ServerError struct { 19 | status int 20 | kind string 21 | message string 22 | } 23 | 24 | // StatusCode implementation. 25 | func (e ServerError) StatusCode() int { 26 | return e.status 27 | } 28 | 29 | // Type implementation. 30 | func (e ServerError) Type() string { 31 | return e.kind 32 | } 33 | 34 | // Error implementation. 35 | func (e ServerError) Error() string { 36 | return e.message 37 | } 38 | 39 | // Error returns a new ServerError with HTTP status code, kind and message. 40 | func Error(status int, kind, message string) error { 41 | return ServerError{ 42 | kind: kind, 43 | status: status, 44 | message: message, 45 | } 46 | } 47 | 48 | // BadRequest returns a new bad request error. 49 | func BadRequest(message string) error { 50 | return Error(http.StatusBadRequest, "bad_request", message) 51 | } 52 | 53 | // Invalid returns a validation error. 54 | func Invalid(message string) error { 55 | return Error(http.StatusBadRequest, "invalid", message) 56 | } 57 | 58 | // serverErrorResponse is an error response. 59 | type serverErrorResponse struct { 60 | Type string `json:"type"` 61 | Message string `json:"message"` 62 | } 63 | 64 | // WriteError writes an error. 65 | // 66 | // If err is a StatusProvider the status code provided 67 | // is used, otherwise it defaults to StatusInternalServerError. 68 | // 69 | // If err is a TypeProvider the type provided is used, 70 | // otherwise it defaults to "internal". 71 | // 72 | // The message in the response uses the Error() 73 | // implementation. 74 | // 75 | func WriteError(w http.ResponseWriter, err error) { 76 | w.Header().Set("Content-Type", "application/json") 77 | 78 | if e, ok := err.(StatusProvider); ok { 79 | w.WriteHeader(e.StatusCode()) 80 | } else { 81 | w.WriteHeader(http.StatusInternalServerError) 82 | } 83 | 84 | var body serverErrorResponse 85 | 86 | if e, ok := err.(TypeProvider); ok { 87 | body.Type = e.Type() 88 | } else { 89 | body.Type = "internal" 90 | } 91 | 92 | body.Message = err.Error() 93 | enc := json.NewEncoder(w) 94 | enc.SetIndent("", " ") 95 | enc.Encode(body) 96 | } 97 | -------------------------------------------------------------------------------- /generators/dotnetclient/testdata/todo_client.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net.Http; 4 | using System.Threading.Tasks; 5 | using Newtonsoft.Json; 6 | 7 | namespace ApexLogs 8 | { 9 | public class Client 10 | { 11 | class ApexLogsException : Exception 12 | { 13 | public ApexLogsException(int status) : base($"{status} response") 14 | { } 15 | 16 | public ApexLogsException(int status, string type, string message) 17 | : base($"{status} response: ${type}: {message}") 18 | { } 19 | } 20 | 21 | private readonly string _url; 22 | private readonly string _authToken; 23 | private readonly HttpClient _httpClient; 24 | 25 | public Client(HttpClient httpClient, string url, string authToken) 26 | { 27 | _httpClient = httpClient; 28 | _url = url; 29 | _authToken = authToken; 30 | } 31 | 32 | /// adds an item to the list. 33 | public async Task AddItem(AddItemInput parameter) 34 | { 35 | await Call("add_item", parameter); 36 | } 37 | 38 | /// returns all items in the list. 39 | public async Task GetItems() 40 | { 41 | var res = await Call("get_items"); 42 | var output = JsonConvert.DeserializeObject(res); 43 | return output; 44 | } 45 | 46 | /// removes an item from the to-do list. 47 | public async Task RemoveItem(RemoveItemInput parameter) 48 | { 49 | var res = await Call("remove_item", parameter); 50 | var output = JsonConvert.DeserializeObject(res); 51 | return output; 52 | } 53 | 54 | public async Task Call(string method, object parameters = null) 55 | { 56 | var url = $"{_url}/{method}"; 57 | var message = new HttpRequestMessage 58 | { 59 | Method = HttpMethod.Post, 60 | RequestUri = new Uri(url) 61 | }; 62 | message.Headers.Add("Content-Type", "application/json"); 63 | if (!string.IsNullOrWhiteSpace(_authToken)) 64 | message.Headers.Add("Authorization", $"Bearer {_authToken}"); 65 | 66 | if (parameters != null) 67 | message.Content = new StringContent(JsonConvert.SerializeObject(parameters)); 68 | 69 | var response = await _httpClient.SendAsync(message); 70 | var statusCode = (int) response.StatusCode; 71 | var content = await response.Content.ReadAsStringAsync(); 72 | 73 | if (statusCode < 300) return content; 74 | 75 | var body = JsonConvert.DeserializeObject>(content) 76 | ?? throw new ApexLogsException(statusCode); 77 | 78 | throw new ApexLogsException(statusCode, body["type"], body["message"]); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /generators/phpclient/phpclient.go: -------------------------------------------------------------------------------- 1 | package phpclient 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | 7 | "github.com/apex/rpc/internal/format" 8 | "github.com/apex/rpc/schema" 9 | ) 10 | 11 | var call = ` 12 | private function call($method, $body) { 13 | $header = "Content-type: application/json\r\n"; 14 | 15 | if (isset($this->authToken)) { 16 | $header .= "Authorization: Bearer $this->authToken\r\n"; 17 | } 18 | 19 | $options = array( 20 | 'http' => array( 21 | 'header' => $header, 22 | 'method' => 'POST', 23 | 'content' => json_encode($body) 24 | ) 25 | ); 26 | 27 | $url = $this->url . "/" . $method; 28 | $context = stream_context_create($options); 29 | $result = file_get_contents($url, false, $context); 30 | 31 | // TODO: how to check the status code and parse error from the body? 32 | 33 | return json_decode($result); 34 | }` 35 | 36 | var class = ` 37 | class %s { 38 | protected $url; 39 | protected $authToken; 40 | 41 | /** 42 | * Create a new API client. 43 | * 44 | * @param string $url The endpoint URL. 45 | * @param string $authToken The authentication token [optional]. 46 | */ 47 | 48 | public function __construct($url, $authToken = null) { 49 | $this->url = $url; 50 | $this->authToken = $authToken; 51 | } 52 | ` 53 | 54 | // Generate writes the PHP client implementations to w. 55 | func Generate(w io.Writer, s *schema.Schema, className string) error { 56 | out := fmt.Fprintf 57 | 58 | out(w, " 0 { 72 | out(w, " * @param array $params The input parameters.\n") 73 | } 74 | if len(m.Outputs) > 0 { 75 | out(w, " * @return array\n") 76 | } 77 | out(w, " */\n") 78 | 79 | // method 80 | out(w, " public function %s(", name) 81 | 82 | // input arg 83 | if len(m.Inputs) > 0 { 84 | out(w, "array $params") 85 | } 86 | out(w, ") {\n") 87 | 88 | // return 89 | if len(m.Inputs) > 0 { 90 | out(w, " return $this->call(%q, $params);\n", m.Name) 91 | } else { 92 | out(w, " return $this->call(%q, null);\n", m.Name) 93 | } 94 | 95 | // close 96 | out(w, " }\n") 97 | } 98 | 99 | out(w, "%s\n", call) 100 | out(w, "}\n") 101 | 102 | return nil 103 | } 104 | -------------------------------------------------------------------------------- /generators/tstypes/tstypes.go: -------------------------------------------------------------------------------- 1 | package tstypes 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | 7 | "github.com/apex/rpc/internal/format" 8 | "github.com/apex/rpc/internal/schemautil" 9 | "github.com/apex/rpc/schema" 10 | ) 11 | 12 | // Generate writes the TS type implementations to w. 13 | func Generate(w io.Writer, s *schema.Schema) error { 14 | out := fmt.Fprintf 15 | 16 | // types 17 | for _, t := range s.TypesSlice() { 18 | out(w, "// %s %s\n", format.GoName(t.Name), t.Description) 19 | out(w, "export interface %s {\n", format.GoName(t.Name)) 20 | writeFields(w, s, t.Properties) 21 | out(w, "}\n\n") 22 | } 23 | 24 | // method types 25 | for _, m := range s.Methods { 26 | name := format.GoName(m.Name) 27 | 28 | // inputs 29 | if len(m.Inputs) > 0 { 30 | out(w, "// %sInput params.\n", name) 31 | out(w, "interface %sInput {\n", name) 32 | writeFields(w, s, m.Inputs) 33 | out(w, "}\n") 34 | } 35 | 36 | // both 37 | if len(m.Inputs) > 0 && len(m.Outputs) > 0 { 38 | out(w, "\n") 39 | } 40 | 41 | // outputs 42 | if len(m.Outputs) > 0 { 43 | out(w, "// %sOutput params.\n", name) 44 | out(w, "interface %sOutput {\n", name) 45 | writeFields(w, s, m.Outputs) 46 | out(w, "}\n") 47 | } 48 | 49 | out(w, "\n") 50 | } 51 | 52 | return nil 53 | } 54 | 55 | // writeFields to writer. 56 | func writeFields(w io.Writer, s *schema.Schema, fields []schema.Field) { 57 | for i, f := range fields { 58 | writeField(w, s, f) 59 | if i < len(fields)-1 { 60 | fmt.Fprintf(w, "\n") 61 | } 62 | } 63 | } 64 | 65 | // writeField to writer. 66 | func writeField(w io.Writer, s *schema.Schema, f schema.Field) { 67 | fmt.Fprintf(w, " // %s is %s%s\n", f.Name, f.Description, schemautil.FormatExtra(f)) 68 | if f.Required { 69 | fmt.Fprintf(w, " %s: %s\n", f.Name, jsType(s, f)) 70 | } else { 71 | fmt.Fprintf(w, " %s?: %s\n", f.Name, jsType(s, f)) 72 | } 73 | } 74 | 75 | // jsType returns a JS equivalent type for field f. 76 | func jsType(s *schema.Schema, f schema.Field) string { 77 | // ref 78 | if ref := f.Type.Ref.Value; ref != "" { 79 | t := schemautil.ResolveRef(s, f.Type.Ref) 80 | return format.GoName(t.Name) 81 | } 82 | 83 | // type 84 | switch f.Type.Type { 85 | case schema.String: 86 | return "string" 87 | case schema.Int, schema.Float: 88 | return "number" 89 | case schema.Bool: 90 | return "boolean" 91 | case schema.Timestamp: 92 | return "Date" 93 | case schema.Object: 94 | return "object" 95 | case schema.Array: 96 | return jsType(s, schema.Field{ 97 | Type: schema.TypeObject(f.Items), 98 | }) + "[]" 99 | default: 100 | panic("unhandled type") 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /generators/goclient/testdata/todo_client.go: -------------------------------------------------------------------------------- 1 | // Client is the API client. 2 | type Client struct { 3 | // URL is the required API endpoint address. 4 | URL string 5 | 6 | // AuthToken is an optional authentication token. 7 | AuthToken string 8 | 9 | // HTTPClient is the client used for making requests, defaulting to http.DefaultClient. 10 | HTTPClient *http.Client 11 | } 12 | 13 | // AddItem adds an item to the list. 14 | func (c *Client) AddItem(in AddItemInput) error { 15 | return call(c.HTTPClient, c.AuthToken, c.URL, "add_item", in, nil) 16 | } 17 | 18 | // GetItems returns all items in the list. 19 | func (c *Client) GetItems() (*GetItemsOutput, error) { 20 | var out GetItemsOutput 21 | return &out, call(c.HTTPClient, c.AuthToken, c.URL, "get_items", nil, &out) 22 | } 23 | 24 | // RemoveItem removes an item from the to-do list. 25 | func (c *Client) RemoveItem(in RemoveItemInput) (*RemoveItemOutput, error) { 26 | var out RemoveItemOutput 27 | return &out, call(c.HTTPClient, c.AuthToken, c.URL, "remove_item", in, &out) 28 | } 29 | 30 | 31 | // Error is an error returned by the client. 32 | type Error struct { 33 | Status string 34 | StatusCode int 35 | Type string 36 | Message string 37 | } 38 | 39 | // Error implementation. 40 | func (e Error) Error() string { 41 | if e.Type == "" { 42 | return fmt.Sprintf("%s: %d", e.Status, e.StatusCode) 43 | } 44 | return fmt.Sprintf("%s: %s", e.Type, e.Message) 45 | } 46 | 47 | // call implementation. 48 | func call(client *http.Client, authToken, endpoint, method string, in, out interface{}) error { 49 | var body io.Reader 50 | 51 | // default client 52 | if client == nil { 53 | client = http.DefaultClient 54 | } 55 | 56 | // input params 57 | if in != nil { 58 | var buf bytes.Buffer 59 | err := json.NewEncoder(&buf).Encode(in) 60 | if err != nil { 61 | return fmt.Errorf("encoding: %w", err) 62 | } 63 | body = &buf 64 | } 65 | 66 | // POST request 67 | req, err := http.NewRequest("POST", endpoint+"/"+method, body) 68 | if err != nil { 69 | return err 70 | } 71 | req.Header.Set("Content-Type", "application/json") 72 | 73 | // auth token 74 | if authToken != "" { 75 | req.Header.Set("Authorization", "Bearer "+authToken) 76 | } 77 | 78 | // response 79 | res, err := client.Do(req) 80 | if err != nil { 81 | return err 82 | } 83 | defer res.Body.Close() 84 | 85 | // error 86 | if res.StatusCode >= 300 { 87 | var e Error 88 | if res.Header.Get("Content-Type") == "application/json" { 89 | if err := json.NewDecoder(res.Body).Decode(&e); err != nil { 90 | return err 91 | } 92 | } 93 | e.Status = http.StatusText(res.StatusCode) 94 | e.StatusCode = res.StatusCode 95 | return e 96 | } 97 | 98 | // output params 99 | if out != nil { 100 | err = json.NewDecoder(res.Body).Decode(out) 101 | if err != nil { 102 | return err 103 | } 104 | } 105 | 106 | return nil 107 | } 108 | -------------------------------------------------------------------------------- /generators/tsclient/testdata/todo_client.ts: -------------------------------------------------------------------------------- 1 | 2 | // fetch for Node 3 | const fetch = (typeof window == 'undefined' || window.fetch == null) 4 | // @ts-ignore 5 | ? require('node-fetch') 6 | : window.fetch 7 | 8 | /** 9 | * ClientError is an API client error providing the HTTP status code and error type. 10 | */ 11 | 12 | class ClientError extends Error { 13 | status: number; 14 | type?: string; 15 | 16 | constructor(status: number, message?: string, type?: string) { 17 | super(message) 18 | this.status = status 19 | this.type = type 20 | } 21 | } 22 | 23 | /** 24 | * Call method with params via a POST request. 25 | */ 26 | 27 | async function call(url: string, method: string, authToken?: string, params?: any): Promise { 28 | const headers: Record = { 29 | 'Content-Type': 'application/json' 30 | } 31 | 32 | if (authToken != null) { 33 | headers['Authorization'] = `Bearer ${authToken}` 34 | } 35 | 36 | const res = await fetch(url + '/' + method, { 37 | method: 'POST', 38 | body: JSON.stringify(params), 39 | headers 40 | }) 41 | 42 | // we have an error, try to parse a well-formed json 43 | // error response, otherwise default to status code 44 | if (res.status >= 300) { 45 | let err 46 | try { 47 | const { type, message } = await res.json() 48 | err = new ClientError(res.status, message, type) 49 | } catch { 50 | err = new ClientError(res.status, res.statusText) 51 | } 52 | throw err 53 | } 54 | 55 | return res.text() 56 | } 57 | 58 | 59 | const reISO8601 = /(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))/ 60 | 61 | /** 62 | * Client is the API client. 63 | */ 64 | 65 | export class Client { 66 | 67 | private url: string 68 | private authToken?: string 69 | 70 | /** 71 | * Initialize. 72 | */ 73 | 74 | constructor(params: { url: string, authToken?: string }) { 75 | this.url = params.url 76 | this.authToken = params.authToken 77 | } 78 | 79 | /** 80 | * Decoder is used as the reviver parameter when decoding responses. 81 | */ 82 | 83 | private decoder(key: any, value: any) { 84 | return typeof value == 'string' && reISO8601.test(value) 85 | ? new Date(value) 86 | : value 87 | } 88 | 89 | /** 90 | * addItem: adds an item to the list. 91 | */ 92 | 93 | async addItem(params: AddItemInput) { 94 | await call(this.url, 'add_item', this.authToken, params) 95 | } 96 | 97 | /** 98 | * getItems: returns all items in the list. 99 | */ 100 | 101 | async getItems(): Promise { 102 | let res = await call(this.url, 'get_items', this.authToken) 103 | let out: GetItemsOutput = JSON.parse(res, this.decoder) 104 | return out 105 | } 106 | 107 | /** 108 | * removeItem: removes an item from the to-do list. 109 | */ 110 | 111 | async removeItem(params: RemoveItemInput): Promise { 112 | let res = await call(this.url, 'remove_item', this.authToken, params) 113 | let out: RemoveItemOutput = JSON.parse(res, this.decoder) 114 | return out 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /generators/goclient/goclient.go: -------------------------------------------------------------------------------- 1 | package goclient 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | 7 | "github.com/apex/rpc/internal/format" 8 | "github.com/apex/rpc/schema" 9 | ) 10 | 11 | var call = `// Error is an error returned by the client. 12 | type Error struct { 13 | Status string 14 | StatusCode int 15 | Type string 16 | Message string 17 | } 18 | 19 | // Error implementation. 20 | func (e Error) Error() string { 21 | if e.Type == "" { 22 | return fmt.Sprintf("%s: %d", e.Status, e.StatusCode) 23 | } 24 | return fmt.Sprintf("%s: %s", e.Type, e.Message) 25 | } 26 | 27 | // call implementation. 28 | func call(client *http.Client, authToken, endpoint, method string, in, out interface{}) error { 29 | var body io.Reader 30 | 31 | // default client 32 | if client == nil { 33 | client = http.DefaultClient 34 | } 35 | 36 | // input params 37 | if in != nil { 38 | var buf bytes.Buffer 39 | err := json.NewEncoder(&buf).Encode(in) 40 | if err != nil { 41 | return fmt.Errorf("encoding: %w", err) 42 | } 43 | body = &buf 44 | } 45 | 46 | // POST request 47 | req, err := http.NewRequest("POST", endpoint+"/"+method, body) 48 | if err != nil { 49 | return err 50 | } 51 | req.Header.Set("Content-Type", "application/json") 52 | 53 | // auth token 54 | if authToken != "" { 55 | req.Header.Set("Authorization", "Bearer "+authToken) 56 | } 57 | 58 | // response 59 | res, err := client.Do(req) 60 | if err != nil { 61 | return err 62 | } 63 | defer res.Body.Close() 64 | 65 | // error 66 | if res.StatusCode >= 300 { 67 | var e Error 68 | if res.Header.Get("Content-Type") == "application/json" { 69 | if err := json.NewDecoder(res.Body).Decode(&e); err != nil { 70 | return err 71 | } 72 | } 73 | e.Status = http.StatusText(res.StatusCode) 74 | e.StatusCode = res.StatusCode 75 | return e 76 | } 77 | 78 | // output params 79 | if out != nil { 80 | err = json.NewDecoder(res.Body).Decode(out) 81 | if err != nil { 82 | return err 83 | } 84 | } 85 | 86 | return nil 87 | }` 88 | 89 | // Generate writes the Go client implementations to w. 90 | func Generate(w io.Writer, s *schema.Schema) error { 91 | out := fmt.Fprintf 92 | 93 | out(w, "// Client is the API client.\n") 94 | out(w, "type Client struct {\n") 95 | out(w, " // URL is the required API endpoint address.\n") 96 | out(w, " URL string\n\n") 97 | out(w, " // AuthToken is an optional authentication token.\n") 98 | out(w, " AuthToken string\n\n") 99 | out(w, " // HTTPClient is the client used for making requests, defaulting to http.DefaultClient.\n") 100 | out(w, " HTTPClient *http.Client\n") 101 | out(w, "}\n\n") 102 | 103 | for _, m := range s.Methods { 104 | name := format.GoName(m.Name) 105 | out(w, "// %s %s\n", name, m.Description) 106 | out(w, "func (c *Client) %s(", name) 107 | 108 | // input arg 109 | if len(m.Inputs) > 0 { 110 | out(w, "in %sInput", name) 111 | } 112 | out(w, ") ") 113 | 114 | // output arg 115 | if len(m.Outputs) > 0 { 116 | out(w, "(*%sOutput, error) {\n", name) 117 | out(w, " var out %sOutput\n", name) 118 | } else { 119 | out(w, "error {\n") 120 | } 121 | 122 | // return 123 | out(w, " return ") 124 | if len(m.Outputs) > 0 { 125 | out(w, "&out, ") 126 | } 127 | out(w, "call(c.HTTPClient, c.AuthToken, c.URL, \"%s\", ", m.Name) 128 | if len(m.Inputs) > 0 { 129 | out(w, "in, ") 130 | } else { 131 | out(w, "nil, ") 132 | } 133 | if len(m.Outputs) > 0 { 134 | out(w, "&out)\n") 135 | } else { 136 | out(w, "nil)\n") 137 | } 138 | 139 | // close 140 | out(w, "}\n\n") 141 | } 142 | 143 | out(w, "\n%s\n", call) 144 | 145 | return nil 146 | } 147 | -------------------------------------------------------------------------------- /generators/rubyclient/rubyclient.go: -------------------------------------------------------------------------------- 1 | package rubyclient 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "strings" 7 | 8 | "github.com/apex/rpc/schema" 9 | ) 10 | 11 | var module = ` 12 | # Do not edit, this file was generated by github.com/apex/rpc. 13 | 14 | require 'net/http' 15 | require 'net/https' 16 | require 'json' 17 | 18 | module %s 19 | class %s 20 | # Error is raised when an API call fails due to a 4xx or 5xx HTTP error. 21 | class Error < StandardError 22 | attr_reader :type 23 | attr_reader :message 24 | attr_reader :status 25 | 26 | def initialize(status, type = nil, message = nil) 27 | @status = status 28 | @type = type 29 | @message = message 30 | end 31 | 32 | def to_s 33 | if @type 34 | "#{@status} response: #{@type}: #{@message}" 35 | else 36 | "#{@status} response" 37 | end 38 | end 39 | end 40 | 41 | # Initialize the client with API endpoint URL and optional authentication token. 42 | def initialize(url, auth_token = nil) 43 | @url = url 44 | @auth_token = auth_token 45 | end 46 | ` 47 | 48 | var call = ` 49 | private 50 | 51 | # call an API method with optional input parameters. 52 | def call(method, params = nil) 53 | url = @url + "/" + method 54 | header = { "Content-Type" => "application/json" } 55 | 56 | if @auth_token 57 | header["Authorization"] = "Bearer #{@auth_token}" 58 | end 59 | 60 | res = Net::HTTP.post URI(url), params.to_json, header 61 | status = res.code.to_i 62 | 63 | if status >= 400 64 | begin 65 | body = JSON.parse(res.body) 66 | rescue 67 | raise Error.new(status) 68 | end 69 | raise Error.new(status, body["type"], body["message"]) 70 | end 71 | 72 | res.body 73 | end 74 | ` 75 | 76 | // Generate writes the Ruby client implementations to w. 77 | func Generate(w io.Writer, s *schema.Schema, moduleName, className string) error { 78 | out := fmt.Fprintf 79 | 80 | out(w, module, moduleName, className) 81 | 82 | for _, m := range s.Methods { 83 | // comment 84 | out(w, "\n") 85 | out(w, " # %s\n", capitalize(m.Description)) 86 | if len(m.Inputs) > 0 { 87 | out(w, " #\n") 88 | out(w, " # @param [Hash] params the input for this method.\n") 89 | for _, f := range m.Inputs { 90 | out(w, " # @param params [%s] :%s %s\n", rubyType(s, f), f.Name, capitalize(f.Description)) 91 | } 92 | } 93 | 94 | // method 95 | out(w, " def %s", m.Name) 96 | 97 | // input arg 98 | if len(m.Inputs) > 0 { 99 | out(w, "(params)") 100 | } 101 | out(w, "\n") 102 | 103 | // return 104 | if len(m.Inputs) > 0 { 105 | out(w, " call %q, params\n", m.Name) 106 | } else { 107 | out(w, " call %q\n", m.Name) 108 | } 109 | 110 | // close 111 | out(w, " end\n") 112 | } 113 | 114 | out(w, "%s", call) 115 | out(w, " end\n") 116 | out(w, "end\n") 117 | 118 | return nil 119 | } 120 | 121 | // capitalize returns a capitalized string. 122 | func capitalize(s string) string { 123 | return strings.ToUpper(string(s[0])) + string(s[1:]) 124 | } 125 | 126 | // rubyType returns a Ruby equivalent type for field f. 127 | func rubyType(s *schema.Schema, f schema.Field) string { 128 | // TODO: handle reference types, not sure if makes sense 129 | // to generate classes for Ruby inputs or not 130 | switch f.Type.Type { 131 | case schema.String: 132 | return "String" 133 | case schema.Int, schema.Float: 134 | return "Number" 135 | case schema.Bool: 136 | return "Boolean" 137 | case schema.Timestamp: 138 | return "Date" 139 | case schema.Object: 140 | return "Hash" 141 | case schema.Array: 142 | return "Array" 143 | default: 144 | return "" 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /generators/dotnetclient/dotnetclient.go: -------------------------------------------------------------------------------- 1 | package dotnetclient 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | 7 | "github.com/apex/rpc/internal/format" 8 | "github.com/apex/rpc/schema" 9 | ) 10 | 11 | var namespace = `using System; 12 | using System.Collections.Generic; 13 | using System.Net.Http; 14 | using System.Threading.Tasks; 15 | using Newtonsoft.Json; 16 | 17 | namespace %s 18 | { 19 | public class %s 20 | { 21 | class ApexLogsException : Exception 22 | { 23 | public ApexLogsException(int status) : base($"{status} response") 24 | { } 25 | 26 | public ApexLogsException(int status, string type, string message) 27 | : base($"{status} response: ${type}: {message}") 28 | { } 29 | } 30 | 31 | private readonly string _url; 32 | private readonly string _authToken; 33 | private readonly HttpClient _httpClient; 34 | 35 | public Client(HttpClient httpClient, string url, string authToken) 36 | { 37 | _httpClient = httpClient; 38 | _url = url; 39 | _authToken = authToken; 40 | } 41 | ` 42 | 43 | var call = ` 44 | public async Task Call(string method, object parameters = null) 45 | { 46 | var url = $"{_url}/{method}"; 47 | var message = new HttpRequestMessage 48 | { 49 | Method = HttpMethod.Post, 50 | RequestUri = new Uri(url) 51 | }; 52 | message.Headers.Add("Content-Type", "application/json"); 53 | if (!string.IsNullOrWhiteSpace(_authToken)) 54 | message.Headers.Add("Authorization", $"Bearer {_authToken}"); 55 | 56 | if (parameters != null) 57 | message.Content = new StringContent(JsonConvert.SerializeObject(parameters)); 58 | 59 | var response = await _httpClient.SendAsync(message); 60 | var statusCode = (int) response.StatusCode; 61 | var content = await response.Content.ReadAsStringAsync(); 62 | 63 | if (statusCode < 300) return content; 64 | 65 | var body = JsonConvert.DeserializeObject>(content) 66 | ?? throw new ApexLogsException(statusCode); 67 | 68 | throw new ApexLogsException(statusCode, body["type"], body["message"]); 69 | } 70 | ` 71 | 72 | var closeNamespace = ` } 73 | } 74 | ` 75 | 76 | // Generate writes the Dotnet client implementations to w. 77 | func Generate(w io.Writer, s *schema.Schema, namespaceName, className string) error { 78 | out := fmt.Fprintf 79 | 80 | out(w, namespace, namespaceName, className) 81 | 82 | var indentDeclaration = " " 83 | var indentContent = " " 84 | for _, m := range s.Methods { 85 | var name = format.GoName(m.Name) 86 | // comment 87 | out(w, "\n") 88 | out(w, "%s/// %s\n", indentDeclaration, m.Description) 89 | 90 | ///TODO: add parameters description 91 | 92 | // outputs 93 | if len(m.Outputs) > 0 { 94 | out(w, "%spublic async Task<%sOutput> %s(", indentDeclaration, name, name) 95 | } else { 96 | out(w, "%spublic async Task %s(", indentDeclaration, name) 97 | } 98 | 99 | // inputs 100 | if len(m.Inputs) > 0 { 101 | out(w, "%sInput parameter)\n", name) 102 | } else { 103 | out(w, ")\n") 104 | } 105 | out(w, "%s{\n", indentDeclaration) 106 | 107 | // return 108 | if len(m.Outputs) > 0 { 109 | out(w, "%svar res = ", indentContent) 110 | // call 111 | if len(m.Inputs) > 0 { 112 | out(w, "await Call(\"%s\", parameter", m.Name) 113 | } else { 114 | out(w, "await Call(\"%s\"", m.Name) 115 | } 116 | out(w, ");\n") 117 | 118 | out(w, "%svar output = JsonConvert.DeserializeObject<%sOutput>(res);\n", indentContent, name) 119 | out(w, "%sreturn output;\n", indentContent) 120 | } else { 121 | if len(m.Inputs) > 0 { 122 | out(w, "%sawait Call(\"%s\", parameter);\n", indentContent, m.Name) 123 | } else { 124 | out(w, "%sawait Call(\"%s\");\n", indentContent, m.Name) 125 | } 126 | } 127 | 128 | out(w, "%s}\n", indentDeclaration) 129 | } 130 | 131 | out(w, call) 132 | out(w, closeNamespace) 133 | 134 | return nil 135 | } 136 | -------------------------------------------------------------------------------- /generators/goserver/goserver.go: -------------------------------------------------------------------------------- 1 | package goserver 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | 7 | "github.com/apex/rpc/internal/format" 8 | "github.com/apex/rpc/schema" 9 | ) 10 | 11 | // Generate writes the Go server implementations to w. 12 | func Generate(w io.Writer, s *schema.Schema, tracing bool, types string) error { 13 | // router 14 | err := writeRouter(w, s, types) 15 | if err != nil { 16 | return fmt.Errorf("writing router: %w", err) 17 | } 18 | 19 | // method stubs 20 | err = writeMethods(w, s, tracing, types) 21 | if err != nil { 22 | return fmt.Errorf("writing methods: %w", err) 23 | } 24 | 25 | return nil 26 | } 27 | 28 | // writeRouter writes the routing implementation to w. 29 | func writeRouter(w io.Writer, s *schema.Schema, types string) error { 30 | out := fmt.Fprintf 31 | out(w, "// ServeHTTP implementation.\n") 32 | out(w, "func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {\n") 33 | out(w, " if r.Method == \"GET\" {\n") 34 | out(w, " switch r.URL.Path {\n") 35 | out(w, " case \"/_health\":\n") 36 | out(w, " rpc.WriteHealth(w, s)\n") 37 | out(w, " default:\n") 38 | out(w, " rpc.WriteError(w, rpc.BadRequest(\"Invalid method\"))\n") 39 | out(w, " }\n") 40 | out(w, " return\n") 41 | out(w, " }\n\n") 42 | out(w, " if r.Method == \"POST\" {\n") 43 | out(w, " ctx := rpc.NewRequestContext(r.Context(), r)\n") 44 | out(w, " var res interface{}\n") 45 | out(w, " var err error\n") 46 | out(w, " switch r.URL.Path {\n") 47 | for _, m := range s.Methods { 48 | out(w, " case \"/%s\":\n", m.Name) 49 | // parse input 50 | if len(m.Inputs) > 0 { 51 | out(w, " var in %s\n", format.GoInputType(types, m.Name)) 52 | out(w, " err = rpc.ReadRequest(r, &in)\n") 53 | out(w, " if err != nil {\n") 54 | out(w, " break\n") 55 | out(w, " }\n") 56 | out(w, " res, err = s.%s(ctx, in)\n", format.JsName(m.Name)) 57 | } else { 58 | out(w, " res, err = s.%s(ctx)\n", format.JsName(m.Name)) 59 | } 60 | } 61 | out(w, " default:\n") 62 | out(w, " err = rpc.BadRequest(\"Invalid method\")\n") 63 | out(w, " }\n") 64 | out(w, "\n") 65 | out(w, " if err != nil {\n") 66 | out(w, " rpc.WriteError(w, err)\n") 67 | out(w, " return\n") 68 | out(w, " }\n") 69 | out(w, "\n") 70 | out(w, " rpc.WriteResponse(w, res)\n") 71 | out(w, " return\n") 72 | out(w, " }\n") 73 | out(w, "}\n") 74 | return nil 75 | } 76 | 77 | // writeMethods writes method stubs to w. 78 | func writeMethods(w io.Writer, s *schema.Schema, tracing bool, types string) error { 79 | out := fmt.Fprintf 80 | 81 | for _, m := range s.Methods { 82 | out(w, "\n") 83 | out(w, "// %s %s\n", format.JsName(m.Name), m.Description) 84 | 85 | // method signature 86 | if len(m.Inputs) > 0 { 87 | out(w, "func (s *Server) %s(ctx context.Context, in %s) (interface{}, error) {\n", format.JsName(m.Name), format.GoInputType(types, m.Name)) 88 | } else { 89 | out(w, "func (s *Server) %s(ctx context.Context) (interface{}, error) {\n", format.JsName(m.Name)) 90 | } 91 | 92 | // tracing 93 | if tracing { 94 | out(w, " logs := log.FromContext(ctx).WithField(\"method\", %q)\n\n", m.Name) 95 | 96 | if len(m.Inputs) > 0 { 97 | out(w, " logs = logs.WithFields(log.Fields{\n") 98 | for _, f := range m.Inputs { 99 | out(w, " %q: in.%s,\n", f.Name, format.GoName(f.Name)) 100 | } 101 | out(w, " })\n") 102 | } 103 | out(w, "\n") 104 | } 105 | 106 | // invoke method 107 | if len(m.Outputs) > 0 { 108 | out(w, " res, err := s.%s", format.GoName(m.Name)) 109 | } else { 110 | out(w, " err := s.%s", format.GoName(m.Name)) 111 | } 112 | 113 | if tracing { 114 | if len(m.Inputs) > 0 { 115 | out(w, "(log.NewContext(ctx, logs), in)\n") 116 | } else { 117 | out(w, "(log.NewContext(ctx, logs))\n") 118 | } 119 | } else { 120 | if len(m.Inputs) > 0 { 121 | out(w, "(ctx, in)\n") 122 | } else { 123 | out(w, "(ctx)\n") 124 | } 125 | } 126 | 127 | if len(m.Outputs) > 0 { 128 | out(w, " return res, err\n") 129 | } else { 130 | out(w, " return nil, err\n") 131 | } 132 | 133 | out(w, "}\n") 134 | } 135 | out(w, "\n") 136 | 137 | return nil 138 | } 139 | -------------------------------------------------------------------------------- /generators/tsclient/tsclient.go: -------------------------------------------------------------------------------- 1 | package tsclient 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | 7 | "github.com/apex/rpc/internal/format" 8 | "github.com/apex/rpc/schema" 9 | ) 10 | 11 | var require = ` 12 | // fetch for Node 13 | const fetch = (typeof window == 'undefined' || window.fetch == null) 14 | // @ts-ignore 15 | ? require('%s') 16 | : window.fetch 17 | ` 18 | 19 | var call = `/** 20 | * ClientError is an API client error providing the HTTP status code and error type. 21 | */ 22 | 23 | class ClientError extends Error { 24 | status: number; 25 | type?: string; 26 | 27 | constructor(status: number, message?: string, type?: string) { 28 | super(message) 29 | this.status = status 30 | this.type = type 31 | } 32 | } 33 | 34 | /** 35 | * Call method with params via a POST request. 36 | */ 37 | 38 | async function call(url: string, method: string, authToken?: string, params?: any): Promise { 39 | const headers: Record = { 40 | 'Content-Type': 'application/json' 41 | } 42 | 43 | if (authToken != null) { 44 | headers['Authorization'] = ` + "`Bearer ${authToken}`" + ` 45 | } 46 | 47 | const res = await fetch(url + '/' + method, { 48 | method: 'POST', 49 | body: JSON.stringify(params), 50 | headers 51 | }) 52 | 53 | // we have an error, try to parse a well-formed json 54 | // error response, otherwise default to status code 55 | if (res.status >= 300) { 56 | let err 57 | try { 58 | const { type, message } = await res.json() 59 | err = new ClientError(res.status, message, type) 60 | } catch { 61 | err = new ClientError(res.status, res.statusText) 62 | } 63 | throw err 64 | } 65 | 66 | return res.text() 67 | }` 68 | 69 | // Generate writes the TS client implementations to w. 70 | func Generate(w io.Writer, s *schema.Schema, fetchLibrary string) error { 71 | out := fmt.Fprintf 72 | 73 | out(w, require, fetchLibrary) 74 | out(w, "\n%s\n", call) 75 | out(w, "\n\n") 76 | out(w, `const reISO8601 = /(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))/`) 77 | out(w, "\n\n") 78 | out(w, "/**\n") 79 | out(w, " * Client is the API client.\n") 80 | out(w, " */\n") 81 | out(w, "\n") 82 | out(w, "export class Client {\n") 83 | out(w, "\n") 84 | out(w, " private url: string\n") 85 | out(w, " private authToken?: string\n") 86 | out(w, "\n") 87 | out(w, " /**\n") 88 | out(w, " * Initialize.\n") 89 | out(w, " */\n") 90 | out(w, "\n") 91 | out(w, " constructor(params: { url: string, authToken?: string }) {\n") 92 | out(w, " this.url = params.url\n") 93 | out(w, " this.authToken = params.authToken\n") 94 | out(w, " }\n") 95 | out(w, "\n") 96 | out(w, " /**\n") 97 | out(w, " * Decoder is used as the reviver parameter when decoding responses.\n") 98 | out(w, " */\n") 99 | out(w, "\n") 100 | out(w, " private decoder(key: any, value: any) {\n") 101 | out(w, " return typeof value == 'string' && reISO8601.test(value)\n") 102 | out(w, " ? new Date(value)\n") 103 | out(w, " : value\n") 104 | out(w, " }\n") 105 | out(w, "\n") 106 | 107 | // methods 108 | for _, m := range s.Methods { 109 | name := format.JsName(m.Name) 110 | out(w, " /**\n") 111 | out(w, " * %s: %s\n", name, m.Description) 112 | out(w, " */\n\n") 113 | 114 | // input 115 | if len(m.Inputs) > 0 { 116 | out(w, " async %s(params: %sInput)", name, format.GoName(m.Name)) 117 | } else { 118 | out(w, " async %s()", name) 119 | } 120 | 121 | // output 122 | if len(m.Outputs) > 0 { 123 | out(w, ": Promise<%sOutput> {\n", format.GoName(m.Name)) 124 | } else { 125 | out(w, " {\n") 126 | } 127 | 128 | // return 129 | if len(m.Outputs) > 0 { 130 | out(w, " let res = ") 131 | // call 132 | if len(m.Inputs) > 0 { 133 | out(w, "await call(this.url, '%s', this.authToken, params)\n", m.Name) 134 | } else { 135 | out(w, "await call(this.url, '%s', this.authToken)\n", m.Name) 136 | } 137 | out(w, " let out: %sOutput = JSON.parse(res, this.decoder)\n", format.GoName(m.Name)) 138 | out(w, " return out\n") 139 | } else { 140 | // call 141 | if len(m.Inputs) > 0 { 142 | out(w, " await call(this.url, '%s', this.authToken, params)\n", m.Name) 143 | } else { 144 | out(w, " await call(this.url, '%s', this.authToken)\n", m.Name) 145 | } 146 | } 147 | 148 | out(w, " }\n\n") 149 | } 150 | 151 | out(w, "}\n") 152 | 153 | return nil 154 | } 155 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 2 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= 3 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= 8 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 9 | github.com/gookit/color v1.2.0 h1:lHA77Kuyi5JpBnA9ESvwkY+nanLjRZ0mHbWQXRYk2Lk= 10 | github.com/gookit/color v1.2.0/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg= 11 | github.com/gookit/color v1.2.6 h1:f6/ehoHPXwi2tuntjpBRhpBhFLL9YjrnB2m6RWsbCRg= 12 | github.com/gookit/color v1.2.6/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg= 13 | github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334 h1:VHgatEHNcBFEB7inlalqfNqw65aNkM1lGX2yt3NmbS8= 14 | github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE= 15 | github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= 16 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 17 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 18 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 19 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 20 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 21 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 22 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 23 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 24 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= 25 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 26 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 27 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 28 | github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= 29 | github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= 30 | github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= 31 | github.com/shibukawa/cdiff v0.1.3 h1:0ren00CxjQKvP0IqS1aVDZ/eFIcLXNZ9cmru22t6CTU= 32 | github.com/shibukawa/cdiff v0.1.3/go.mod h1:7ewfFiaynzVpGSV03BbT2IsthIWQRPG2ejUVs9AWkCA= 33 | github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= 34 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 35 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 36 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 37 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 38 | github.com/tj/assert v0.0.0-20190920132354-ee03d75cd160 h1:NSWpaDaurcAJY7PkL8Xt0PhZE7qpvbZl5ljd8r6U0bI= 39 | github.com/tj/assert v0.0.0-20190920132354-ee03d75cd160/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0= 40 | github.com/tj/go-fixture v1.0.0 h1:xrAwTwazaUmGrZI8gF3OfJRy6gL1uXsT+DcRHwcjG5M= 41 | github.com/tj/go-fixture v1.0.0/go.mod h1:dBFV0p1KZisXt+gTEqF/rEJ7GP6LoeBgML1ODuNT5v4= 42 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= 43 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= 44 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= 45 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= 46 | github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= 47 | github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= 48 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 49 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 50 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 51 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 52 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 53 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 54 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 55 | gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= 56 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 57 | -------------------------------------------------------------------------------- /generators/elmclient/elmclient.go: -------------------------------------------------------------------------------- 1 | package elmclient 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "strings" 7 | 8 | "github.com/apex/rpc/internal/format" 9 | "github.com/apex/rpc/internal/schemautil" 10 | "github.com/apex/rpc/schema" 11 | ) 12 | 13 | // TODO: imports, exports, encoders, decoders, request functions, formatting, optional fields with Maybe 14 | 15 | var module = ` 16 | -- Do not edit, this file was generated by github.com/apex/rpc. 17 | 18 | ` 19 | 20 | // Generate writes the Elm client implementations to w. 21 | func Generate(w io.Writer, s *schema.Schema) error { 22 | fmt.Fprintf(w, module) 23 | generateTypes(w, s) 24 | generateMethodTypes(w, s) 25 | generateMethodFuncs(w, s) 26 | generateDecoderFuncs(w, s) 27 | return nil 28 | } 29 | 30 | // generateTypes writes types to w. 31 | func generateTypes(w io.Writer, s *schema.Schema) { 32 | out := fmt.Fprintf 33 | out(w, "-- TYPES\n\n") 34 | for _, t := range s.TypesSlice() { 35 | name := format.GoName(t.Name) 36 | out(w, "{-| %s %s -}\n", name, t.Description) 37 | out(w, "type alias %s =\n", name) 38 | writeFields(w, s, t.Properties) 39 | out(w, "\n\n") 40 | } 41 | } 42 | 43 | // generateMethodTypes writes method types to w. 44 | func generateMethodTypes(w io.Writer, s *schema.Schema) { 45 | out := fmt.Fprintf 46 | out(w, "-- METHOD PARAMS\n\n") 47 | for _, m := range s.Methods { 48 | name := format.GoName(m.Name) 49 | 50 | if len(m.Inputs) > 0 { 51 | out(w, "{-| %sInput params. -}\n", name) 52 | out(w, "type alias %sInput =\n", name) 53 | writeFields(w, s, m.Inputs) 54 | out(w, "\n\n") 55 | } 56 | 57 | if len(m.Outputs) > 0 { 58 | out(w, "{-| %sOutput params. -}\n", name) 59 | out(w, "type alias %sOutput =\n", name) 60 | writeFields(w, s, m.Outputs) 61 | out(w, "\n\n") 62 | } 63 | } 64 | } 65 | 66 | // generateMethodFuncs writes method functions to w. 67 | func generateMethodFuncs(w io.Writer, s *schema.Schema) { 68 | out := fmt.Fprintf 69 | out(w, "-- METHODS\n\n") 70 | for _, m := range s.Methods { 71 | name := format.JsName(m.Name) 72 | out(w, "%s : %sInput \n", name, format.GoName(m.Name)) 73 | out(w, "%s = \n ...", name) 74 | out(w, "\n\n") 75 | } 76 | } 77 | 78 | // generateDecoderFuncs writes json decoder functions to w. 79 | func generateDecoderFuncs(w io.Writer, s *schema.Schema) { 80 | out := fmt.Fprintf 81 | out(w, "-- DECODERS\n\n") 82 | for _, t := range s.TypesSlice() { 83 | fname := format.JsName(t.Name) + "Decoder" 84 | tname := format.GoName(t.Name) 85 | writeDecoderFunc(w, s, fname, tname, t.Properties) 86 | } 87 | 88 | for _, m := range s.Methods { 89 | if len(m.Inputs) > 0 { 90 | fname := format.JsName(m.Name) + "InputDecoder" 91 | tname := format.GoName(m.Name) + "Input" 92 | writeDecoderFunc(w, s, fname, tname, m.Inputs) 93 | } 94 | 95 | if len(m.Outputs) > 0 { 96 | fname := format.JsName(m.Name) + "OutputDecoder" 97 | tname := format.GoName(m.Name) + "Output" 98 | writeDecoderFunc(w, s, fname, tname, m.Outputs) 99 | } 100 | } 101 | } 102 | 103 | // writeDecoderFunc to writer. 104 | func writeDecoderFunc(w io.Writer, s *schema.Schema, funcName, typeName string, fields []schema.Field) { 105 | out := fmt.Fprintf 106 | out(w, "%s : Decoder %s\n", funcName, typeName) 107 | out(w, "%s =\n", funcName) 108 | out(w, " Decode.success %s\n", typeName) 109 | writeDecoderFields(w, s, fields) 110 | out(w, "\n\n") 111 | } 112 | 113 | // writeDecoderFields to writer. 114 | func writeDecoderFields(w io.Writer, s *schema.Schema, fields []schema.Field) { 115 | for _, f := range fields { 116 | // TODO: handle optional 117 | fmt.Fprintf(w, " |> required %q %s\n", f.Name, elmDecoderType(s, f)) 118 | } 119 | } 120 | 121 | // writeFields to writer. 122 | func writeFields(w io.Writer, s *schema.Schema, fields []schema.Field) { 123 | out := fmt.Fprintf 124 | out(w, " {") 125 | for i, f := range fields { 126 | out(w, " ") 127 | if i > 0 { 128 | out(w, " , ") 129 | } 130 | writeField(w, s, f) 131 | } 132 | out(w, " }") 133 | } 134 | 135 | // writeField to writer. 136 | func writeField(w io.Writer, s *schema.Schema, f schema.Field) { 137 | fmt.Fprintf(w, "%s : %s\n", format.JsName(f.Name), elmType(s, f)) 138 | } 139 | 140 | // capitalize returns a capitalized string. 141 | func capitalize(s string) string { 142 | return strings.ToUpper(string(s[0])) + string(s[1:]) 143 | } 144 | 145 | // elmDecoderType returns an Elm decoder for field f. 146 | func elmDecoderType(s *schema.Schema, f schema.Field) string { 147 | // ref 148 | if ref := f.Type.Ref.Value; ref != "" { 149 | t := schemautil.ResolveRef(s, f.Type.Ref) 150 | return format.JsName(t.Name) + "Decoder" 151 | } 152 | 153 | // TODO: decide on import, prefix these 154 | 155 | // type 156 | switch f.Type.Type { 157 | case schema.String: 158 | return "string" 159 | case schema.Int: 160 | return "int" 161 | case schema.Float: 162 | return "float" 163 | case schema.Bool: 164 | return "bool" 165 | case schema.Timestamp: 166 | return "string" // TODO: handle dates 167 | case schema.Object: 168 | return "object" // TODO: handle Dicts 169 | case schema.Array: 170 | return "(list " + elmDecoderType(s, schema.Field{ 171 | Type: schema.TypeObject(f.Items), 172 | }) + ")" 173 | default: 174 | panic("unhandled type") 175 | } 176 | } 177 | 178 | // elmType returns a Elm equivalent type for field f. 179 | func elmType(s *schema.Schema, f schema.Field) string { 180 | // ref 181 | if ref := f.Type.Ref.Value; ref != "" { 182 | t := schemautil.ResolveRef(s, f.Type.Ref) 183 | return format.GoName(t.Name) 184 | } 185 | 186 | // type 187 | switch f.Type.Type { 188 | case schema.String: 189 | return "String" 190 | case schema.Int: 191 | return "Int" 192 | case schema.Float: 193 | return "Float" 194 | case schema.Bool: 195 | return "Bool" 196 | case schema.Timestamp: 197 | return "String" // TODO: handle dates 198 | case schema.Object: 199 | return "object" // TODO: handle Dicts 200 | case schema.Array: 201 | return "List " + elmType(s, schema.Field{ 202 | Type: schema.TypeObject(f.Items), 203 | }) 204 | default: 205 | panic("unhandled type") 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /schema/schema.go: -------------------------------------------------------------------------------- 1 | //go:generate file2go -in schema.json -pkg schema 2 | 3 | // Package schema provides the Apex RPC schema. 4 | package schema 5 | 6 | import ( 7 | "encoding/json" 8 | "fmt" 9 | "os" 10 | "sort" 11 | 12 | "github.com/xeipuuv/gojsonschema" 13 | ) 14 | 15 | // ValidationError is a validation error. 16 | type ValidationError struct { 17 | Result *gojsonschema.Result 18 | } 19 | 20 | // Error implementation. 21 | func (e ValidationError) Error() (s string) { 22 | s = "validation failed:\n" 23 | for _, e := range e.Result.Errors() { 24 | s += fmt.Sprintf(" - %s\n", e) 25 | } 26 | return 27 | } 28 | 29 | // Kind is a value type. 30 | type Kind string 31 | 32 | // Types available. 33 | const ( 34 | String Kind = "string" 35 | Bool Kind = "boolean" 36 | Int Kind = "integer" 37 | Float Kind = "float" 38 | Array Kind = "array" 39 | Object Kind = "object" 40 | Timestamp Kind = "timestamp" 41 | ) 42 | 43 | // Ref model. 44 | type Ref struct { 45 | Value string `json:"$ref"` 46 | } 47 | 48 | // TypeObject model. 49 | type TypeObject struct { 50 | Type Kind `json:"type"` 51 | Ref 52 | } 53 | 54 | // UnmarshalJSON implementation. 55 | func (t *TypeObject) UnmarshalJSON(b []byte) error { 56 | // "type": { "$ref": ... } 57 | if b[0] == '{' { 58 | var r Ref 59 | if err := json.Unmarshal(b, &r); err != nil { 60 | return err 61 | } 62 | t.Ref = r 63 | return nil 64 | } 65 | 66 | // "type": "integer" 67 | var s string 68 | if err := json.Unmarshal(b, &s); err != nil { 69 | return err 70 | } 71 | 72 | t.Type = Kind(s) 73 | return nil 74 | } 75 | 76 | // ItemsObject model. 77 | type ItemsObject struct { 78 | Type Kind `json:"type"` 79 | Ref 80 | } 81 | 82 | // Schema model. 83 | type Schema struct { 84 | Name string `json:"name"` 85 | Version string `json:"version"` 86 | Description string `json:"description"` 87 | Methods []Method `json:"methods"` 88 | Groups []Group `json:"groups"` 89 | Types map[string]Type `json:"types"` 90 | Go struct { 91 | Tags []string `json:"tags"` 92 | } `json:"go"` 93 | } 94 | 95 | // Method model. 96 | type Method struct { 97 | Name string `json:"name"` 98 | Description string `json:"description"` 99 | Private bool `json:"private"` 100 | Group string `json:"group"` 101 | Inputs []Field `json:"inputs"` 102 | Outputs []Field `json:"outputs"` 103 | Examples []MethodExample `json:"examples"` 104 | } 105 | 106 | // MethodExample model. 107 | type MethodExample struct { 108 | Name string `json:"name"` 109 | Description string `json:"description"` 110 | Input interface{} `json:"input"` 111 | Output interface{} `json:"output"` 112 | } 113 | 114 | // Field model. 115 | type Field struct { 116 | Name string `json:"name"` 117 | Description string `json:"description"` 118 | Required bool `json:"required"` 119 | ReadOnly bool `json:"readonly"` 120 | Default interface{} `json:"default"` 121 | Type TypeObject `json:"type"` 122 | Items ItemsObject `json:"items"` 123 | Enum []string `json:"enum"` 124 | } 125 | 126 | // Type model. 127 | type Type struct { 128 | Name string `json:"name"` 129 | Description string `json:"description"` 130 | Private bool `json:"private"` 131 | Properties []Field `json:"properties"` 132 | Examples []Example `json:"examples"` 133 | } 134 | 135 | // Example model. 136 | type Example struct { 137 | Description string `json:"description"` 138 | Value interface{} `json:"value"` 139 | } 140 | 141 | // Group model. 142 | type Group struct { 143 | Name string `json:"name"` 144 | Description string `json:"description"` 145 | Summary string `json:"summary"` 146 | } 147 | 148 | // TypesSlice returns a sorted slice of types. 149 | func (s Schema) TypesSlice() (v []Type) { 150 | for _, t := range s.Types { 151 | // sort fields 152 | sort.Slice(t.Properties, func(i, j int) bool { 153 | a := t.Properties[i] 154 | b := t.Properties[j] 155 | return a.Name < b.Name 156 | }) 157 | 158 | v = append(v, t) 159 | } 160 | 161 | // sort types 162 | sort.Slice(v, func(i, j int) bool { 163 | return v[i].Name < v[j].Name 164 | }) 165 | 166 | return 167 | } 168 | 169 | // Load returns a schema loaded and validated from path. 170 | func Load(path string) (*Schema, error) { 171 | // TODO: bake into the binary with Go's native 'embed' stuff once it's available 172 | schema := gojsonschema.NewBytesLoader(SchemaJson) 173 | doc := gojsonschema.NewReferenceLoader("file://" + path) 174 | 175 | // validate 176 | result, err := gojsonschema.Validate(schema, doc) 177 | if err != nil { 178 | return nil, err 179 | } 180 | 181 | if !result.Valid() { 182 | return nil, &ValidationError{ 183 | Result: result, 184 | } 185 | } 186 | 187 | var s Schema 188 | 189 | // open 190 | f, err := os.Open(path) 191 | if err != nil { 192 | return nil, err 193 | } 194 | defer f.Close() 195 | 196 | // unmarshal 197 | err = json.NewDecoder(f).Decode(&s) 198 | if err != nil { 199 | return nil, err 200 | } 201 | 202 | // populate type names 203 | for k, v := range s.Types { 204 | v.Name = k 205 | s.Types[k] = v 206 | } 207 | 208 | // sort groups 209 | sort.Slice(s.Groups, func(i, j int) bool { 210 | a := s.Groups[i] 211 | b := s.Groups[j] 212 | return a.Name < b.Name 213 | }) 214 | 215 | // sort methods 216 | sort.Slice(s.Methods, func(i, j int) bool { 217 | a := s.Methods[i] 218 | b := s.Methods[j] 219 | return a.Name < b.Name 220 | }) 221 | 222 | // sort method inputs & outputs 223 | for _, m := range s.Methods { 224 | sort.Slice(m.Inputs, func(i, j int) bool { 225 | a := m.Inputs[i] 226 | b := m.Inputs[j] 227 | return a.Name < b.Name 228 | }) 229 | 230 | sort.Slice(m.Outputs, func(i, j int) bool { 231 | a := m.Outputs[i] 232 | b := m.Outputs[j] 233 | return a.Name < b.Name 234 | }) 235 | } 236 | 237 | return &s, nil 238 | } 239 | 240 | // IsBuiltin returns true if the type is built-in. 241 | func IsBuiltin(kind Kind) bool { 242 | switch kind { 243 | case String, Int, Bool, Float, Array, Object, Timestamp: 244 | return true 245 | default: 246 | return false 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /schema/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$id": "https://example.com/rpc.schema.json", 3 | "$schema": "http://json-schema.org/draft-07/schema#", 4 | "title": "rpc", 5 | "type": "object", 6 | "required": [ 7 | "name", 8 | "version", 9 | "methods" 10 | ], 11 | "additionalProperties": true, 12 | "properties": { 13 | "name": { 14 | "type": "string", 15 | "description": "The name of the API." 16 | }, 17 | "version": { 18 | "type": "string", 19 | "description": "The version of the API." 20 | }, 21 | "description": { 22 | "description": "The description of the API", 23 | "type": "string" 24 | }, 25 | "methods": { 26 | "description": "The methods provided by the API.", 27 | "type": "array", 28 | "items": { 29 | "$ref": "#/definitions/methodObject" 30 | } 31 | }, 32 | "types": { 33 | "description": "Custom type definitions.", 34 | "patternProperties": { 35 | "[0-z]+": { 36 | "$ref": "#/definitions/typeObject" 37 | } 38 | } 39 | } 40 | }, 41 | "definitions": { 42 | "primitives": { 43 | "enum": [ 44 | "array", 45 | "boolean", 46 | "float", 47 | "integer", 48 | "object", 49 | "string", 50 | "timestamp" 51 | ] 52 | }, 53 | "typeObject": { 54 | "type": "object", 55 | "required": [ 56 | "properties" 57 | ], 58 | "additionalProperties": true, 59 | "properties": { 60 | "name": { 61 | "description": "The type name.", 62 | "type": "string" 63 | }, 64 | "description": { 65 | "description": "The type description.", 66 | "type": "string" 67 | }, 68 | "properties": { 69 | "description": "The property definitions.", 70 | "type": "array", 71 | "items": { 72 | "$ref": "#/definitions/fieldObject" 73 | } 74 | }, 75 | "examples": { 76 | "description": "The example definitions.", 77 | "type": "array", 78 | "items": { 79 | "$ref": "#/definitions/exampleObject" 80 | } 81 | } 82 | } 83 | }, 84 | "methodObject": { 85 | "type": "object", 86 | "required": [ 87 | "name", 88 | "description" 89 | ], 90 | "additionalProperties": true, 91 | "properties": { 92 | "name": { 93 | "description": "The method name.", 94 | "type": "string" 95 | }, 96 | "description": { 97 | "description": "The method description.", 98 | "type": "string" 99 | }, 100 | "inputs": { 101 | "description": "The method input parameters.", 102 | "type": "array", 103 | "items": { 104 | "$ref": "#/definitions/fieldObject" 105 | } 106 | }, 107 | "outputs": { 108 | "description": "The method output parameters.", 109 | "type": "array", 110 | "items": { 111 | "$ref": "#/definitions/fieldObject" 112 | } 113 | }, 114 | "since": { 115 | "description": "The API version that the method was introduced in.", 116 | "type": "string" 117 | }, 118 | "deprecated": { 119 | "description": "Whether or not the method is deprecated.", 120 | "type": "boolean" 121 | } 122 | } 123 | }, 124 | "fieldObject": { 125 | "type": "object", 126 | "required": [ 127 | "name", 128 | "type" 129 | ], 130 | "additionalProperties": true, 131 | "properties": { 132 | "name": { 133 | "description": "The field name.", 134 | "type": "string" 135 | }, 136 | "description": { 137 | "description": "The field description.", 138 | "type": "string" 139 | }, 140 | "type": { 141 | "description": "The field type.", 142 | "oneOf": [ 143 | { 144 | "$ref": "#/definitions/primitives" 145 | }, 146 | { 147 | "$ref": "#/definitions/referenceObject" 148 | } 149 | ] 150 | }, 151 | "enum": { 152 | "description": "An enumeration of possible values.", 153 | "type": "array", 154 | "items": { 155 | "type": "string" 156 | } 157 | }, 158 | "required": { 159 | "description": "Whether or not the field is required.", 160 | "type": "boolean" 161 | }, 162 | "items": { 163 | "description": "Array item definition.", 164 | "oneOf": [ 165 | { 166 | "$ref": "#/definitions/itemObject" 167 | }, 168 | { 169 | "$ref": "#/definitions/referenceObject" 170 | } 171 | ] 172 | }, 173 | "min": { 174 | "description": "The minimum value.", 175 | "type": "integer" 176 | }, 177 | "max": { 178 | "description": "The maximum value.", 179 | "type": "integer" 180 | }, 181 | "length": { 182 | "description": "The maximum length of a string.", 183 | "type": "integer" 184 | }, 185 | "default": { 186 | "description": "The default value." 187 | } 188 | } 189 | }, 190 | "referenceObject": { 191 | "type": "object", 192 | "additionalProperties": false, 193 | "required": [ 194 | "$ref" 195 | ], 196 | "properties": { 197 | "$ref": { 198 | "type": "string", 199 | "format": "uri-reference" 200 | } 201 | } 202 | }, 203 | "itemObject": { 204 | "type": "object", 205 | "additionalProperties": false, 206 | "required": [ 207 | "type" 208 | ], 209 | "properties": { 210 | "type": { 211 | "$ref": "#/definitions/primitives" 212 | } 213 | } 214 | }, 215 | "groupObject": { 216 | "type": "object", 217 | "additionalProperties": false, 218 | "required": [ 219 | "name", 220 | "summary", 221 | "description" 222 | ], 223 | "properties": { 224 | "name": { 225 | "type": "string" 226 | }, 227 | "summary": { 228 | "type": "string" 229 | }, 230 | "description": { 231 | "type": "string" 232 | } 233 | } 234 | }, 235 | "exampleObject": { 236 | "type": "object", 237 | "required": [ 238 | "description", 239 | "value" 240 | ], 241 | "properties": { 242 | "description": { 243 | "type": "string" 244 | }, 245 | "value": { 246 | 247 | } 248 | } 249 | }, 250 | "methodExampleObject": { 251 | "type": "object", 252 | "required": [ 253 | "input", 254 | "output" 255 | ], 256 | "properties": { 257 | "name": { 258 | "type": "string" 259 | }, 260 | "description": { 261 | "type": "string" 262 | }, 263 | "input": { 264 | 265 | }, 266 | "output": { 267 | 268 | } 269 | } 270 | } 271 | } 272 | } -------------------------------------------------------------------------------- /generators/gotypes/gotypes.go: -------------------------------------------------------------------------------- 1 | package gotypes 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/apex/rpc/internal/format" 10 | "github.com/apex/rpc/internal/schemautil" 11 | "github.com/apex/rpc/schema" 12 | ) 13 | 14 | var utils = `// oneOf returns true if s is in the values. 15 | func oneOf(s string, values []string) bool { 16 | for _, v := range values { 17 | if s == v { 18 | return true 19 | } 20 | } 21 | return false 22 | }` 23 | 24 | // Generate writes the Go type implementations to w, with optional validation methods. 25 | func Generate(w io.Writer, s *schema.Schema, validate bool) error { 26 | out := fmt.Fprintf 27 | 28 | // default tags 29 | if s.Go.Tags == nil { 30 | s.Go.Tags = []string{"json"} 31 | } 32 | 33 | // types 34 | for _, t := range s.TypesSlice() { 35 | out(w, "// %s %s\n", format.GoName(t.Name), t.Description) 36 | out(w, "type %s struct {\n", format.GoName(t.Name)) 37 | writeFields(w, s, t.Properties) 38 | out(w, "}\n\n") 39 | if validate { 40 | writeValidation(w, format.GoName(t.Name), t.Properties) 41 | out(w, "\n") 42 | } 43 | } 44 | 45 | // methods 46 | for _, m := range s.Methods { 47 | name := format.GoName(m.Name) 48 | 49 | // inputs 50 | if len(m.Inputs) > 0 { 51 | out(w, "// %sInput params.\n", name) 52 | out(w, "type %sInput struct {\n", name) 53 | writeFields(w, s, m.Inputs) 54 | out(w, "}\n") 55 | if validate { 56 | out(w, "\n") 57 | writeValidation(w, name+"Input", m.Inputs) 58 | } 59 | } 60 | 61 | // both 62 | if len(m.Inputs) > 0 && len(m.Outputs) > 0 { 63 | out(w, "\n") 64 | } 65 | 66 | // outputs 67 | if len(m.Outputs) > 0 { 68 | out(w, "// %sOutput params.\n", name) 69 | out(w, "type %sOutput struct {\n", name) 70 | writeFields(w, s, m.Outputs) 71 | out(w, "}\n") 72 | } 73 | 74 | out(w, "\n") 75 | } 76 | 77 | if validate { 78 | out(w, "\n%s\n", utils) 79 | } 80 | 81 | return nil 82 | } 83 | 84 | // writeFields to writer. 85 | func writeFields(w io.Writer, s *schema.Schema, fields []schema.Field) { 86 | for i, f := range fields { 87 | writeField(w, s, f) 88 | if i < len(fields)-1 { 89 | fmt.Fprintf(w, "\n") 90 | } 91 | } 92 | } 93 | 94 | // writeField to writer. 95 | func writeField(w io.Writer, s *schema.Schema, f schema.Field) { 96 | fmt.Fprintf(w, " // %s is %s%s\n", format.GoName(f.Name), f.Description, schemautil.FormatExtra(f)) 97 | fmt.Fprintf(w, " %s %s %s\n", format.GoName(f.Name), goType(s, f), fieldTags(f, s.Go.Tags)) 98 | } 99 | 100 | // goType returns a Go equivalent type for field f. 101 | func goType(s *schema.Schema, f schema.Field) string { 102 | // ref 103 | if ref := f.Type.Ref.Value; ref != "" { 104 | t := schemautil.ResolveRef(s, f.Type.Ref) 105 | return format.GoName(t.Name) 106 | } 107 | 108 | // type 109 | switch f.Type.Type { 110 | case schema.String: 111 | return "string" 112 | case schema.Int: 113 | return "int" 114 | case schema.Bool: 115 | return "bool" 116 | case schema.Float: 117 | return "float64" 118 | case schema.Timestamp: 119 | return "time.Time" 120 | case schema.Object: 121 | return "map[string]interface{}" 122 | case schema.Array: 123 | return "[]" + goType(s, schema.Field{ 124 | Type: schema.TypeObject(f.Items), 125 | }) 126 | default: 127 | panic("unhandled type") 128 | } 129 | } 130 | 131 | // fieldTags returns tags for a field. 132 | func fieldTags(f schema.Field, tags []string) string { 133 | var pairs [][]string 134 | 135 | for _, tag := range tags { 136 | pairs = append(pairs, []string{tag, f.Name}) 137 | } 138 | 139 | return formatTags(pairs) 140 | } 141 | 142 | // formatTags returns field tags. 143 | func formatTags(tags [][]string) string { 144 | var s []string 145 | for _, t := range tags { 146 | if len(t) == 2 { 147 | s = append(s, fmt.Sprintf("%s:%q", t[0], t[1])) 148 | } else { 149 | s = append(s, fmt.Sprintf("%s", t[0])) 150 | } 151 | } 152 | return fmt.Sprintf("`%s`", strings.Join(s, " ")) 153 | } 154 | 155 | // writeValidation writes a validation method implementation to w. 156 | func writeValidation(w io.Writer, name string, fields []schema.Field) error { 157 | out := fmt.Fprintf 158 | recv := strings.ToLower(name)[0] 159 | out(w, "// Validate implementation.\n") 160 | out(w, "func (%c *%s) Validate() error {\n", recv, name) 161 | for _, f := range fields { 162 | writeFieldDefaults(w, f, recv) 163 | writeFieldValidation(w, f, recv) 164 | } 165 | out(w, " return nil\n") 166 | out(w, "}\n") 167 | return nil 168 | } 169 | 170 | // writeFieldDefaults writes field defaults to w. 171 | func writeFieldDefaults(w io.Writer, f schema.Field, recv byte) error { 172 | // TODO: write out a separate Default() method? 173 | if f.Default == nil { 174 | return nil 175 | } 176 | 177 | out := fmt.Fprintf 178 | name := format.GoName(f.Name) 179 | 180 | switch f.Type.Type { 181 | case schema.Int: 182 | out(w, " if %c.%s == 0 {\n", recv, name) 183 | out(w, " %c.%s = %v\n", recv, name, f.Default) 184 | out(w, " }\n\n") 185 | case schema.String: 186 | out(w, " if %c.%s == \"\" {\n", recv, name) 187 | out(w, " %c.%s = %q\n", recv, name, f.Default) 188 | out(w, " }\n\n") 189 | } 190 | 191 | return nil 192 | } 193 | 194 | // writeFieldValidation writes field validation to w. 195 | func writeFieldValidation(w io.Writer, f schema.Field, recv byte) error { 196 | out := fmt.Fprintf 197 | name := format.GoName(f.Name) 198 | 199 | writeError := func(msg string) { 200 | out(w, " return rpc.ValidationError{ Field: %q, Message: %q }\n", f.Name, msg) 201 | } 202 | 203 | // required 204 | if f.Required { 205 | switch f.Type.Type { 206 | case schema.Int: 207 | out(w, " if %c.%s == 0 {\n", recv, name) 208 | writeError("is required") 209 | out(w, " }\n\n") 210 | case schema.String: 211 | out(w, " if %c.%s == \"\" {\n", recv, name) 212 | writeError("is required") 213 | out(w, " }\n\n") 214 | case schema.Array, schema.Object: 215 | out(w, " if %c.%s == nil {\n", recv, name) 216 | writeError("is required") 217 | out(w, " }\n\n") 218 | case schema.Timestamp: 219 | out(w, " if %c.%s.IsZero() {\n", recv, name) 220 | writeError("is required") 221 | out(w, " }\n\n") 222 | } 223 | } 224 | 225 | // enums 226 | if f.Type.Type == schema.String && f.Enum != nil { 227 | field := fmt.Sprintf("%c.%s", recv, name) 228 | out(w, " if %s != \"\" && !oneOf(%s, %s) {\n", field, field, formatSlice(f.Enum)) 229 | writeError(fmt.Sprintf("must be one of: %s", formatEnum(f.Enum))) 230 | out(w, " }\n\n") 231 | } 232 | 233 | // validate the children of non-primitive arrays 234 | // TODO: HasRef() or similar? 235 | if f.Type.Type == schema.Array && f.Items.Ref.Value != "" { 236 | out(w, " for i, v := range %c.%s {\n", recv, name) 237 | out(w, " if err := v.Validate(); err != nil {\n") 238 | out(w, " return fmt.Errorf(\"element %%d: %%s\", i, err.Error())\n") 239 | out(w, " }\n") 240 | out(w, " }\n\n") 241 | } 242 | 243 | return nil 244 | } 245 | 246 | // formatSlice returns a formatted slice from enum. 247 | func formatSlice(values []string) string { 248 | var vals []string 249 | for _, l := range values { 250 | vals = append(vals, strconv.Quote(l)) 251 | } 252 | return fmt.Sprintf("[]string{%s}", strings.Join(vals, ", ")) 253 | } 254 | 255 | // formatEnum returns a formatted enum values. 256 | func formatEnum(values []string) string { 257 | var vals []string 258 | for _, l := range values { 259 | vals = append(vals, strconv.Quote(l)) 260 | } 261 | return strings.Join(vals, ", ") 262 | } 263 | -------------------------------------------------------------------------------- /generators/mddocs/mddocs.go: -------------------------------------------------------------------------------- 1 | package mddocs 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | 11 | "github.com/apex/rpc/internal/format" 12 | "github.com/apex/rpc/internal/schemautil" 13 | "github.com/apex/rpc/schema" 14 | ) 15 | 16 | // Generate markdown documentation in the given dir. 17 | func Generate(s *schema.Schema, dir string) error { 18 | // types dir 19 | typesDir := filepath.Join(dir, "types") 20 | if err := os.MkdirAll(typesDir, 0755); err != nil { 21 | return err 22 | } 23 | 24 | // types index 25 | if err := generateTypesIndex(s.TypesSlice(), typesDir); err != nil { 26 | return fmt.Errorf("generating types index: %w", err) 27 | } 28 | 29 | // types 30 | for _, t := range s.Types { 31 | if err := generateType(t, typesDir); err != nil { 32 | return fmt.Errorf("generating type: %w", err) 33 | } 34 | } 35 | 36 | // methods dir 37 | methodsDir := filepath.Join(dir, "methods") 38 | if err := os.MkdirAll(methodsDir, 0755); err != nil { 39 | return err 40 | } 41 | 42 | // methods index 43 | if err := generateMethodsIndex(s, methodsDir); err != nil { 44 | return fmt.Errorf("generating methods index: %w", err) 45 | } 46 | 47 | // methods 48 | for _, m := range s.Methods { 49 | if err := generateMethod(m, methodsDir); err != nil { 50 | return fmt.Errorf("generating methods: %w", err) 51 | } 52 | } 53 | 54 | return nil 55 | } 56 | 57 | // generateTypesIndex generates type index documentation. 58 | func generateTypesIndex(types []schema.Type, dir string) error { 59 | path := filepath.Join(dir, "index.md") 60 | 61 | fmt.Printf(" ==> Create %s\n", path) 62 | f, err := os.Create(path) 63 | if err != nil { 64 | return err 65 | } 66 | defer f.Close() 67 | 68 | writeTypeIndex(f, types) 69 | return nil 70 | } 71 | 72 | // writeTypeIndex writes type index documentation to w. 73 | func writeTypeIndex(w io.Writer, types []schema.Type) { 74 | fmt.Fprintf(w, "# Types\n\n") 75 | for _, t := range types { 76 | name := format.GoName(t.Name) 77 | fmt.Fprintf(w, " - [%s](./%s.md) — %s\n", name, name, t.Description) 78 | } 79 | } 80 | 81 | // generateType generates type documentation. 82 | func generateType(t schema.Type, dir string) error { 83 | path := filepath.Join(dir, format.GoName(t.Name)+".md") 84 | 85 | fmt.Printf(" ==> Create %s\n", path) 86 | f, err := os.Create(path) 87 | if err != nil { 88 | return err 89 | } 90 | defer f.Close() 91 | 92 | writeType(f, t) 93 | return nil 94 | } 95 | 96 | // writeType writes type documentation to w. 97 | func writeType(w io.Writer, t schema.Type) { 98 | fmt.Fprintf(w, "# %s\n\n", format.GoName(t.Name)) 99 | fmt.Fprintf(w, "The `%s` %s\n\n", format.GoName(t.Name), t.Description) 100 | writeTableHeader(w, "Name", "Type", "Description") 101 | for _, f := range t.Properties { 102 | writeField(w, f) 103 | } 104 | writeTypeExamples(w, t.Examples) 105 | } 106 | 107 | // writeTypeExamples writes type examples to w. 108 | func writeTypeExamples(w io.Writer, examples []schema.Example) { 109 | if len(examples) == 0 { 110 | return 111 | } 112 | 113 | enc := json.NewEncoder(w) 114 | enc.SetIndent("", " ") 115 | enc.SetEscapeHTML(false) 116 | 117 | fmt.Fprintf(w, "\n## Examples\n\n") 118 | for _, e := range examples { 119 | fmt.Fprintf(w, "%s\n\n", e.Description) 120 | fmt.Fprintf(w, "```json\n") 121 | enc.Encode(e.Value) 122 | fmt.Fprintf(w, "```\n\n") 123 | } 124 | } 125 | 126 | // generateMethodsIndex generates method index documentation. 127 | func generateMethodsIndex(s *schema.Schema, dir string) error { 128 | path := filepath.Join(dir, "index.md") 129 | 130 | fmt.Printf(" ==> Create %s\n", path) 131 | f, err := os.Create(path) 132 | if err != nil { 133 | return err 134 | } 135 | defer f.Close() 136 | 137 | writeMethodIndex(f, s) 138 | return nil 139 | } 140 | 141 | // writeMethodIndex writes method index documentation to w. 142 | func writeMethodIndex(w io.Writer, s *schema.Schema) { 143 | fmt.Fprintf(w, "# Methods\n\n") 144 | for _, g := range s.Groups { 145 | fmt.Fprintf(w, "## %s\n\n", capitalize(g.Name)) 146 | fmt.Fprintf(w, "%s\n\n", g.Description) 147 | for _, m := range s.Methods { 148 | if m.Group != g.Name { 149 | continue 150 | } 151 | fmt.Fprintf(w, " - [%s](./%s.md) — %s\n", m.Name, m.Name, m.Description) 152 | } 153 | fmt.Fprintf(w, "\n") 154 | } 155 | } 156 | 157 | // generateMethod generates method documentation. 158 | func generateMethod(m schema.Method, dir string) error { 159 | path := filepath.Join(dir, m.Name+".md") 160 | 161 | fmt.Printf(" ==> Create %s\n", path) 162 | f, err := os.Create(path) 163 | if err != nil { 164 | return err 165 | } 166 | defer f.Close() 167 | 168 | writeMethod(f, m) 169 | return nil 170 | } 171 | 172 | // writeMethod writes method documentation to w. 173 | func writeMethod(w io.Writer, m schema.Method) { 174 | fmt.Fprintf(w, "# %s\n\n", m.Name) 175 | fmt.Fprintf(w, "The `%s` method %s\n\n", m.Name, m.Description) 176 | 177 | // inputs 178 | if len(m.Inputs) > 0 { 179 | fmt.Fprintf(w, " Inputs:\n\n") 180 | writeTableHeader(w, "Name", "Type", "Description") 181 | for _, f := range m.Inputs { 182 | writeField(w, f) 183 | } 184 | fmt.Fprintf(w, "\n") 185 | } 186 | 187 | // outputs 188 | if len(m.Outputs) > 0 { 189 | fmt.Fprintf(w, " Outputs:\n\n") 190 | writeTableHeader(w, "Name", "Type", "Description") 191 | for _, f := range m.Outputs { 192 | writeField(w, f) 193 | } 194 | } 195 | 196 | writeMethodExamples(w, m.Examples) 197 | fmt.Fprintf(w, "\n") 198 | } 199 | 200 | // writeMethodExamples writes method examples to w. 201 | func writeMethodExamples(w io.Writer, examples []schema.MethodExample) { 202 | if len(examples) == 0 { 203 | return 204 | } 205 | 206 | enc := json.NewEncoder(w) 207 | enc.SetIndent("", " ") 208 | enc.SetEscapeHTML(false) 209 | 210 | if len(examples) > 1 { 211 | fmt.Fprintf(w, "\n## Examples\n\n") 212 | } else { 213 | fmt.Fprintf(w, "\n## Example\n\n") 214 | } 215 | 216 | for _, e := range examples { 217 | if e.Name != "" { 218 | fmt.Fprintf(w, "### %s\n\n", e.Name) 219 | } 220 | 221 | if e.Description != "" { 222 | fmt.Fprintf(w, "%s\n\n", e.Description) 223 | } 224 | 225 | fmt.Fprintf(w, "Input:\n\n") 226 | if e.Input == nil { 227 | fmt.Fprintf(w, " None.\n\n") 228 | } else { 229 | fmt.Fprintf(w, "```json\n") 230 | enc.Encode(e.Input) 231 | fmt.Fprintf(w, "```\n\n") 232 | } 233 | 234 | fmt.Fprintf(w, "Output:\n\n") 235 | if e.Output == nil { 236 | fmt.Fprintf(w, " None.\n") 237 | } else { 238 | fmt.Fprintf(w, "```json\n") 239 | enc.Encode(e.Output) 240 | fmt.Fprintf(w, "```\n\n") 241 | } 242 | } 243 | } 244 | 245 | // writeField writes a field to w. 246 | func writeField(w io.Writer, f schema.Field) { 247 | name := fmt.Sprintf("`%s`", f.Name) 248 | kind := formatType(f.Type) 249 | if f.Type.Type == "array" { 250 | kind = fmt.Sprintf("__array__ of %s", formatType(schema.TypeObject(f.Items))) 251 | } 252 | writeTableRow(w, name, kind, capitalize(f.Description)+schemautil.FormatExtra(f)) 253 | } 254 | 255 | // writeTableRow writes a table row to w. 256 | func writeTableRow(w io.Writer, cells ...string) { 257 | fmt.Fprintf(w, "%s\n", strings.Join(cells, " | ")) 258 | } 259 | 260 | // writeTableHeader writes a table header to w. 261 | func writeTableHeader(w io.Writer, cells ...string) { 262 | for i, c := range cells { 263 | cells[i] = fmt.Sprintf("__%s__", c) 264 | } 265 | fmt.Fprintf(w, "%s\n", strings.Join(cells, " | ")) 266 | fmt.Fprintf(w, "%s\n", strings.Repeat("--- | ", len(cells))) 267 | } 268 | 269 | // formatType returns a formatted type. 270 | func formatType(t schema.TypeObject) string { 271 | if t.Ref.Value != "" { 272 | parts := strings.Split(t.Ref.Value, "/") 273 | name := format.GoName(parts[len(parts)-1]) 274 | return fmt.Sprintf("[%s](../types/%s.md)", name, name) 275 | } 276 | 277 | return fmt.Sprintf("__%s__", t.Type) 278 | } 279 | 280 | // capitalize returns a capitalized string. 281 | func capitalize(s string) string { 282 | return strings.ToUpper(string(s[0])) + string(s[1:]) 283 | } 284 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # RPC 2 | 3 | Simple RPC style APIs with generated clients & servers. 4 | 5 | ## About 6 | 7 | All RPC methods are invoked with the POST method, and the RPC method name is placed in the URL path. Input is passed as a JSON object in the body, following a JSON response for the output as shown here: 8 | 9 | ```sh 10 | $ curl -d '{ "project_id": "ping_production" }' https://api.example.com/get_alerts 11 | { 12 | "alerts": [...] 13 | } 14 | ``` 15 | 16 | All inputs are objects, all outputs are objects, this improves future-proofing as additional fields can be added without breaking existing clients. This is similar to the approach AWS takes with their APIs. 17 | 18 | ## Commands 19 | 20 | There are several commands provided for generating clients, servers, and documentation. Each of these commands accept a `-schema` flag defaulting to `schema.json`, see the `-h` help output for additional usage details. 21 | 22 | ### Clients 23 | 24 | - `rpc-dotnet-client` generates .NET clients 25 | - `rpc-ruby-client` generates Ruby clients 26 | - `rpc-php-client` generates PHP clients 27 | - `rpc-elm-client` generates Elm clients 28 | - `rpc-go-client` generates Go clients 29 | - `rpc-go-types` generates Go type definitions 30 | - `rpc-ts-client` generates TypeScript clients 31 | 32 | ### Servers 33 | 34 | - `rpc-go-server` generates Go servers 35 | 36 | ### Documentation 37 | 38 | - `rpc-md-docs` generates markdown documentation 39 | 40 | ## Schemas 41 | 42 | Currently the schemas are loosely a superset of [JSON Schema](https://json-schema.org/), however, this is a work in progress. See the [example schema](./examples/todo/schema.json). 43 | 44 | ## FAQ 45 | 46 |
47 | Why did you create this project? 48 | There are many great options when it comes to building APIs, but to me the most important aspect is simplicity, for myself and for the end user. Simple JSON in, and JSON out is appropriate for 99% of my API work, there's no need for the additional performance provided by alternative encoding schemes, and rarely a need for more complex features such as bi-directional streaming provided by gRPC. 49 |
50 | 51 |
52 | Should I use this in production? 53 | Only if you're confident that it supports everything you need, or you're comfortable with forking. I created this project for my work at Apex Software, it may not suit your needs. 54 |
55 | 56 |
57 | Why JSON schemas? 58 | I think concise schemas using a DSL are great, until they're a limiting factor. Personally I have no problem with JSON, and it's easy to expand upon when you introduce a new feature, such as inline examples for documentation. 59 |
60 | 61 |
62 | Why doesn't it follow the JSON-RPC spec? 63 | I would argue this spec is outdated, there is little reason to support batching at the request level, as HTTP/2 handles this for you. 64 |
65 | 66 |
67 | What does the client output look like? 68 | See the Apex Logs Go client for an example, client code is designed to be concise and idiomatic. 69 |
70 | 71 | --- 72 | 73 | [![GoDoc](https://godoc.org/github.com/apex/rpc?status.svg)](https://godoc.org/github.com/apex/rpc) 74 | ![](https://img.shields.io/badge/license-MIT-blue.svg) 75 | ![](https://img.shields.io/badge/status-stable-green.svg) 76 | 77 | Sponsored by my [GitHub sponsors](https://github.com/sponsors/tj): 78 | 79 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/0) 80 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/1) 81 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/2) 82 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/3) 83 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/4) 84 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/5) 85 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/6) 86 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/7) 87 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/8) 88 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/9) 89 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/10) 90 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/11) 91 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/12) 92 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/13) 93 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/14) 94 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/15) 95 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/16) 96 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/17) 97 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/18) 98 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/19) 99 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/20) 100 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/21) 101 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/22) 102 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/23) 103 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/24) 104 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/25) 105 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/26) 106 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/27) 107 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/28) 108 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/29) 109 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/30) 110 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/31) 111 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/32) 112 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/33) 113 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/34) 114 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/35) 115 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/36) 116 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/37) 117 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/38) 118 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/39) 119 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/40) 120 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/41) 121 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/42) 122 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/43) 123 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/44) 124 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/45) 125 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/46) 126 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/47) 127 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/48) 128 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/49) 129 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/50) 130 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/51) 131 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/52) 132 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/53) 133 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/54) 134 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/55) 135 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/56) 136 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/57) 137 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/58) 138 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/59) 139 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/60) 140 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/61) 141 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/62) 142 | -------------------------------------------------------------------------------- /schema/schema_json.go: -------------------------------------------------------------------------------- 1 | // Code generated by "file2go -in schema.json -pkg schema"; DO NOT EDIT. 2 | 3 | package schema 4 | 5 | // SchemaJson holds content of schema.json 6 | var SchemaJson = []byte{ 7 | 0x7b, 0x0a, 0x20, 0x20, 0x22, 0x24, 0x69, 0x64, 0x22, 0x3a, 0x20, 0x22, 8 | 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x65, 0x78, 0x61, 0x6d, 9 | 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x72, 0x70, 0x63, 0x2e, 10 | 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x6a, 0x73, 0x6f, 0x6e, 0x22, 11 | 0x2c, 0x0a, 0x20, 0x20, 0x22, 0x24, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 12 | 0x22, 0x3a, 0x20, 0x22, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x6a, 13 | 0x73, 0x6f, 0x6e, 0x2d, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x6f, 14 | 0x72, 0x67, 0x2f, 0x64, 0x72, 0x61, 0x66, 0x74, 0x2d, 0x30, 0x37, 0x2f, 15 | 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x23, 0x22, 0x2c, 0x0a, 0x20, 0x20, 16 | 0x22, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x72, 0x70, 17 | 0x63, 0x22, 0x2c, 0x0a, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 18 | 0x3a, 0x20, 0x22, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x22, 0x2c, 0x0a, 19 | 0x20, 0x20, 0x22, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x22, 20 | 0x3a, 0x20, 0x5b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6e, 0x61, 0x6d, 21 | 0x65, 0x22, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x22, 0x76, 0x65, 0x72, 22 | 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x22, 23 | 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x22, 0x0a, 0x20, 0x20, 0x5d, 24 | 0x2c, 0x0a, 0x20, 0x20, 0x22, 0x61, 0x64, 0x64, 0x69, 0x74, 0x69, 0x6f, 25 | 0x6e, 0x61, 0x6c, 0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 26 | 0x73, 0x22, 0x3a, 0x20, 0x74, 0x72, 0x75, 0x65, 0x2c, 0x0a, 0x20, 0x20, 27 | 0x22, 0x70, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x22, 28 | 0x3a, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6e, 0x61, 0x6d, 29 | 0x65, 0x22, 0x3a, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 30 | 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x73, 0x74, 0x72, 31 | 0x69, 0x6e, 0x67, 0x22, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 32 | 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 33 | 0x22, 0x3a, 0x20, 0x22, 0x54, 0x68, 0x65, 0x20, 0x6e, 0x61, 0x6d, 0x65, 34 | 0x20, 0x6f, 0x66, 0x20, 0x74, 0x68, 0x65, 0x20, 0x41, 0x50, 0x49, 0x2e, 35 | 0x22, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x2c, 0x0a, 0x20, 0x20, 0x20, 36 | 0x20, 0x22, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x3a, 0x20, 37 | 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 38 | 0x65, 0x22, 0x3a, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x22, 39 | 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 40 | 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x3a, 0x20, 0x22, 41 | 0x54, 0x68, 0x65, 0x20, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x20, 42 | 0x6f, 0x66, 0x20, 0x74, 0x68, 0x65, 0x20, 0x41, 0x50, 0x49, 0x2e, 0x22, 43 | 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 44 | 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 45 | 0x22, 0x3a, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 46 | 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x22, 47 | 0x3a, 0x20, 0x22, 0x54, 0x68, 0x65, 0x20, 0x64, 0x65, 0x73, 0x63, 0x72, 48 | 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x6f, 0x66, 0x20, 0x74, 0x68, 49 | 0x65, 0x20, 0x41, 0x50, 0x49, 0x22, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 50 | 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x73, 51 | 0x74, 0x72, 0x69, 0x6e, 0x67, 0x22, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d, 52 | 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6d, 0x65, 0x74, 0x68, 0x6f, 53 | 0x64, 0x73, 0x22, 0x3a, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 54 | 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 55 | 0x6e, 0x22, 0x3a, 0x20, 0x22, 0x54, 0x68, 0x65, 0x20, 0x6d, 0x65, 0x74, 56 | 0x68, 0x6f, 0x64, 0x73, 0x20, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 57 | 0x64, 0x20, 0x62, 0x79, 0x20, 0x74, 0x68, 0x65, 0x20, 0x41, 0x50, 0x49, 58 | 0x2e, 0x22, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 59 | 0x79, 0x70, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x61, 0x72, 0x72, 0x61, 0x79, 60 | 0x22, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x69, 0x74, 61 | 0x65, 0x6d, 0x73, 0x22, 0x3a, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 62 | 0x20, 0x20, 0x20, 0x20, 0x22, 0x24, 0x72, 0x65, 0x66, 0x22, 0x3a, 0x20, 63 | 0x22, 0x23, 0x2f, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 64 | 0x6e, 0x73, 0x2f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x4f, 0x62, 0x6a, 65 | 0x65, 0x63, 0x74, 0x22, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 66 | 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 67 | 0x22, 0x74, 0x79, 0x70, 0x65, 0x73, 0x22, 0x3a, 0x20, 0x7b, 0x0a, 0x20, 68 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 69 | 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x3a, 0x20, 0x22, 0x43, 0x75, 0x73, 70 | 0x74, 0x6f, 0x6d, 0x20, 0x74, 0x79, 0x70, 0x65, 0x20, 0x64, 0x65, 0x66, 71 | 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x22, 0x2c, 0x0a, 72 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x61, 0x74, 0x74, 0x65, 73 | 0x72, 0x6e, 0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 74 | 0x22, 0x3a, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 75 | 0x20, 0x22, 0x5b, 0x30, 0x2d, 0x7a, 0x5d, 0x2b, 0x22, 0x3a, 0x20, 0x7b, 76 | 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 77 | 0x24, 0x72, 0x65, 0x66, 0x22, 0x3a, 0x20, 0x22, 0x23, 0x2f, 0x64, 0x65, 78 | 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x74, 0x79, 79 | 0x70, 0x65, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x22, 0x0a, 0x20, 0x20, 80 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x0a, 0x20, 0x20, 0x20, 0x20, 81 | 0x20, 0x20, 0x7d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x0a, 0x20, 0x20, 82 | 0x7d, 0x2c, 0x0a, 0x20, 0x20, 0x22, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x69, 83 | 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x3a, 0x20, 0x7b, 0x0a, 0x20, 0x20, 84 | 0x20, 0x20, 0x22, 0x70, 0x72, 0x69, 0x6d, 0x69, 0x74, 0x69, 0x76, 0x65, 85 | 0x73, 0x22, 0x3a, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 86 | 0x22, 0x65, 0x6e, 0x75, 0x6d, 0x22, 0x3a, 0x20, 0x5b, 0x0a, 0x20, 0x20, 87 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x61, 0x72, 0x72, 0x61, 0x79, 88 | 0x22, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 89 | 0x62, 0x6f, 0x6f, 0x6c, 0x65, 0x61, 0x6e, 0x22, 0x2c, 0x0a, 0x20, 0x20, 90 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x6c, 0x6f, 0x61, 0x74, 91 | 0x22, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 92 | 0x69, 0x6e, 0x74, 0x65, 0x67, 0x65, 0x72, 0x22, 0x2c, 0x0a, 0x20, 0x20, 93 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6f, 0x62, 0x6a, 0x65, 0x63, 94 | 0x74, 0x22, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 95 | 0x22, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x22, 0x2c, 0x0a, 0x20, 0x20, 96 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x69, 0x6d, 0x65, 0x73, 97 | 0x74, 0x61, 0x6d, 0x70, 0x22, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 98 | 0x5d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x2c, 0x0a, 0x20, 0x20, 0x20, 99 | 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 100 | 0x22, 0x3a, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 101 | 0x74, 0x79, 0x70, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x6f, 0x62, 0x6a, 0x65, 102 | 0x63, 0x74, 0x22, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 103 | 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x22, 0x3a, 0x20, 0x5b, 104 | 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x72, 105 | 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x22, 0x0a, 0x20, 0x20, 106 | 0x20, 0x20, 0x20, 0x20, 0x5d, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 107 | 0x20, 0x22, 0x61, 0x64, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 108 | 0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x22, 0x3a, 109 | 0x20, 0x74, 0x72, 0x75, 0x65, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 110 | 0x20, 0x22, 0x70, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 111 | 0x22, 0x3a, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 112 | 0x20, 0x22, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x3a, 0x20, 0x7b, 0x0a, 0x20, 113 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 114 | 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x3a, 0x20, 115 | 0x22, 0x54, 0x68, 0x65, 0x20, 0x74, 0x79, 0x70, 0x65, 0x20, 0x6e, 0x61, 116 | 0x6d, 0x65, 0x2e, 0x22, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 117 | 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3a, 0x20, 118 | 0x22, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x22, 0x0a, 0x20, 0x20, 0x20, 119 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 120 | 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 121 | 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x3a, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 122 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 123 | 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x3a, 0x20, 0x22, 0x54, 124 | 0x68, 0x65, 0x20, 0x74, 0x79, 0x70, 0x65, 0x20, 0x64, 0x65, 0x73, 0x63, 125 | 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x22, 0x2c, 0x0a, 0x20, 126 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 127 | 0x70, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 128 | 0x22, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x2c, 129 | 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x72, 130 | 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x22, 0x3a, 0x20, 0x7b, 131 | 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 132 | 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x22, 133 | 0x3a, 0x20, 0x22, 0x54, 0x68, 0x65, 0x20, 0x70, 0x72, 0x6f, 0x70, 0x65, 134 | 0x72, 0x74, 0x79, 0x20, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 135 | 0x6f, 0x6e, 0x73, 0x2e, 0x22, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 136 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3a, 137 | 0x20, 0x22, 0x61, 0x72, 0x72, 0x61, 0x79, 0x22, 0x2c, 0x0a, 0x20, 0x20, 138 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x69, 0x74, 0x65, 139 | 0x6d, 0x73, 0x22, 0x3a, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 140 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x24, 0x72, 0x65, 0x66, 141 | 0x22, 0x3a, 0x20, 0x22, 0x23, 0x2f, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x69, 142 | 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x4f, 143 | 0x62, 0x6a, 0x65, 0x63, 0x74, 0x22, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 144 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 145 | 0x20, 0x20, 0x20, 0x7d, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 146 | 0x20, 0x20, 0x22, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x73, 0x22, 147 | 0x3a, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 148 | 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 149 | 0x6f, 0x6e, 0x22, 0x3a, 0x20, 0x22, 0x54, 0x68, 0x65, 0x20, 0x65, 0x78, 150 | 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x20, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x69, 151 | 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x22, 0x2c, 0x0a, 0x20, 0x20, 0x20, 152 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 153 | 0x22, 0x3a, 0x20, 0x22, 0x61, 0x72, 0x72, 0x61, 0x79, 0x22, 0x2c, 0x0a, 154 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x69, 155 | 0x74, 0x65, 0x6d, 0x73, 0x22, 0x3a, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 156 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x24, 0x72, 157 | 0x65, 0x66, 0x22, 0x3a, 0x20, 0x22, 0x23, 0x2f, 0x64, 0x65, 0x66, 0x69, 158 | 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x65, 0x78, 0x61, 0x6d, 159 | 0x70, 0x6c, 0x65, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x22, 0x0a, 0x20, 160 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x0a, 0x20, 161 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x0a, 0x20, 0x20, 0x20, 162 | 0x20, 0x20, 0x20, 0x7d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x2c, 0x0a, 163 | 0x20, 0x20, 0x20, 0x20, 0x22, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x4f, 164 | 0x62, 0x6a, 0x65, 0x63, 0x74, 0x22, 0x3a, 0x20, 0x7b, 0x0a, 0x20, 0x20, 165 | 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3a, 0x20, 166 | 0x22, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x22, 0x2c, 0x0a, 0x20, 0x20, 167 | 0x20, 0x20, 0x20, 0x20, 0x22, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 168 | 0x64, 0x22, 0x3a, 0x20, 0x5b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 169 | 0x20, 0x20, 0x22, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x2c, 0x0a, 0x20, 0x20, 170 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 171 | 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x0a, 0x20, 0x20, 0x20, 0x20, 172 | 0x20, 0x20, 0x5d, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 173 | 0x61, 0x64, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x50, 0x72, 174 | 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x22, 0x3a, 0x20, 0x74, 175 | 0x72, 0x75, 0x65, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 176 | 0x70, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x22, 0x3a, 177 | 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 178 | 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x3a, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 179 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 180 | 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x3a, 0x20, 0x22, 0x54, 181 | 0x68, 0x65, 0x20, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x20, 0x6e, 0x61, 182 | 0x6d, 0x65, 0x2e, 0x22, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 183 | 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3a, 0x20, 184 | 0x22, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x22, 0x0a, 0x20, 0x20, 0x20, 185 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 186 | 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 187 | 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x3a, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 188 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 189 | 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x3a, 0x20, 0x22, 0x54, 190 | 0x68, 0x65, 0x20, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x20, 0x64, 0x65, 191 | 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x22, 0x2c, 192 | 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 193 | 0x74, 0x79, 0x70, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 194 | 0x6e, 0x67, 0x22, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 195 | 0x7d, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 196 | 0x69, 0x6e, 0x70, 0x75, 0x74, 0x73, 0x22, 0x3a, 0x20, 0x7b, 0x0a, 0x20, 197 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 198 | 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x3a, 0x20, 199 | 0x22, 0x54, 0x68, 0x65, 0x20, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x20, 200 | 0x69, 0x6e, 0x70, 0x75, 0x74, 0x20, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 201 | 0x74, 0x65, 0x72, 0x73, 0x2e, 0x22, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 202 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 203 | 0x3a, 0x20, 0x22, 0x61, 0x72, 0x72, 0x61, 0x79, 0x22, 0x2c, 0x0a, 0x20, 204 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x69, 0x74, 205 | 0x65, 0x6d, 0x73, 0x22, 0x3a, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 206 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x24, 0x72, 0x65, 207 | 0x66, 0x22, 0x3a, 0x20, 0x22, 0x23, 0x2f, 0x64, 0x65, 0x66, 0x69, 0x6e, 208 | 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x66, 0x69, 0x65, 0x6c, 0x64, 209 | 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x22, 0x0a, 0x20, 0x20, 0x20, 0x20, 210 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x0a, 0x20, 0x20, 0x20, 0x20, 211 | 0x20, 0x20, 0x20, 0x20, 0x7d, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 212 | 0x20, 0x20, 0x20, 0x22, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x73, 0x22, 213 | 0x3a, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 214 | 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 215 | 0x6f, 0x6e, 0x22, 0x3a, 0x20, 0x22, 0x54, 0x68, 0x65, 0x20, 0x6d, 0x65, 216 | 0x74, 0x68, 0x6f, 0x64, 0x20, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x20, 217 | 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x2e, 0x22, 218 | 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 219 | 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x61, 0x72, 0x72, 220 | 0x61, 0x79, 0x22, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 221 | 0x20, 0x20, 0x20, 0x22, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x22, 0x3a, 0x20, 222 | 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 223 | 0x20, 0x20, 0x22, 0x24, 0x72, 0x65, 0x66, 0x22, 0x3a, 0x20, 0x22, 0x23, 224 | 0x2f, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 225 | 0x2f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 226 | 0x22, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 227 | 0x7d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x2c, 228 | 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x69, 229 | 0x6e, 0x63, 0x65, 0x22, 0x3a, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 230 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 231 | 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x3a, 0x20, 0x22, 0x54, 0x68, 232 | 0x65, 0x20, 0x41, 0x50, 0x49, 0x20, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 233 | 0x6e, 0x20, 0x74, 0x68, 0x61, 0x74, 0x20, 0x74, 0x68, 0x65, 0x20, 0x6d, 234 | 0x65, 0x74, 0x68, 0x6f, 0x64, 0x20, 0x77, 0x61, 0x73, 0x20, 0x69, 0x6e, 235 | 0x74, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x65, 0x64, 0x20, 0x69, 0x6e, 0x2e, 236 | 0x22, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 237 | 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x73, 0x74, 238 | 0x72, 0x69, 0x6e, 0x67, 0x22, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 239 | 0x20, 0x20, 0x7d, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 240 | 0x20, 0x22, 0x64, 0x65, 0x70, 0x72, 0x65, 0x63, 0x61, 0x74, 0x65, 0x64, 241 | 0x22, 0x3a, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 242 | 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 243 | 0x69, 0x6f, 0x6e, 0x22, 0x3a, 0x20, 0x22, 0x57, 0x68, 0x65, 0x74, 0x68, 244 | 0x65, 0x72, 0x20, 0x6f, 0x72, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x74, 0x68, 245 | 0x65, 0x20, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x20, 0x69, 0x73, 0x20, 246 | 0x64, 0x65, 0x70, 0x72, 0x65, 0x63, 0x61, 0x74, 0x65, 0x64, 0x2e, 0x22, 247 | 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 248 | 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x62, 0x6f, 0x6f, 249 | 0x6c, 0x65, 0x61, 0x6e, 0x22, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 250 | 0x20, 0x20, 0x7d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x0a, 251 | 0x20, 0x20, 0x20, 0x20, 0x7d, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x22, 252 | 0x66, 0x69, 0x65, 0x6c, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x22, 253 | 0x3a, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 254 | 0x79, 0x70, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x6f, 0x62, 0x6a, 0x65, 0x63, 255 | 0x74, 0x22, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x72, 256 | 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x22, 0x3a, 0x20, 0x5b, 0x0a, 257 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6e, 0x61, 0x6d, 258 | 0x65, 0x22, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 259 | 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 260 | 0x20, 0x5d, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x61, 261 | 0x64, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x50, 0x72, 0x6f, 262 | 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x22, 0x3a, 0x20, 0x74, 0x72, 263 | 0x75, 0x65, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 264 | 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x22, 0x3a, 0x20, 265 | 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6e, 266 | 0x61, 0x6d, 0x65, 0x22, 0x3a, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 267 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 268 | 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x3a, 0x20, 0x22, 0x54, 0x68, 269 | 0x65, 0x20, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x20, 0x6e, 0x61, 0x6d, 0x65, 270 | 0x2e, 0x22, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 271 | 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x73, 272 | 0x74, 0x72, 0x69, 0x6e, 0x67, 0x22, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 273 | 0x20, 0x20, 0x20, 0x7d, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 274 | 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 275 | 0x6f, 0x6e, 0x22, 0x3a, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 276 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 277 | 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x3a, 0x20, 0x22, 0x54, 0x68, 0x65, 278 | 0x20, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x20, 0x64, 0x65, 0x73, 0x63, 0x72, 279 | 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x22, 0x2c, 0x0a, 0x20, 0x20, 280 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 281 | 0x65, 0x22, 0x3a, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x22, 282 | 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x2c, 0x0a, 283 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 284 | 0x65, 0x22, 0x3a, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 285 | 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 286 | 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x3a, 0x20, 0x22, 0x54, 0x68, 0x65, 0x20, 287 | 0x66, 0x69, 0x65, 0x6c, 0x64, 0x20, 0x74, 0x79, 0x70, 0x65, 0x2e, 0x22, 288 | 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 289 | 0x22, 0x6f, 0x6e, 0x65, 0x4f, 0x66, 0x22, 0x3a, 0x20, 0x5b, 0x0a, 0x20, 290 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7b, 291 | 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 292 | 0x20, 0x20, 0x20, 0x22, 0x24, 0x72, 0x65, 0x66, 0x22, 0x3a, 0x20, 0x22, 293 | 0x23, 0x2f, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 294 | 0x73, 0x2f, 0x70, 0x72, 0x69, 0x6d, 0x69, 0x74, 0x69, 0x76, 0x65, 0x73, 295 | 0x22, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 296 | 0x20, 0x20, 0x7d, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 297 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 298 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x24, 0x72, 299 | 0x65, 0x66, 0x22, 0x3a, 0x20, 0x22, 0x23, 0x2f, 0x64, 0x65, 0x66, 0x69, 300 | 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x72, 0x65, 0x66, 0x65, 301 | 0x72, 0x65, 0x6e, 0x63, 0x65, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x22, 302 | 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 303 | 0x20, 0x7d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 304 | 0x20, 0x5d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 305 | 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x65, 306 | 0x6e, 0x75, 0x6d, 0x22, 0x3a, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 307 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 308 | 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x3a, 0x20, 0x22, 0x41, 0x6e, 309 | 0x20, 0x65, 0x6e, 0x75, 0x6d, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 310 | 0x20, 0x6f, 0x66, 0x20, 0x70, 0x6f, 0x73, 0x73, 0x69, 0x62, 0x6c, 0x65, 311 | 0x20, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x2e, 0x22, 0x2c, 0x0a, 0x20, 312 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 313 | 0x70, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x61, 0x72, 0x72, 0x61, 0x79, 0x22, 314 | 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 315 | 0x22, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x22, 0x3a, 0x20, 0x7b, 0x0a, 0x20, 316 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 317 | 0x74, 0x79, 0x70, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 318 | 0x6e, 0x67, 0x22, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 319 | 0x20, 0x20, 0x7d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 320 | 0x7d, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 321 | 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x22, 0x3a, 0x20, 0x7b, 322 | 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 323 | 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x22, 324 | 0x3a, 0x20, 0x22, 0x57, 0x68, 0x65, 0x74, 0x68, 0x65, 0x72, 0x20, 0x6f, 325 | 0x72, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x74, 0x68, 0x65, 0x20, 0x66, 0x69, 326 | 0x65, 0x6c, 0x64, 0x20, 0x69, 0x73, 0x20, 0x72, 0x65, 0x71, 0x75, 0x69, 327 | 0x72, 0x65, 0x64, 0x2e, 0x22, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 328 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3a, 329 | 0x20, 0x22, 0x62, 0x6f, 0x6f, 0x6c, 0x65, 0x61, 0x6e, 0x22, 0x0a, 0x20, 330 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x2c, 0x0a, 0x20, 0x20, 331 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x69, 0x74, 0x65, 0x6d, 0x73, 332 | 0x22, 0x3a, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 333 | 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 334 | 0x69, 0x6f, 0x6e, 0x22, 0x3a, 0x20, 0x22, 0x41, 0x72, 0x72, 0x61, 0x79, 335 | 0x20, 0x69, 0x74, 0x65, 0x6d, 0x20, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x69, 336 | 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x22, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 337 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6f, 0x6e, 0x65, 0x4f, 0x66, 338 | 0x22, 0x3a, 0x20, 0x5b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 339 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 340 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x24, 0x72, 341 | 0x65, 0x66, 0x22, 0x3a, 0x20, 0x22, 0x23, 0x2f, 0x64, 0x65, 0x66, 0x69, 342 | 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x69, 0x74, 0x65, 0x6d, 343 | 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x22, 0x0a, 0x20, 0x20, 0x20, 0x20, 344 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x2c, 0x0a, 0x20, 345 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7b, 346 | 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 347 | 0x20, 0x20, 0x20, 0x22, 0x24, 0x72, 0x65, 0x66, 0x22, 0x3a, 0x20, 0x22, 348 | 0x23, 0x2f, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 349 | 0x73, 0x2f, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x4f, 350 | 0x62, 0x6a, 0x65, 0x63, 0x74, 0x22, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 351 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x0a, 0x20, 0x20, 0x20, 352 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5d, 0x0a, 0x20, 0x20, 0x20, 353 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 354 | 0x20, 0x20, 0x20, 0x20, 0x22, 0x6d, 0x69, 0x6e, 0x22, 0x3a, 0x20, 0x7b, 355 | 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 356 | 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x22, 357 | 0x3a, 0x20, 0x22, 0x54, 0x68, 0x65, 0x20, 0x6d, 0x69, 0x6e, 0x69, 0x6d, 358 | 0x75, 0x6d, 0x20, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x2e, 0x22, 0x2c, 0x0a, 359 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 360 | 0x79, 0x70, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x69, 0x6e, 0x74, 0x65, 0x67, 361 | 0x65, 0x72, 0x22, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 362 | 0x7d, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 363 | 0x6d, 0x61, 0x78, 0x22, 0x3a, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 364 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 365 | 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x3a, 0x20, 0x22, 0x54, 0x68, 366 | 0x65, 0x20, 0x6d, 0x61, 0x78, 0x69, 0x6d, 0x75, 0x6d, 0x20, 0x76, 0x61, 367 | 0x6c, 0x75, 0x65, 0x2e, 0x22, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 368 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3a, 369 | 0x20, 0x22, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x65, 0x72, 0x22, 0x0a, 0x20, 370 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x2c, 0x0a, 0x20, 0x20, 371 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6c, 0x65, 0x6e, 0x67, 0x74, 372 | 0x68, 0x22, 0x3a, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 373 | 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 374 | 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x3a, 0x20, 0x22, 0x54, 0x68, 0x65, 0x20, 375 | 0x6d, 0x61, 0x78, 0x69, 0x6d, 0x75, 0x6d, 0x20, 0x6c, 0x65, 0x6e, 0x67, 376 | 0x74, 0x68, 0x20, 0x6f, 0x66, 0x20, 0x61, 0x20, 0x73, 0x74, 0x72, 0x69, 377 | 0x6e, 0x67, 0x2e, 0x22, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 378 | 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3a, 0x20, 379 | 0x22, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x65, 0x72, 0x22, 0x0a, 0x20, 0x20, 380 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x2c, 0x0a, 0x20, 0x20, 0x20, 381 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 382 | 0x74, 0x22, 0x3a, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 383 | 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 384 | 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x3a, 0x20, 0x22, 0x54, 0x68, 0x65, 0x20, 385 | 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x20, 0x76, 0x61, 0x6c, 0x75, 386 | 0x65, 0x2e, 0x22, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 387 | 0x7d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x0a, 0x20, 0x20, 388 | 0x20, 0x20, 0x7d, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x22, 0x72, 0x65, 389 | 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x4f, 0x62, 0x6a, 0x65, 0x63, 390 | 0x74, 0x22, 0x3a, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 391 | 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x6f, 0x62, 0x6a, 392 | 0x65, 0x63, 0x74, 0x22, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 393 | 0x22, 0x61, 0x64, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x50, 394 | 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x22, 0x3a, 0x20, 395 | 0x66, 0x61, 0x6c, 0x73, 0x65, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 396 | 0x20, 0x22, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x22, 0x3a, 397 | 0x20, 0x5b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 398 | 0x24, 0x72, 0x65, 0x66, 0x22, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 399 | 0x5d, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x72, 400 | 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x22, 0x3a, 0x20, 0x7b, 401 | 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x24, 0x72, 402 | 0x65, 0x66, 0x22, 0x3a, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 403 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3a, 404 | 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x22, 0x2c, 0x0a, 0x20, 405 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x66, 0x6f, 406 | 0x72, 0x6d, 0x61, 0x74, 0x22, 0x3a, 0x20, 0x22, 0x75, 0x72, 0x69, 0x2d, 407 | 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x22, 0x0a, 0x20, 408 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x0a, 0x20, 0x20, 0x20, 409 | 0x20, 0x20, 0x20, 0x7d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x2c, 0x0a, 410 | 0x20, 0x20, 0x20, 0x20, 0x22, 0x69, 0x74, 0x65, 0x6d, 0x4f, 0x62, 0x6a, 411 | 0x65, 0x63, 0x74, 0x22, 0x3a, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 412 | 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x6f, 413 | 0x62, 0x6a, 0x65, 0x63, 0x74, 0x22, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 414 | 0x20, 0x20, 0x22, 0x61, 0x64, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x61, 415 | 0x6c, 0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x22, 416 | 0x3a, 0x20, 0x66, 0x61, 0x6c, 0x73, 0x65, 0x2c, 0x0a, 0x20, 0x20, 0x20, 417 | 0x20, 0x20, 0x20, 0x22, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 418 | 0x22, 0x3a, 0x20, 0x5b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 419 | 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x0a, 0x20, 0x20, 0x20, 0x20, 420 | 0x20, 0x20, 0x5d, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 421 | 0x70, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x22, 0x3a, 422 | 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 423 | 0x74, 0x79, 0x70, 0x65, 0x22, 0x3a, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 424 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x24, 0x72, 0x65, 0x66, 425 | 0x22, 0x3a, 0x20, 0x22, 0x23, 0x2f, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x69, 426 | 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x70, 0x72, 0x69, 0x6d, 0x69, 0x74, 427 | 0x69, 0x76, 0x65, 0x73, 0x22, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 428 | 0x20, 0x20, 0x7d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x0a, 429 | 0x20, 0x20, 0x20, 0x20, 0x7d, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x22, 430 | 0x67, 0x72, 0x6f, 0x75, 0x70, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x22, 431 | 0x3a, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 432 | 0x79, 0x70, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x6f, 0x62, 0x6a, 0x65, 0x63, 433 | 0x74, 0x22, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x61, 434 | 0x64, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x50, 0x72, 0x6f, 435 | 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x22, 0x3a, 0x20, 0x66, 0x61, 436 | 0x6c, 0x73, 0x65, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 437 | 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x22, 0x3a, 0x20, 0x5b, 438 | 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6e, 0x61, 439 | 0x6d, 0x65, 0x22, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 440 | 0x20, 0x22, 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x22, 0x2c, 0x0a, 441 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 442 | 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x0a, 0x20, 0x20, 443 | 0x20, 0x20, 0x20, 0x20, 0x5d, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 444 | 0x20, 0x22, 0x70, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 445 | 0x22, 0x3a, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 446 | 0x20, 0x22, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x3a, 0x20, 0x7b, 0x0a, 0x20, 447 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 448 | 0x70, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 449 | 0x22, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x2c, 450 | 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x75, 451 | 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x22, 0x3a, 0x20, 0x7b, 0x0a, 0x20, 0x20, 452 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 453 | 0x65, 0x22, 0x3a, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x22, 454 | 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x2c, 0x0a, 455 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 456 | 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x3a, 0x20, 0x7b, 457 | 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 458 | 0x74, 0x79, 0x70, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 459 | 0x6e, 0x67, 0x22, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 460 | 0x7d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x0a, 0x20, 0x20, 461 | 0x20, 0x20, 0x7d, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x22, 0x65, 0x78, 462 | 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x22, 463 | 0x3a, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 464 | 0x79, 0x70, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x6f, 0x62, 0x6a, 0x65, 0x63, 465 | 0x74, 0x22, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x72, 466 | 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x22, 0x3a, 0x20, 0x5b, 0x0a, 467 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 468 | 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x2c, 0x0a, 0x20, 469 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x76, 0x61, 0x6c, 0x75, 470 | 0x65, 0x22, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5d, 0x2c, 0x0a, 471 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x72, 0x6f, 0x70, 0x65, 472 | 0x72, 0x74, 0x69, 0x65, 0x73, 0x22, 0x3a, 0x20, 0x7b, 0x0a, 0x20, 0x20, 473 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 474 | 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x3a, 0x20, 0x7b, 0x0a, 0x20, 475 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 476 | 0x70, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 477 | 0x22, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x2c, 478 | 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x76, 0x61, 479 | 0x6c, 0x75, 0x65, 0x22, 0x3a, 0x20, 0x7b, 0x0a, 0x0a, 0x20, 0x20, 0x20, 480 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 481 | 0x20, 0x7d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x2c, 0x0a, 0x20, 0x20, 482 | 0x20, 0x20, 0x22, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x45, 0x78, 0x61, 483 | 0x6d, 0x70, 0x6c, 0x65, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x22, 0x3a, 484 | 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 485 | 0x70, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 486 | 0x22, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x72, 0x65, 487 | 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x22, 0x3a, 0x20, 0x5b, 0x0a, 0x20, 488 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x69, 0x6e, 0x70, 0x75, 489 | 0x74, 0x22, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 490 | 0x22, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x22, 0x0a, 0x20, 0x20, 0x20, 491 | 0x20, 0x20, 0x20, 0x5d, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 492 | 0x22, 0x70, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x22, 493 | 0x3a, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 494 | 0x22, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x3a, 0x20, 0x7b, 0x0a, 0x20, 0x20, 495 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 496 | 0x65, 0x22, 0x3a, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x22, 497 | 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x2c, 0x0a, 498 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x65, 0x73, 499 | 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x3a, 0x20, 0x7b, 500 | 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 501 | 0x74, 0x79, 0x70, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x73, 0x74, 0x72, 0x69, 502 | 0x6e, 0x67, 0x22, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 503 | 0x7d, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 504 | 0x69, 0x6e, 0x70, 0x75, 0x74, 0x22, 0x3a, 0x20, 0x7b, 0x0a, 0x0a, 0x20, 505 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x2c, 0x0a, 0x20, 0x20, 506 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6f, 0x75, 0x74, 0x70, 0x75, 507 | 0x74, 0x22, 0x3a, 0x20, 0x7b, 0x0a, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 508 | 0x20, 0x20, 0x20, 0x7d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 509 | 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x0a, 0x20, 0x20, 0x7d, 0x0a, 0x7d, 510 | } 511 | --------------------------------------------------------------------------------