feat(format-servers): add json format option

This commit is contained in:
Quentin McGaw
2024-08-16 10:13:55 +00:00
parent 01aaf2c86a
commit 1f2882434a
5 changed files with 92 additions and 47 deletions

View File

@@ -16,7 +16,6 @@ import (
) )
var ( var (
ErrFormatNotRecognized = errors.New("format is not recognized")
ErrProviderUnspecified = errors.New("VPN provider to format was not specified") ErrProviderUnspecified = errors.New("VPN provider to format was not specified")
ErrMultipleProvidersToFormat = errors.New("more than one VPN provider to format were specified") ErrMultipleProvidersToFormat = errors.New("more than one VPN provider to format were specified")
) )
@@ -43,7 +42,7 @@ func (c *CLI) FormatServers(args []string) error {
providersToFormat[provider] = new(bool) providersToFormat[provider] = new(bool)
} }
flagSet := flag.NewFlagSet("format-servers", flag.ExitOnError) flagSet := flag.NewFlagSet("format-servers", flag.ExitOnError)
flagSet.StringVar(&format, "format", "markdown", "Format to use which can be: 'markdown'") flagSet.StringVar(&format, "format", "markdown", "Format to use which can be: 'markdown' or 'json'")
flagSet.StringVar(&output, "output", "/dev/stdout", "Output file to write the formatted data to") flagSet.StringVar(&output, "output", "/dev/stdout", "Output file to write the formatted data to")
titleCaser := cases.Title(language.English) titleCaser := cases.Title(language.English)
for _, provider := range allProviderFlags { for _, provider := range allProviderFlags {
@@ -53,9 +52,7 @@ func (c *CLI) FormatServers(args []string) error {
return err return err
} }
if format != "markdown" { // Note the format is validated by storage.Format
return fmt.Errorf("%w: %s", ErrFormatNotRecognized, format)
}
// Verify only one provider is set to be formatted. // Verify only one provider is set to be formatted.
var providers []string var providers []string
@@ -87,7 +84,10 @@ func (c *CLI) FormatServers(args []string) error {
return fmt.Errorf("creating servers storage: %w", err) return fmt.Errorf("creating servers storage: %w", err)
} }
formatted := storage.FormatToMarkdown(providerToFormat) formatted, err := storage.Format(providerToFormat, format)
if err != nil {
return fmt.Errorf("formatting servers: %w", err)
}
output = filepath.Clean(output) output = filepath.Clean(output)
file, err := os.OpenFile(output, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, 0644) file, err := os.OpenFile(output, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, 0644)

View File

