Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this isn't actually being called anywhere but the tests yet, but I was hacking into main.go and passing in the file name to test it out locally. will work on actually calling this in a generate file in a follow up PR.

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) {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not running the comparison directly on each op led to flakey tests - if there's a better way to do this LMK!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not getting test failures when running go test ./cmd/resources/ -run TestGetTemplateData -count=100 after changing the test to

t.Run("succeeds with single get resource", func(t *testing.T) {
	assert.Equal(t, expected, actual)
})

What failures were you seeing?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

...I swear I had some. But you're right, updated.

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