diff --git a/internal/cli/formatservers.go b/internal/cli/formatservers.go index d980f041..4014ace1 100644 --- a/internal/cli/formatservers.go +++ b/internal/cli/formatservers.go @@ -16,7 +16,6 @@ import ( ) var ( - ErrFormatNotRecognized = errors.New("format is not recognized") ErrProviderUnspecified = errors.New("VPN provider to format was not 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) } 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") titleCaser := cases.Title(language.English) for _, provider := range allProviderFlags { @@ -53,9 +52,7 @@ func (c *CLI) FormatServers(args []string) error { return err } - if format != "markdown" { - return fmt.Errorf("%w: %s", ErrFormatNotRecognized, format) - } + // Note the format is validated by storage.Format // Verify only one provider is set to be formatted. var providers []string @@ -87,7 +84,10 @@ func (c *CLI) FormatServers(args []string) error { 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) file, err := os.OpenFile(output, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, 0644) diff --git a/internal/models/markdown.go b/internal/models/markdown.go index 83968b7b..1dc5e619 100644 --- a/internal/models/markdown.go +++ b/internal/models/markdown.go @@ -1,6 +1,7 @@ package models import ( + "errors" "fmt" "strings" @@ -90,8 +91,11 @@ func (s *Server) ToMarkdown(headers ...string) (markdown string) { return "| " + strings.Join(fields, " | ") + " |" } -func (s *Servers) ToMarkdown(vpnProvider string) (markdown string) { - headers := getMarkdownHeaders(vpnProvider) +func (s *Servers) toMarkdown(vpnProvider string) (formatted string, err error) { + headers, err := getMarkdownHeaders(vpnProvider) + if err != nil { + return "", fmt.Errorf("getting markdown headers: %w", err) + } legend := markdownTableHeading(headers...) @@ -100,63 +104,67 @@ func (s *Servers) ToMarkdown(vpnProvider string) (markdown string) { entries[i] = server.ToMarkdown(headers...) } - markdown = legend + "\n" + + formatted = legend + "\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 { case providers.Airvpn: return []string{regionHeader, countryHeader, cityHeader, vpnHeader, - udpHeader, tcpHeader, hostnameHeader, nameHeader} + udpHeader, tcpHeader, hostnameHeader, nameHeader}, nil case providers.Cyberghost: - return []string{countryHeader, hostnameHeader, tcpHeader, udpHeader} + return []string{countryHeader, hostnameHeader, tcpHeader, udpHeader}, nil case providers.Expressvpn: - return []string{countryHeader, cityHeader, hostnameHeader, tcpHeader, udpHeader} + return []string{countryHeader, cityHeader, hostnameHeader, tcpHeader, udpHeader}, nil case providers.Fastestvpn: - return []string{countryHeader, hostnameHeader, vpnHeader, tcpHeader, udpHeader} + return []string{countryHeader, hostnameHeader, vpnHeader, tcpHeader, udpHeader}, nil case providers.HideMyAss: - return []string{countryHeader, regionHeader, cityHeader, hostnameHeader, tcpHeader, udpHeader} + return []string{countryHeader, regionHeader, cityHeader, hostnameHeader, tcpHeader, udpHeader}, nil case providers.Ipvanish: - return []string{countryHeader, cityHeader, hostnameHeader, tcpHeader, udpHeader} + return []string{countryHeader, cityHeader, hostnameHeader, tcpHeader, udpHeader}, nil 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: - return []string{countryHeader, cityHeader, ispHeader, ownedHeader, hostnameHeader, vpnHeader} + return []string{countryHeader, cityHeader, ispHeader, ownedHeader, hostnameHeader, vpnHeader}, nil case providers.Nordvpn: - return []string{countryHeader, regionHeader, cityHeader, hostnameHeader, vpnHeader, categoriesHeader} + return []string{countryHeader, regionHeader, cityHeader, hostnameHeader, vpnHeader, categoriesHeader}, nil case providers.Perfectprivacy: - return []string{cityHeader, tcpHeader, udpHeader} + return []string{cityHeader, tcpHeader, udpHeader}, nil case providers.Privado: - return []string{countryHeader, regionHeader, cityHeader, hostnameHeader} + return []string{countryHeader, regionHeader, cityHeader, hostnameHeader}, nil case providers.PrivateInternetAccess: - return []string{regionHeader, hostnameHeader, nameHeader, tcpHeader, udpHeader, portForwardHeader} + return []string{regionHeader, hostnameHeader, nameHeader, tcpHeader, udpHeader, portForwardHeader}, nil case providers.Privatevpn: - return []string{countryHeader, cityHeader, hostnameHeader} + return []string{countryHeader, cityHeader, hostnameHeader}, nil case providers.Protonvpn: return []string{countryHeader, regionHeader, cityHeader, hostnameHeader, vpnHeader, - freeHeader, portForwardHeader, secureHeader, torHeader} + freeHeader, portForwardHeader, secureHeader, torHeader}, nil case providers.Purevpn: - return []string{countryHeader, regionHeader, cityHeader, hostnameHeader, tcpHeader, udpHeader} + return []string{countryHeader, regionHeader, cityHeader, hostnameHeader, tcpHeader, udpHeader}, nil case providers.SlickVPN: - return []string{regionHeader, countryHeader, cityHeader, hostnameHeader} + return []string{regionHeader, countryHeader, cityHeader, hostnameHeader}, nil case providers.Surfshark: return []string{regionHeader, countryHeader, cityHeader, hostnameHeader, - vpnHeader, multiHopHeader, tcpHeader, udpHeader} + vpnHeader, multiHopHeader, tcpHeader, udpHeader}, nil case providers.Torguard: - return []string{countryHeader, cityHeader, hostnameHeader, tcpHeader, udpHeader} + return []string{countryHeader, cityHeader, hostnameHeader, tcpHeader, udpHeader}, nil case providers.VPNSecure: - return []string{regionHeader, cityHeader, hostnameHeader, premiumHeader} + return []string{regionHeader, cityHeader, hostnameHeader, premiumHeader}, nil 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: - return []string{regionHeader, hostnameHeader, tcpHeader, udpHeader} + return []string{regionHeader, hostnameHeader, tcpHeader, udpHeader}, nil case providers.Wevpn: - return []string{cityHeader, hostnameHeader, tcpHeader, udpHeader} + return []string{cityHeader, hostnameHeader, tcpHeader, udpHeader}, nil case providers.Windscribe: - return []string{regionHeader, cityHeader, hostnameHeader, vpnHeader} + return []string{regionHeader, cityHeader, hostnameHeader, vpnHeader}, nil default: - return nil + return nil, fmt.Errorf("%w: for %s", ErrMarkdownHeadersNotDefined, vpnProvider) } } diff --git a/internal/models/markdown_test.go b/internal/models/markdown_test.go index 764e5da2..c62750cf 100644 --- a/internal/models/markdown_test.go +++ b/internal/models/markdown_test.go @@ -12,10 +12,17 @@ func Test_Servers_ToMarkdown(t *testing.T) { t.Parallel() testCases := map[string]struct { - provider string - servers Servers - expectedMarkdown string + provider string + servers Servers + formatted string + errWrapped error + errMessage string }{ + "unsupported_provider": { + provider: "unsupported", + errWrapped: ErrMarkdownHeadersNotDefined, + errMessage: "getting markdown headers: markdown headers not defined: for unsupported", + }, providers.Cyberghost: { provider: providers.Cyberghost, servers: Servers{ @@ -24,7 +31,7 @@ func Test_Servers_ToMarkdown(t *testing.T) { {Country: "b", TCP: true, Hostname: "xb"}, }, }, - expectedMarkdown: "| Country | Hostname | TCP | UDP |\n" + + formatted: "| Country | Hostname | TCP | UDP |\n" + "| --- | --- | --- | --- |\n" + "| a | `xa` | ❌ | ✅ |\n" + "| b | `xb` | ✅ | ❌ |\n", @@ -37,7 +44,7 @@ func Test_Servers_ToMarkdown(t *testing.T) { {Country: "b", Hostname: "xb", VPN: vpn.OpenVPN, UDP: true}, }, }, - expectedMarkdown: "| Country | Hostname | VPN | TCP | UDP |\n" + + formatted: "| Country | Hostname | VPN | TCP | UDP |\n" + "| --- | --- | --- | --- | --- |\n" + "| a | `xa` | 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.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) + } }) } } diff --git a/internal/models/servers.go b/internal/models/servers.go index 7d5db0dd..7f719086 100644 --- a/internal/models/servers.go +++ b/internal/models/servers.go @@ -3,6 +3,7 @@ package models import ( "bytes" "encoding/json" + "errors" "fmt" "math" "reflect" @@ -156,3 +157,29 @@ type Servers struct { Timestamp int64 `json:"timestamp"` 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 +} diff --git a/internal/storage/servers.go b/internal/storage/servers.go index 88aa4245..76579eb1 100644 --- a/internal/storage/servers.go +++ b/internal/storage/servers.go @@ -46,19 +46,18 @@ func (s *Storage) GetServersCount(provider string) (count int) { 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. -func (s *Storage) FormatToMarkdown(provider string) (formatted string) { +func (s *Storage) Format(provider, format string) (formatted string, err error) { if provider == providers.Custom { - return "" + return "", nil } s.mergedMutex.RLock() defer s.mergedMutex.RUnlock() serversObject := s.getMergedServersObject(provider) - formatted = serversObject.ToMarkdown(provider) - return formatted + return serversObject.Format(provider, format) } // GetServersCount returns the number of servers for the provider given.