Skip to content

Commit

Permalink
Websockets (#94)
Browse files Browse the repository at this point in the history
* fix gopatch

Signed-off-by: Jess Frazelle <[email protected]>

* add websockets

Signed-off-by: Jess Frazelle <[email protected]>

* bump version

Signed-off-by: Jess Frazelle <[email protected]>

---------

Signed-off-by: Jess Frazelle <[email protected]>
  • Loading branch information
jessfraz authored Aug 1, 2023
1 parent 2212e40 commit 78942c2
Show file tree
Hide file tree
Showing 14 changed files with 267 additions and 95 deletions.
2 changes: 1 addition & 1 deletion VERSION.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v0.2.19
v0.2.20
11 changes: 10 additions & 1 deletion cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,16 @@ func run() error {
if err != nil {
logrus.Errorf("error comparing old and new openAPI spec: %v", err)
}
patchJSON, err := json.MarshalIndent(patch, "", " ")

// Make sure we are not doing any "Remove" operations.
newPatch := jsondiff.Patch{}
for i, d := range patch {
if d.Type != jsondiff.OperationRemove {
newPatch = append(newPatch, patch[i])
}
}

patchJSON, err := json.MarshalIndent(newPatch, "", " ")
if err != nil {
return fmt.Errorf("error marshalling openAPI spec: %v", err)
}
Expand Down
14 changes: 12 additions & 2 deletions cmd/paths.go
Original file line number Diff line number Diff line change
Expand Up @@ -267,15 +267,25 @@ func (data *Data) generateMethod(doc *openapi3.T, method string, pathName string
// Now we can get the description since we have filled in everything else.
function.Description = function.getDescription(operation)

exampleTemplatePath := "function-example.tmpl"
if _, ok := operation.Extensions["x-dropshot-websocket"]; ok {
exampleTemplatePath = "function-example-ws.tmpl"
}

// Build the example function.
example, err := templateToString("function-example.tmpl", function)
example, err := templateToString(exampleTemplatePath, function)
if err != nil {
return err
}
data.Examples = append(data.Examples, example)

templatePath := "path.tmpl"
if _, ok := operation.Extensions["x-dropshot-websocket"]; ok {
templatePath = "websocket.tmpl"
}

// Print the template for the function.
f, err := templateToString("path.tmpl", function)
f, err := templateToString(templatePath, function)
if err != nil {
return err
}
Expand Down
64 changes: 64 additions & 0 deletions cmd/tmpl/function-example-ws.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// {{.Description}}
func Example{{.Tag}}Service_{{.Name}}() {
client, err := {{.PackageName}}.NewClientFromEnv("your apps user agent")
if err != nil {
panic(err)
}

// Create the websocket connection.
ws, err := client.{{.Tag}}.{{.Name}}({{range .Args -}}{{.Example}},{{end -}}{{if .RequestBody}}{{.RequestBody.Example}}{{end -}})
if err != nil {
panic(err)
}

defer ws.Close()

done := make(chan struct{})

go func() {
defer close(done)
for {
_, message, err := ws.ReadMessage()
if err != nil {
log.Println("read:", err)
return
}
log.Printf("recv: %s", message)
}
}()

ticker := time.NewTicker(time.Second)
defer ticker.Stop()

interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt)

for {
select {
case <-done:
return
case t := <-ticker.C:
err := ws.WriteMessage(websocket.TextMessage, []byte(t.String()))
if err != nil {
log.Println("write:", err)
return
}
case <-interrupt:
log.Println("interrupt")

// Cleanly close the connection by sending a close message and then
// waiting (with timeout) for the server to close the connection.
err := ws.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
if err != nil {
log.Println("write close:", err)
return
}
select {
case <-done:
case <-time.After(time.Second):
}
return
}
}

}
1 change: 1 addition & 0 deletions cmd/tmpl/paths.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package {{.PackageName}}

import (
"github.com/gorilla/schema"
"github.com/gorilla/websocket"
)

{{range .Paths -}}
Expand Down
16 changes: 16 additions & 0 deletions cmd/tmpl/websocket.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// {{.Description}}
func (s *{{.Tag}}Service) {{.Name}}({{range .Args -}}{{.Name}} {{.Type}},{{end -}}{{if .RequestBody}}body {{.RequestBody.Type}}{{end}}) (*websocket.Conn, error) {
// Create the url.
path := "{{.Path}}"
uri := resolveRelative(s.client.server, path)

headers := http.Header{}
headers["Authorization"] = []string{fmt.Sprintf("Bearer %s", s.client.token)}

conn, _, err := websocket.DefaultDialer.Dial(strings.ReplaceAll(uri, "https://", "wss://"), headers)
if err != nil {
return nil, err
}

return conn, nil
}
5 changes: 5 additions & 0 deletions cmd/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,11 @@ func (data *Data) generateObjectType(name string, s *openapi3.Schema, spec *open
}
}

if v.Value.Deprecated {
objectValue.Description += "\n//\n// Deprecated: " + printProperty(k) + " is deprecated."

}

object.Values[k] = objectValue

// If this property is an object, we need to generate it as well.
Expand Down
119 changes: 115 additions & 4 deletions examples_test.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ require (
github.com/getkin/kin-openapi v0.118.0
github.com/google/uuid v1.3.0
github.com/gorilla/schema v1.2.0
github.com/gorilla/websocket v1.5.0
github.com/iancoleman/strcase v0.3.0
github.com/sirupsen/logrus v1.9.3
github.com/wI2L/jsondiff v0.4.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc=
github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI=
github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/invopop/yaml v0.1.0 h1:YW3WGUoJEXYfzWBjn00zIlrw7brGVD0fUKRYDPAPhrc=
Expand Down
52 changes: 2 additions & 50 deletions kittycad.go.patch.json
Original file line number Diff line number Diff line change
@@ -1,52 +1,4 @@
[
{
"op": "remove",
"path": "/components/schemas/AsyncApiCallOutput/oneOf/0/properties/type"
},
{
"op": "remove",
"path": "/components/schemas/AsyncApiCallOutput/oneOf/1/properties/type"
},
{
"op": "remove",
"path": "/components/schemas/AsyncApiCallOutput/oneOf/2/properties/type"
},
{
"op": "remove",
"path": "/components/schemas/AsyncApiCallOutput/oneOf/3/properties/type"
},
{
"op": "remove",
"path": "/components/schemas/AsyncApiCallOutput/oneOf/4/properties/type"
},
{
"op": "remove",
"path": "/components/schemas/AsyncApiCallOutput/oneOf/5/properties/type"
},
{
"op": "remove",
"path": "/components/schemas/InputFormat/oneOf/0/properties"
},
{
"op": "remove",
"path": "/components/schemas/InputFormat/oneOf/2/properties/type"
},
{
"op": "remove",
"path": "/components/schemas/InputFormat/oneOf/4/properties/type"
},
{
"op": "remove",
"path": "/components/schemas/OutputFormat/oneOf/0/properties/type"
},
{
"op": "remove",
"path": "/components/schemas/OutputFormat/oneOf/1/properties/type"
},
{
"op": "remove",
"path": "/components/schemas/OutputFormat/oneOf/3/properties/type"
},
{
"value": {
"client": "// Create a client with your token.\nfunc ExampleNewClient() {\n\tclient, err := kittycad.NewClient(\"$TOKEN\", \"your apps user agent\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Call the client's methods.\n\tresult, err := client.Meta.Ping()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(result)\n}\n\n// - OR -\n\n// Create a new client with your token parsed from the environment\n// variable: `KITTYCAD_API_TOKEN`.\nfunc ExampleNewClientFromEnv() {\n\tclient, err := kittycad.NewClientFromEnv(\"your apps user agent\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Call the client's methods.\n\tresult, err := client.Meta.Ping()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Printf(\"%#v\", result)\n}\n",
Expand Down Expand Up @@ -657,15 +609,15 @@
},
{
"value": {
"example": "// CreateTerm: Create a terminal.\n// \n// Attach to a docker container to create an interactive terminal.\n// \n// Create a client with your token.\nfunc ExampleExecutorService_CreateTerm() {\n\tclient, err := kittycad.NewClientFromEnv(\"your apps user agent\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tif err := client.Executor.CreateTerm(); err != nil {\n\t\tpanic(err)\n\t}\n\n}\n",
"example": "// CreateTerm: Create a terminal.\n// \n// Attach to a docker container to create an interactive terminal.\n// \n// CreateTerm: Create a terminal.\n// Attach to a docker container to create an interactive terminal.\nfunc ExampleExecutorService_CreateTerm() {\n\tclient, err := kittycad.NewClientFromEnv(\"your apps user agent\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Create the websocket connection.\n\tws, err := client.Executor.CreateTerm()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tdefer ws.Close()\n\n\tdone := make(chan struct{})\n\n\tgo func() {\n\t\tdefer close(done)\n\t\tfor {\n\t\t\t_, message, err := ws.ReadMessage()\n\t\t\tif err != nil {\n\t\t\t\tlog.Println(\"read:\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tlog.Printf(\"recv: %s\", message)\n\t\t}\n\t}()\n\n\tticker := time.NewTicker(time.Second)\n\tdefer ticker.Stop()\n\n\tinterrupt := make(chan os.Signal, 1)\n\tsignal.Notify(interrupt, os.Interrupt)\n\n\tfor {\n\t\tselect {\n\t\tcase \u003c-done:\n\t\t\treturn\n\t\tcase t := \u003c-ticker.C:\n\t\t\terr := ws.WriteMessage(websocket.TextMessage, []byte(t.String()))\n\t\t\tif err != nil {\n\t\t\t\tlog.Println(\"write:\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\tcase \u003c-interrupt:\n\t\t\tlog.Println(\"interrupt\")\n\n\t\t\t// Cleanly close the connection by sending a close message and then\n\t\t\t// waiting (with timeout) for the server to close the connection.\n\t\t\terr := ws.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, \"\"))\n\t\t\tif err != nil {\n\t\t\t\tlog.Println(\"write close:\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tselect {\n\t\t\tcase \u003c-done:\n\t\t\tcase \u003c-time.After(time.Second):\n\t\t\t}\n\t\t\treturn\n\t\t}\n\t}\n\n}\n",
"libDocsLink": "https://pkg.go.dev/github.com/kittycad/kittycad.go/#ExecutorService.CreateTerm"
},
"op": "add",
"path": "/paths/~1ws~1executor~1term/get/x-go"
},
{
"value": {
"example": "// CommandsWs: Open a websocket which accepts modeling commands.\n// \n// Pass those commands to the engine via websocket, and pass responses back to the client. Basically, this is a websocket proxy between the frontend/client and the engine.\n// \n// Create a client with your token.\nfunc ExampleModelingService_CommandsWs() {\n\tclient, err := kittycad.NewClientFromEnv(\"your apps user agent\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tif err := client.Modeling.CommandsWs(); err != nil {\n\t\tpanic(err)\n\t}\n\n}\n",
"example": "// CommandsWs: Open a websocket which accepts modeling commands.\n// \n// Pass those commands to the engine via websocket, and pass responses back to the client. Basically, this is a websocket proxy between the frontend/client and the engine.\n// \n// CommandsWs: Open a websocket which accepts modeling commands.\n// Pass those commands to the engine via websocket, and pass responses back to the client. Basically, this is a websocket proxy between the frontend/client and the engine.\nfunc ExampleModelingService_CommandsWs() {\n\tclient, err := kittycad.NewClientFromEnv(\"your apps user agent\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Create the websocket connection.\n\tws, err := client.Modeling.CommandsWs()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tdefer ws.Close()\n\n\tdone := make(chan struct{})\n\n\tgo func() {\n\t\tdefer close(done)\n\t\tfor {\n\t\t\t_, message, err := ws.ReadMessage()\n\t\t\tif err != nil {\n\t\t\t\tlog.Println(\"read:\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tlog.Printf(\"recv: %s\", message)\n\t\t}\n\t}()\n\n\tticker := time.NewTicker(time.Second)\n\tdefer ticker.Stop()\n\n\tinterrupt := make(chan os.Signal, 1)\n\tsignal.Notify(interrupt, os.Interrupt)\n\n\tfor {\n\t\tselect {\n\t\tcase \u003c-done:\n\t\t\treturn\n\t\tcase t := \u003c-ticker.C:\n\t\t\terr := ws.WriteMessage(websocket.TextMessage, []byte(t.String()))\n\t\t\tif err != nil {\n\t\t\t\tlog.Println(\"write:\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\tcase \u003c-interrupt:\n\t\t\tlog.Println(\"interrupt\")\n\n\t\t\t// Cleanly close the connection by sending a close message and then\n\t\t\t// waiting (with timeout) for the server to close the connection.\n\t\t\terr := ws.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, \"\"))\n\t\t\tif err != nil {\n\t\t\t\tlog.Println(\"write close:\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tselect {\n\t\t\tcase \u003c-done:\n\t\t\tcase \u003c-time.After(time.Second):\n\t\t\t}\n\t\t\treturn\n\t\t}\n\t}\n\n}\n",
"libDocsLink": "https://pkg.go.dev/github.com/kittycad/kittycad.go/#ModelingService.CommandsWs"
},
"op": "add",
Expand Down
Loading

0 comments on commit 78942c2

Please sign in to comment.