@@ -1,6 +1,7 @@
package models package models
import ( import (
"errors"
"fmt" "fmt"
"strings" "strings"
@@ -90,8 +91,11 @@ func (s *Server) ToMarkdown(headers ...string) (markdown string) {
return "| " + strings.Join(fields, " | ") + " |" return "| " + strings.Join(fields, " | ") + " |"
} }
func (s *Servers) ToMarkdown(vpnProvider string) (markdown string) { func (s *Servers) toMarkdown(vpnProvider string) (formatted string, err error) {
headers := getMarkdownHeaders(vpnProvider) headers, err := getMarkdownHeaders(vpnProvider)
if err != nil {
return "", fmt.Errorf("getting markdown headers: %w", err)
}
legend := markdownTableHeading(headers...) legend := markdownTableHeading(headers...)
@@ -100,63 +104,67 @@ func (s *Servers) ToMarkdown(vpnProvider string) (markdown string) {
entries[i] = server.ToMarkdown(headers...) entries[i] = server.ToMarkdown(headers...)
} }
markdown = legend + "\n" + formatted = legend + "\n" +
strings.Join(entries, "\n") + "\n" strings.Join(entries, "\n") + "\n"
return markdown return formatted, nil
} }
func getMarkdownHeaders(vpnProvider string) (headers []string) { var (
ErrMarkdownHeadersNotDefined = errors.New("markdown headers not defined")
)
func getMarkdownHeaders(vpnProvider string) (headers []string, err error) {
switch vpnProvider { switch vpnProvider {
case providers.Airvpn: case providers.Airvpn:
return []string{regionHeader, countryHeader, cityHeader, vpnHeader, return []string{regionHeader, countryHeader, cityHeader, vpnHeader,
udpHeader, tcpHeader, hostnameHeader, nameHeader} udpHeader, tcpHeader, hostnameHeader, nameHeader}, nil
case providers.Cyberghost: case providers.Cyberghost:
return []string{countryHeader, hostnameHeader, tcpHeader, udpHeader} return []string{countryHeader, hostnameHeader, tcpHeader, udpHeader}, nil
case providers.Expressvpn: case providers.Expressvpn:
return []string{countryHeader, cityHeader, hostnameHeader, tcpHeader, udpHeader} return []string{countryHeader, cityHeader, hostnameHeader, tcpHeader, udpHeader}, nil
case providers.Fastestvpn: case providers.Fastestvpn:
return []string{countryHeader, hostnameHeader, vpnHeader, tcpHeader, udpHeader} return []string{countryHeader, hostnameHeader, vpnHeader, tcpHeader, udpHeader}, nil
case providers.HideMyAss: case providers.HideMyAss:
return []string{countryHeader, regionHeader, cityHeader, hostnameHeader, tcpHeader, udpHeader} return []string{countryHeader, regionHeader, cityHeader, hostnameHeader, tcpHeader, udpHeader}, nil
case providers.Ipvanish: case providers.Ipvanish:
return []string{countryHeader, cityHeader, hostnameHeader, tcpHeader, udpHeader} return []string{countryHeader, cityHeader, hostnameHeader, tcpHeader, udpHeader}, nil
case providers.Ivpn: case providers.Ivpn:
return []string{countryHeader, cityHeader, ispHeader, hostnameHeader, vpnHeader, tcpHeader, udpHeader} return []string{countryHeader, cityHeader, ispHeader, hostnameHeader, vpnHeader, tcpHeader, udpHeader}, nil
case providers.Mullvad: case providers.Mullvad:
return []string{countryHeader, cityHeader, ispHeader, ownedHeader, hostnameHeader, vpnHeader} return []string{countryHeader, cityHeader, ispHeader, ownedHeader, hostnameHeader, vpnHeader}, nil
case providers.Nordvpn: case providers.Nordvpn:
return []string{countryHeader, regionHeader, cityHeader, hostnameHeader, vpnHeader, categoriesHeader} return []string{countryHeader, regionHeader, cityHeader, hostnameHeader, vpnHeader, categoriesHeader}, nil
case providers.Perfectprivacy: case providers.Perfectprivacy:
return []string{cityHeader, tcpHeader, udpHeader} return []string{cityHeader, tcpHeader, udpHeader}, nil
case providers.Privado: case providers.Privado:
return []string{countryHeader, regionHeader, cityHeader, hostnameHeader} return []string{countryHeader, regionHeader, cityHeader, hostnameHeader}, nil
case providers.PrivateInternetAccess: case providers.PrivateInternetAccess:
return []string{regionHeader, hostnameHeader, nameHeader, tcpHeader, udpHeader, portForwardHeader} return []string{regionHeader, hostnameHeader, nameHeader, tcpHeader, udpHeader, portForwardHeader}, nil
case providers.Privatevpn: case providers.Privatevpn:
return []string{countryHeader, cityHeader, hostnameHeader} return []string{countryHeader, cityHeader, hostnameHeader}, nil
case providers.Protonvpn: case providers.Protonvpn:
return []string{countryHeader, regionHeader, cityHeader, hostnameHeader, vpnHeader, return []string{countryHeader, regionHeader, cityHeader, hostnameHeader, vpnHeader,
freeHeader, portForwardHeader, secureHeader, torHeader} freeHeader, portForwardHeader, secureHeader, torHeader}, nil
case providers.Purevpn: case providers.Purevpn:
return []string{countryHeader, regionHeader, cityHeader, hostnameHeader, tcpHeader, udpHeader} return []string{countryHeader, regionHeader, cityHeader, hostnameHeader, tcpHeader, udpHeader}, nil
case providers.SlickVPN: case providers.SlickVPN:
return []string{regionHeader, countryHeader, cityHeader, hostnameHeader} return []string{regionHeader, countryHeader, cityHeader, hostnameHeader}, nil
case providers.Surfshark: case providers.Surfshark:
return []string{regionHeader, countryHeader, cityHeader, hostnameHeader, return []string{regionHeader, countryHeader, cityHeader, hostnameHeader,
vpnHeader, multiHopHeader, tcpHeader, udpHeader} vpnHeader, multiHopHeader, tcpHeader, udpHeader}, nil
case providers.Torguard: case providers.Torguard:
return []string{countryHeader, cityHeader, hostnameHeader, tcpHeader, udpHeader} return []string{countryHeader, cityHeader, hostnameHeader, tcpHeader, udpHeader}, nil
case providers.VPNSecure: case providers.VPNSecure:
return []string{regionHeader, cityHeader, hostnameHeader, premiumHeader} return []string{regionHeader, cityHeader, hostnameHeader, premiumHeader}, nil
case providers.VPNUnlimited: case providers.VPNUnlimited:
return []string{countryHeader, cityHeader, hostnameHeader, freeHeader, streamHeader, tcpHeader, udpHeader} return []string{countryHeader, cityHeader, hostnameHeader, freeHeader, streamHeader, tcpHeader, udpHeader}, nil
case providers.Vyprvpn: case providers.Vyprvpn:
return []string{regionHeader, hostnameHeader, tcpHeader, udpHeader} return []string{regionHeader, hostnameHeader, tcpHeader, udpHeader}, nil
case providers.Wevpn: case providers.Wevpn:
return []string{cityHeader, hostnameHeader, tcpHeader, udpHeader} return []string{cityHeader, hostnameHeader, tcpHeader, udpHeader}, nil
case providers.Windscribe: case providers.Windscribe:
return []string{regionHeader, cityHeader, hostnameHeader, vpnHeader} return []string{regionHeader, cityHeader, hostnameHeader, vpnHeader}, nil
default: default:
return nil return nil, fmt.Errorf("%w: for %s", ErrMarkdownHeadersNotDefined, vpnProvider)
} }
} }

