Skip to content

Commit

Permalink
Merge pull request ribice#33 from ribice/output-types
Browse files Browse the repository at this point in the history
Add support for csv/json formats and file output
  • Loading branch information
ribice authored Jun 29, 2022
2 parents 2dd9314 + a2ffc59 commit f3705d0
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 36 deletions.
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Golang license and dependency checker. Prints list of all dependencies, their UR

## Introduction

glice analyzes the go.mod file of your project and prints it in a tabular format - name, URL, and license short-name (MIT, GPL...).
glice analyzes the go.mod file of your project and prints it in a tabular format [csv and json available as well] - name, URL, and license short-name (MIT, GPL...).

## Installation

Expand Down Expand Up @@ -40,9 +40,9 @@ Alternatively, you can provide path which you want to be scanned with -p flag:

By default glice:

- Prints only to stdout
- Prints to stdout

- Gets dependencies from go.mod
- Gets dependencies from go.mod

- Fetches licenses for dependencies hosted on GitHub

Expand All @@ -56,7 +56,8 @@ All flags are optional. Glice supports the following flags:
- p [string - path] // Path to be scanned in form of github.com/author/repo
- t [boolean - thanks] // if GitHub API key is provided, setting this flag will star all GitHub repos from dependency. __In order to do this, API key must have access to public_repo__
- v (boolean - verbose) // If enabled, will log dependencies before fetching and printing them.
- fmt (string - format) // Format of the output. Defaults to table, other available options are `csv` and `json`.
- o (string - otuput) // Destination of the output, defaults to stdout. Other option is `file`.
```

Don't forget `-help` flag for detailed usage information.
Expand Down
18 changes: 9 additions & 9 deletions api.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package glice

import (
"context"
"fmt"
"net/http"

"github.com/fatih/color"
Expand All @@ -28,13 +27,14 @@ var licenseCol = map[string]licenseFormat{

// Repository holds information about the repository
type Repository struct {
Name string
Shortname string
URL string
Host string
Author string
Project string
Text string
Name string `json:"name,omitempty"`
Shortname string `json:"-"`
URL string `json:"url,omitempty"`
Host string `json:"host,omitempty"`
Author string `json:"author,omitempty"`
Project string `json:"project,omitempty"`
Text string `json:"-"`
License string `json:"license"`
}

func newGitClient(c context.Context, keys map[string]string, star bool) *gitClient {
Expand Down Expand Up @@ -72,7 +72,6 @@ func (gc *gitClient) GetLicense(ctx context.Context, r *Repository) error {
case "github.com":
rl, _, err := gc.gh.Repositories.License(ctx, r.Author, r.Project)
if err != nil {
fmt.Println(r.Author, r.Project)
return err
}

Expand All @@ -82,6 +81,7 @@ func (gc *gitClient) GetLicense(ctx context.Context, r *Repository) error {
clr = color.FgYellow
}
r.Shortname = color.New(clr).Sprintf(name)
r.License = name
r.Text = rl.GetContent()

if gc.star && gc.gh.logged {
Expand Down
21 changes: 19 additions & 2 deletions cmd/glice/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"flag"
"fmt"
"io"
"log"
"os"
Expand All @@ -16,6 +17,13 @@ func main() {
path = flag.String("p", "", `Path of desired directory to be scanned with Glice (e.g. "github.com/ribice/glice/v2")`)
thx = flag.Bool("t", false, "Stars dependent repos. Needs GITHUB_API_KEY env variable to work")
verbose = flag.Bool("v", false, "Adds verbose logging")
format = flag.String("fmt", "table", "Output format [table | json | csv]")
output = flag.String("o", "stdout", "Output location [stdout | file]")
extension = map[string]string{
"table": "txt",
"json": "json",
"csv": "csv",
}
)

flag.Parse()
Expand All @@ -31,12 +39,21 @@ func main() {
log.SetFlags(0)
}

cl, err := glice.NewClient(*path)
cl, err := glice.NewClient(*path, *format, *output)
checkErr(err)

checkErr(cl.ParseDependencies(*indirect, *thx))

cl.Print(os.Stdout)
switch *output {
case "stdout":
cl.Print(os.Stdout)
case "file":
fileName := fmt.Sprintf("dependencies.%s", extension[*format])
f, err := os.Create(fileName)
checkErr(err)
cl.Print(f)
f.Close()
}

if *fileWrite {
checkErr(cl.WriteLicensesToFile())
Expand Down
82 changes: 68 additions & 14 deletions glice.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package glice
import (
"context"
"encoding/base64"
"encoding/csv"
"encoding/json"
"errors"
"fmt"
"io"
Expand All @@ -25,19 +27,41 @@ var (

// ErrNoAPIKey is returned when thanks flag is enabled without providing GITHUB_API_KEY env variable
ErrNoAPIKey = errors.New("cannot use thanks feature without github api key")

validFormats = map[string]bool{
"table": true,
"json": true,
"csv": true,
}

// validOutputs to print to
validOutputs = map[string]bool{
"stdout": true,
"file": true,
}
)

type Client struct {
dependencies []*Repository
path string
format string
output string
}

func NewClient(path string) (*Client, error) {
func NewClient(path, format, output string) (*Client, error) {
if !validFormats[format] {
return nil, fmt.Errorf("invalid format provided (%s) - allowed ones are [table, json, csv]", output)
}

if !validOutputs[output] {
return nil, fmt.Errorf("invalid output provided (%s) - allowed ones are [stdout, file]", output)
}

if !mod.Exists(path) {
return nil, ErrNoGoMod
}

return &Client{path: path}, nil
return &Client{path: path, format: format, output: output}, nil
}

func (c *Client) ParseDependencies(includeIndirect, thanks bool) error {
Expand Down Expand Up @@ -65,20 +89,51 @@ func (c *Client) ParseDependencies(includeIndirect, thanks bool) error {
return nil
}

func (c *Client) Print(output io.Writer) {
var (
headerRow = []string{"Dependency", "RepoURL", "License"}
)

func (c *Client) Print(writeTo io.Writer) error {
if len(c.dependencies) < 1 {
return
return nil
}
tw := tablewriter.NewWriter(output)
tw.SetHeader([]string{"Dependency", "RepoURL", "License"})
for _, d := range c.dependencies {
tw.Append([]string{d.Name, color.BlueString(d.URL), d.Shortname})

switch c.format {
case "table":
tw := tablewriter.NewWriter(writeTo)
tw.SetHeader(headerRow)
for _, d := range c.dependencies {
tw.Append([]string{d.Name, color.BlueString(d.URL), d.Shortname})
}
tw.Render()
case "json":
return json.NewEncoder(writeTo).Encode(c.dependencies)
case "csv":
csvW := csv.NewWriter(writeTo)
defer csvW.Flush()
err := csvW.Write(headerRow)
if err != nil {
return err
}
for _, d := range c.dependencies {
err = csvW.Write([]string{d.Project, d.URL, d.License})
if err != nil {
return err
}
}
return csvW.Error()
}
tw.Render()

// shouldn't be possible to get this error
return fmt.Errorf("invalid output provided (%s) - allowed ones are [stdout, json, csv]", c.output)
}

func Print(path string, indirect bool, writeTo io.Writer) error {
c, err := NewClient(path)
return PrintTo(path, "table", "stdout", indirect, writeTo)
}

func PrintTo(path, format, output string, indirect bool, writeTo io.Writer) error {
c, err := NewClient(path, format, output)
if err != nil {
return err
}
Expand All @@ -98,10 +153,9 @@ func ListRepositories(path string, withIndirect bool) ([]*Repository, error) {
return nil, err
}

var repos []*Repository

for _, mods := range modules {
repos = append(repos, getRepository(mods))
repos := make([]*Repository, len(modules))
for i, mods := range modules {
repos[i] = getRepository(mods)
}

return repos, nil
Expand Down
70 changes: 63 additions & 7 deletions glice_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func TestClient_ParseDependencies(t *testing.T) {
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
c := &Client{path: tt.path}
c := &Client{path: tt.path, format: "table", output: "stdout"}
if err := c.ParseDependencies(tt.includeIndirect, tt.thanks); (err != nil) != tt.wantErr {
t.Errorf("ParseDependencies() error = %v, wantErr %v", err, tt.wantErr)
}
Expand All @@ -67,7 +67,6 @@ func TestClient_ParseDependencies(t *testing.T) {
}

func TestPrint(t *testing.T) {

tests := map[string]struct {
path string
wantWriteOutput bool
Expand Down Expand Up @@ -97,6 +96,48 @@ func TestPrint(t *testing.T) {
}
}

func TestPrintTo(t *testing.T) {
tests := map[string]struct {
path string
format string
wantWriteOutput bool
wantErr bool
}{
"invalid path": {
path: "invalid",
wantErr: true,
},
"json format": {
path: wd(),
wantWriteOutput: true,
format: "json",
},
"csv format": {
path: wd(),
wantWriteOutput: true,
format: "csv",
},
"valid path": {
path: wd(),
wantWriteOutput: true,
format: "table",
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
writeTo := &bytes.Buffer{}
err := PrintTo(tt.path, tt.format, "stdout", false, writeTo)
if (err != nil) != tt.wantErr {
t.Errorf("Print() error = %v, wantErr %v", err, tt.wantErr)
return
}
if (writeTo.String() != "") != tt.wantWriteOutput {
t.Error("wantWriteOutput and gotOutput do not match")
}
})
}
}

func TestClient_Print(t *testing.T) {
tests := map[string]struct {
dependencies []*Repository
Expand All @@ -110,7 +151,7 @@ func TestClient_Print(t *testing.T) {
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
c := &Client{dependencies: tt.dependencies}
c := &Client{dependencies: tt.dependencies, format: "table", output: "stdout"}
output := &bytes.Buffer{}
c.Print(output)
if (output.String() != "") != tt.wantOutput {
Expand Down Expand Up @@ -150,7 +191,7 @@ func TestClient_WriteLicensesToFile(t *testing.T) {
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
c := &Client{dependencies: tt.dependencies}
c := &Client{dependencies: tt.dependencies, format: "table", output: "stdout"}
err := c.WriteLicensesToFile()
if (err != nil) != tt.wantErr {
t.Errorf("Print() error = %v, wantErr %v", err, tt.wantErr)
Expand Down Expand Up @@ -202,19 +243,34 @@ func TestListRepositories(t *testing.T) {
func TestNewClient(t *testing.T) {
tests := map[string]struct {
path string
output string
format string
wantErr bool
}{
"invalid format": {
format: "invalid",
wantErr: true,
},
"invalid output": {
format: "csv",
output: "invalid",
wantErr: true,
},
"invalid path": {
format: "csv",
output: "stdout",
path: "invalid",
wantErr: true,
},
"valid path": {
path: wd(),
"invalid path": {
format: "csv",
output: "stdout",
path: wd(),
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
_, err := NewClient(tt.path)
_, err := NewClient(tt.path, tt.format, tt.output)
if (err != nil) != tt.wantErr {
t.Errorf("NewClient() error = %v, wantErr %v", err, tt.wantErr)
return
Expand Down

0 comments on commit f3705d0

Please sign in to comment.