Skip to content

Commit

Permalink
Made the table formatting function more general.
Browse files Browse the repository at this point in the history
  • Loading branch information
OliverLok committed Dec 6, 2024
1 parent e73d107 commit b42587a
Show file tree
Hide file tree
Showing 6 changed files with 107 additions and 122 deletions.
26 changes: 15 additions & 11 deletions cmd/call-endpoint/expected_output_test.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
package main

const EXPECTED_SERVER_USER_LIST_TABLE = `email name role type courses
[email protected] course-admin user server {"course-languages":{"id":"course-languages","name":"Course Using Different Languages.","role":"admin"},"course101":{"id":"course101","name":"Course 101","role":"admin"}}
[email protected] course-grader user server {"course-languages":{"id":"course-languages","name":"Course Using Different Languages.","role":"grader"},"course101":{"id":"course101","name":"Course 101","role":"grader"}}
[email protected] course-other user server {"course-languages":{"id":"course-languages","name":"Course Using Different Languages.","role":"other"},"course101":{"id":"course101","name":"Course 101","role":"other"}}
[email protected] course-owner user server {"course-languages":{"id":"course-languages","name":"Course Using Different Languages.","role":"owner"},"course101":{"id":"course101","name":"Course 101","role":"owner"}}
[email protected] course-student user server {"course-languages":{"id":"course-languages","name":"Course Using Different Languages.","role":"student"},"course101":{"id":"course101","name":"Course 101","role":"student"}}
root root root server {}
[email protected] server-admin admin server {}
[email protected] server-creator creator server {}
[email protected] server-owner owner server {}
[email protected] server-user user server {}
const EXPECTED_SERVER_USER_LIST_TABLE = `courses email name role type
{"course-languages":{"id":"course-languages","name":"Course Using Different Languages.","role":"admin"},"course101":{"id":"course101","name":"Course 101","role":"admin"}} [email protected] course-admin user server
{"course-languages":{"id":"course-languages","name":"Course Using Different Languages.","role":"grader"},"course101":{"id":"course101","name":"Course 101","role":"grader"}} [email protected] course-grader user server
{"course-languages":{"id":"course-languages","name":"Course Using Different Languages.","role":"other"},"course101":{"id":"course101","name":"Course 101","role":"other"}} [email protected] course-other user server
{"course-languages":{"id":"course-languages","name":"Course Using Different Languages.","role":"owner"},"course101":{"id":"course101","name":"Course 101","role":"owner"}} [email protected] course-owner user server
{"course-languages":{"id":"course-languages","name":"Course Using Different Languages.","role":"student"},"course101":{"id":"course101","name":"Course 101","role":"student"}} [email protected] course-student user server
{} root root root server
{} [email protected] server-admin admin server
{} [email protected] server-creator creator server
{} [email protected] server-owner owner server
{} [email protected] server-user user server
`

const EXPECTED_COURSE_USER_LIST_TABLE = `email lms-id name role type
Expand All @@ -20,3 +20,7 @@ [email protected] [email protected] course-other oth
[email protected] [email protected] course-owner owner course
[email protected] [email protected] course-student student course
`

const EXPECTED_COURSE_ASSIGNMENTS_LIST_TABLE = `id name
hw0 Homework 0
`
106 changes: 4 additions & 102 deletions cmd/call-endpoint/main.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
package main

import (
"fmt"
"sort"
"strconv"
"strings"

"github.com/alecthomas/kong"
Expand All @@ -13,7 +10,6 @@ import (
"github.com/edulinq/autograder/internal/cmd"
"github.com/edulinq/autograder/internal/config"
"github.com/edulinq/autograder/internal/log"
"github.com/edulinq/autograder/internal/util"
)

var args struct {
Expand All @@ -22,17 +18,12 @@ var args struct {

Endpoint string `help:"Endpoint of the desired API." arg:""`
Parameters []string `help:"Parameter for the endpoint in the format 'key:value', e.g., 'id:123'." arg:"" optional:""`
Table bool `help:"Attempt to output data as a TSV. Will default to JSON." default:"false"`
Table bool `help:"Attempt to output data as a TSV; defaults to JSON if there are issues." default:"false"`
}

const (
USERS_KEY = "users"
COURSES_KEY = "courses"
)

func main() {
kong.Parse(&args,
kong.Description(generateHelpDescription()),
kong.Description("Execute an API request to the specified endpoint. For more information on available API endpoints, see the API resource file at: 'resources/api.json'."),
)

err := config.HandleConfigArgs(args.ConfigArgs)
Expand Down Expand Up @@ -74,99 +65,10 @@ func main() {
request[parts[0]] = parts[1]
}

var printFunc cmd.CustomResponseFormatter
var printFunc cmd.CustomResponseFormatter = nil
if args.Table {
printFunc = printCMDResponseTable
printFunc = cmd.PrintCMDResponseTable
}

cmd.MustHandleCMDRequestAndExitFull(args.Endpoint, request, nil, args.CommonOptions, printFunc)
}

func generateHelpDescription() string {
baseDescription := "Execute an API request to the specified endpoint.\n\n"

var endpointList strings.Builder
endpointList.WriteString("List of endpoints:\n")

apiDescription := api.Describe(*api.GetRoutes())
for endpoint := range apiDescription.Endpoints {
endpointList.WriteString(fmt.Sprintf(" - %s\n", endpoint))
}

return baseDescription + endpointList.String()
}

func printCMDResponseTable(response core.APIResponse) string {
responseContent, ok := response.Content.(map[string]any)
if !ok {
return ""
}

// Don't try to format a response that has multiple keys.
if len(responseContent) != 1 {
return ""
}

users, ok := responseContent[USERS_KEY].([]any)
if !ok {
return ""
}

firstUser, ok := users[0].(map[string]any)
if !ok {
return ""
}

var headers []string
for key := range firstUser {
if key == COURSES_KEY {
continue
}

headers = append(headers, key)
}

sort.Strings(headers)

_, exists := firstUser[COURSES_KEY]
if exists {
// Add courses to the end of the slice for better readability in the output.
headers = append(headers, COURSES_KEY)
}

var usersTable strings.Builder
usersTable.WriteString(strings.Join(headers, "\t"))

headerKeys := strings.Split(usersTable.String(), "\t")

usersTable.WriteString("\n")

for i, user := range users {
userMap, ok := user.(map[string]any)
if !ok {
return ""
}

var row []string
for _, key := range headerKeys {
switch value := userMap[key].(type) {
case string:
row = append(row, value)
case int:
row = append(row, strconv.Itoa(value))
case bool:
row = append(row, strconv.FormatBool(value))
default:
row = append(row, util.MustToJSON(value))
}
}

usersTable.WriteString(strings.Join(row, "\t"))

if i < len(users)-1 {
usersTable.WriteString("\n")
}
}

return usersTable.String()
}
10 changes: 10 additions & 0 deletions cmd/call-endpoint/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@ func TestCallApiEndpointBase(test *testing.T) {
},

// Custom Output Formatters.
{
CommonCMDTestCase: cmd.CommonCMDTestCase{
ExpectedStdout: EXPECTED_COURSE_ASSIGNMENTS_LIST_TABLE,
},
endpoint: "courses/assignments/list",
parameters: []string{
"course-id:course101",
"--table",
},
},
{
CommonCMDTestCase: cmd.CommonCMDTestCase{
ExpectedStdout: EXPECTED_SERVER_USER_LIST_TABLE,
Expand Down
4 changes: 2 additions & 2 deletions cmd/course-user-list/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func main() {
cmd.MustHandleCMDRequestAndExitFull(`courses/users/list`, request, users.ListResponse{}, args.CommonOptions, printFunc)
}

func listCourseUsersTable(response core.APIResponse) string {
func listCourseUsersTable(response core.APIResponse) (string, bool) {
var responseContent users.ListResponse
util.MustJSONFromString(util.MustToJSON(response.Content), &responseContent)

Expand All @@ -68,5 +68,5 @@ func listCourseUsersTable(response core.APIResponse) string {
courseUsersTable.WriteString(user.LMSID)
}

return courseUsersTable.String()
return courseUsersTable.String(), true
}
4 changes: 2 additions & 2 deletions cmd/user-list/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func main() {
cmd.MustHandleCMDRequestAndExitFull(`users/list`, request, users.ListResponse{}, args.CommonOptions, printFunc)
}

func listServerUsersTable(response core.APIResponse) string {
func listServerUsersTable(response core.APIResponse) (string, bool) {
var responseContent users.ListResponse
util.MustJSONFromString(util.MustToJSON(response.Content), &responseContent)

Expand All @@ -63,5 +63,5 @@ func listServerUsersTable(response core.APIResponse) string {
serverUsersTable.WriteString(util.MustToJSON(user.Courses))
}

return serverUsersTable.String()
return serverUsersTable.String(), true
}
79 changes: 74 additions & 5 deletions internal/cmd/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"fmt"
"net"
"reflect"
"sort"
"strings"

"github.com/edulinq/autograder/internal/api/core"
"github.com/edulinq/autograder/internal/api/server"
Expand All @@ -18,7 +20,7 @@ type CommonOptions struct {
Verbose bool `help:"Add the full request and response to the output. Be aware that the output will include extra text beyond the expected format." default:"false"`
}

type CustomResponseFormatter func(response core.APIResponse) string
type CustomResponseFormatter func(response core.APIResponse) (string, bool)

func MustHandleCMDRequestAndExit(endpoint string, request any, responseType any) {
MustHandleCMDRequestAndExitFull(endpoint, request, responseType, CommonOptions{}, nil)
Expand Down Expand Up @@ -105,13 +107,14 @@ func PrintCMDResponseFull(request any, response core.APIResponse, responseType a
fmt.Printf("\nAutograder Response:\n---\n%s\n---\n", util.MustToJSONIndent(response))
}

customOutput := ""
successfulConversion := false
customPrintOutput := ""
if customPrintFunc != nil {
customOutput = customPrintFunc(response)
customPrintOutput, successfulConversion = customPrintFunc(response)
}

if len(customOutput) > 0 {
fmt.Println(customOutput)
if successfulConversion && customPrintFunc != nil {
fmt.Println(customPrintOutput)
} else if responseType == nil {
fmt.Println(util.MustToJSONIndent(response.Content))
} else {
Expand All @@ -120,3 +123,69 @@ func PrintCMDResponseFull(request any, response core.APIResponse, responseType a
fmt.Println(util.MustToJSONIndent(responseContent))
}
}

func PrintCMDResponseTable(response core.APIResponse) (string, bool) {
responseContent, ok := response.Content.(map[string]any)
if !ok {
return "", false
}

// Don't try to format a response that has multiple keys.
if len(responseContent) != 1 {
return "", false
}

responseContentKey := reflect.ValueOf(responseContent).MapKeys()

// Get the rows that will be added to the table.
entries, ok := responseContent[responseContentKey[0].String()].([]any)
if !ok {
return "", false
}

// Use the first entry to create the headers of the table.
firstEntry, ok := entries[0].(map[string]any)
if !ok {
return "", false
}

var headers []string
for key := range firstEntry {
headers = append(headers, key)
}

sort.Strings(headers)

var customTable strings.Builder
customTable.WriteString(strings.Join(headers, "\t"))

headerKeys := strings.Split(customTable.String(), "\t")

customTable.WriteString("\n")

// Turn each entry into a row of the table.
for i, entry := range entries {
entryMap, ok := entry.(map[string]any)
if !ok {
return "", false
}

var row []string
for _, key := range headerKeys {
switch value := entryMap[key].(type) {
case map[string]any:
row = append(row, util.MustToJSON(value))
default:
row = append(row, fmt.Sprintf("%v", value))
}
}

customTable.WriteString(strings.Join(row, "\t"))

if i < len(entries)-1 {
customTable.WriteString("\n")
}
}

return customTable.String(), true
}

0 comments on commit b42587a

Please sign in to comment.