cli/pkg/cmd/project/shared/format/json.go
Heath Stewart 0cf5d22ead
Support template, jq flags with standard format flag (#7832)
* Use standard JSON flags for project command

Deprecates the --format flag and adds the standard JSON flags to properly filter, template, and write JSON.

* Add format flags cmdutil

Resolves PR feedback

* Remove unnecessary fields from JSON format flags

* Add standard format help to remaining commands

* Add JSON format regression tests

Also fixed a number of `project` commands that didn't format the right object as JSON.

* Resolve PR feedback
2024-01-09 09:46:39 -08:00

386 lines
10 KiB
Go

package format
import (
"strings"
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
)
// JSONProject serializes a Project to JSON.
func JSONProject(project queries.Project) ProjectJSON {
return ProjectJSON{
Number: project.Number,
URL: project.URL,
ShortDescription: project.ShortDescription,
Public: project.Public,
Closed: project.Closed,
// The Template field is commented out due to https://github.com/cli/cli/issues/8103.
// The Template field does not exist on GHES 3.8 and older, once GHES 3.8 gets
// deprecated on 2024-03-07 we can start populating this field again.
// Template: project.Template,
Title: project.Title,
ID: project.ID,
Readme: project.Readme,
Items: struct {
TotalCount int `json:"totalCount"`
}{
TotalCount: project.Items.TotalCount,
},
Fields: struct {
TotalCount int `json:"totalCount"`
}{
TotalCount: project.Fields.TotalCount,
},
Owner: struct {
Type string `json:"type"`
Login string `json:"login"`
}{
Type: project.OwnerType(),
Login: project.OwnerLogin(),
},
}
}
// JSONProjects serializes a slice of Projects to JSON.
// JSON fields are `totalCount` and `projects`.
func JSONProjects(projects []queries.Project, totalCount int) ProjectsJSON {
var result []ProjectJSON
for _, p := range projects {
result = append(result, ProjectJSON{
Number: p.Number,
URL: p.URL,
ShortDescription: p.ShortDescription,
Public: p.Public,
Closed: p.Closed,
// The Template field is commented out due to https://github.com/cli/cli/issues/8103.
// The Template field does not exist on GHES 3.8 and older, once GHES 3.8 gets
// deprecated on 2024-03-07 we can start populating this field again.
// Template: p.Template,
Title: p.Title,
ID: p.ID,
Readme: p.Readme,
Items: struct {
TotalCount int `json:"totalCount"`
}{
TotalCount: p.Items.TotalCount,
},
Fields: struct {
TotalCount int `json:"totalCount"`
}{
TotalCount: p.Fields.TotalCount,
},
Owner: struct {
Type string `json:"type"`
Login string `json:"login"`
}{
Type: p.OwnerType(),
Login: p.OwnerLogin(),
},
})
}
return ProjectsJSON{
Projects: result,
TotalCount: totalCount,
}
}
type ProjectJSON struct {
Number int32 `json:"number"`
URL string `json:"url"`
ShortDescription string `json:"shortDescription"`
Public bool `json:"public"`
Closed bool `json:"closed"`
// The Template field is commented out due to https://github.com/cli/cli/issues/8103.
// The Template field does not exist on GHES 3.8 and older, once GHES 3.8 gets
// deprecated on 2024-03-07 we can start populating this field again.
// Template bool `json:"template"`
Title string `json:"title"`
ID string `json:"id"`
Readme string `json:"readme"`
Items struct {
TotalCount int `json:"totalCount"`
} `graphql:"items(first: 100)" json:"items"`
Fields struct {
TotalCount int `json:"totalCount"`
} `graphql:"fields(first:100)" json:"fields"`
Owner struct {
Type string `json:"type"`
Login string `json:"login"`
} `json:"owner"`
}
type ProjectsJSON struct {
Projects []ProjectJSON `json:"projects"`
TotalCount int `json:"totalCount"`
}
// JSONProjectField serializes a ProjectField to JSON.
func JSONProjectField(field queries.ProjectField) ProjectFieldJSON {
val := ProjectFieldJSON{
ID: field.ID(),
Name: field.Name(),
Type: field.Type(),
}
for _, o := range field.Options() {
val.Options = append(val.Options, SingleSelectOptionJSON{
Name: o.Name,
ID: o.ID,
})
}
return val
}
// JSONProjectFields serializes a slice of ProjectFields to JSON.
// JSON fields are `totalCount` and `fields`.
func JSONProjectFields(project *queries.Project) ProjectFieldsJSON {
var result []ProjectFieldJSON
for _, f := range project.Fields.Nodes {
val := ProjectFieldJSON{
ID: f.ID(),
Name: f.Name(),
Type: f.Type(),
}
for _, o := range f.Options() {
val.Options = append(val.Options, SingleSelectOptionJSON{
Name: o.Name,
ID: o.ID,
})
}
result = append(result, val)
}
return ProjectFieldsJSON{
Fields: result,
TotalCount: project.Fields.TotalCount,
}
}
type ProjectFieldJSON struct {
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Options []SingleSelectOptionJSON `json:"options,omitempty"`
}
type ProjectFieldsJSON struct {
Fields []ProjectFieldJSON `json:"fields"`
TotalCount int `json:"totalCount"`
}
type SingleSelectOptionJSON struct {
ID string `json:"id"`
Name string `json:"name"`
}
// JSONProjectItem serializes a ProjectItem to JSON.
func JSONProjectItem(item queries.ProjectItem) ProjectItemJSON {
return ProjectItemJSON{
ID: item.ID(),
Title: item.Title(),
Body: item.Body(),
Type: item.Type(),
URL: item.URL(),
}
}
type ProjectItemJSON struct {
ID string `json:"id"`
Title string `json:"title"`
Body string `json:"body"`
Type string `json:"type"`
URL string `json:"url,omitempty"`
}
// JSONProjectDraftIssue serializes a DraftIssue to JSON.
// This is needed because the field for
// https://docs.github.com/en/graphql/reference/mutations#updateprojectv2draftissue
// is a DraftIssue https://docs.github.com/en/graphql/reference/objects#draftissue
// and not a ProjectV2Item https://docs.github.com/en/graphql/reference/objects#projectv2item
func JSONProjectDraftIssue(item queries.DraftIssue) DraftIssueJSON {
return DraftIssueJSON{
ID: item.ID,
Title: item.Title,
Body: item.Body,
Type: "DraftIssue",
}
}
type DraftIssueJSON struct {
ID string `json:"id"`
Title string `json:"title"`
Body string `json:"body"`
Type string `json:"type"`
}
func projectItemContent(p queries.ProjectItem) any {
switch p.Content.TypeName {
case "DraftIssue":
return struct {
Type string `json:"type"`
Body string `json:"body"`
Title string `json:"title"`
}{
Type: p.Type(),
Body: p.Body(),
Title: p.Title(),
}
case "Issue":
return struct {
Type string `json:"type"`
Body string `json:"body"`
Title string `json:"title"`
Number int `json:"number"`
Repository string `json:"repository"`
URL string `json:"url"`
}{
Type: p.Type(),
Body: p.Body(),
Title: p.Title(),
Number: p.Number(),
Repository: p.Repo(),
URL: p.URL(),
}
case "PullRequest":
return struct {
Type string `json:"type"`
Body string `json:"body"`
Title string `json:"title"`
Number int `json:"number"`
Repository string `json:"repository"`
URL string `json:"url"`
}{
Type: p.Type(),
Body: p.Body(),
Title: p.Title(),
Number: p.Number(),
Repository: p.Repo(),
URL: p.URL(),
}
}
return nil
}
func projectFieldValueData(v queries.FieldValueNodes) any {
switch v.Type {
case "ProjectV2ItemFieldDateValue":
return v.ProjectV2ItemFieldDateValue.Date
case "ProjectV2ItemFieldIterationValue":
return struct {
Title string `json:"title"`
StartDate string `json:"startDate"`
Duration int `json:"duration"`
}{
Title: v.ProjectV2ItemFieldIterationValue.Title,
StartDate: v.ProjectV2ItemFieldIterationValue.StartDate,
Duration: v.ProjectV2ItemFieldIterationValue.Duration,
}
case "ProjectV2ItemFieldNumberValue":
return v.ProjectV2ItemFieldNumberValue.Number
case "ProjectV2ItemFieldSingleSelectValue":
return v.ProjectV2ItemFieldSingleSelectValue.Name
case "ProjectV2ItemFieldTextValue":
return v.ProjectV2ItemFieldTextValue.Text
case "ProjectV2ItemFieldMilestoneValue":
return struct {
Title string `json:"title"`
Description string `json:"description"`
DueOn string `json:"dueOn"`
}{
Title: v.ProjectV2ItemFieldMilestoneValue.Milestone.Title,
Description: v.ProjectV2ItemFieldMilestoneValue.Milestone.Description,
DueOn: v.ProjectV2ItemFieldMilestoneValue.Milestone.DueOn,
}
case "ProjectV2ItemFieldLabelValue":
names := make([]string, 0)
for _, p := range v.ProjectV2ItemFieldLabelValue.Labels.Nodes {
names = append(names, p.Name)
}
return names
case "ProjectV2ItemFieldPullRequestValue":
urls := make([]string, 0)
for _, p := range v.ProjectV2ItemFieldPullRequestValue.PullRequests.Nodes {
urls = append(urls, p.Url)
}
return urls
case "ProjectV2ItemFieldRepositoryValue":
return v.ProjectV2ItemFieldRepositoryValue.Repository.Url
case "ProjectV2ItemFieldUserValue":
logins := make([]string, 0)
for _, p := range v.ProjectV2ItemFieldUserValue.Users.Nodes {
logins = append(logins, p.Login)
}
return logins
case "ProjectV2ItemFieldReviewerValue":
names := make([]string, 0)
for _, p := range v.ProjectV2ItemFieldReviewerValue.Reviewers.Nodes {
if p.Type == "Team" {
names = append(names, p.Team.Name)
} else if p.Type == "User" {
names = append(names, p.User.Login)
}
}
return names
}
return nil
}
// serialize creates a map from field to field values
func serializeProjectWithItems(project *queries.Project) []map[string]any {
fields := make(map[string]string)
// make a map of fields by ID
for _, f := range project.Fields.Nodes {
fields[f.ID()] = camelCase(f.Name())
}
itemsSlice := make([]map[string]any, 0)
// for each value, look up the name by ID
// and set the value to the field value
for _, i := range project.Items.Nodes {
o := make(map[string]any)
o["id"] = i.Id
o["content"] = projectItemContent(i)
for _, v := range i.FieldValues.Nodes {
id := v.ID()
value := projectFieldValueData(v)
o[fields[id]] = value
}
itemsSlice = append(itemsSlice, o)
}
return itemsSlice
}
// JSONProjectWithItems returns a detailed JSON representation of project items.
// JSON fields are `totalCount` and `items`.
func JSONProjectDetailedItems(project *queries.Project) ProjectDetailedItems {
items := serializeProjectWithItems(project)
return ProjectDetailedItems{
Items: items,
TotalCount: project.Items.TotalCount,
}
}
type ProjectDetailedItems struct {
Items []map[string]any `json:"items"`
TotalCount int `json:"totalCount"`
}
// camelCase converts a string to camelCase, which is useful for turning Go field names to JSON keys.
func camelCase(s string) string {
if len(s) == 0 {
return ""
}
if len(s) == 1 {
return strings.ToLower(s)
}
return strings.ToLower(s[0:1]) + s[1:]
}