Skip to content

Commit

Permalink
feat: integrate Viper's internal encoding package
Browse files Browse the repository at this point in the history
Signed-off-by: Zsolt Rappi <[email protected]>
  • Loading branch information
rappizs committed Aug 18, 2023
1 parent 6315735 commit 8a23efb
Show file tree
Hide file tree
Showing 12 changed files with 393 additions and 34 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,16 @@ in

### Application Configuration

Arguments to the program may be passed on the command line or in a JSON
configuration file.
Arguments to the program may be passed on the command line or in a configuration file.
Currently supported configuration file types are: JSON, YAML, TOML.

For the command line arguments, run:

```console
gh-jira-issue-sync help
```

The JSON format is a single, flat object, with the argument long
The configuration is a single, flat object, with the argument long
names as keys.

Configuration arguments are as follows:
Expand Down
2 changes: 1 addition & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ var RootCmd = &cobra.Command{
logrus.Error(err)
}
if !cfg.IsDryRun() {
if err := cfg.SaveConfig(); err != nil {
if err := cfg.UpdateConfig(); err != nil {
// TODO(log): Better error message
logrus.Error(err)
}
Expand Down
6 changes: 3 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ require (
github.com/fsnotify/fsnotify v1.6.0
github.com/google/go-github/v53 v53.2.0
github.com/magefile/mage v1.15.0
github.com/mitchellh/mapstructure v1.5.0
github.com/pelletier/go-toml/v2 v2.0.8
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.7.0
github.com/spf13/viper v1.16.0
Expand All @@ -25,6 +27,7 @@ require (
github.com/uwu-tools/magex v0.10.0
golang.org/x/oauth2 v0.11.0
golang.org/x/term v0.11.0
gopkg.in/yaml.v3 v3.0.1
sigs.k8s.io/release-sdk v0.10.3
sigs.k8s.io/release-utils v0.7.5-0.20230814131120-e16435f5a2de
)
Expand Down Expand Up @@ -59,9 +62,7 @@ require (
github.com/klauspost/pgzip v1.2.5 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mholt/archiver/v3 v3.5.1 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/nwaples/rardecode v1.1.0 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/pierrec/lz4/v4 v4.1.2 // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
Expand All @@ -85,6 +86,5 @@ require (
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect
)
115 changes: 88 additions & 27 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ package config

import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
Expand All @@ -31,12 +30,17 @@ import (

"github.com/dghubble/oauth1"
"github.com/fsnotify/fsnotify"
"github.com/mitchellh/mapstructure"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
jira "github.com/uwu-tools/go-jira/v2/cloud"
"golang.org/x/term"

"github.com/uwu-tools/gh-jira-issue-sync/internal/encoding"
"github.com/uwu-tools/gh-jira-issue-sync/internal/encoding/json"
"github.com/uwu-tools/gh-jira-issue-sync/internal/encoding/toml"
"github.com/uwu-tools/gh-jira-issue-sync/internal/encoding/yaml"
"github.com/uwu-tools/gh-jira-issue-sync/internal/github"
"github.com/uwu-tools/gh-jira-issue-sync/internal/options"
)
Expand Down Expand Up @@ -78,6 +82,9 @@ type Config struct {
// cmdFile is the file Viper is using for its configuration.
cmdFile string

// cmdFileType is the extension of the config file.
cmdFileType string

// cmdConfig is the Viper configuration object created from the command line and config file.
cmdConfig viper.Viper

Expand All @@ -94,6 +101,9 @@ type Config struct {
// project represents the Jira project the user has requested.
project *jira.Project

// encoderRegistry is used for encoding the config file to supported Viper types.
encoderRegistry *encoding.EncoderRegistry

// since is the parsed value of the `since` configuration parameter, which is the earliest that
// a GitHub issue can have been updated to be retrieved.
since time.Time
Expand Down Expand Up @@ -132,7 +142,8 @@ func New(ctx context.Context, cmd *cobra.Command) (*Config, error) {

log.Debugf("using config file: %s", cfgFilePath)
cfg.cmdFile = cfgFilePath
cfg.cmdConfig = *newViper(options.AppName, cfg.cmdFile)
cfg.cmdFileType = getConfigTypeFromName(cfgFilePath)
cfg.cmdConfig = *newViper(options.AppName, cfg.cmdFile, cfg.cmdFileType)

Check warning on line 146 in internal/config/config.go

View check run for this annotation

Codecov / codecov/patch

internal/config/config.go#L145-L146

Added lines #L145 - L146 were not covered by tests
cfg.cmdConfig.BindPFlags(cmd.Flags()) //nolint:errcheck

cfg.cmdFile = cfg.cmdConfig.ConfigFileUsed()
Expand All @@ -143,6 +154,10 @@ func New(ctx context.Context, cmd *cobra.Command) (*Config, error) {
return nil, err
}

if err := cfg.resetEncoding(); err != nil {
return nil, err
}

Check warning on line 159 in internal/config/config.go

View check run for this annotation

Codecov / codecov/patch

internal/config/config.go#L157-L159

Added lines #L157 - L159 were not covered by tests

return &cfg, nil
}

Expand Down Expand Up @@ -272,24 +287,24 @@ func (c *Config) SetJiraToken(token *oauth1.Token) {

// configFile is a serializable representation of the current Viper configuration.
type configFile struct {
LogLevel string `json:"log-level,omitempty" mapstructure:"log-level"`
GithubToken string `json:"github-token,omitempty" mapstructure:"github-token"`
JiraUser string `json:"jira-user,omitempty" mapstructure:"jira-user"`
JiraPass string `json:"jira-pass,omitempty" mapstructure:"jira-pass"`
JiraToken string `json:"jira-token,omitempty" mapstructure:"jira-token"`
JiraSecret string `json:"jira-secret,omitempty" mapstructure:"jira-secret"`
JiraKey string `json:"jira-private-key-path,omitempty" mapstructure:"jira-private-key-path"`
JiraCKey string `json:"jira-consumer-key,omitempty" mapstructure:"jira-consumer-key"`
RepoName string `json:"repo-name,omitempty" mapstructure:"repo-name"`
JiraURI string `json:"jira-uri,omitempty" mapstructure:"jira-uri"`
JiraProject string `json:"jira-project,omitempty" mapstructure:"jira-project"`
Since string `json:"since,omitempty" mapstructure:"since"`
Confirm bool `json:"confirm,omitempty" mapstructure:"confirm"`
Timeout time.Duration `json:"timeout,omitempty" mapstructure:"timeout"`
LogLevel string `mapstructure:"log-level,omitempty"`
GithubToken string `mapstructure:"github-token,omitempty"`
JiraUser string `mapstructure:"jira-user,omitempty"`
JiraPass string `mapstructure:"jira-pass,omitempty"`
JiraToken string `mapstructure:"jira-token,omitempty"`
JiraSecret string `mapstructure:"jira-secret,omitempty"`
JiraKey string `mapstructure:"jira-private-key-path,omitempty"`
JiraCKey string `mapstructure:"jira-consumer-key,omitempty"`
RepoName string `mapstructure:"repo-name,omitempty"`
JiraURI string `mapstructure:"jira-uri,omitempty"`
JiraProject string `mapstructure:"jira-project,omitempty"`
Since string `mapstructure:"since,omitempty"`
Confirm bool `mapstructure:"confirm,omitempty"`
Timeout time.Duration `mapstructure:"timeout,omitempty"`
}

// SaveConfig updates the `since` parameter to now, then saves the configuration file.
func (c *Config) SaveConfig() error {
// UpdateConfig updates the `since` parameter to now, then saves the configuration file.
func (c *Config) UpdateConfig() error {

Check warning on line 307 in internal/config/config.go

View check run for this annotation

Codecov / codecov/patch

internal/config/config.go#L307

Added line #L307 was not covered by tests
c.cmdConfig.Set(
options.ConfigKeySince,
time.Now().Format(options.DateFormat),
Expand All @@ -300,9 +315,14 @@ func (c *Config) SaveConfig() error {
return fmt.Errorf("unmarshalling config: %w", err)
}

b, err := json.MarshalIndent(cf, "", " ")
cfMap := make(map[string]interface{})
if err := mapstructure.Decode(cf, &cfMap); err != nil {
return fmt.Errorf("decoding configFile struct to map[string]interface{}: %w", err)
}

Check warning on line 321 in internal/config/config.go

View check run for this annotation

Codecov / codecov/patch

internal/config/config.go#L318-L321

Added lines #L318 - L321 were not covered by tests

b, err := c.encoderRegistry.Encode(c.cmdFileType, cfMap)

Check warning on line 323 in internal/config/config.go

View check run for this annotation

Codecov / codecov/patch

internal/config/config.go#L323

Added line #L323 was not covered by tests
if err != nil {
return fmt.Errorf("marshalling config: %w", err)
return fmt.Errorf("encoding config to %s format: %w", c.cmdFileType, err)

Check warning on line 325 in internal/config/config.go

View check run for this annotation

Codecov / codecov/patch

internal/config/config.go#L325

Added line #L325 was not covered by tests
}

f, err := os.OpenFile(c.cmdConfig.ConfigFileUsed(), os.O_RDWR|os.O_TRUNC|os.O_CREATE, 0o644)
Expand All @@ -324,7 +344,7 @@ func (c *Config) SaveConfig() error {
// command line options, configuration file options, and
// default configuration values. This viper object becomes
// the single source of truth for the app configuration.
func newViper(appName, cfgFile string) *viper.Viper {
func newViper(appName, cfgFile, cfgFileType string) *viper.Viper {

Check warning on line 347 in internal/config/config.go

View check run for this annotation

Codecov / codecov/patch

internal/config/config.go#L347

Added line #L347 was not covered by tests
logger := log.New()
v := viper.New()

Expand All @@ -337,7 +357,7 @@ func newViper(appName, cfgFile string) *viper.Viper {
if cfgFile != "" {
v.SetConfigFile(cfgFile)
}
v.SetConfigType(getConfigTypeFromName(cfgFile))
v.SetConfigType(cfgFileType)

Check warning on line 360 in internal/config/config.go

View check run for this annotation

Codecov / codecov/patch

internal/config/config.go#L360

Added line #L360 was not covered by tests

if err := v.ReadInConfig(); err == nil {
log.WithField("file", v.ConfigFileUsed()).Infof("config file loaded")
Expand Down Expand Up @@ -517,6 +537,47 @@ func (c *Config) getFieldIDs(client *jira.Client) (*fields, error) {
return &fieldIDs, nil
}

// resetEncoding creates a new encoding registry with the currently supported file formats.
func (c *Config) resetEncoding() error {
encoderRegistry := encoding.NewEncoderRegistry()
registerEncoder := func(format string, codec encoding.Encoder) error {
if err := encoderRegistry.RegisterEncoder(format, codec); err != nil {
return fmt.Errorf("registering encoder for %s extension: %w", format, err)
}

Check warning on line 546 in internal/config/config.go

View check run for this annotation

Codecov / codecov/patch

internal/config/config.go#L541-L546

Added lines #L541 - L546 were not covered by tests

return nil

Check warning on line 548 in internal/config/config.go

View check run for this annotation

Codecov / codecov/patch

internal/config/config.go#L548

Added line #L548 was not covered by tests
}

{
codec := &yaml.Codec{}
if err := registerEncoder("yaml", codec); err != nil {
return err
}
if err := registerEncoder("yml", codec); err != nil {
return err
}

Check warning on line 558 in internal/config/config.go

View check run for this annotation

Codecov / codecov/patch

internal/config/config.go#L551-L558

Added lines #L551 - L558 were not covered by tests
}

{
codec := &json.Codec{
Indent: " ",
}
if err := registerEncoder("json", codec); err != nil {
return err
}

Check warning on line 567 in internal/config/config.go

View check run for this annotation

Codecov / codecov/patch

internal/config/config.go#L561-L567

Added lines #L561 - L567 were not covered by tests
}

{
codec := &toml.Codec{}
if err := registerEncoder("toml", codec); err != nil {
return err
}

Check warning on line 574 in internal/config/config.go

View check run for this annotation

Codecov / codecov/patch

internal/config/config.go#L570-L574

Added lines #L570 - L574 were not covered by tests
}

c.encoderRegistry = encoderRegistry
return nil

Check warning on line 578 in internal/config/config.go

View check run for this annotation

Codecov / codecov/patch

internal/config/config.go#L577-L578

Added lines #L577 - L578 were not covered by tests
}

// Errors

var (
Expand All @@ -543,12 +604,12 @@ func errCustomFieldIDNotFound(field string) error {
// getConfigTypeFromName extracts the extension from the passed in filename,
// returns `json` if it's empty.
func getConfigTypeFromName(filename string) string {
if filename == "" {
// it's enough to rely on the type from the filename because
// viper will parse and validate the the file's content
ext := filepath.Ext(filename)
if ext == "" {
return "json"
}

// it's enough to rely on the type from the filename because
// viper will parse and validate the the file's content
parts := strings.Split(filename, ".")
return parts[len(parts)-1]
return strings.TrimPrefix(ext, ".")
}
66 changes: 66 additions & 0 deletions internal/encoding/encoder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package encoding

import (
"fmt"
"sync"
)

// Encoder encodes the contents of v into a byte representation.
// It's primarily used for encoding a map[string]interface{} into a file format.
type Encoder interface {
Encode(v map[string]interface{}) ([]byte, error)
}

const (
// ErrEncoderNotFound is returned when there is no encoder registered for a format.
ErrEncoderNotFound = encodingError("encoder not found for this format")

// ErrEncoderFormatAlreadyRegistered is returned when an encoder is already registered for a format.
ErrEncoderFormatAlreadyRegistered = encodingError("encoder already registered for this format")
)

// EncoderRegistry can choose an appropriate Encoder based on the provided format.
type EncoderRegistry struct {
encoders map[string]Encoder

mu sync.RWMutex
}

// NewEncoderRegistry returns a new, initialized EncoderRegistry.
func NewEncoderRegistry() *EncoderRegistry {
return &EncoderRegistry{
encoders: make(map[string]Encoder),
}
}

// RegisterEncoder registers an Encoder for a format.
// Registering a Encoder for an already existing format is not supported.
func (e *EncoderRegistry) RegisterEncoder(format string, enc Encoder) error {
e.mu.Lock()
defer e.mu.Unlock()

if _, ok := e.encoders[format]; ok {
return ErrEncoderFormatAlreadyRegistered
}

e.encoders[format] = enc

return nil
}

func (e *EncoderRegistry) Encode(format string, v map[string]interface{}) ([]byte, error) {
e.mu.RLock()
encoder, ok := e.encoders[format]
e.mu.RUnlock()

if !ok {
return nil, ErrEncoderNotFound
}

b, err := encoder.Encode(v)
if err != nil {
return nil, fmt.Errorf("encoding map to %s format: %w", format, err)
}

return b, nil
}
7 changes: 7 additions & 0 deletions internal/encoding/error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package encoding

type encodingError string

func (e encodingError) Error() string {
return string(e)
}
24 changes: 24 additions & 0 deletions internal/encoding/json/codec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package json

import (
"encoding/json"
"fmt"
)

// Codec implements the encoding.Encoder interface for JSON encoding.
type Codec struct {
// prefix for JSON marshal.
Prefix string

// indentation for JSON marshal.
Indent string
}

func (c *Codec) Encode(v map[string]interface{}) ([]byte, error) {
b, err := json.MarshalIndent(v, c.Prefix, c.Indent)
if err != nil {
return nil, fmt.Errorf("json marshalling value: %w", err)
}

Check warning on line 21 in internal/encoding/json/codec.go

View check run for this annotation

Codecov / codecov/patch

internal/encoding/json/codec.go#L20-L21

Added lines #L20 - L21 were not covered by tests

return b, nil
}
Loading

0 comments on commit 8a23efb

Please sign in to comment.