View File

@@ -14,8 +14,15 @@ func Test_Servers_ToMarkdown(t *testing.T) {
testCases := map[string]struct { testCases := map[string]struct {
provider string provider string
servers Servers servers Servers
expectedMarkdown string formatted string
errWrapped error
errMessage string
}{ }{
"unsupported_provider": {
provider: "unsupported",
errWrapped: ErrMarkdownHeadersNotDefined,
errMessage: "getting markdown headers: markdown headers not defined: for unsupported",
},
providers.Cyberghost: { providers.Cyberghost: {
provider: providers.Cyberghost, provider: providers.Cyberghost,
servers: Servers{ servers: Servers{
@@ -24,7 +31,7 @@ func Test_Servers_ToMarkdown(t *testing.T) {
{Country: "b", TCP: true, Hostname: "xb"}, {Country: "b", TCP: true, Hostname: "xb"},
}, },
}, },
expectedMarkdown: "| Country | Hostname | TCP | UDP |\n" + formatted: "| Country | Hostname | TCP | UDP |\n" +
"| --- | --- | --- | --- |\n" + "| --- | --- | --- | --- |\n" +
"| a | `xa` | ❌ | ✅ |\n" + "| a | `xa` | ❌ | ✅ |\n" +
"| b | `xb` | ✅ | ❌ |\n", "| b | `xb` | ✅ | ❌ |\n",
@@ -37,7 +44,7 @@ func Test_Servers_ToMarkdown(t *testing.T) {
{Country: "b", Hostname: "xb", VPN: vpn.OpenVPN, UDP: true}, {Country: "b", Hostname: "xb", VPN: vpn.OpenVPN, UDP: true},
}, },
}, },
expectedMarkdown: "| Country | Hostname | VPN | TCP | UDP |\n" + formatted: "| Country | Hostname | VPN | TCP | UDP |\n" +
"| --- | --- | --- | --- | --- |\n" + "| --- | --- | --- | --- | --- |\n" +
"| a | `xa` | openvpn | ✅ | ❌ |\n" + "| a | `xa` | openvpn | ✅ | ❌ |\n" +
"| b | `xb` | openvpn | ❌ | ✅ |\n", "| b | `xb` | openvpn | ❌ | ✅ |\n",
@@ -49,9 +56,13 @@ func Test_Servers_ToMarkdown(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
t.Parallel() t.Parallel()
markdown := testCase.servers.ToMarkdown(testCase.provider) markdown, err := testCase.servers.toMarkdown(testCase.provider)
assert.Equal(t, testCase.expectedMarkdown, markdown) assert.Equal(t, testCase.formatted, markdown)
assert.ErrorIs(t, err, testCase.errWrapped)
if testCase.errWrapped != nil {
assert.EqualError(t, err, testCase.errMessage)
}
}) })
} }
} }

View File

@@ -3,6 +3,7 @@ package models
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"math" "math"
"reflect" "reflect"
@@ -156,3 +157,29 @@ type Servers struct {
Timestamp int64 `json:"timestamp"` Timestamp int64 `json:"timestamp"`
Servers []Server `json:"servers,omitempty"` Servers []Server `json:"servers,omitempty"`
} }
var (
ErrServersFormatNotSupported = errors.New("servers format not supported")
)
func (s *Servers) Format(vpnProvider, format string) (formatted string, err error) {
switch format {
case "markdown":
return s.toMarkdown(vpnProvider)
case "json":
return s.toJSON()
default:
return "", fmt.Errorf("%w: %s", ErrServersFormatNotSupported, format)
}
}
func (s *Servers) toJSON() (formatted string, err error) {
buffer := bytes.NewBuffer(nil)
encoder := json.NewEncoder(buffer)
encoder.SetIndent("", " ")
err = encoder.Encode(s.Servers)
if err != nil {
return "", fmt.Errorf("encoding servers: %w", err)
}
return buffer.String(), nil
}

View File

@@ -46,19 +46,18 @@ func (s *Storage) GetServersCount(provider string) (count int) {
return len(serversObject.Servers) return len(serversObject.Servers)
} }
// FormatToMarkdown Markdown formats the servers for the provider given // Format formats the servers for the provider using the format given
// and returns the resulting string. // and returns the resulting string.
func (s *Storage) FormatToMarkdown(provider string) (formatted string) { func (s *Storage) Format(provider, format string) (formatted string, err error) {
if provider == providers.Custom { if provider == providers.Custom {
return "" return "", nil
} }
s.mergedMutex.RLock() s.mergedMutex.RLock()
defer s.mergedMutex.RUnlock() defer s.mergedMutex.RUnlock()
serversObject := s.getMergedServersObject(provider) serversObject := s.getMergedServersObject(provider)
formatted = serversObject.ToMarkdown(provider) return serversObject.Format(provider, format)
return formatted
} }
// GetServersCount returns the number of servers for the provider given. // GetServersCount returns the number of servers for the provider given.