-
Notifications
You must be signed in to change notification settings - Fork 13
feat: generate teams operation data from openapi spec #226
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
75f8ec4
3e9d49e
c48ca3e
c7a594f
151d745
ef0e6f7
6e6a0c6
a47c5dc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
| } | ||
| 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) { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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!
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not getting test failures when running What failures were you seeing?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
| }) | ||
| } | ||
| 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" | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
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.goand 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.