diff --git a/pkg/cmd/project/close/close.go b/pkg/cmd/project/close/close.go index 73c61e888..d24278a23 100644 --- a/pkg/cmd/project/close/close.go +++ b/pkg/cmd/project/close/close.go @@ -6,7 +6,6 @@ import ( "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/pkg/cmd/project/shared/client" - "github.com/cli/cli/v2/pkg/cmd/project/shared/format" "github.com/cli/cli/v2/pkg/cmd/project/shared/queries" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/iostreams" @@ -104,7 +103,7 @@ func runClose(config closeConfig) error { } if config.opts.exporter != nil { - return printJSON(config, query.UpdateProjectV2.ProjectV2) + return config.opts.exporter.Write(config.io, query.UpdateProjectV2.ProjectV2) } return printResults(config, query.UpdateProjectV2.ProjectV2) @@ -132,8 +131,3 @@ func printResults(config closeConfig, project queries.Project) error { _, err := fmt.Fprintf(config.io.Out, "%s\n", project.URL) return err } - -func printJSON(config closeConfig, project queries.Project) error { - projectJSON := format.JSONProject(project) - return config.opts.exporter.Write(config.io, projectJSON) -} diff --git a/pkg/cmd/project/copy/copy.go b/pkg/cmd/project/copy/copy.go index 19fa3b0c1..63047f0a3 100644 --- a/pkg/cmd/project/copy/copy.go +++ b/pkg/cmd/project/copy/copy.go @@ -6,7 +6,6 @@ import ( "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/pkg/cmd/project/shared/client" - "github.com/cli/cli/v2/pkg/cmd/project/shared/format" "github.com/cli/cli/v2/pkg/cmd/project/shared/queries" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/iostreams" @@ -114,7 +113,7 @@ func runCopy(config copyConfig) error { } if config.opts.exporter != nil { - return printJSON(config, query.CopyProjectV2.ProjectV2) + return config.opts.exporter.Write(config.io, query.CopyProjectV2.ProjectV2) } return printResults(config, query.CopyProjectV2.ProjectV2) @@ -142,8 +141,3 @@ func printResults(config copyConfig, project queries.Project) error { _, err := fmt.Fprintf(config.io.Out, "%s\n", project.URL) return err } - -func printJSON(config copyConfig, project queries.Project) error { - projectJSON := format.JSONProject(project) - return config.opts.exporter.Write(config.io, projectJSON) -} diff --git a/pkg/cmd/project/create/create.go b/pkg/cmd/project/create/create.go index a914e0229..a261cb600 100644 --- a/pkg/cmd/project/create/create.go +++ b/pkg/cmd/project/create/create.go @@ -5,7 +5,6 @@ import ( "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/pkg/cmd/project/shared/client" - "github.com/cli/cli/v2/pkg/cmd/project/shared/format" "github.com/cli/cli/v2/pkg/cmd/project/shared/queries" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/iostreams" @@ -86,7 +85,7 @@ func runCreate(config createConfig) error { } if config.opts.exporter != nil { - return printJSON(config, query.CreateProjectV2.ProjectV2) + return config.opts.exporter.Write(config.io, query.CreateProjectV2.ProjectV2) } return printResults(config, query.CreateProjectV2.ProjectV2) @@ -113,8 +112,3 @@ func printResults(config createConfig, project queries.Project) error { _, err := fmt.Fprintf(config.io.Out, "%s\n", project.URL) return err } - -func printJSON(config createConfig, project queries.Project) error { - projectJSON := format.JSONProject(project) - return config.opts.exporter.Write(config.io, projectJSON) -} diff --git a/pkg/cmd/project/delete/delete.go b/pkg/cmd/project/delete/delete.go index 52c26ed4d..4848a52ae 100644 --- a/pkg/cmd/project/delete/delete.go +++ b/pkg/cmd/project/delete/delete.go @@ -6,7 +6,6 @@ import ( "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/pkg/cmd/project/shared/client" - "github.com/cli/cli/v2/pkg/cmd/project/shared/format" "github.com/cli/cli/v2/pkg/cmd/project/shared/queries" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/iostreams" @@ -97,7 +96,7 @@ func runDelete(config deleteConfig) error { } if config.opts.exporter != nil { - return printJSON(config, query.DeleteProject.Project) + return config.opts.exporter.Write(config.io, query.DeleteProject.Project) } return printResults(config, query.DeleteProject.Project) @@ -124,8 +123,3 @@ func printResults(config deleteConfig, project queries.Project) error { _, err := fmt.Fprintf(config.io.Out, "Deleted project %d\n", project.Number) return err } - -func printJSON(config deleteConfig, project queries.Project) error { - projectJSON := format.JSONProject(project) - return config.opts.exporter.Write(config.io, projectJSON) -} diff --git a/pkg/cmd/project/edit/edit.go b/pkg/cmd/project/edit/edit.go index 67f01e63d..f303ce357 100644 --- a/pkg/cmd/project/edit/edit.go +++ b/pkg/cmd/project/edit/edit.go @@ -6,7 +6,6 @@ import ( "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/pkg/cmd/project/shared/client" - "github.com/cli/cli/v2/pkg/cmd/project/shared/format" "github.com/cli/cli/v2/pkg/cmd/project/shared/queries" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/iostreams" @@ -111,7 +110,7 @@ func runEdit(config editConfig) error { } if config.opts.exporter != nil { - return printJSON(config, query.UpdateProjectV2.ProjectV2) + return config.opts.exporter.Write(config.io, query.UpdateProjectV2.ProjectV2) } return printResults(config, query.UpdateProjectV2.ProjectV2) @@ -153,8 +152,3 @@ func printResults(config editConfig, project queries.Project) error { _, err := fmt.Fprintf(config.io.Out, "%s\n", project.URL) return err } - -func printJSON(config editConfig, project queries.Project) error { - projectJSON := format.JSONProject(project) - return config.opts.exporter.Write(config.io, projectJSON) -} diff --git a/pkg/cmd/project/field-create/field_create.go b/pkg/cmd/project/field-create/field_create.go index c65c164de..56a305480 100644 --- a/pkg/cmd/project/field-create/field_create.go +++ b/pkg/cmd/project/field-create/field_create.go @@ -6,7 +6,6 @@ import ( "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/pkg/cmd/project/shared/client" - "github.com/cli/cli/v2/pkg/cmd/project/shared/format" "github.com/cli/cli/v2/pkg/cmd/project/shared/queries" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/iostreams" @@ -114,7 +113,7 @@ func runCreateField(config createFieldConfig) error { } if config.opts.exporter != nil { - return printJSON(config, query.CreateProjectV2Field.Field) + return config.opts.exporter.Write(config.io, query.CreateProjectV2Field.Field) } return printResults(config, query.CreateProjectV2Field.Field) @@ -151,8 +150,3 @@ func printResults(config createFieldConfig, field queries.ProjectField) error { _, err := fmt.Fprintf(config.io.Out, "Created field\n") return err } - -func printJSON(config createFieldConfig, field queries.ProjectField) error { - projectFieldJSON := format.JSONProjectField(field) - return config.opts.exporter.Write(config.io, projectFieldJSON) -} diff --git a/pkg/cmd/project/field-delete/field_delete.go b/pkg/cmd/project/field-delete/field_delete.go index a98d10b89..f8b97826b 100644 --- a/pkg/cmd/project/field-delete/field_delete.go +++ b/pkg/cmd/project/field-delete/field_delete.go @@ -4,7 +4,6 @@ import ( "fmt" "github.com/cli/cli/v2/pkg/cmd/project/shared/client" - "github.com/cli/cli/v2/pkg/cmd/project/shared/format" "github.com/cli/cli/v2/pkg/cmd/project/shared/queries" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/iostreams" @@ -71,7 +70,7 @@ func runDeleteField(config deleteFieldConfig) error { } if config.opts.exporter != nil { - return printJSON(config, query.DeleteProjectV2Field.Field) + return config.opts.exporter.Write(config.io, query.DeleteProjectV2Field.Field) } return printResults(config, query.DeleteProjectV2Field.Field) @@ -93,8 +92,3 @@ func printResults(config deleteFieldConfig, field queries.ProjectField) error { _, err := fmt.Fprintf(config.io.Out, "Deleted field\n") return err } - -func printJSON(config deleteFieldConfig, field queries.ProjectField) error { - projectFieldJSON := format.JSONProjectField(field) - return config.opts.exporter.Write(config.io, projectFieldJSON) -} diff --git a/pkg/cmd/project/field-list/field_list.go b/pkg/cmd/project/field-list/field_list.go index 88f261eeb..11964cb52 100644 --- a/pkg/cmd/project/field-list/field_list.go +++ b/pkg/cmd/project/field-list/field_list.go @@ -7,7 +7,6 @@ import ( "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/internal/tableprinter" "github.com/cli/cli/v2/pkg/cmd/project/shared/client" - "github.com/cli/cli/v2/pkg/cmd/project/shared/format" "github.com/cli/cli/v2/pkg/cmd/project/shared/queries" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/iostreams" @@ -95,7 +94,7 @@ func runList(config listConfig) error { } if config.opts.exporter != nil { - return printJSON(config, project) + return config.opts.exporter.Write(config.io, project.Fields) } return printResults(config, project.Fields.Nodes, owner.Login) @@ -117,8 +116,3 @@ func printResults(config listConfig, fields []queries.ProjectField, login string return tp.Render() } - -func printJSON(config listConfig, project *queries.Project) error { - projectsJSON := format.JSONProjectFields(project) - return config.opts.exporter.Write(config.io, projectsJSON) -} diff --git a/pkg/cmd/project/item-add/item_add.go b/pkg/cmd/project/item-add/item_add.go index e3417952d..5e4eeff10 100644 --- a/pkg/cmd/project/item-add/item_add.go +++ b/pkg/cmd/project/item-add/item_add.go @@ -6,7 +6,6 @@ import ( "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/pkg/cmd/project/shared/client" - "github.com/cli/cli/v2/pkg/cmd/project/shared/format" "github.com/cli/cli/v2/pkg/cmd/project/shared/queries" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/iostreams" @@ -108,7 +107,7 @@ func runAddItem(config addItemConfig) error { } if config.opts.exporter != nil { - return printJSON(config, query.CreateProjectItem.ProjectV2Item) + return config.opts.exporter.Write(config.io, query.CreateProjectItem.ProjectV2Item) } return printResults(config, query.CreateProjectItem.ProjectV2Item) @@ -132,8 +131,3 @@ func printResults(config addItemConfig, item queries.ProjectItem) error { _, err := fmt.Fprintf(config.io.Out, "Added item\n") return err } - -func printJSON(config addItemConfig, item queries.ProjectItem) error { - projectItemJSON := format.JSONProjectItem(item) - return config.opts.exporter.Write(config.io, projectItemJSON) -} diff --git a/pkg/cmd/project/item-archive/item_archive.go b/pkg/cmd/project/item-archive/item_archive.go index 5eecd6d8a..18a820e86 100644 --- a/pkg/cmd/project/item-archive/item_archive.go +++ b/pkg/cmd/project/item-archive/item_archive.go @@ -6,7 +6,6 @@ import ( "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/pkg/cmd/project/shared/client" - "github.com/cli/cli/v2/pkg/cmd/project/shared/format" "github.com/cli/cli/v2/pkg/cmd/project/shared/queries" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/iostreams" @@ -110,7 +109,7 @@ func runArchiveItem(config archiveItemConfig) error { } if config.opts.exporter != nil { - return printJSON(config, query.UnarchiveProjectItem.ProjectV2Item) + return config.opts.exporter.Write(config.io, query.UnarchiveProjectItem.ProjectV2Item) } return printResults(config, query.UnarchiveProjectItem.ProjectV2Item) @@ -122,7 +121,7 @@ func runArchiveItem(config archiveItemConfig) error { } if config.opts.exporter != nil { - return printJSON(config, query.ArchiveProjectItem.ProjectV2Item) + return config.opts.exporter.Write(config.io, query.ArchiveProjectItem.ProjectV2Item) } return printResults(config, query.ArchiveProjectItem.ProjectV2Item) @@ -159,8 +158,3 @@ func printResults(config archiveItemConfig, item queries.ProjectItem) error { _, err := fmt.Fprintf(config.io.Out, "Archived item\n") return err } - -func printJSON(config archiveItemConfig, item queries.ProjectItem) error { - projectItemJSON := format.JSONProjectItem(item) - return config.opts.exporter.Write(config.io, projectItemJSON) -} diff --git a/pkg/cmd/project/item-create/item_create.go b/pkg/cmd/project/item-create/item_create.go index 3b686916f..d1775172d 100644 --- a/pkg/cmd/project/item-create/item_create.go +++ b/pkg/cmd/project/item-create/item_create.go @@ -6,7 +6,6 @@ import ( "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/pkg/cmd/project/shared/client" - "github.com/cli/cli/v2/pkg/cmd/project/shared/format" "github.com/cli/cli/v2/pkg/cmd/project/shared/queries" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/iostreams" @@ -104,7 +103,7 @@ func runCreateItem(config createItemConfig) error { } if config.opts.exporter != nil { - return printJSON(config, query.CreateProjectDraftItem.ProjectV2Item) + return config.opts.exporter.Write(config.io, query.CreateProjectDraftItem.ProjectV2Item) } return printResults(config, query.CreateProjectDraftItem.ProjectV2Item) @@ -128,8 +127,3 @@ func printResults(config createItemConfig, item queries.ProjectItem) error { _, err := fmt.Fprintf(config.io.Out, "Created item\n") return err } - -func printJSON(config createItemConfig, item queries.ProjectItem) error { - projectItemJSON := format.JSONProjectItem(item) - return config.opts.exporter.Write(config.io, projectItemJSON) -} diff --git a/pkg/cmd/project/item-edit/item_edit.go b/pkg/cmd/project/item-edit/item_edit.go index b55d0b48f..63dcb3b39 100644 --- a/pkg/cmd/project/item-edit/item_edit.go +++ b/pkg/cmd/project/item-edit/item_edit.go @@ -7,7 +7,6 @@ import ( "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/pkg/cmd/project/shared/client" - "github.com/cli/cli/v2/pkg/cmd/project/shared/format" "github.com/cli/cli/v2/pkg/cmd/project/shared/queries" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/iostreams" @@ -219,11 +218,6 @@ func printDraftIssueResults(config editItemConfig, item queries.DraftIssue) erro return err } -func printDraftIssueJSON(config editItemConfig, item queries.DraftIssue) error { - projectDraftItemJSON := format.JSONProjectDraftIssue(item) - return config.opts.exporter.Write(config.io, projectDraftItemJSON) -} - func printItemResults(config editItemConfig, item *queries.ProjectItem) error { if !config.io.IsStdoutTTY() { return nil @@ -232,11 +226,6 @@ func printItemResults(config editItemConfig, item *queries.ProjectItem) error { return err } -func printItemJSON(config editItemConfig, item *queries.ProjectItem) error { - projectItemJSON := format.JSONProjectItem(*item) - return config.opts.exporter.Write(config.io, projectItemJSON) -} - func clearItemFieldValue(config editItemConfig) error { if err := fieldIdAndProjectIdPresence(config); err != nil { return err @@ -248,7 +237,7 @@ func clearItemFieldValue(config editItemConfig) error { } if config.opts.exporter != nil { - return printItemJSON(config, &query.Clear.Item) + return config.opts.exporter.Write(config.io, &query.Clear.Item) } return printItemResults(config, &query.Clear.Item) @@ -267,7 +256,7 @@ func updateDraftIssue(config editItemConfig) error { } if config.opts.exporter != nil { - return printDraftIssueJSON(config, query.UpdateProjectV2DraftIssue.DraftIssue) + return config.opts.exporter.Write(config.io, query.UpdateProjectV2DraftIssue.DraftIssue) } return printDraftIssueResults(config, query.UpdateProjectV2DraftIssue.DraftIssue) @@ -294,7 +283,7 @@ func updateItemValues(config editItemConfig) error { } if config.opts.exporter != nil { - return printItemJSON(config, &query.Update.Item) + return config.opts.exporter.Write(config.io, &query.Update.Item) } return printItemResults(config, &query.Update.Item) diff --git a/pkg/cmd/project/item-edit/item_edit_test.go b/pkg/cmd/project/item-edit/item_edit_test.go index 9734c2b0d..fb9097d19 100644 --- a/pkg/cmd/project/item-edit/item_edit_test.go +++ b/pkg/cmd/project/item-edit/item_edit_test.go @@ -546,6 +546,7 @@ func TestRunItemEdit_JSON(t *testing.T) { "data": map[string]interface{}{ "updateProjectV2DraftIssue": map[string]interface{}{ "draftIssue": map[string]interface{}{ + "id": "DI_item_id", "title": "a title", "body": "a new body", }, @@ -571,6 +572,6 @@ func TestRunItemEdit_JSON(t *testing.T) { assert.NoError(t, err) assert.JSONEq( t, - `{"id":"","title":"a title","body":"a new body","type":"DraftIssue"}`, + `{"id":"DI_item_id","title":"a title","body":"a new body","type":"DraftIssue"}`, stdout.String()) } diff --git a/pkg/cmd/project/item-list/item_list.go b/pkg/cmd/project/item-list/item_list.go index 5f711f8e8..d62eb2716 100644 --- a/pkg/cmd/project/item-list/item_list.go +++ b/pkg/cmd/project/item-list/item_list.go @@ -7,7 +7,6 @@ import ( "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/internal/tableprinter" "github.com/cli/cli/v2/pkg/cmd/project/shared/client" - "github.com/cli/cli/v2/pkg/cmd/project/shared/format" "github.com/cli/cli/v2/pkg/cmd/project/shared/queries" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/iostreams" @@ -94,7 +93,7 @@ func runList(config listConfig) error { } if config.opts.exporter != nil { - return printJSON(config, project) + return config.opts.exporter.Write(config.io, project.DetailedItems()) } return printResults(config, project.Items.Nodes, owner.Login) @@ -122,8 +121,3 @@ func printResults(config listConfig, items []queries.ProjectItem, login string) return tp.Render() } - -func printJSON(config listConfig, project *queries.Project) error { - projectDetailedItemsJSON := format.JSONProjectDetailedItems(project) - return config.opts.exporter.Write(config.io, projectDetailedItemsJSON) -} diff --git a/pkg/cmd/project/list/list.go b/pkg/cmd/project/list/list.go index e345e3166..ba6e1d747 100644 --- a/pkg/cmd/project/list/list.go +++ b/pkg/cmd/project/list/list.go @@ -97,14 +97,14 @@ func runList(config listConfig) error { return err } - projects, totalCount, err := config.client.Projects(config.opts.owner, owner.Type, config.opts.limit, false) + projects, err := config.client.Projects(config.opts.owner, owner.Type, config.opts.limit, false) if err != nil { return err } projects = filterProjects(projects, config) if config.opts.exporter != nil { - return printJSON(config, projects, totalCount) + return config.opts.exporter.Write(config.io, projects) } return printResults(config, projects, owner.Login) @@ -138,26 +138,29 @@ func buildURL(config listConfig) (string, error) { return url, nil } -func filterProjects(nodes []queries.Project, config listConfig) []queries.Project { - projects := make([]queries.Project, 0, len(nodes)) - for _, p := range nodes { - if !config.opts.closed && p.Closed { +func filterProjects(nodes queries.Projects, config listConfig) queries.Projects { + filtered := queries.Projects{ + Nodes: make([]queries.Project, 0, len(nodes.Nodes)), + TotalCount: nodes.TotalCount, + } + for _, project := range nodes.Nodes { + if !config.opts.closed && project.Closed { continue } - projects = append(projects, p) + filtered.Nodes = append(filtered.Nodes, project) } - return projects + return filtered } -func printResults(config listConfig, projects []queries.Project, owner string) error { - if len(projects) == 0 { +func printResults(config listConfig, projects queries.Projects, owner string) error { + if len(projects.Nodes) == 0 { return cmdutil.NewNoResultsError(fmt.Sprintf("No projects found for %s", owner)) } tp := tableprinter.New(config.io, tableprinter.WithHeader("Number", "Title", "State", "ID")) cs := config.io.ColorScheme() - for _, p := range projects { + for _, p := range projects.Nodes { tp.AddField( strconv.Itoa(int(p.Number)), tableprinter.WithTruncate(nil), @@ -173,8 +176,3 @@ func printResults(config listConfig, projects []queries.Project, owner string) e return tp.Render() } - -func printJSON(config listConfig, projects []queries.Project, totalCount int) error { - projectsJSON := format.JSONProjects(projects, totalCount) - return config.opts.exporter.Write(config.io, projectsJSON) -} diff --git a/pkg/cmd/project/mark-template/mark_template.go b/pkg/cmd/project/mark-template/mark_template.go index 8931a2816..025fe2838 100644 --- a/pkg/cmd/project/mark-template/mark_template.go +++ b/pkg/cmd/project/mark-template/mark_template.go @@ -6,7 +6,6 @@ import ( "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/pkg/cmd/project/shared/client" - "github.com/cli/cli/v2/pkg/cmd/project/shared/format" "github.com/cli/cli/v2/pkg/cmd/project/shared/queries" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/iostreams" @@ -108,7 +107,7 @@ func runMarkTemplate(config markTemplateConfig) error { } if config.opts.exporter != nil { - return printJSON(config, *project) + return config.opts.exporter.Write(config.io, *project) } return printResults(config, query.TemplateProject.Project) @@ -121,7 +120,7 @@ func runMarkTemplate(config markTemplateConfig) error { } if config.opts.exporter != nil { - return printJSON(config, query.TemplateProject.Project) + return config.opts.exporter.Write(config.io, query.TemplateProject.Project) } return printResults(config, query.TemplateProject.Project) @@ -164,8 +163,3 @@ func printResults(config markTemplateConfig, project queries.Project) error { _, err := fmt.Fprintf(config.io.Out, "Marked project %d as a template.\n", project.Number) return err } - -func printJSON(config markTemplateConfig, project queries.Project) error { - projectJSON := format.JSONProject(project) - return config.opts.exporter.Write(config.io, projectJSON) -} diff --git a/pkg/cmd/project/shared/format/json.go b/pkg/cmd/project/shared/format/json.go deleted file mode 100644 index 8f09f8ea4..000000000 --- a/pkg/cmd/project/shared/format/json.go +++ /dev/null @@ -1,386 +0,0 @@ -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:] -} diff --git a/pkg/cmd/project/shared/format/json_test.go b/pkg/cmd/project/shared/queries/export_data_test.go similarity index 61% rename from pkg/cmd/project/shared/format/json_test.go rename to pkg/cmd/project/shared/queries/export_data_test.go index 374fa8751..976839d43 100644 --- a/pkg/cmd/project/shared/format/json_test.go +++ b/pkg/cmd/project/shared/queries/export_data_test.go @@ -1,16 +1,15 @@ -package format +package queries import ( "encoding/json" "testing" - "github.com/cli/cli/v2/pkg/cmd/project/shared/queries" - "github.com/stretchr/testify/assert" ) +// Regression test from before ExportData was implemented. func TestJSONProject_User(t *testing.T) { - project := queries.Project{ + project := Project{ ID: "123", Number: 2, URL: "a url", @@ -23,14 +22,15 @@ func TestJSONProject_User(t *testing.T) { project.Fields.TotalCount = 2 project.Owner.TypeName = "User" project.Owner.User.Login = "monalisa" - b, err := json.Marshal(JSONProject(project)) + b, err := json.Marshal(project.ExportData(nil)) assert.NoError(t, err) - assert.Equal(t, `{"number":2,"url":"a url","shortDescription":"short description","public":true,"closed":false,"title":"","id":"123","readme":"readme","items":{"totalCount":1},"fields":{"totalCount":2},"owner":{"type":"User","login":"monalisa"}}`, string(b)) + assert.JSONEq(t, `{"number":2,"url":"a url","shortDescription":"short description","public":true,"closed":false,"title":"","id":"123","readme":"readme","items":{"totalCount":1},"fields":{"totalCount":2},"owner":{"type":"User","login":"monalisa"}}`, string(b)) } +// Regression test from before ExportData was implemented. func TestJSONProject_Org(t *testing.T) { - project := queries.Project{ + project := Project{ ID: "123", Number: 2, URL: "a url", @@ -43,14 +43,15 @@ func TestJSONProject_Org(t *testing.T) { project.Fields.TotalCount = 2 project.Owner.TypeName = "Organization" project.Owner.Organization.Login = "github" - b, err := json.Marshal(JSONProject(project)) + b, err := json.Marshal(project.ExportData(nil)) assert.NoError(t, err) - assert.Equal(t, `{"number":2,"url":"a url","shortDescription":"short description","public":true,"closed":false,"title":"","id":"123","readme":"readme","items":{"totalCount":1},"fields":{"totalCount":2},"owner":{"type":"Organization","login":"github"}}`, string(b)) + assert.JSONEq(t, `{"number":2,"url":"a url","shortDescription":"short description","public":true,"closed":false,"title":"","id":"123","readme":"readme","items":{"totalCount":1},"fields":{"totalCount":2},"owner":{"type":"Organization","login":"github"}}`, string(b)) } +// Regression test from before ExportData was implemented. func TestJSONProjects(t *testing.T) { - userProject := queries.Project{ + userProject := Project{ ID: "123", Number: 2, URL: "a url", @@ -64,7 +65,7 @@ func TestJSONProjects(t *testing.T) { userProject.Owner.TypeName = "User" userProject.Owner.User.Login = "monalisa" - orgProject := queries.Project{ + orgProject := Project{ ID: "123", Number: 2, URL: "a url", @@ -77,34 +78,38 @@ func TestJSONProjects(t *testing.T) { orgProject.Fields.TotalCount = 2 orgProject.Owner.TypeName = "Organization" orgProject.Owner.Organization.Login = "github" - projectsJSON := JSONProjects([]queries.Project{userProject, orgProject}, 2) - b, err := json.Marshal(projectsJSON) + + projects := Projects{ + Nodes: []Project{userProject, orgProject}, + TotalCount: 2, + } + b, err := json.Marshal(projects.ExportData(nil)) assert.NoError(t, err) - assert.Equal( + assert.JSONEq( t, `{"projects":[{"number":2,"url":"a url","shortDescription":"short description","public":true,"closed":false,"title":"","id":"123","readme":"readme","items":{"totalCount":1},"fields":{"totalCount":2},"owner":{"type":"User","login":"monalisa"}},{"number":2,"url":"a url","shortDescription":"short description","public":true,"closed":false,"title":"","id":"123","readme":"readme","items":{"totalCount":1},"fields":{"totalCount":2},"owner":{"type":"Organization","login":"github"}}],"totalCount":2}`, string(b)) } func TestJSONProjectField_FieldType(t *testing.T) { - field := queries.ProjectField{} + field := ProjectField{} field.TypeName = "ProjectV2Field" field.Field.ID = "123" field.Field.Name = "name" - b, err := json.Marshal(JSONProjectField(field)) + b, err := json.Marshal(field.ExportData(nil)) assert.NoError(t, err) assert.Equal(t, `{"id":"123","name":"name","type":"ProjectV2Field"}`, string(b)) } func TestJSONProjectField_SingleSelectType(t *testing.T) { - field := queries.ProjectField{} + field := ProjectField{} field.TypeName = "ProjectV2SingleSelectField" field.SingleSelectField.ID = "123" field.SingleSelectField.Name = "name" - field.SingleSelectField.Options = []queries.SingleSelectFieldOptions{ + field.SingleSelectField.Options = []SingleSelectFieldOptions{ { ID: "123", Name: "name", @@ -115,35 +120,35 @@ func TestJSONProjectField_SingleSelectType(t *testing.T) { }, } - b, err := json.Marshal(JSONProjectField(field)) + b, err := json.Marshal(field.ExportData(nil)) assert.NoError(t, err) - assert.Equal(t, `{"id":"123","name":"name","type":"ProjectV2SingleSelectField","options":[{"id":"123","name":"name"},{"id":"456","name":"name2"}]}`, string(b)) + assert.JSONEq(t, `{"id":"123","name":"name","type":"ProjectV2SingleSelectField","options":[{"id":"123","name":"name"},{"id":"456","name":"name2"}]}`, string(b)) } func TestJSONProjectField_ProjectV2IterationField(t *testing.T) { - field := queries.ProjectField{} + field := ProjectField{} field.TypeName = "ProjectV2IterationField" field.IterationField.ID = "123" field.IterationField.Name = "name" - b, err := json.Marshal(JSONProjectField(field)) + b, err := json.Marshal(field.ExportData(nil)) assert.NoError(t, err) assert.Equal(t, `{"id":"123","name":"name","type":"ProjectV2IterationField"}`, string(b)) } func TestJSONProjectFields(t *testing.T) { - field := queries.ProjectField{} + field := ProjectField{} field.TypeName = "ProjectV2Field" field.Field.ID = "123" field.Field.Name = "name" - field2 := queries.ProjectField{} + field2 := ProjectField{} field2.TypeName = "ProjectV2SingleSelectField" field2.SingleSelectField.ID = "123" field2.SingleSelectField.Name = "name" - field2.SingleSelectField.Options = []queries.SingleSelectFieldOptions{ + field2.SingleSelectField.Options = []SingleSelectFieldOptions{ { ID: "123", Name: "name", @@ -154,73 +159,72 @@ func TestJSONProjectFields(t *testing.T) { }, } - p := &queries.Project{ + p := &Project{ Fields: struct { TotalCount int - Nodes []queries.ProjectField - PageInfo queries.PageInfo + Nodes []ProjectField + PageInfo PageInfo }{ - Nodes: []queries.ProjectField{field, field2}, + Nodes: []ProjectField{field, field2}, TotalCount: 5, }, } - projectFieldsJSON := JSONProjectFields(p) - b, err := json.Marshal(projectFieldsJSON) + b, err := json.Marshal(p.Fields.ExportData(nil)) assert.NoError(t, err) - assert.Equal(t, `{"fields":[{"id":"123","name":"name","type":"ProjectV2Field"},{"id":"123","name":"name","type":"ProjectV2SingleSelectField","options":[{"id":"123","name":"name"},{"id":"456","name":"name2"}]}],"totalCount":5}`, string(b)) + assert.JSONEq(t, `{"fields":[{"id":"123","name":"name","type":"ProjectV2Field"},{"id":"123","name":"name","type":"ProjectV2SingleSelectField","options":[{"id":"123","name":"name"},{"id":"456","name":"name2"}]}],"totalCount":5}`, string(b)) } func TestJSONProjectItem_DraftIssue(t *testing.T) { - item := queries.ProjectItem{} + item := ProjectItem{} item.Content.TypeName = "DraftIssue" item.Id = "123" item.Content.DraftIssue.Title = "title" item.Content.DraftIssue.Body = "a body" - b, err := json.Marshal(JSONProjectItem(item)) + b, err := json.Marshal(item.ExportData(nil)) assert.NoError(t, err) - assert.Equal(t, `{"id":"123","title":"title","body":"a body","type":"DraftIssue"}`, string(b)) + assert.JSONEq(t, `{"id":"123","title":"title","body":"a body","type":"DraftIssue"}`, string(b)) } func TestJSONProjectItem_Issue(t *testing.T) { - item := queries.ProjectItem{} + item := ProjectItem{} item.Content.TypeName = "Issue" item.Id = "123" item.Content.Issue.Title = "title" item.Content.Issue.Body = "a body" item.Content.Issue.URL = "a-url" - b, err := json.Marshal(JSONProjectItem(item)) + b, err := json.Marshal(item.ExportData(nil)) assert.NoError(t, err) - assert.Equal(t, `{"id":"123","title":"title","body":"a body","type":"Issue","url":"a-url"}`, string(b)) + assert.JSONEq(t, `{"id":"123","title":"title","body":"a body","type":"Issue","url":"a-url"}`, string(b)) } func TestJSONProjectItem_PullRequest(t *testing.T) { - item := queries.ProjectItem{} + item := ProjectItem{} item.Content.TypeName = "PullRequest" item.Id = "123" item.Content.PullRequest.Title = "title" item.Content.PullRequest.Body = "a body" item.Content.PullRequest.URL = "a-url" - b, err := json.Marshal(JSONProjectItem(item)) + b, err := json.Marshal(item.ExportData(nil)) assert.NoError(t, err) - assert.Equal(t, `{"id":"123","title":"title","body":"a body","type":"PullRequest","url":"a-url"}`, string(b)) + assert.JSONEq(t, `{"id":"123","title":"title","body":"a body","type":"PullRequest","url":"a-url"}`, string(b)) } func TestJSONProjectDetailedItems(t *testing.T) { - p := &queries.Project{} + p := &Project{} p.Items.TotalCount = 5 - p.Items.Nodes = []queries.ProjectItem{ + p.Items.Nodes = []ProjectItem{ { Id: "issueId", - Content: queries.ProjectItemContent{ + Content: ProjectItemContent{ TypeName: "Issue", - Issue: queries.Issue{ + Issue: Issue{ Title: "Issue title", Body: "a body", Number: 1, @@ -235,9 +239,9 @@ func TestJSONProjectDetailedItems(t *testing.T) { }, { Id: "pullRequestId", - Content: queries.ProjectItemContent{ + Content: ProjectItemContent{ TypeName: "PullRequest", - PullRequest: queries.PullRequest{ + PullRequest: PullRequest{ Title: "Pull Request title", Body: "a body", Number: 2, @@ -252,9 +256,9 @@ func TestJSONProjectDetailedItems(t *testing.T) { }, { Id: "draftIssueId", - Content: queries.ProjectItemContent{ + Content: ProjectItemContent{ TypeName: "DraftIssue", - DraftIssue: queries.DraftIssue{ + DraftIssue: DraftIssue{ Title: "Pull Request title", Body: "a body", }, @@ -262,56 +266,56 @@ func TestJSONProjectDetailedItems(t *testing.T) { }, } - out, err := json.Marshal(JSONProjectDetailedItems(p)) + out, err := json.Marshal(p.DetailedItems()) assert.NoError(t, err) - assert.Equal( + assert.JSONEq( t, `{"items":[{"content":{"type":"Issue","body":"a body","title":"Issue title","number":1,"repository":"cli/go-gh","url":"issue-url"},"id":"issueId"},{"content":{"type":"PullRequest","body":"a body","title":"Pull Request title","number":2,"repository":"cli/go-gh","url":"pr-url"},"id":"pullRequestId"},{"content":{"type":"DraftIssue","body":"a body","title":"Pull Request title"},"id":"draftIssueId"}],"totalCount":5}`, string(out)) } func TestJSONProjectDraftIssue(t *testing.T) { - item := queries.DraftIssue{} + item := DraftIssue{} item.ID = "123" item.Title = "title" item.Body = "a body" - b, err := json.Marshal(JSONProjectDraftIssue(item)) + b, err := json.Marshal(item.ExportData(nil)) assert.NoError(t, err) - assert.Equal(t, `{"id":"123","title":"title","body":"a body","type":"DraftIssue"}`, string(b)) + assert.JSONEq(t, `{"id":"123","title":"title","body":"a body","type":"DraftIssue"}`, string(b)) } func TestJSONProjectItem_DraftIssue_ProjectV2ItemFieldIterationValue(t *testing.T) { - iterationField := queries.ProjectField{TypeName: "ProjectV2IterationField"} + iterationField := ProjectField{TypeName: "ProjectV2IterationField"} iterationField.IterationField.ID = "sprint" iterationField.IterationField.Name = "Sprint" - iterationFieldValue := queries.FieldValueNodes{Type: "ProjectV2ItemFieldIterationValue"} + iterationFieldValue := FieldValueNodes{Type: "ProjectV2ItemFieldIterationValue"} iterationFieldValue.ProjectV2ItemFieldIterationValue.Title = "Iteration Title" iterationFieldValue.ProjectV2ItemFieldIterationValue.Field = iterationField - draftIssue := queries.ProjectItem{ + draftIssue := ProjectItem{ Id: "draftIssueId", - Content: queries.ProjectItemContent{ + Content: ProjectItemContent{ TypeName: "DraftIssue", - DraftIssue: queries.DraftIssue{ + DraftIssue: DraftIssue{ Title: "Pull Request title", Body: "a body", }, }, } - draftIssue.FieldValues.Nodes = []queries.FieldValueNodes{ + draftIssue.FieldValues.Nodes = []FieldValueNodes{ iterationFieldValue, } - p := &queries.Project{} - p.Fields.Nodes = []queries.ProjectField{iterationField} + p := &Project{} + p.Fields.Nodes = []ProjectField{iterationField} p.Items.TotalCount = 5 - p.Items.Nodes = []queries.ProjectItem{ + p.Items.Nodes = []ProjectItem{ draftIssue, } - out, err := json.Marshal(JSONProjectDetailedItems(p)) + out, err := json.Marshal(p.DetailedItems()) assert.NoError(t, err) assert.JSONEq( t, @@ -321,35 +325,35 @@ func TestJSONProjectItem_DraftIssue_ProjectV2ItemFieldIterationValue(t *testing. } func TestJSONProjectItem_DraftIssue_ProjectV2ItemFieldMilestoneValue(t *testing.T) { - milestoneField := queries.ProjectField{TypeName: "ProjectV2IterationField"} + milestoneField := ProjectField{TypeName: "ProjectV2IterationField"} milestoneField.IterationField.ID = "milestone" milestoneField.IterationField.Name = "Milestone" - milestoneFieldValue := queries.FieldValueNodes{Type: "ProjectV2ItemFieldMilestoneValue"} + milestoneFieldValue := FieldValueNodes{Type: "ProjectV2ItemFieldMilestoneValue"} milestoneFieldValue.ProjectV2ItemFieldMilestoneValue.Milestone.Title = "Milestone Title" milestoneFieldValue.ProjectV2ItemFieldMilestoneValue.Field = milestoneField - draftIssue := queries.ProjectItem{ + draftIssue := ProjectItem{ Id: "draftIssueId", - Content: queries.ProjectItemContent{ + Content: ProjectItemContent{ TypeName: "DraftIssue", - DraftIssue: queries.DraftIssue{ + DraftIssue: DraftIssue{ Title: "Pull Request title", Body: "a body", }, }, } - draftIssue.FieldValues.Nodes = []queries.FieldValueNodes{ + draftIssue.FieldValues.Nodes = []FieldValueNodes{ milestoneFieldValue, } - p := &queries.Project{} - p.Fields.Nodes = []queries.ProjectField{milestoneField} + p := &Project{} + p.Fields.Nodes = []ProjectField{milestoneField} p.Items.TotalCount = 5 - p.Items.Nodes = []queries.ProjectItem{ + p.Items.Nodes = []ProjectItem{ draftIssue, } - out, err := json.Marshal(JSONProjectDetailedItems(p)) + out, err := json.Marshal(p.DetailedItems()) assert.NoError(t, err) assert.JSONEq( t, @@ -357,10 +361,3 @@ func TestJSONProjectItem_DraftIssue_ProjectV2ItemFieldMilestoneValue(t *testing. string(out)) } - -func TestCamelCase(t *testing.T) { - assert.Equal(t, "camelCase", camelCase("camelCase")) - assert.Equal(t, "camelCase", camelCase("CamelCase")) - assert.Equal(t, "c", camelCase("C")) - assert.Equal(t, "", camelCase("")) -} diff --git a/pkg/cmd/project/shared/queries/queries.go b/pkg/cmd/project/shared/queries/queries.go index c1c518848..354cf2cca 100644 --- a/pkg/cmd/project/shared/queries/queries.go +++ b/pkg/cmd/project/shared/queries/queries.go @@ -128,12 +128,8 @@ type Project struct { TotalCount int Nodes []ProjectItem } `graphql:"items(first: $firstItems, after: $afterItems)"` - Fields struct { - TotalCount int - Nodes []ProjectField - PageInfo PageInfo - } `graphql:"fields(first: $firstFields, after: $afterFields)"` - Owner struct { + Fields ProjectFields `graphql:"fields(first: $firstFields, after: $afterFields)"` + Owner struct { TypeName string `graphql:"__typename"` User struct { Login string @@ -144,6 +140,36 @@ type Project struct { } } +func (p Project) DetailedItems() map[string]interface{} { + return map[string]interface{}{ + "items": serializeProjectWithItems(&p), + "totalCount": p.Items.TotalCount, + } +} + +func (p Project) ExportData(_ []string) map[string]interface{} { + return map[string]interface{}{ + "number": p.Number, + "url": p.URL, + "shortDescription": p.ShortDescription, + "public": p.Public, + "closed": p.Closed, + "title": p.Title, + "id": p.ID, + "readme": p.Readme, + "items": map[string]interface{}{ + "totalCount": p.Items.TotalCount, + }, + "fields": map[string]interface{}{ + "totalCount": p.Fields.TotalCount, + }, + "owner": map[string]interface{}{ + "type": p.OwnerType(), + "login": p.OwnerLogin(), + }, + } +} + func (p Project) OwnerType() string { return p.Owner.TypeName } @@ -155,6 +181,22 @@ func (p Project) OwnerLogin() string { return p.Owner.Organization.Login } +type Projects struct { + Nodes []Project + TotalCount int +} + +func (p Projects) ExportData(_ []string) map[string]interface{} { + v := make([]map[string]interface{}, len(p.Nodes)) + for i := range p.Nodes { + v[i] = p.Nodes[i].ExportData(nil) + } + return map[string]interface{}{ + "projects": v, + "totalCount": p.TotalCount, + } +} + // ProjectItem is a ProjectV2Item GraphQL object https://docs.github.com/en/graphql/reference/objects#projectv2item. type ProjectItem struct { Content ProjectItemContent @@ -284,6 +326,19 @@ type DraftIssue struct { Title string } +func (i DraftIssue) ExportData(_ []string) map[string]interface{} { + v := map[string]interface{}{ + "title": i.Title, + "body": i.Body, + "type": "DraftIssue", + } + // Emulate omitempty. + if i.ID != "" { + v["id"] = i.ID + } + return v +} + type PullRequest struct { Body string Title string @@ -294,6 +349,17 @@ type PullRequest struct { } } +func (pr PullRequest) ExportData(_ []string) map[string]interface{} { + return map[string]interface{}{ + "type": "PullRequest", + "body": pr.Body, + "title": pr.Title, + "number": pr.Number, + "repository": pr.Repository.NameWithOwner, + "url": pr.URL, + } +} + type Issue struct { Body string Title string @@ -304,6 +370,50 @@ type Issue struct { } } +func (i Issue) ExportData(_ []string) map[string]interface{} { + return map[string]interface{}{ + "type": "Issue", + "body": i.Body, + "title": i.Title, + "number": i.Number, + "repository": i.Repository.NameWithOwner, + "url": i.URL, + } +} + +func (p ProjectItem) DetailedItem() exportable { + switch p.Type() { + case "DraftIssue": + return DraftIssue{ + Body: p.Body(), + Title: p.Title(), + } + + case "Issue": + return Issue{ + Body: p.Body(), + Title: p.Title(), + Number: p.Number(), + Repository: struct{ NameWithOwner string }{ + NameWithOwner: p.Repo(), + }, + URL: p.URL(), + } + + case "PullRequest": + return PullRequest{ + Body: p.Body(), + Title: p.Title(), + Number: p.Number(), + Repository: struct{ NameWithOwner string }{ + NameWithOwner: p.Repo(), + }, + URL: p.URL(), + } + } + return nil +} + // Type is the underlying type of the project item. func (p ProjectItem) Type() string { return p.Content.TypeName @@ -374,6 +484,20 @@ func (p ProjectItem) URL() string { return "" } +func (p ProjectItem) ExportData(_ []string) map[string]interface{} { + v := map[string]interface{}{ + "id": p.ID(), + "title": p.Title(), + "body": p.Body(), + "type": p.Type(), + } + // Emulate omitempty. + if url := p.URL(); url != "" { + v["url"] = url + } + return v +} + // ProjectItems returns the items of a project. If the OwnerType is VIEWER, no login is required. // If limit is 0, the default limit is used. func (c *Client) ProjectItems(o *Owner, number int32, limit int) (*Project, error) { @@ -631,6 +755,13 @@ type SingleSelectFieldOptions struct { Name string } +func (f SingleSelectFieldOptions) ExportData(_ []string) map[string]interface{} { + return map[string]interface{}{ + "id": f.ID, + "name": f.Name, + } +} + func (p ProjectField) Options() []SingleSelectFieldOptions { if p.TypeName == "ProjectV2SingleSelectField" { var options []SingleSelectFieldOptions @@ -645,6 +776,40 @@ func (p ProjectField) Options() []SingleSelectFieldOptions { return nil } +func (p ProjectField) ExportData(_ []string) map[string]interface{} { + v := map[string]interface{}{ + "id": p.ID(), + "name": p.Name(), + "type": p.Type(), + } + // Emulate omitempty + if opts := p.Options(); len(opts) != 0 { + options := make([]map[string]interface{}, len(opts)) + for i, opt := range opts { + options[i] = opt.ExportData(nil) + } + v["options"] = options + } + return v +} + +type ProjectFields struct { + TotalCount int + Nodes []ProjectField + PageInfo PageInfo +} + +func (p ProjectFields) ExportData(_ []string) map[string]interface{} { + fields := make([]map[string]interface{}, len(p.Nodes)) + for i := range p.Nodes { + fields[i] = p.Nodes[i].ExportData(nil) + } + return map[string]interface{}{ + "fields": fields, + "totalCount": p.TotalCount, + } +} + // ProjectFields returns a project with fields. If the OwnerType is VIEWER, no login is required. // If limit is 0, the default limit is used. func (c *Client) ProjectFields(o *Owner, number int32, limit int) (*Project, error) { @@ -1084,17 +1249,17 @@ func (c *Client) NewProject(canPrompt bool, o *Owner, number int32, fields bool) return nil, fmt.Errorf("project number is required when not running interactively") } - projects, _, err := c.Projects(o.Login, o.Type, 0, fields) + projects, err := c.Projects(o.Login, o.Type, 0, fields) if err != nil { return nil, err } - if len(projects) == 0 { + if len(projects.Nodes) == 0 { return nil, fmt.Errorf("no projects found for %s", o.Login) } - options := make([]string, 0, len(projects)) - for _, p := range projects { + options := make([]string, 0, len(projects.Nodes)) + for _, p := range projects.Nodes { title := fmt.Sprintf("%s (#%d)", p.Title, p.Number) options = append(options, title) } @@ -1104,16 +1269,17 @@ func (c *Client) NewProject(canPrompt bool, o *Owner, number int32, fields bool) return nil, err } - return &projects[answerIndex], nil + return &projects.Nodes[answerIndex], nil } // Projects returns all the projects for an Owner. If the OwnerType is VIEWER, no login is required. // If limit is 0, the default limit is used. -func (c *Client) Projects(login string, t OwnerType, limit int, fields bool) ([]Project, int, error) { - projects := make([]Project, 0) +func (c *Client) Projects(login string, t OwnerType, limit int, fields bool) (Projects, error) { + projects := Projects{ + Nodes: make([]Project, 0), + } cursor := (*githubv4.String)(nil) hasNextPage := false - totalCount := 0 if limit == 0 { limit = LimitDefault @@ -1149,38 +1315,38 @@ func (c *Client) Projects(login string, t OwnerType, limit int, fields bool) ([] if t == UserOwner { var query userProjects if err := c.doQuery("UserProjects", &query, variables); err != nil { - return projects, 0, err + return projects, err } - projects = append(projects, query.Owner.Projects.Nodes...) + projects.Nodes = append(projects.Nodes, query.Owner.Projects.Nodes...) hasNextPage = query.Owner.Projects.PageInfo.HasNextPage cursor = &query.Owner.Projects.PageInfo.EndCursor - totalCount = query.Owner.Projects.TotalCount + projects.TotalCount = query.Owner.Projects.TotalCount } else if t == OrgOwner { var query orgProjects if err := c.doQuery("OrgProjects", &query, variables); err != nil { - return projects, 0, err + return projects, err } - projects = append(projects, query.Owner.Projects.Nodes...) + projects.Nodes = append(projects.Nodes, query.Owner.Projects.Nodes...) hasNextPage = query.Owner.Projects.PageInfo.HasNextPage cursor = &query.Owner.Projects.PageInfo.EndCursor - totalCount = query.Owner.Projects.TotalCount + projects.TotalCount = query.Owner.Projects.TotalCount } else if t == ViewerOwner { var query viewerProjects if err := c.doQuery("ViewerProjects", &query, variables); err != nil { - return projects, 0, err + return projects, err } - projects = append(projects, query.Owner.Projects.Nodes...) + projects.Nodes = append(projects.Nodes, query.Owner.Projects.Nodes...) hasNextPage = query.Owner.Projects.PageInfo.HasNextPage cursor = &query.Owner.Projects.PageInfo.EndCursor - totalCount = query.Owner.Projects.TotalCount + projects.TotalCount = query.Owner.Projects.TotalCount } - if !hasNextPage || len(projects) >= limit { - return projects, totalCount, nil + if !hasNextPage || len(projects.Nodes) >= limit { + return projects, nil } - if len(projects)+LimitMax > limit { - first := limit - len(projects) + if len(projects.Nodes)+LimitMax > limit { + first := limit - len(projects.Nodes) variables["first"] = githubv4.Int(first) } variables["after"] = cursor @@ -1223,3 +1389,107 @@ func requiredScopesFromServerMessage(msg string) []string { } return scopes } + +func projectFieldValueData(v FieldValueNodes) interface{} { + switch v.Type { + case "ProjectV2ItemFieldDateValue": + return v.ProjectV2ItemFieldDateValue.Date + case "ProjectV2ItemFieldIterationValue": + return map[string]interface{}{ + "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 map[string]interface{}{ + "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 *Project) []map[string]interface{} { + 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]interface{}, 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]interface{}) + o["id"] = i.Id + if projectItem := i.DetailedItem(); projectItem != nil { + o["content"] = projectItem.ExportData(nil) + } else { + o["content"] = nil + } + for _, v := range i.FieldValues.Nodes { + id := v.ID() + value := projectFieldValueData(v) + + o[fields[id]] = value + } + itemsSlice = append(itemsSlice, o) + } + return itemsSlice +} + +// 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:] +} + +type exportable interface { + ExportData([]string) map[string]interface{} +} diff --git a/pkg/cmd/project/shared/queries/queries_test.go b/pkg/cmd/project/shared/queries/queries_test.go index 5767929a3..bfdd4d95c 100644 --- a/pkg/cmd/project/shared/queries/queries_test.go +++ b/pkg/cmd/project/shared/queries/queries_test.go @@ -428,3 +428,10 @@ func TestProjectItems_FieldTitle(t *testing.T) { assert.Equal(t, project.Items.Nodes[0].FieldValues.Nodes[0].ProjectV2ItemFieldIterationValue.Title, "Iteration Title 1") assert.Equal(t, project.Items.Nodes[0].FieldValues.Nodes[1].ProjectV2ItemFieldMilestoneValue.Milestone.Title, "Milestone Title 1") } + +func TestCamelCase(t *testing.T) { + assert.Equal(t, "camelCase", camelCase("camelCase")) + assert.Equal(t, "camelCase", camelCase("CamelCase")) + assert.Equal(t, "c", camelCase("C")) + assert.Equal(t, "", camelCase("")) +} diff --git a/pkg/cmd/project/view/view.go b/pkg/cmd/project/view/view.go index 8340149b5..ce54e1f09 100644 --- a/pkg/cmd/project/view/view.go +++ b/pkg/cmd/project/view/view.go @@ -7,7 +7,6 @@ import ( "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/pkg/cmd/project/shared/client" - "github.com/cli/cli/v2/pkg/cmd/project/shared/format" "github.com/cli/cli/v2/pkg/cmd/project/shared/queries" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/iostreams" @@ -107,7 +106,7 @@ func runView(config viewConfig) error { } if config.opts.exporter != nil { - return printJSON(config, *project) + return config.opts.exporter.Write(config.io, *project) } return printResults(config, project) @@ -191,8 +190,3 @@ func printResults(config viewConfig, project *queries.Project) error { _, err = fmt.Fprint(config.io.Out, out) return err } - -func printJSON(config viewConfig, project queries.Project) error { - projectJSON := format.JSONProject(project) - return config.opts.exporter.Write(config.io, projectJSON) -}