aboutsummaryrefslogtreecommitdiffstats
path: root/internal/exporter
diff options
context:
space:
mode:
authorAdam Mathes <adam@adammathes.com>2026-02-14 08:58:38 -0800
committerAdam Mathes <adam@adammathes.com>2026-02-14 08:58:38 -0800
commite3c379d069ffa9661561d25cdbf2f5894a2f8ee8 (patch)
tree24d0e9f5610dd9c8f873c5b78e6bc1c88d32840a /internal/exporter
parent4b06155fbde91a1bef6361ef36efb28789861928 (diff)
downloadneko-e3c379d069ffa9661561d25cdbf2f5894a2f8ee8.tar.gz
neko-e3c379d069ffa9661561d25cdbf2f5894a2f8ee8.tar.bz2
neko-e3c379d069ffa9661561d25cdbf2f5894a2f8ee8.zip
Refactor: project structure, implement dependency injection, and align v2 UI with v1
Diffstat (limited to 'internal/exporter')
-rw-r--r--internal/exporter/exporter.go61
-rw-r--r--internal/exporter/exporter_test.go111
2 files changed, 172 insertions, 0 deletions
diff --git a/internal/exporter/exporter.go b/internal/exporter/exporter.go
new file mode 100644
index 0000000..9172fec
--- /dev/null
+++ b/internal/exporter/exporter.go
@@ -0,0 +1,61 @@
+package exporter
+
+import (
+ "adammathes.com/neko/models/feed"
+ "bytes"
+ "encoding/json"
+ "encoding/xml"
+ "fmt"
+ "html/template"
+)
+
+func ExportFeeds(format string) string {
+ feeds, err := feed.All()
+ if err != nil {
+ panic(err)
+ }
+
+ s := ""
+ switch format {
+ case "text":
+ for _, f := range feeds {
+ s = s + fmt.Sprintf("%s\n", f.Url)
+ }
+
+ case "opml":
+ s = s + fmt.Sprintf(`<opml version="2.0"><head><title>neko feeds</title></head><body>`)
+ s = s + fmt.Sprintf("\n")
+ for _, f := range feeds {
+ b, _ := xml.Marshal(f)
+ s = s + fmt.Sprintf("%s\n", string(b))
+ }
+ s = s + fmt.Sprintf(`</body></opml>`)
+
+ case "json":
+ js, _ := json.Marshal(feeds)
+ s = fmt.Sprintf("%s\n", js)
+
+ case "html":
+ htmlTemplateString := `<html>
+<head>
+<title>feeds</title>
+</head>
+<body>
+<ul>
+{{ range . }}
+<li><a href="{{.WebUrl}}">{{.Title}}</a> | <a href="{{.Url}}">xml</a></li>
+{{ end }}
+</ul>
+</body>
+</html>`
+ var bts bytes.Buffer
+ htmlTemplate, err := template.New("feeds").Parse(htmlTemplateString)
+ err = htmlTemplate.Execute(&bts, feeds)
+ if err != nil {
+ panic(err)
+ }
+ s = bts.String()
+ }
+
+ return s
+}
diff --git a/internal/exporter/exporter_test.go b/internal/exporter/exporter_test.go
new file mode 100644
index 0000000..d4cc994
--- /dev/null
+++ b/internal/exporter/exporter_test.go
@@ -0,0 +1,111 @@
+package exporter
+
+import (
+ "encoding/json"
+ "strings"
+ "testing"
+
+ "adammathes.com/neko/config"
+ "adammathes.com/neko/models"
+)
+
+func setupTestDB(t *testing.T) {
+ t.Helper()
+ config.Config.DBFile = ":memory:"
+ models.InitDB()
+ t.Cleanup(func() {
+ if models.DB != nil {
+ models.DB.Close()
+ }
+ })
+}
+
+func seedFeeds(t *testing.T) {
+ t.Helper()
+ _, err := models.DB.Exec("INSERT INTO feed(url, web_url, title, category) VALUES(?, ?, ?, ?)",
+ "https://a.com/feed", "https://a.com", "Alpha Feed", "tech")
+ if err != nil {
+ t.Fatal(err)
+ }
+ _, err = models.DB.Exec("INSERT INTO feed(url, web_url, title, category) VALUES(?, ?, ?, ?)",
+ "https://b.com/feed", "https://b.com", "Beta Feed", "news")
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestExportText(t *testing.T) {
+ setupTestDB(t)
+ seedFeeds(t)
+
+ result := ExportFeeds("text")
+ if !strings.Contains(result, "https://a.com/feed") {
+ t.Error("text export should contain feed URL a")
+ }
+ if !strings.Contains(result, "https://b.com/feed") {
+ t.Error("text export should contain feed URL b")
+ }
+}
+
+func TestExportJSON(t *testing.T) {
+ setupTestDB(t)
+ seedFeeds(t)
+
+ result := ExportFeeds("json")
+ var feeds []interface{}
+ err := json.Unmarshal([]byte(result), &feeds)
+ if err != nil {
+ t.Fatalf("JSON export should be valid JSON: %v", err)
+ }
+ if len(feeds) != 2 {
+ t.Errorf("JSON export should contain 2 feeds, got %d", len(feeds))
+ }
+}
+
+func TestExportOPML(t *testing.T) {
+ setupTestDB(t)
+ seedFeeds(t)
+
+ result := ExportFeeds("opml")
+ if !strings.Contains(result, "<opml") {
+ t.Error("OPML export should contain opml tag")
+ }
+ if !strings.Contains(result, "Alpha Feed") || !strings.Contains(result, "Beta Feed") {
+ t.Error("OPML export should contain feed titles")
+ }
+ if !strings.Contains(result, "</opml>") {
+ t.Error("OPML export should close opml tag")
+ }
+}
+
+func TestExportHTML(t *testing.T) {
+ setupTestDB(t)
+ seedFeeds(t)
+
+ result := ExportFeeds("html")
+ if !strings.Contains(result, "<html>") {
+ t.Error("HTML export should contain html tag")
+ }
+ if !strings.Contains(result, "Alpha Feed") {
+ t.Error("HTML export should contain feed title")
+ }
+}
+
+func TestExportUnknownFormat(t *testing.T) {
+ setupTestDB(t)
+ seedFeeds(t)
+
+ result := ExportFeeds("unknown")
+ if result != "" {
+ t.Errorf("Unknown format should return empty string, got %q", result)
+ }
+}
+
+func TestExportEmpty(t *testing.T) {
+ setupTestDB(t)
+
+ result := ExportFeeds("text")
+ if result != "" {
+ t.Errorf("Export with no feeds should be empty, got %q", result)
+ }
+}