Skip to content

Commit

Permalink
feat: generate teams operation data from openapi spec (#226)
Browse files Browse the repository at this point in the history
* add temp openapi spec just for teams

* vendor openapi loader, read from file

* generate template data from openapi spec (teams only)

* get schema from component name not tag

* add tests

* fix imports

* fix tests, remove pointers
  • Loading branch information
k3llymariee authored May 1, 2024
1 parent 0141d07 commit e96fb54
Show file tree
Hide file tree
Showing 131 changed files with 22,824 additions and 21 deletions.
139 changes: 139 additions & 0 deletions cmd/resources/gen_resources.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package resources

import (
"log"
"os"
"strconv"
"strings"

"github.com/getkin/kin-openapi/openapi3"
)

type TemplateData struct {
Resources map[string]ResourceData
}

type ResourceData struct {
Name string
Description string
Operations map[string]OperationData
}

type OperationData struct {
Short string
Long string
Use string
Params []Param
HTTPMethod string
RequiresBody bool
Path string
SupportsSemanticPatch bool
}

type Param struct {
Name string
In string
Description string
Type string
Required bool
}

func GetTemplateData(fileName string) (TemplateData, error) {
rawFile, err := os.ReadFile(fileName)
if err != nil {
return TemplateData{}, err
}

loader := openapi3.NewLoader()
spec, err := loader.LoadFromData(rawFile)
if err != nil {
return TemplateData{}, err
}

resources := make(map[string]ResourceData)
for _, r := range spec.Tags {
resources[r.Name] = ResourceData{
Name: r.Name,
Description: r.Description,
Operations: make(map[string]OperationData, 0),
}
}

for path, pathItem := range spec.Paths.Map() {
for method, op := range pathItem.Operations() {
tag := op.Tags[0] // TODO: confirm each op only has one tag
resource, ok := resources[tag]
if !ok {
log.Printf("Matching resource not found for %s operation's tag: %s", op.OperationID, tag)
continue
}

use := getCmdUse(method, op, spec)

operation := OperationData{
Short: op.Summary,
Long: op.Description,
Use: use,
Params: make([]Param, 0),
HTTPMethod: method,
RequiresBody: method == "PUT" || method == "POST" || method == "PATCH",
Path: path,
}

for _, p := range op.Parameters {
if p.Value != nil {
// TODO: confirm if we only have one type per param b/c somehow this is a slice
types := *p.Value.Schema.Value.Type
param := Param{
Name: p.Value.Name,
In: p.Value.In,
Description: p.Value.Description,
Type: types[0],
Required: p.Value.Required,
}
operation.Params = append(operation.Params, param)
}
}

resource.Operations[op.OperationID] = operation
}
}

return TemplateData{Resources: resources}, nil
}

func getCmdUse(method string, op *openapi3.Operation, spec *openapi3.T) string {
methodMap := map[string]string{
"GET": "get",
"POST": "create",
"PUT": "replace", // TODO: confirm this
"DELETE": "delete",
"PATCH": "update",
}

use := methodMap[method]

var schema *openapi3.SchemaRef
for respType, respInfo := range op.Responses.Map() {
respCode, _ := strconv.Atoi(respType)
if respCode < 300 {
for _, s := range respInfo.Value.Content {
schemaName := strings.TrimPrefix(s.Schema.Ref, "#/components/schemas/")
schema = spec.Components.Schemas[schemaName]
}
}
}

if schema == nil {
// probably won't need to keep this logging in but leaving it for debugging purposes
log.Printf("No response type defined for %s", op.OperationID)
} else {
for propName := range schema.Value.Properties {
if propName == "items" {
use = "list"
break
}
}
}
return use
}
28 changes: 28 additions & 0 deletions cmd/resources/gen_resources_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package resources_test

import (
"encoding/json"
"os"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"ldcli/cmd/resources"
)

func TestGetTemplateData(t *testing.T) {
actual, err := resources.GetTemplateData("test_data/test-openapi.json")
assert.NoError(t, err)

expectedFromFile, err := os.ReadFile("test_data/expected_template_data.json")
require.NoError(t, err)

var expected resources.TemplateData
err = json.Unmarshal(expectedFromFile, &expected)
require.NoError(t, err)

t.Run("succeeds with single get resource", func(t *testing.T) {
assert.Equal(t, expected, actual)
})
}
1 change: 1 addition & 0 deletions cmd/resources/resource_cmds.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ func AddAllResourceCmds(rootCmd *cobra.Command, client resources.Client, analyti
Short: "Create team",
Long: "Create a team. To learn more, read [Creating a team](https://docs.launchdarkly.com/home/teams/creating).\n\n### Expanding the teams response\nLaunchDarkly supports four fields for expanding the \"Create team\" response. By default, these fields are **not** included in the response.\n\nTo expand the response, append the `expand` query parameter and add a comma-separated list with any of the following fields:\n\n* `members` includes the total count of members that belong to the team.\n* `roles` includes a paginated list of the custom roles that you have assigned to the team.\n* `projects` includes a paginated list of the projects that the team has any write access to.\n* `maintainers` includes a paginated list of the maintainers that you have assigned to the team.\n\nFor example, `expand=members,roles` includes the `members` and `roles` fields in the response.\n",
Use: "create", // TODO: translate post -> create

Params: []Param{
{
Name: "expand",
Expand Down
19 changes: 0 additions & 19 deletions cmd/resources/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,25 +39,6 @@ func NewResourceCmd(parentCmd *cobra.Command, analyticsTracker analytics.Tracker
return cmd
}

type OperationData struct {
Short string
Long string
Use string
Params []Param
HTTPMethod string
RequiresBody bool
Path string
SupportsSemanticPatch bool // TBD on how to actually determine from openapi spec
}

type Param struct {
Name string
In string
Description string
Type string
Required bool
}

type OperationCmd struct {
OperationData
client resources.Client
Expand Down
109 changes: 109 additions & 0 deletions cmd/resources/test_data/expected_template_data.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
{
"Resources": {
"Teams": {
"Name": "Teams",
"Description": "A team is a group of members in your LaunchDarkly account.",
"Operations": {
"deleteTeam": {
"Short": "Delete team",
"Long": "Delete a team by key.",
"Use": "delete",
"Params": [
{
"Name": "teamKey",
"In": "path",
"Description": "The team key",
"Type": "string",
"Required": true
}
],
"HTTPMethod": "DELETE",
"RequiresBody": false,
"Path": "/api/v2/teams/{teamKey}"
},
"getTeam": {
"Short": "Get team",
"Long": "Get team",
"Use": "get",
"Params": [
{
"Name": "teamKey",
"In": "path",
"Description": "The team key.",
"Type": "string",
"Required": true
},
{
"Name": "expand",
"In": "query",
"Description": "A comma-separated list of properties that can reveal additional information in the response.",
"Type": "string",
"Required": false
}
],
"HTTPMethod": "GET",
"RequiresBody": false,
"Path": "/api/v2/teams/{teamKey}"
},
"getTeams": {
"Short": "List teams",
"Long": "Return a list of teams.",
"Use": "list",
"Params": [
{
"Name": "limit",
"In": "query",
"Description": "The number of teams to return in the response. Defaults to 20.",
"Type": "integer",
"Required": false
}
],
"HTTPMethod": "GET",
"RequiresBody": false,
"Path": "/api/v2/teams"
},
"patchTeam": {
"Short": "Update team",
"Long": "Perform a partial update to a team.",
"Use": "update",
"Params": [
{
"Name": "teamKey",
"In": "path",
"Description": "The team key",
"Type": "string",
"Required": true
},
{
"Name": "expand",
"In": "query",
"Description": "A comma-separated list of properties.",
"Type": "string",
"Required": false
}
],
"HTTPMethod": "PATCH",
"RequiresBody": true,
"Path": "/api/v2/teams/{teamKey}"
},
"postTeam": {
"Short": "Create team",
"Long": "Create a team.",
"Use": "create",
"Params": [
{
"Name": "expand",
"In": "query",
"Description": "A comma-separated list of properties.",
"Type": "string",
"Required": false
}
],
"HTTPMethod": "POST",
"RequiresBody": true,
"Path": "/api/v2/teams"
}
}
}
}
}
Loading

0 comments on commit e96fb54

Please sign in to comment.