From e4008c02d7552cfdc46c1cead976b5bb463b6e6d Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Thu, 12 Jun 2025 17:26:41 +0800 Subject: [PATCH 01/35] star working on parser --- cli/main.go | 114 ++++++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 3 +- go.sum | 32 ++------------- 3 files changed, 119 insertions(+), 30 deletions(-) create mode 100644 cli/main.go diff --git a/cli/main.go b/cli/main.go new file mode 100644 index 00000000..9f282b9d --- /dev/null +++ b/cli/main.go @@ -0,0 +1,114 @@ +package main + +import ( + "fmt" + "gopkg.in/yaml.v3" + "io" + "log" + "net/http" + "regexp" + "strings" +) + +// FrontMatter holds the YAML metadata fields +type FrontMatter struct { + Title string `yaml:"title"` + Description string `yaml:"description"` + Slug string `yaml:"slug"` + Author string `yaml:"author"` +} + +// Post combines front-matter, header image, and markdown content +type Post struct { + FrontMatter + ImageURL string + ImageAlt string + Content string +} + +// fetchMarkdown downloads the markdown file from a public URL +func fetchMarkdown(url string) (string, error) { + resp, err := http.Get(url) + if err != nil { + return "", err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("failed to fetch markdown: status %d", resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + return string(body), nil +} + +// parseMarkdown splits the document into front-matter and content, then parses YAML +// It also extracts a leading Markdown image (header image) if present +func parseMarkdown(data string) (Post, error) { + var post Post + // Expecting format: ---\n---\n + sections := strings.SplitN(data, "---", 3) + if len(sections) < 3 { + return post, fmt.Errorf("invalid front-matter format") + } + + fm := strings.TrimSpace(sections[1]) + body := strings.TrimSpace(sections[2]) + + // Unmarshal YAML into FrontMatter + err := yaml.Unmarshal([]byte(fm), &post.FrontMatter) + if err != nil { + return post, err + } + + // Look for a header image at the top of the content + // Markdown image syntax: ![alt](url) + re := regexp.MustCompile(`^!\[(.*?)\]\((.*?)\)`) + // Split first line from rest of content + parts := strings.SplitN(body, "\n", 2) + first := strings.TrimSpace(parts[0]) + if m := re.FindStringSubmatch(first); len(m) == 3 { + post.ImageAlt = m[1] + post.ImageURL = m[2] + // Remaining content excludes the header image line + if len(parts) > 1 { + post.Content = strings.TrimSpace(parts[1]) + } else { + post.Content = "" + } + } else { + // No header image found; entire body is content + post.ImageAlt = "" + post.ImageURL = "" + post.Content = body + } + + return post, nil +} + +func main() { + url := "https://raw.githubusercontent.com/oullin/content/refs/heads/main/leadership/2025-04-02-embrace-growth-through-movement.md" + + data, err := fetchMarkdown(url) + if err != nil { + log.Fatalf("Error fetching markdown: %v", err) + } + + post, err := parseMarkdown(data) + if err != nil { + log.Fatalf("Error parsing markdown: %v", err) + } + + // Output parsed fields + fmt.Printf("Title: %s\n", post.Title) + fmt.Printf("Description: %s\n", post.Description) + fmt.Printf("Slug: %s\n", post.Slug) + fmt.Printf("Author: %s\n", post.Author) + fmt.Printf("Image URL: %s\n", post.ImageURL) + fmt.Printf("Image Alt: %s\n", post.ImageAlt) + fmt.Println("--- Content ---") + fmt.Println(post.Content) +} diff --git a/go.mod b/go.mod index edd8e591..5c854d03 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/joho/godotenv v1.5.1 github.com/lib/pq v1.10.9 golang.org/x/crypto v0.39.0 + gopkg.in/yaml.v3 v3.0.1 gorm.io/driver/postgres v1.6.0 gorm.io/gorm v1.30.0 ) @@ -17,13 +18,13 @@ require ( github.com/gabriel-vasile/mimetype v1.4.9 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/gocanto/blog v0.0.0-20250606062855-154f82d4ca21 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgx/v5 v5.7.5 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect + github.com/kr/text v0.2.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect golang.org/x/net v0.41.0 // indirect golang.org/x/sync v0.15.0 // indirect diff --git a/go.sum b/go.sum index 1a2e584b..d33cd06b 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,9 @@ +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= -github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= -github.com/getsentry/sentry-go v0.32.0 h1:YKs+//QmwE3DcYtfKRH8/KyOOF/I6Qnx7qYGNHCGmCY= -github.com/getsentry/sentry-go v0.32.0/go.mod h1:CYNcMMz73YigoHljQRG+qPF+eMq8gG72XcGN/p71BAY= github.com/getsentry/sentry-go v0.33.0 h1:YWyDii0KGVov3xOaamOnF0mjOrqSjBqwv48UEzn7QFg= github.com/getsentry/sentry-go v0.33.0/go.mod h1:C55omcY9ChRQIUcVcGcs+Zdy4ZpQGvNJ7JYHIoSWOtE= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= @@ -19,8 +16,6 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= -github.com/gocanto/blog v0.0.0-20250606062855-154f82d4ca21 h1:geDmTIixTWigb/xl7UgJ/6Fvn3YSSUnhgfUkK4yTjbE= -github.com/gocanto/blog v0.0.0-20250606062855-154f82d4ca21/go.mod h1:1/ZDzK7KbNhxsw79APdtsdoSQOfHcnCnkWzZ3PYbLG8= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -29,8 +24,6 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg= -github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs= github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= @@ -43,6 +36,8 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= @@ -55,7 +50,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= -github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -63,43 +57,23 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= -golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= -golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= -golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= -golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= -golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= -golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= -golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= -golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= -golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= -golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= -golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= -golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= -golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314= -gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= -gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= -gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs= gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= From 08d98c1c709a2f378b8a4d91b56f28438d6b9678 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Thu, 12 Jun 2025 17:28:18 +0800 Subject: [PATCH 02/35] format --- cli/main.go | 162 ++++++++++++++++++++++++++-------------------------- 1 file changed, 81 insertions(+), 81 deletions(-) diff --git a/cli/main.go b/cli/main.go index 9f282b9d..2a6a203e 100644 --- a/cli/main.go +++ b/cli/main.go @@ -1,114 +1,114 @@ package main import ( - "fmt" - "gopkg.in/yaml.v3" - "io" - "log" - "net/http" - "regexp" - "strings" + "fmt" + "gopkg.in/yaml.v3" + "io" + "log" + "net/http" + "regexp" + "strings" ) // FrontMatter holds the YAML metadata fields type FrontMatter struct { - Title string `yaml:"title"` - Description string `yaml:"description"` - Slug string `yaml:"slug"` - Author string `yaml:"author"` + Title string `yaml:"title"` + Description string `yaml:"description"` + Slug string `yaml:"slug"` + Author string `yaml:"author"` } // Post combines front-matter, header image, and markdown content type Post struct { - FrontMatter - ImageURL string - ImageAlt string - Content string + FrontMatter + ImageURL string + ImageAlt string + Content string } // fetchMarkdown downloads the markdown file from a public URL func fetchMarkdown(url string) (string, error) { - resp, err := http.Get(url) - if err != nil { - return "", err - } - defer resp.Body.Close() + resp, err := http.Get(url) + if err != nil { + return "", err + } + defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("failed to fetch markdown: status %d", resp.StatusCode) - } + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("failed to fetch markdown: status %d", resp.StatusCode) + } - body, err := io.ReadAll(resp.Body) - if err != nil { - return "", err - } - return string(body), nil + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + return string(body), nil } // parseMarkdown splits the document into front-matter and content, then parses YAML // It also extracts a leading Markdown image (header image) if present func parseMarkdown(data string) (Post, error) { - var post Post - // Expecting format: ---\n---\n - sections := strings.SplitN(data, "---", 3) - if len(sections) < 3 { - return post, fmt.Errorf("invalid front-matter format") - } + var post Post + // Expecting format: ---\n---\n + sections := strings.SplitN(data, "---", 3) + if len(sections) < 3 { + return post, fmt.Errorf("invalid front-matter format") + } - fm := strings.TrimSpace(sections[1]) - body := strings.TrimSpace(sections[2]) + fm := strings.TrimSpace(sections[1]) + body := strings.TrimSpace(sections[2]) - // Unmarshal YAML into FrontMatter - err := yaml.Unmarshal([]byte(fm), &post.FrontMatter) - if err != nil { - return post, err - } + // Unmarshal YAML into FrontMatter + err := yaml.Unmarshal([]byte(fm), &post.FrontMatter) + if err != nil { + return post, err + } - // Look for a header image at the top of the content - // Markdown image syntax: ![alt](url) - re := regexp.MustCompile(`^!\[(.*?)\]\((.*?)\)`) - // Split first line from rest of content - parts := strings.SplitN(body, "\n", 2) - first := strings.TrimSpace(parts[0]) - if m := re.FindStringSubmatch(first); len(m) == 3 { - post.ImageAlt = m[1] - post.ImageURL = m[2] - // Remaining content excludes the header image line - if len(parts) > 1 { - post.Content = strings.TrimSpace(parts[1]) - } else { - post.Content = "" - } - } else { - // No header image found; entire body is content - post.ImageAlt = "" - post.ImageURL = "" - post.Content = body - } + // Look for a header image at the top of the content + // Markdown image syntax: ![alt](url) + re := regexp.MustCompile(`^!\[(.*?)\]\((.*?)\)`) + // Split first line from rest of content + parts := strings.SplitN(body, "\n", 2) + first := strings.TrimSpace(parts[0]) + if m := re.FindStringSubmatch(first); len(m) == 3 { + post.ImageAlt = m[1] + post.ImageURL = m[2] + // Remaining content excludes the header image line + if len(parts) > 1 { + post.Content = strings.TrimSpace(parts[1]) + } else { + post.Content = "" + } + } else { + // No header image found; entire body is content + post.ImageAlt = "" + post.ImageURL = "" + post.Content = body + } - return post, nil + return post, nil } func main() { - url := "https://raw.githubusercontent.com/oullin/content/refs/heads/main/leadership/2025-04-02-embrace-growth-through-movement.md" + url := "https://raw.githubusercontent.com/oullin/content/refs/heads/main/leadership/2025-04-02-embrace-growth-through-movement.md" - data, err := fetchMarkdown(url) - if err != nil { - log.Fatalf("Error fetching markdown: %v", err) - } + data, err := fetchMarkdown(url) + if err != nil { + log.Fatalf("Error fetching markdown: %v", err) + } - post, err := parseMarkdown(data) - if err != nil { - log.Fatalf("Error parsing markdown: %v", err) - } + post, err := parseMarkdown(data) + if err != nil { + log.Fatalf("Error parsing markdown: %v", err) + } - // Output parsed fields - fmt.Printf("Title: %s\n", post.Title) - fmt.Printf("Description: %s\n", post.Description) - fmt.Printf("Slug: %s\n", post.Slug) - fmt.Printf("Author: %s\n", post.Author) - fmt.Printf("Image URL: %s\n", post.ImageURL) - fmt.Printf("Image Alt: %s\n", post.ImageAlt) - fmt.Println("--- Content ---") - fmt.Println(post.Content) + // Output parsed fields + fmt.Printf("Title: %s\n", post.Title) + fmt.Printf("Description: %s\n", post.Description) + fmt.Printf("Slug: %s\n", post.Slug) + fmt.Printf("Author: %s\n", post.Author) + fmt.Printf("Image URL: %s\n", post.ImageURL) + fmt.Printf("Image Alt: %s\n", post.ImageAlt) + fmt.Println("--- Content ---") + fmt.Println(post.Content) } From b9e3b03a4fee6c82b4d9065d4152a0f26be6abbf Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Fri, 13 Jun 2025 10:55:41 +0800 Subject: [PATCH 03/35] better front-matter --- cli/main.go | 189 +++++++++++++++++++++++++++------------------------- 1 file changed, 98 insertions(+), 91 deletions(-) diff --git a/cli/main.go b/cli/main.go index 2a6a203e..44b639c4 100644 --- a/cli/main.go +++ b/cli/main.go @@ -1,114 +1,121 @@ package main import ( - "fmt" - "gopkg.in/yaml.v3" - "io" - "log" - "net/http" - "regexp" - "strings" + "fmt" + "gopkg.in/yaml.v3" + "io" + "log" + "net/http" + "regexp" + "strings" ) // FrontMatter holds the YAML metadata fields type FrontMatter struct { - Title string `yaml:"title"` - Description string `yaml:"description"` - Slug string `yaml:"slug"` - Author string `yaml:"author"` + Title string `yaml:"title"` + Excerpt string `yaml:"excerpt"` + Slug string `yaml:"slug"` + Author string `yaml:"author"` + Category string `yaml:"category"` + Tags []string `yaml:"tags"` } // Post combines front-matter, header image, and markdown content type Post struct { - FrontMatter - ImageURL string - ImageAlt string - Content string + FrontMatter + ImageURL string + ImageAlt string + Content string } -// fetchMarkdown downloads the markdown file from a public URL +// fetchMarkdown downloads the Markdown file from a public URL func fetchMarkdown(url string) (string, error) { - resp, err := http.Get(url) - if err != nil { - return "", err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("failed to fetch markdown: status %d", resp.StatusCode) - } - - body, err := io.ReadAll(resp.Body) - if err != nil { - return "", err - } - return string(body), nil + resp, err := http.Get(url) + if err != nil { + return "", err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("failed to fetch markdown: status %d", resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + return string(body), nil } // parseMarkdown splits the document into front-matter and content, then parses YAML // It also extracts a leading Markdown image (header image) if present func parseMarkdown(data string) (Post, error) { - var post Post - // Expecting format: ---\n---\n - sections := strings.SplitN(data, "---", 3) - if len(sections) < 3 { - return post, fmt.Errorf("invalid front-matter format") - } - - fm := strings.TrimSpace(sections[1]) - body := strings.TrimSpace(sections[2]) - - // Unmarshal YAML into FrontMatter - err := yaml.Unmarshal([]byte(fm), &post.FrontMatter) - if err != nil { - return post, err - } - - // Look for a header image at the top of the content - // Markdown image syntax: ![alt](url) - re := regexp.MustCompile(`^!\[(.*?)\]\((.*?)\)`) - // Split first line from rest of content - parts := strings.SplitN(body, "\n", 2) - first := strings.TrimSpace(parts[0]) - if m := re.FindStringSubmatch(first); len(m) == 3 { - post.ImageAlt = m[1] - post.ImageURL = m[2] - // Remaining content excludes the header image line - if len(parts) > 1 { - post.Content = strings.TrimSpace(parts[1]) - } else { - post.Content = "" - } - } else { - // No header image found; entire body is content - post.ImageAlt = "" - post.ImageURL = "" - post.Content = body - } - - return post, nil + var post Post + // Expecting format: ---\n---\n + sections := strings.SplitN(data, "---", 3) + if len(sections) < 3 { + return post, fmt.Errorf("invalid front-matter format") + } + + fm := strings.TrimSpace(sections[1]) + body := strings.TrimSpace(sections[2]) + + // Unmarshal YAML into FrontMatter + err := yaml.Unmarshal([]byte(fm), &post.FrontMatter) + if err != nil { + return post, err + } + + // Look for a header image at the top of the content + // Markdown image syntax: ![alt](url) + re := regexp.MustCompile(`^!\[(.*?)\]\((.*?)\)`) + + // Split first line from rest of content + parts := strings.SplitN(body, "\n", 2) + first := strings.TrimSpace(parts[0]) + + if m := re.FindStringSubmatch(first); len(m) == 3 { + post.ImageAlt = m[1] + post.ImageURL = m[2] + + // Remaining content excludes the header image line + if len(parts) > 1 { + post.Content = strings.TrimSpace(parts[1]) + } else { + post.Content = "" + } + } else { + // No header image found; the entire body is content + post.ImageAlt = "" + post.ImageURL = "" + post.Content = body + } + + return post, nil } func main() { - url := "https://raw.githubusercontent.com/oullin/content/refs/heads/main/leadership/2025-04-02-embrace-growth-through-movement.md" - - data, err := fetchMarkdown(url) - if err != nil { - log.Fatalf("Error fetching markdown: %v", err) - } - - post, err := parseMarkdown(data) - if err != nil { - log.Fatalf("Error parsing markdown: %v", err) - } - - // Output parsed fields - fmt.Printf("Title: %s\n", post.Title) - fmt.Printf("Description: %s\n", post.Description) - fmt.Printf("Slug: %s\n", post.Slug) - fmt.Printf("Author: %s\n", post.Author) - fmt.Printf("Image URL: %s\n", post.ImageURL) - fmt.Printf("Image Alt: %s\n", post.ImageAlt) - fmt.Println("--- Content ---") - fmt.Println(post.Content) + url := "https://raw.githubusercontent.com/oullin/content/refs/heads/main/leadership/2025-04-02-embrace-growth-through-movement.md" + + data, err := fetchMarkdown(url) + if err != nil { + log.Fatalf("Error fetching markdown: %v", err) + } + + post, err := parseMarkdown(data) + if err != nil { + log.Fatalf("Error parsing markdown: %v", err) + } + + // Output parsed fields + fmt.Printf("Title: %s\n", post.Title) + fmt.Printf("Excerpt: %s\n", post.Excerpt) + fmt.Printf("Slug: %s\n", post.Slug) + fmt.Printf("Author: %s\n", post.Author) + fmt.Printf("Image URL: %s\n", post.ImageURL) + fmt.Printf("Image Alt: %s\n", post.ImageAlt) + fmt.Printf("Category Alt: %s\n", post.Category) + fmt.Printf("Tags Alt: %s\n", post.Tags) + fmt.Println("--- Content ---") + fmt.Println(post.Content) } From 5e381aa608b82213a9683d35471963ef6e301412 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Fri, 13 Jun 2025 11:00:49 +0800 Subject: [PATCH 04/35] purge cache --- cli/main.go | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/cli/main.go b/cli/main.go index 44b639c4..d5a1ab94 100644 --- a/cli/main.go +++ b/cli/main.go @@ -8,6 +8,7 @@ import ( "net/http" "regexp" "strings" + "time" ) // FrontMatter holds the YAML metadata fields @@ -30,7 +31,23 @@ type Post struct { // fetchMarkdown downloads the Markdown file from a public URL func fetchMarkdown(url string) (string, error) { - resp, err := http.Get(url) + // Bust CDN or proxy caches by adding a unique timestamp + sep := "?" + if strings.Contains(url, "?") { + sep = "&" + } + timestampedURL := fmt.Sprintf("%s%sts=%d", url, sep, time.Now().UnixNano()) + + req, err := http.NewRequest("GET", timestampedURL, nil) + if err != nil { + return "", err + } + // Instruct intermediate caches to revalidate + req.Header.Set("Cache-Control", "no-cache") + req.Header.Set("Pragma", "no-cache") + + client := &http.Client{} + resp, err := client.Do(req) if err != nil { return "", err } @@ -44,6 +61,7 @@ func fetchMarkdown(url string) (string, error) { if err != nil { return "", err } + return string(body), nil } From 2291d44f1d2bc6caafb69d793abf6ecf7dd315d2 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Fri, 13 Jun 2025 11:26:03 +0800 Subject: [PATCH 05/35] extract pkg --- cli/main.go | 112 ++------------------------------------------ pkg/flow/handler.go | 94 +++++++++++++++++++++++++++++++++++++ pkg/flow/schema.go | 17 +++++++ 3 files changed, 114 insertions(+), 109 deletions(-) create mode 100644 pkg/flow/handler.go create mode 100644 pkg/flow/schema.go diff --git a/cli/main.go b/cli/main.go index d5a1ab94..158fba2e 100644 --- a/cli/main.go +++ b/cli/main.go @@ -2,125 +2,19 @@ package main import ( "fmt" - "gopkg.in/yaml.v3" - "io" + "github.com/oullin/pkg/flow" "log" - "net/http" - "regexp" - "strings" - "time" ) -// FrontMatter holds the YAML metadata fields -type FrontMatter struct { - Title string `yaml:"title"` - Excerpt string `yaml:"excerpt"` - Slug string `yaml:"slug"` - Author string `yaml:"author"` - Category string `yaml:"category"` - Tags []string `yaml:"tags"` -} - -// Post combines front-matter, header image, and markdown content -type Post struct { - FrontMatter - ImageURL string - ImageAlt string - Content string -} - -// fetchMarkdown downloads the Markdown file from a public URL -func fetchMarkdown(url string) (string, error) { - // Bust CDN or proxy caches by adding a unique timestamp - sep := "?" - if strings.Contains(url, "?") { - sep = "&" - } - timestampedURL := fmt.Sprintf("%s%sts=%d", url, sep, time.Now().UnixNano()) - - req, err := http.NewRequest("GET", timestampedURL, nil) - if err != nil { - return "", err - } - // Instruct intermediate caches to revalidate - req.Header.Set("Cache-Control", "no-cache") - req.Header.Set("Pragma", "no-cache") - - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - return "", err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("failed to fetch markdown: status %d", resp.StatusCode) - } - - body, err := io.ReadAll(resp.Body) - if err != nil { - return "", err - } - - return string(body), nil -} - -// parseMarkdown splits the document into front-matter and content, then parses YAML -// It also extracts a leading Markdown image (header image) if present -func parseMarkdown(data string) (Post, error) { - var post Post - // Expecting format: ---\n---\n - sections := strings.SplitN(data, "---", 3) - if len(sections) < 3 { - return post, fmt.Errorf("invalid front-matter format") - } - - fm := strings.TrimSpace(sections[1]) - body := strings.TrimSpace(sections[2]) - - // Unmarshal YAML into FrontMatter - err := yaml.Unmarshal([]byte(fm), &post.FrontMatter) - if err != nil { - return post, err - } - - // Look for a header image at the top of the content - // Markdown image syntax: ![alt](url) - re := regexp.MustCompile(`^!\[(.*?)\]\((.*?)\)`) - - // Split first line from rest of content - parts := strings.SplitN(body, "\n", 2) - first := strings.TrimSpace(parts[0]) - - if m := re.FindStringSubmatch(first); len(m) == 3 { - post.ImageAlt = m[1] - post.ImageURL = m[2] - - // Remaining content excludes the header image line - if len(parts) > 1 { - post.Content = strings.TrimSpace(parts[1]) - } else { - post.Content = "" - } - } else { - // No header image found; the entire body is content - post.ImageAlt = "" - post.ImageURL = "" - post.Content = body - } - - return post, nil -} - func main() { url := "https://raw.githubusercontent.com/oullin/content/refs/heads/main/leadership/2025-04-02-embrace-growth-through-movement.md" - data, err := fetchMarkdown(url) + data, err := flow.FetchMarkdown(url) if err != nil { log.Fatalf("Error fetching markdown: %v", err) } - post, err := parseMarkdown(data) + post, err := flow.ParseMarkdown(data) if err != nil { log.Fatalf("Error parsing markdown: %v", err) } diff --git a/pkg/flow/handler.go b/pkg/flow/handler.go new file mode 100644 index 00000000..0ee6051f --- /dev/null +++ b/pkg/flow/handler.go @@ -0,0 +1,94 @@ +package flow + +import ( + "fmt" + "gopkg.in/yaml.v3" + "io" + "net/http" + "regexp" + "strings" + "time" +) + +// FetchMarkdown downloads the Markdown file from a public URL +func FetchMarkdown(url string) (string, error) { + // Bust CDN or proxy caches by adding a unique timestamp + sep := "?" + if strings.Contains(url, "?") { + sep = "&" + } + timestampedURL := fmt.Sprintf("%s%sts=%d", url, sep, time.Now().UnixNano()) + + req, err := http.NewRequest("GET", timestampedURL, nil) + if err != nil { + return "", err + } + // Instruct intermediate caches to revalidate + req.Header.Set("Cache-Control", "no-cache") + req.Header.Set("Pragma", "no-cache") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("failed to fetch markdown: status %d", resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + + return string(body), nil +} + +// ParseMarkdown splits the document into front-matter and content, then parses YAML +// It also extracts a leading Markdown image (header image) if present +func ParseMarkdown(data string) (Post, error) { + var post Post + // Expecting format: ---\n---\n + sections := strings.SplitN(data, "---", 3) + if len(sections) < 3 { + return post, fmt.Errorf("invalid front-matter format") + } + + fm := strings.TrimSpace(sections[1]) + body := strings.TrimSpace(sections[2]) + + // Unmarshal YAML into FrontMatter + err := yaml.Unmarshal([]byte(fm), &post.FrontMatter) + if err != nil { + return post, err + } + + // Look for a header image at the top of the content + // Markdown image syntax: ![alt](url) + re := regexp.MustCompile(`^!\[(.*?)\]\((.*?)\)`) + + // Split first line from rest of content + parts := strings.SplitN(body, "\n", 2) + first := strings.TrimSpace(parts[0]) + + if m := re.FindStringSubmatch(first); len(m) == 3 { + post.ImageAlt = m[1] + post.ImageURL = m[2] + + // Remaining content excludes the header image line + if len(parts) > 1 { + post.Content = strings.TrimSpace(parts[1]) + } else { + post.Content = "" + } + } else { + // No header image found; the entire body is content + post.ImageAlt = "" + post.ImageURL = "" + post.Content = body + } + + return post, nil +} diff --git a/pkg/flow/schema.go b/pkg/flow/schema.go new file mode 100644 index 00000000..bb355cb5 --- /dev/null +++ b/pkg/flow/schema.go @@ -0,0 +1,17 @@ +package flow + +type FrontMatter struct { + Title string `yaml:"title"` + Excerpt string `yaml:"excerpt"` + Slug string `yaml:"slug"` + Author string `yaml:"author"` + Category string `yaml:"category"` + Tags []string `yaml:"tags"` +} + +type Post struct { + FrontMatter + ImageURL string + ImageAlt string + Content string +} From 9f1a6587297bb312e11cd5223e555ff7192d6db0 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Fri, 13 Jun 2025 11:38:07 +0800 Subject: [PATCH 06/35] start working on abstraction --- cli/main.go | 33 ----------------------------- cli/post.go | 35 +++++++++++++++++++++++++++++++ pkg/{flow => markdown}/handler.go | 18 ++++++++-------- pkg/{flow => markdown}/schema.go | 6 +++++- 4 files changed, 49 insertions(+), 43 deletions(-) delete mode 100644 cli/main.go create mode 100644 cli/post.go rename pkg/{flow => markdown}/handler.go (80%) rename pkg/{flow => markdown}/schema.go (86%) diff --git a/cli/main.go b/cli/main.go deleted file mode 100644 index 158fba2e..00000000 --- a/cli/main.go +++ /dev/null @@ -1,33 +0,0 @@ -package main - -import ( - "fmt" - "github.com/oullin/pkg/flow" - "log" -) - -func main() { - url := "https://raw.githubusercontent.com/oullin/content/refs/heads/main/leadership/2025-04-02-embrace-growth-through-movement.md" - - data, err := flow.FetchMarkdown(url) - if err != nil { - log.Fatalf("Error fetching markdown: %v", err) - } - - post, err := flow.ParseMarkdown(data) - if err != nil { - log.Fatalf("Error parsing markdown: %v", err) - } - - // Output parsed fields - fmt.Printf("Title: %s\n", post.Title) - fmt.Printf("Excerpt: %s\n", post.Excerpt) - fmt.Printf("Slug: %s\n", post.Slug) - fmt.Printf("Author: %s\n", post.Author) - fmt.Printf("Image URL: %s\n", post.ImageURL) - fmt.Printf("Image Alt: %s\n", post.ImageAlt) - fmt.Printf("Category Alt: %s\n", post.Category) - fmt.Printf("Tags Alt: %s\n", post.Tags) - fmt.Println("--- Content ---") - fmt.Println(post.Content) -} diff --git a/cli/post.go b/cli/post.go new file mode 100644 index 00000000..3e5f48cd --- /dev/null +++ b/cli/post.go @@ -0,0 +1,35 @@ +package main + +import ( + "fmt" + "github.com/oullin/pkg/markdown" + "log" +) + +func main() { + file := markdown.File{ + Url: "https://raw.githubusercontent.com/oullin/content/refs/heads/main/leadership/2025-04-02-embrace-growth-through-movement.md", + } + + data, err := markdown.Fetch(file) + if err != nil { + log.Fatalf("Error fetching markdown: %v", err) + } + + post, err := markdown.Parse(data) + if err != nil { + log.Fatalf("Error parsing markdown: %v", err) + } + + // Output parsed fields + fmt.Printf("Title: %s\n", post.Title) + fmt.Printf("Excerpt: %s\n", post.Excerpt) + fmt.Printf("Slug: %s\n", post.Slug) + fmt.Printf("Author: %s\n", post.Author) + fmt.Printf("Image URL: %s\n", post.ImageURL) + fmt.Printf("Image Alt: %s\n", post.ImageAlt) + fmt.Printf("Category Alt: %s\n", post.Category) + fmt.Printf("Tags Alt: %s\n", post.Tags) + fmt.Println("--- Content ---") + fmt.Println(post.Content) +} diff --git a/pkg/flow/handler.go b/pkg/markdown/handler.go similarity index 80% rename from pkg/flow/handler.go rename to pkg/markdown/handler.go index 0ee6051f..cfe6b412 100644 --- a/pkg/flow/handler.go +++ b/pkg/markdown/handler.go @@ -1,4 +1,4 @@ -package flow +package markdown import ( "fmt" @@ -10,14 +10,14 @@ import ( "time" ) -// FetchMarkdown downloads the Markdown file from a public URL -func FetchMarkdown(url string) (string, error) { +func Fetch(file File) (string, error) { // Bust CDN or proxy caches by adding a unique timestamp sep := "?" - if strings.Contains(url, "?") { + if strings.Contains(file.Url, "?") { sep = "&" } - timestampedURL := fmt.Sprintf("%s%sts=%d", url, sep, time.Now().UnixNano()) + + timestampedURL := fmt.Sprintf("%s%sts=%d", file.Url, sep, time.Now().UnixNano()) req, err := http.NewRequest("GET", timestampedURL, nil) if err != nil { @@ -46,9 +46,9 @@ func FetchMarkdown(url string) (string, error) { return string(body), nil } -// ParseMarkdown splits the document into front-matter and content, then parses YAML -// It also extracts a leading Markdown image (header image) if present -func ParseMarkdown(data string) (Post, error) { +// Parse splits the document into front-matter and content, then parses YAML +// It also extracts a leading File image (header image) if present +func Parse(data string) (Post, error) { var post Post // Expecting format: ---\n---\n sections := strings.SplitN(data, "---", 3) @@ -66,7 +66,7 @@ func ParseMarkdown(data string) (Post, error) { } // Look for a header image at the top of the content - // Markdown image syntax: ![alt](url) + // File image syntax: ![alt](url) re := regexp.MustCompile(`^!\[(.*?)\]\((.*?)\)`) // Split first line from rest of content diff --git a/pkg/flow/schema.go b/pkg/markdown/schema.go similarity index 86% rename from pkg/flow/schema.go rename to pkg/markdown/schema.go index bb355cb5..94b4ce87 100644 --- a/pkg/flow/schema.go +++ b/pkg/markdown/schema.go @@ -1,4 +1,4 @@ -package flow +package markdown type FrontMatter struct { Title string `yaml:"title"` @@ -15,3 +15,7 @@ type Post struct { ImageAlt string Content string } + +type File struct { + Url string +} From 0cb8fc8c117960518c33471b44e5a07315c519cc Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Fri, 13 Jun 2025 11:49:20 +0800 Subject: [PATCH 07/35] format --- cli/post.go | 17 +++++++++-------- pkg/markdown/handler.go | 32 +++++++++++++++++++------------- pkg/markdown/schema.go | 2 +- 3 files changed, 29 insertions(+), 22 deletions(-) diff --git a/cli/post.go b/cli/post.go index 3e5f48cd..dcdda183 100644 --- a/cli/post.go +++ b/cli/post.go @@ -3,25 +3,26 @@ package main import ( "fmt" "github.com/oullin/pkg/markdown" - "log" ) func main() { - file := markdown.File{ + file := markdown.Parser{ Url: "https://raw.githubusercontent.com/oullin/content/refs/heads/main/leadership/2025-04-02-embrace-growth-through-movement.md", } - data, err := markdown.Fetch(file) + response, err := file.Fetch() + if err != nil { - log.Fatalf("Error fetching markdown: %v", err) + panic(fmt.Sprintf("Error fetching the markdown content: %v", err)) } - post, err := markdown.Parse(data) + post, err := markdown.Parse(response) + if err != nil { - log.Fatalf("Error parsing markdown: %v", err) + panic(fmt.Sprintf("Error parsing markdown: %v", err)) } - // Output parsed fields + // --- All good! fmt.Printf("Title: %s\n", post.Title) fmt.Printf("Excerpt: %s\n", post.Excerpt) fmt.Printf("Slug: %s\n", post.Slug) @@ -30,6 +31,6 @@ func main() { fmt.Printf("Image Alt: %s\n", post.ImageAlt) fmt.Printf("Category Alt: %s\n", post.Category) fmt.Printf("Tags Alt: %s\n", post.Tags) - fmt.Println("--- Content ---") + fmt.Println("\n--- Content ---") fmt.Println(post.Content) } diff --git a/pkg/markdown/handler.go b/pkg/markdown/handler.go index cfe6b412..df9e5fc2 100644 --- a/pkg/markdown/handler.go +++ b/pkg/markdown/handler.go @@ -10,20 +10,13 @@ import ( "time" ) -func Fetch(file File) (string, error) { - // Bust CDN or proxy caches by adding a unique timestamp - sep := "?" - if strings.Contains(file.Url, "?") { - sep = "&" - } - - timestampedURL := fmt.Sprintf("%s%sts=%d", file.Url, sep, time.Now().UnixNano()) +func (p Parser) Fetch() (string, error) { + req, err := http.NewRequest("GET", p.GetUrl(), nil) - req, err := http.NewRequest("GET", timestampedURL, nil) if err != nil { return "", err } - // Instruct intermediate caches to revalidate + req.Header.Set("Cache-Control", "no-cache") req.Header.Set("Pragma", "no-cache") @@ -32,6 +25,7 @@ func Fetch(file File) (string, error) { if err != nil { return "", err } + defer resp.Body.Close() if resp.StatusCode != http.StatusOK { @@ -39,6 +33,7 @@ func Fetch(file File) (string, error) { } body, err := io.ReadAll(resp.Body) + if err != nil { return "", err } @@ -46,10 +41,21 @@ func Fetch(file File) (string, error) { return string(body), nil } -// Parse splits the document into front-matter and content, then parses YAML -// It also extracts a leading File image (header image) if present +func (p Parser) GetUrl() string { + sep := "?" + + if strings.Contains(p.Url, "?") { + sep = "&" + } + + return fmt.Sprintf("%s%sts=%d", p.Url, sep, time.Now().UnixNano()) +} + +// Parse splits the document into front-matter and content, then parses YAML. +// It also extracts a leading Parser image (header image) if present. func Parse(data string) (Post, error) { var post Post + // Expecting format: ---\n---\n sections := strings.SplitN(data, "---", 3) if len(sections) < 3 { @@ -66,7 +72,7 @@ func Parse(data string) (Post, error) { } // Look for a header image at the top of the content - // File image syntax: ![alt](url) + // Parser image syntax: ![alt](url) re := regexp.MustCompile(`^!\[(.*?)\]\((.*?)\)`) // Split first line from rest of content diff --git a/pkg/markdown/schema.go b/pkg/markdown/schema.go index 94b4ce87..2be4a234 100644 --- a/pkg/markdown/schema.go +++ b/pkg/markdown/schema.go @@ -16,6 +16,6 @@ type Post struct { Content string } -type File struct { +type Parser struct { Url string } From a4538d72d1d1dcbd8d7042bfb15db6863f8df77f Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Fri, 13 Jun 2025 12:05:12 +0800 Subject: [PATCH 08/35] read uri from CLI --- cli/post.go | 8 ++++++-- pkg/markdown/input.go | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 pkg/markdown/input.go diff --git a/cli/post.go b/cli/post.go index dcdda183..851533a0 100644 --- a/cli/post.go +++ b/cli/post.go @@ -6,10 +6,14 @@ import ( ) func main() { - file := markdown.Parser{ - Url: "https://raw.githubusercontent.com/oullin/content/refs/heads/main/leadership/2025-04-02-embrace-growth-through-movement.md", + uri, err := markdown.ReadURL() + + if err != nil { + panic(fmt.Sprintf("Error reading the URL: %v", err)) } + file := markdown.Parser{Url: *uri} + response, err := file.Fetch() if err != nil { diff --git a/pkg/markdown/input.go b/pkg/markdown/input.go new file mode 100644 index 00000000..dcdcfd3e --- /dev/null +++ b/pkg/markdown/input.go @@ -0,0 +1,33 @@ +package markdown + +import ( + "flag" + "fmt" + "net/http" + "net/url" +) + +func ReadURL() (*string, error) { + uri := flag.String("uri", "", "URL of the markdown file to parse. (required)") + flag.Parse() + + if *uri == "" { + return nil, fmt.Errorf("uri is required") + } + + if u, err := url.Parse(*uri); err != nil || u.Scheme != "https" || u.Host != "raw.githubusercontent.com" { + return nil, fmt.Errorf("invalid uri: %w", err) + } + + response, err := http.Head(*uri) + if err != nil { + return nil, err + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + return nil, fmt.Errorf("failed to fetch the markdown file cntent: status %d", response.StatusCode) + } + + return uri, nil +} From 349a193e353191f8097887c82493765f2eb24ccc Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Fri, 13 Jun 2025 12:06:15 +0800 Subject: [PATCH 09/35] format --- pkg/markdown/handler.go | 150 ++++++++++++++++++++-------------------- 1 file changed, 75 insertions(+), 75 deletions(-) diff --git a/pkg/markdown/handler.go b/pkg/markdown/handler.go index df9e5fc2..d1934328 100644 --- a/pkg/markdown/handler.go +++ b/pkg/markdown/handler.go @@ -1,100 +1,100 @@ package markdown import ( - "fmt" - "gopkg.in/yaml.v3" - "io" - "net/http" - "regexp" - "strings" - "time" + "fmt" + "gopkg.in/yaml.v3" + "io" + "net/http" + "regexp" + "strings" + "time" ) func (p Parser) Fetch() (string, error) { - req, err := http.NewRequest("GET", p.GetUrl(), nil) + req, err := http.NewRequest("GET", p.GetUrl(), nil) - if err != nil { - return "", err - } + if err != nil { + return "", err + } - req.Header.Set("Cache-Control", "no-cache") - req.Header.Set("Pragma", "no-cache") + req.Header.Set("Cache-Control", "no-cache") + req.Header.Set("Pragma", "no-cache") - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - return "", err - } + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return "", err + } - defer resp.Body.Close() + defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("failed to fetch markdown: status %d", resp.StatusCode) - } + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("failed to fetch markdown: status %d", resp.StatusCode) + } - body, err := io.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) - if err != nil { - return "", err - } + if err != nil { + return "", err + } - return string(body), nil + return string(body), nil } func (p Parser) GetUrl() string { - sep := "?" + sep := "?" - if strings.Contains(p.Url, "?") { - sep = "&" - } + if strings.Contains(p.Url, "?") { + sep = "&" + } - return fmt.Sprintf("%s%sts=%d", p.Url, sep, time.Now().UnixNano()) + return fmt.Sprintf("%s%sts=%d", p.Url, sep, time.Now().UnixNano()) } // Parse splits the document into front-matter and content, then parses YAML. // It also extracts a leading Parser image (header image) if present. func Parse(data string) (Post, error) { - var post Post - - // Expecting format: ---\n---\n - sections := strings.SplitN(data, "---", 3) - if len(sections) < 3 { - return post, fmt.Errorf("invalid front-matter format") - } - - fm := strings.TrimSpace(sections[1]) - body := strings.TrimSpace(sections[2]) - - // Unmarshal YAML into FrontMatter - err := yaml.Unmarshal([]byte(fm), &post.FrontMatter) - if err != nil { - return post, err - } - - // Look for a header image at the top of the content - // Parser image syntax: ![alt](url) - re := regexp.MustCompile(`^!\[(.*?)\]\((.*?)\)`) - - // Split first line from rest of content - parts := strings.SplitN(body, "\n", 2) - first := strings.TrimSpace(parts[0]) - - if m := re.FindStringSubmatch(first); len(m) == 3 { - post.ImageAlt = m[1] - post.ImageURL = m[2] - - // Remaining content excludes the header image line - if len(parts) > 1 { - post.Content = strings.TrimSpace(parts[1]) - } else { - post.Content = "" - } - } else { - // No header image found; the entire body is content - post.ImageAlt = "" - post.ImageURL = "" - post.Content = body - } - - return post, nil + var post Post + + // Expecting format: ---\n---\n + sections := strings.SplitN(data, "---", 3) + if len(sections) < 3 { + return post, fmt.Errorf("invalid front-matter format") + } + + fm := strings.TrimSpace(sections[1]) + body := strings.TrimSpace(sections[2]) + + // Unmarshal YAML into FrontMatter + err := yaml.Unmarshal([]byte(fm), &post.FrontMatter) + if err != nil { + return post, err + } + + // Look for a header image at the top of the content + // Parser image syntax: ![alt](url) + re := regexp.MustCompile(`^!\[(.*?)\]\((.*?)\)`) + + // Split first line from rest of content + parts := strings.SplitN(body, "\n", 2) + first := strings.TrimSpace(parts[0]) + + if m := re.FindStringSubmatch(first); len(m) == 3 { + post.ImageAlt = m[1] + post.ImageURL = m[2] + + // Remaining content excludes the header image line + if len(parts) > 1 { + post.Content = strings.TrimSpace(parts[1]) + } else { + post.Content = "" + } + } else { + // No header image found; the entire body is content + post.ImageAlt = "" + post.ImageURL = "" + post.Content = body + } + + return post, nil } From ba90c4b32d98902168602ee26d50820579d7e1d7 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Fri, 13 Jun 2025 15:45:12 +0800 Subject: [PATCH 10/35] start working on menu --- cli/main.go | 55 +++++++++++++++++++++ cli/menu/panel.go | 80 +++++++++++++++++++++++++++++++ cli/menu/schema.go | 17 +++++++ cli/{post.go => posts/handler.go} | 12 +++-- go.mod | 1 + go.sum | 2 + 6 files changed, 162 insertions(+), 5 deletions(-) create mode 100644 cli/main.go create mode 100644 cli/menu/panel.go create mode 100644 cli/menu/schema.go rename cli/{post.go => posts/handler.go} (75%) diff --git a/cli/main.go b/cli/main.go new file mode 100644 index 00000000..93511c78 --- /dev/null +++ b/cli/main.go @@ -0,0 +1,55 @@ +package main + +import ( + "bufio" + "fmt" + "github.com/oullin/cli/menu" + "github.com/oullin/pkg/cli" + "os" + "time" +) + +func main() { + reader := bufio.NewReader(os.Stdin) + panel := menu.Panel{Reader: reader} + + for { + choice, err := panel.CaptureInput() + + if err != nil { + cli.MakeTextColour(err.Error(), cli.Red).Println() + continue + } + + switch *choice { + case 1: + sayHello() + case 2: + showTime() + case 3: + doSomethingElse() + case 0: + fmt.Println(menu.ColorGreen + "Goodbye!" + menu.ColorReset) + return + default: + fmt.Println(menu.ColorRed, "Unknown option. Try again.", menu.ColorReset) + } + + fmt.Print("\nPress Enter to continue...") + + _, _ = reader.ReadString('\n') + } +} + +func sayHello() { + fmt.Println(menu.ColorGreen + "\nHello, world!" + menu.ColorReset) +} + +func showTime() { + now := time.Now().Format("2006-01-02 15:04:05") + fmt.Println(menu.ColorGreen, "\nCurrent time is", now, menu.ColorReset) +} + +func doSomethingElse() { + fmt.Println(menu.ColorGreen + "\nDoing something else..." + menu.ColorReset) +} diff --git a/cli/menu/panel.go b/cli/menu/panel.go new file mode 100644 index 00000000..db182489 --- /dev/null +++ b/cli/menu/panel.go @@ -0,0 +1,80 @@ +package menu + +import ( + "fmt" + "golang.org/x/term" + "os" + "strconv" + "strings" +) + +func (p Panel) CaptureInput() (*int, error) { + fmt.Print(ColorYellow + "Select an option: " + ColorReset) + input, err := p.Reader.ReadString('\n') + + if err != nil { + return nil, fmt.Errorf("%s error reading input: %v %s", ColorRed, err, ColorReset) + } + + input = strings.TrimSpace(input) + choice, err := strconv.Atoi(input) + + if err != nil { + return nil, fmt.Errorf("%s Please enter a valid number. %s", ColorRed, ColorReset) + } + + return &choice, nil +} + +func (p Panel) PrintMenu() { + // Try to get the terminal width; default to 80 if it fails + width, _, err := term.GetSize(int(os.Stdout.Fd())) + + if err != nil || width < 20 { + width = 80 + } + + inner := width - 2 // space between the two border chars + + // Build box pieces + border := "╔" + strings.Repeat("═", inner) + "╗" + title := "║" + p.CenterText(" Main Menu ", inner) + "║" + divider := "╠" + strings.Repeat("═", inner) + "╣" + footer := "╚" + strings.Repeat("═", inner) + "╝" + + // Print in color + fmt.Println() + fmt.Println(ColorCyan + border) + fmt.Println(title) + fmt.Println(divider) + + p.PrintOption("1) Say Hello", inner) + p.PrintOption("2) Show Time", inner) + p.PrintOption("3) Do Something Else", inner) + p.PrintOption("0) Exit", inner) + + fmt.Println(footer + ColorReset) +} + +// PrintOption left-pads a space, writes the text, then fills to the full inner width. +func (p Panel) PrintOption(text string, inner int) { + content := " " + text + if len(content) > inner { + content = content[:inner] + } + padding := inner - len(content) + fmt.Printf("║%s%s║\n", content, strings.Repeat(" ", padding)) +} + +// CenterText centers s within width, padding with spaces. +func (p Panel) CenterText(s string, width int) string { + if len(s) >= width { + return s[:width] + } + + pad := width - len(s) + left := pad / 2 + right := pad - left + + return strings.Repeat(" ", left) + s + strings.Repeat(" ", right) +} diff --git a/cli/menu/schema.go b/cli/menu/schema.go new file mode 100644 index 00000000..fecae417 --- /dev/null +++ b/cli/menu/schema.go @@ -0,0 +1,17 @@ +package menu + +import ( + "bufio" +) + +const ( + ColorReset = "\033[0m" + ColorCyan = "\033[36m" + ColorGreen = "\033[32m" + ColorYellow = "\033[33m" + ColorRed = "\033[31m" +) + +type Panel struct { + Reader *bufio.Reader +} diff --git a/cli/post.go b/cli/posts/handler.go similarity index 75% rename from cli/post.go rename to cli/posts/handler.go index 851533a0..f133d60e 100644 --- a/cli/post.go +++ b/cli/posts/handler.go @@ -1,15 +1,15 @@ -package main +package posts import ( "fmt" "github.com/oullin/pkg/markdown" ) -func main() { +func Handle() error { uri, err := markdown.ReadURL() if err != nil { - panic(fmt.Sprintf("Error reading the URL: %v", err)) + return fmt.Errorf("error reading the URL: %v", err) } file := markdown.Parser{Url: *uri} @@ -17,13 +17,13 @@ func main() { response, err := file.Fetch() if err != nil { - panic(fmt.Sprintf("Error fetching the markdown content: %v", err)) + return fmt.Errorf("error fetching the markdown content: %v", err) } post, err := markdown.Parse(response) if err != nil { - panic(fmt.Sprintf("Error parsing markdown: %v", err)) + return fmt.Errorf("error parsing markdown: %v", err) } // --- All good! @@ -37,4 +37,6 @@ func main() { fmt.Printf("Tags Alt: %s\n", post.Tags) fmt.Println("\n--- Content ---") fmt.Println(post.Content) + + return nil } diff --git a/go.mod b/go.mod index 5c854d03..e91df159 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/joho/godotenv v1.5.1 github.com/lib/pq v1.10.9 golang.org/x/crypto v0.39.0 + golang.org/x/term v0.32.0 gopkg.in/yaml.v3 v3.0.1 gorm.io/driver/postgres v1.6.0 gorm.io/gorm v1.30.0 diff --git a/go.sum b/go.sum index d33cd06b..76880e72 100644 --- a/go.sum +++ b/go.sum @@ -65,6 +65,8 @@ golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 4442eba30402c5072bd251658ed45587e0d02c81 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Fri, 13 Jun 2025 15:45:42 +0800 Subject: [PATCH 11/35] format --- cli/main.go | 78 ++++++++++++++++++++++++++--------------------------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/cli/main.go b/cli/main.go index 93511c78..935c68cf 100644 --- a/cli/main.go +++ b/cli/main.go @@ -1,55 +1,55 @@ package main import ( - "bufio" - "fmt" - "github.com/oullin/cli/menu" - "github.com/oullin/pkg/cli" - "os" - "time" + "bufio" + "fmt" + "github.com/oullin/cli/menu" + "github.com/oullin/pkg/cli" + "os" + "time" ) func main() { - reader := bufio.NewReader(os.Stdin) - panel := menu.Panel{Reader: reader} - - for { - choice, err := panel.CaptureInput() - - if err != nil { - cli.MakeTextColour(err.Error(), cli.Red).Println() - continue - } - - switch *choice { - case 1: - sayHello() - case 2: - showTime() - case 3: - doSomethingElse() - case 0: - fmt.Println(menu.ColorGreen + "Goodbye!" + menu.ColorReset) - return - default: - fmt.Println(menu.ColorRed, "Unknown option. Try again.", menu.ColorReset) - } - - fmt.Print("\nPress Enter to continue...") - - _, _ = reader.ReadString('\n') - } + reader := bufio.NewReader(os.Stdin) + panel := menu.Panel{Reader: reader} + + for { + choice, err := panel.CaptureInput() + + if err != nil { + cli.MakeTextColour(err.Error(), cli.Red).Println() + continue + } + + switch *choice { + case 1: + sayHello() + case 2: + showTime() + case 3: + doSomethingElse() + case 0: + fmt.Println(menu.ColorGreen + "Goodbye!" + menu.ColorReset) + return + default: + fmt.Println(menu.ColorRed, "Unknown option. Try again.", menu.ColorReset) + } + + fmt.Print("\nPress Enter to continue...") + + _, _ = reader.ReadString('\n') + } } func sayHello() { - fmt.Println(menu.ColorGreen + "\nHello, world!" + menu.ColorReset) + fmt.Println(menu.ColorGreen + "\nHello, world!" + menu.ColorReset) } func showTime() { - now := time.Now().Format("2006-01-02 15:04:05") - fmt.Println(menu.ColorGreen, "\nCurrent time is", now, menu.ColorReset) + now := time.Now().Format("2006-01-02 15:04:05") + fmt.Println(menu.ColorGreen, "\nCurrent time is", now, menu.ColorReset) } func doSomethingElse() { - fmt.Println(menu.ColorGreen + "\nDoing something else..." + menu.ColorReset) + fmt.Println(menu.ColorGreen + "\nDoing something else..." + menu.ColorReset) } From 3ad1f4b901114951c3b5a6099abb7cbe20047c8c Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Fri, 13 Jun 2025 15:50:30 +0800 Subject: [PATCH 12/35] fix --- cli/main.go | 80 +++++++++++++++++++++++++++-------------------------- 1 file changed, 41 insertions(+), 39 deletions(-) diff --git a/cli/main.go b/cli/main.go index 935c68cf..862a57ec 100644 --- a/cli/main.go +++ b/cli/main.go @@ -1,55 +1,57 @@ package main import ( - "bufio" - "fmt" - "github.com/oullin/cli/menu" - "github.com/oullin/pkg/cli" - "os" - "time" + "bufio" + "fmt" + "github.com/oullin/cli/menu" + "github.com/oullin/pkg/cli" + "os" + "time" ) func main() { - reader := bufio.NewReader(os.Stdin) - panel := menu.Panel{Reader: reader} - - for { - choice, err := panel.CaptureInput() - - if err != nil { - cli.MakeTextColour(err.Error(), cli.Red).Println() - continue - } - - switch *choice { - case 1: - sayHello() - case 2: - showTime() - case 3: - doSomethingElse() - case 0: - fmt.Println(menu.ColorGreen + "Goodbye!" + menu.ColorReset) - return - default: - fmt.Println(menu.ColorRed, "Unknown option. Try again.", menu.ColorReset) - } - - fmt.Print("\nPress Enter to continue...") - - _, _ = reader.ReadString('\n') - } + reader := bufio.NewReader(os.Stdin) + panel := menu.Panel{Reader: reader} + + panel.PrintMenu() + + for { + choice, err := panel.CaptureInput() + + if err != nil { + cli.MakeTextColour(err.Error(), cli.Red).Println() + continue + } + + switch *choice { + case 1: + sayHello() + case 2: + showTime() + case 3: + doSomethingElse() + case 0: + fmt.Println(menu.ColorGreen + "Goodbye!" + menu.ColorReset) + return + default: + fmt.Println(menu.ColorRed, "Unknown option. Try again.", menu.ColorReset) + } + + fmt.Print("\nPress Enter to continue...") + + _, _ = reader.ReadString('\n') + } } func sayHello() { - fmt.Println(menu.ColorGreen + "\nHello, world!" + menu.ColorReset) + fmt.Println(menu.ColorGreen + "\nHello, world!" + menu.ColorReset) } func showTime() { - now := time.Now().Format("2006-01-02 15:04:05") - fmt.Println(menu.ColorGreen, "\nCurrent time is", now, menu.ColorReset) + now := time.Now().Format("2006-01-02 15:04:05") + fmt.Println(menu.ColorGreen, "\nCurrent time is", now, menu.ColorReset) } func doSomethingElse() { - fmt.Println(menu.ColorGreen + "\nDoing something else..." + menu.ColorReset) + fmt.Println(menu.ColorGreen + "\nDoing something else..." + menu.ColorReset) } From 6e783c40d5d1120586fb9a8151f5aadc42bdf449 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Fri, 13 Jun 2025 15:53:13 +0800 Subject: [PATCH 13/35] reuse colours --- cli/main.go | 82 +++++++++++++++++++++++----------------------- cli/menu/panel.go | 11 ++++--- cli/menu/schema.go | 8 ----- 3 files changed, 47 insertions(+), 54 deletions(-) diff --git a/cli/main.go b/cli/main.go index 862a57ec..b27a480b 100644 --- a/cli/main.go +++ b/cli/main.go @@ -1,57 +1,57 @@ package main import ( - "bufio" - "fmt" - "github.com/oullin/cli/menu" - "github.com/oullin/pkg/cli" - "os" - "time" + "bufio" + "fmt" + "github.com/oullin/cli/menu" + "github.com/oullin/pkg/cli" + "os" + "time" ) func main() { - reader := bufio.NewReader(os.Stdin) - panel := menu.Panel{Reader: reader} - - panel.PrintMenu() - - for { - choice, err := panel.CaptureInput() - - if err != nil { - cli.MakeTextColour(err.Error(), cli.Red).Println() - continue - } - - switch *choice { - case 1: - sayHello() - case 2: - showTime() - case 3: - doSomethingElse() - case 0: - fmt.Println(menu.ColorGreen + "Goodbye!" + menu.ColorReset) - return - default: - fmt.Println(menu.ColorRed, "Unknown option. Try again.", menu.ColorReset) - } - - fmt.Print("\nPress Enter to continue...") - - _, _ = reader.ReadString('\n') - } + reader := bufio.NewReader(os.Stdin) + panel := menu.Panel{Reader: reader} + + panel.PrintMenu() + + for { + choice, err := panel.CaptureInput() + + if err != nil { + cli.MakeTextColour(err.Error(), cli.Red).Println() + continue + } + + switch *choice { + case 1: + sayHello() + case 2: + showTime() + case 3: + doSomethingElse() + case 0: + fmt.Println(cli.Green + "Goodbye!" + cli.Reset) + return + default: + fmt.Println(cli.Red, "Unknown option. Try again.", cli.Reset) + } + + fmt.Print("\nPress Enter to continue...") + + _, _ = reader.ReadString('\n') + } } func sayHello() { - fmt.Println(menu.ColorGreen + "\nHello, world!" + menu.ColorReset) + fmt.Println(cli.Green + "\nHello, world!" + cli.Reset) } func showTime() { - now := time.Now().Format("2006-01-02 15:04:05") - fmt.Println(menu.ColorGreen, "\nCurrent time is", now, menu.ColorReset) + now := time.Now().Format("2006-01-02 15:04:05") + fmt.Println(cli.Green, "\nCurrent time is", now, cli.Reset) } func doSomethingElse() { - fmt.Println(menu.ColorGreen + "\nDoing something else..." + menu.ColorReset) + fmt.Println(cli.Green + "\nDoing something else..." + cli.Reset) } diff --git a/cli/menu/panel.go b/cli/menu/panel.go index db182489..ceec61b8 100644 --- a/cli/menu/panel.go +++ b/cli/menu/panel.go @@ -2,6 +2,7 @@ package menu import ( "fmt" + "github.com/oullin/pkg/cli" "golang.org/x/term" "os" "strconv" @@ -9,18 +10,18 @@ import ( ) func (p Panel) CaptureInput() (*int, error) { - fmt.Print(ColorYellow + "Select an option: " + ColorReset) + fmt.Print(cli.Yellow + "Select an option: " + cli.Reset) input, err := p.Reader.ReadString('\n') if err != nil { - return nil, fmt.Errorf("%s error reading input: %v %s", ColorRed, err, ColorReset) + return nil, fmt.Errorf("%s error reading input: %v %s", cli.Red, err, cli.Reset) } input = strings.TrimSpace(input) choice, err := strconv.Atoi(input) if err != nil { - return nil, fmt.Errorf("%s Please enter a valid number. %s", ColorRed, ColorReset) + return nil, fmt.Errorf("%s Please enter a valid number. %s", cli.Red, cli.Reset) } return &choice, nil @@ -44,7 +45,7 @@ func (p Panel) PrintMenu() { // Print in color fmt.Println() - fmt.Println(ColorCyan + border) + fmt.Println(cli.Cyan + border) fmt.Println(title) fmt.Println(divider) @@ -53,7 +54,7 @@ func (p Panel) PrintMenu() { p.PrintOption("3) Do Something Else", inner) p.PrintOption("0) Exit", inner) - fmt.Println(footer + ColorReset) + fmt.Println(footer + cli.Reset) } // PrintOption left-pads a space, writes the text, then fills to the full inner width. diff --git a/cli/menu/schema.go b/cli/menu/schema.go index fecae417..0fe61cc0 100644 --- a/cli/menu/schema.go +++ b/cli/menu/schema.go @@ -4,14 +4,6 @@ import ( "bufio" ) -const ( - ColorReset = "\033[0m" - ColorCyan = "\033[36m" - ColorGreen = "\033[32m" - ColorYellow = "\033[33m" - ColorRed = "\033[31m" -) - type Panel struct { Reader *bufio.Reader } From 5bf20dbcfb4a0e2560a5a3732d199ea2be195a82 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Fri, 13 Jun 2025 16:00:52 +0800 Subject: [PATCH 14/35] tweaks --- cli/main.go | 83 +++++++++++++++++++++++----------------------- cli/menu/panel.go | 24 ++++++++++---- cli/menu/schema.go | 1 + 3 files changed, 60 insertions(+), 48 deletions(-) diff --git a/cli/main.go b/cli/main.go index b27a480b..974697ac 100644 --- a/cli/main.go +++ b/cli/main.go @@ -1,57 +1,58 @@ package main import ( - "bufio" - "fmt" - "github.com/oullin/cli/menu" - "github.com/oullin/pkg/cli" - "os" - "time" + "bufio" + "fmt" + "github.com/oullin/cli/menu" + "github.com/oullin/pkg/cli" + "os" + "time" ) func main() { - reader := bufio.NewReader(os.Stdin) - panel := menu.Panel{Reader: reader} - - panel.PrintMenu() - - for { - choice, err := panel.CaptureInput() - - if err != nil { - cli.MakeTextColour(err.Error(), cli.Red).Println() - continue - } - - switch *choice { - case 1: - sayHello() - case 2: - showTime() - case 3: - doSomethingElse() - case 0: - fmt.Println(cli.Green + "Goodbye!" + cli.Reset) - return - default: - fmt.Println(cli.Red, "Unknown option. Try again.", cli.Reset) - } - - fmt.Print("\nPress Enter to continue...") - - _, _ = reader.ReadString('\n') - } + panel := menu.Panel{ + Reader: bufio.NewReader(os.Stdin), + } + + panel.PrintMenu() + + for { + err := panel.CaptureInput() + + if err != nil { + cli.MakeTextColour(err.Error(), cli.Red).Println() + continue + } + + switch panel.GetChoice() { + case 1: + sayHello() + case 2: + showTime() + case 3: + doSomethingElse() + case 0: + fmt.Println(cli.Green + "Goodbye!" + cli.Reset) + return + default: + fmt.Println(cli.Red, "Unknown option. Try again.", cli.Reset) + } + + fmt.Print("\nPress Enter to continue...") + + panel.PrintLine() + } } func sayHello() { - fmt.Println(cli.Green + "\nHello, world!" + cli.Reset) + fmt.Println(cli.Green + "\nHello, world!" + cli.Reset) } func showTime() { - now := time.Now().Format("2006-01-02 15:04:05") - fmt.Println(cli.Green, "\nCurrent time is", now, cli.Reset) + now := time.Now().Format("2006-01-02 15:04:05") + fmt.Println(cli.Green, "\nCurrent time is", now, cli.Reset) } func doSomethingElse() { - fmt.Println(cli.Green + "\nDoing something else..." + cli.Reset) + fmt.Println(cli.Green + "\nDoing something else..." + cli.Reset) } diff --git a/cli/menu/panel.go b/cli/menu/panel.go index ceec61b8..b5b2c493 100644 --- a/cli/menu/panel.go +++ b/cli/menu/panel.go @@ -9,25 +9,35 @@ import ( "strings" ) -func (p Panel) CaptureInput() (*int, error) { +func (p *Panel) PrintLine() { + _, _ = p.Reader.ReadString('\n') +} + +func (p *Panel) GetChoice() int { + return *p.Choice +} + +func (p *Panel) CaptureInput() error { fmt.Print(cli.Yellow + "Select an option: " + cli.Reset) input, err := p.Reader.ReadString('\n') if err != nil { - return nil, fmt.Errorf("%s error reading input: %v %s", cli.Red, err, cli.Reset) + return fmt.Errorf("%s error reading input: %v %s", cli.Red, err, cli.Reset) } input = strings.TrimSpace(input) choice, err := strconv.Atoi(input) if err != nil { - return nil, fmt.Errorf("%s Please enter a valid number. %s", cli.Red, cli.Reset) + return fmt.Errorf("%s Please enter a valid number. %s", cli.Red, cli.Reset) } - return &choice, nil + p.Choice = &choice + + return nil } -func (p Panel) PrintMenu() { +func (p *Panel) PrintMenu() { // Try to get the terminal width; default to 80 if it fails width, _, err := term.GetSize(int(os.Stdout.Fd())) @@ -58,7 +68,7 @@ func (p Panel) PrintMenu() { } // PrintOption left-pads a space, writes the text, then fills to the full inner width. -func (p Panel) PrintOption(text string, inner int) { +func (p *Panel) PrintOption(text string, inner int) { content := " " + text if len(content) > inner { content = content[:inner] @@ -68,7 +78,7 @@ func (p Panel) PrintOption(text string, inner int) { } // CenterText centers s within width, padding with spaces. -func (p Panel) CenterText(s string, width int) string { +func (p *Panel) CenterText(s string, width int) string { if len(s) >= width { return s[:width] } diff --git a/cli/menu/schema.go b/cli/menu/schema.go index 0fe61cc0..f1619f4b 100644 --- a/cli/menu/schema.go +++ b/cli/menu/schema.go @@ -6,4 +6,5 @@ import ( type Panel struct { Reader *bufio.Reader + Choice *int } From d6bc04571b64bca31313be2a3cc6f07be7005d36 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Fri, 13 Jun 2025 16:02:01 +0800 Subject: [PATCH 15/35] format --- cli/main.go | 84 ++++++++++++++++++++++++++--------------------------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/cli/main.go b/cli/main.go index 974697ac..c3cd9764 100644 --- a/cli/main.go +++ b/cli/main.go @@ -1,58 +1,58 @@ package main import ( - "bufio" - "fmt" - "github.com/oullin/cli/menu" - "github.com/oullin/pkg/cli" - "os" - "time" + "bufio" + "fmt" + "github.com/oullin/cli/menu" + "github.com/oullin/pkg/cli" + "os" + "time" ) func main() { - panel := menu.Panel{ - Reader: bufio.NewReader(os.Stdin), - } - - panel.PrintMenu() - - for { - err := panel.CaptureInput() - - if err != nil { - cli.MakeTextColour(err.Error(), cli.Red).Println() - continue - } - - switch panel.GetChoice() { - case 1: - sayHello() - case 2: - showTime() - case 3: - doSomethingElse() - case 0: - fmt.Println(cli.Green + "Goodbye!" + cli.Reset) - return - default: - fmt.Println(cli.Red, "Unknown option. Try again.", cli.Reset) - } - - fmt.Print("\nPress Enter to continue...") - - panel.PrintLine() - } + panel := menu.Panel{ + Reader: bufio.NewReader(os.Stdin), + } + + panel.PrintMenu() + + for { + err := panel.CaptureInput() + + if err != nil { + cli.MakeTextColour(err.Error(), cli.Red).Println() + continue + } + + switch panel.GetChoice() { + case 1: + sayHello() + case 2: + showTime() + case 3: + doSomethingElse() + case 0: + fmt.Println(cli.Green + "Goodbye!" + cli.Reset) + return + default: + fmt.Println(cli.Red, "Unknown option. Try again.", cli.Reset) + } + + fmt.Print("\nPress Enter to continue...") + + panel.PrintLine() + } } func sayHello() { - fmt.Println(cli.Green + "\nHello, world!" + cli.Reset) + fmt.Println(cli.Green + "\nHello, world!" + cli.Reset) } func showTime() { - now := time.Now().Format("2006-01-02 15:04:05") - fmt.Println(cli.Green, "\nCurrent time is", now, cli.Reset) + now := time.Now().Format("2006-01-02 15:04:05") + fmt.Println(cli.Green, "\nCurrent time is", now, cli.Reset) } func doSomethingElse() { - fmt.Println(cli.Green + "\nDoing something else..." + cli.Reset) + fmt.Println(cli.Green + "\nDoing something else..." + cli.Reset) } From 1c76698c01c91ffb151e091f65bd67773c2934ad Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Fri, 13 Jun 2025 17:28:36 +0800 Subject: [PATCH 16/35] connect parsing with menu --- boost/spark.go | 2 +- boost/validate.go | 12 ----------- cli/main.go | 30 ++++++++++++++++----------- cli/menu/panel.go | 45 +++++++++++++++++++++++++++++++++++++++-- cli/menu/schema.go | 6 ++++-- cli/posts/handler.go | 16 +++++++-------- cli/posts/schema.go | 5 +++++ pkg/markdown/handler.go | 2 +- pkg/markdown/input.go | 33 ------------------------------ pkg/validator.go | 8 ++++++++ 10 files changed, 87 insertions(+), 72 deletions(-) delete mode 100644 boost/validate.go create mode 100644 cli/posts/schema.go delete mode 100644 pkg/markdown/input.go diff --git a/boost/spark.go b/boost/spark.go index 52fd5bb6..4f87dce7 100644 --- a/boost/spark.go +++ b/boost/spark.go @@ -7,7 +7,7 @@ import ( ) func Spark(envPath string) (*env.Environment, *pkg.Validator) { - validate := GetDefaultValidate() + validate := pkg.GetDefaultValidator() envMap, err := godotenv.Read(envPath) diff --git a/boost/validate.go b/boost/validate.go deleted file mode 100644 index ab166e2f..00000000 --- a/boost/validate.go +++ /dev/null @@ -1,12 +0,0 @@ -package boost - -import ( - baseValidator "github.com/go-playground/validator/v10" - "github.com/oullin/pkg" -) - -func GetDefaultValidate() *pkg.Validator { - return pkg.MakeValidatorFrom(baseValidator.New( - baseValidator.WithRequiredStructEnabled(), - )) -} diff --git a/cli/main.go b/cli/main.go index c3cd9764..a62fb7e0 100644 --- a/cli/main.go +++ b/cli/main.go @@ -4,6 +4,7 @@ import ( "bufio" "fmt" "github.com/oullin/cli/menu" + "github.com/oullin/pkg" "github.com/oullin/pkg/cli" "os" "time" @@ -11,7 +12,8 @@ import ( func main() { panel := menu.Panel{ - Reader: bufio.NewReader(os.Stdin), + Reader: bufio.NewReader(os.Stdin), + Validator: pkg.GetDefaultValidator(), } panel.PrintMenu() @@ -26,11 +28,23 @@ func main() { switch panel.GetChoice() { case 1: - sayHello() + uri, err := panel.CapturePostURL() + + if err != nil { + fmt.Println(err) + continue + } + + err = uri.Parse() + + if err != nil { + fmt.Println(err) + continue + } + + return case 2: showTime() - case 3: - doSomethingElse() case 0: fmt.Println(cli.Green + "Goodbye!" + cli.Reset) return @@ -44,15 +58,7 @@ func main() { } } -func sayHello() { - fmt.Println(cli.Green + "\nHello, world!" + cli.Reset) -} - func showTime() { now := time.Now().Format("2006-01-02 15:04:05") fmt.Println(cli.Green, "\nCurrent time is", now, cli.Reset) } - -func doSomethingElse() { - fmt.Println(cli.Green + "\nDoing something else..." + cli.Reset) -} diff --git a/cli/menu/panel.go b/cli/menu/panel.go index b5b2c493..5112e625 100644 --- a/cli/menu/panel.go +++ b/cli/menu/panel.go @@ -2,8 +2,10 @@ package menu import ( "fmt" + "github.com/oullin/cli/posts" "github.com/oullin/pkg/cli" "golang.org/x/term" + "net/url" "os" "strconv" "strings" @@ -59,9 +61,8 @@ func (p *Panel) PrintMenu() { fmt.Println(title) fmt.Println(divider) - p.PrintOption("1) Say Hello", inner) + p.PrintOption("1) Parse Posts", inner) p.PrintOption("2) Show Time", inner) - p.PrintOption("3) Do Something Else", inner) p.PrintOption("0) Exit", inner) fmt.Println(footer + cli.Reset) @@ -89,3 +90,43 @@ func (p *Panel) CenterText(s string, width int) string { return strings.Repeat(" ", left) + s + strings.Repeat(" ", right) } + +func (p *Panel) CapturePostURL() (*posts.Input, error) { + fmt.Print("Enter the post markdown file URL: ") + uri, err := p.Reader.ReadString('\n') + + if err != nil { + return nil, fmt.Errorf("%sError reading the given post URL: %v %s", cli.Red, err, cli.Reset) + } + + uri = strings.TrimSpace(uri) + if uri == "" { + return nil, fmt.Errorf("%sError: no URL provided: %s", cli.Red, cli.Reset) + } + + parsedURL, err := url.Parse(uri) + if err != nil { + return nil, fmt.Errorf("%sError: invalid URL: %v %s", cli.Red, err, cli.Reset) + } + + if parsedURL.Scheme != "https" || parsedURL.Host != "raw.githubusercontent.com" { + return nil, fmt.Errorf("%sError: URL must begin with https://raw.githubusercontent.com: %v %s", cli.Red, err, cli.Reset) + } + + input := posts.Input{Url: parsedURL.String()} + validate := p.Validator + + if _, err := validate.Rejects(input); err != nil { + return nil, fmt.Errorf( + "%sError validating the given post URL: %v %s \n%sViolations:%s %s", + cli.Red, + err, + cli.Reset, + cli.Blue, + cli.Reset, + validate.GetErrorsAsJason(), + ) + } + + return &input, nil +} diff --git a/cli/menu/schema.go b/cli/menu/schema.go index f1619f4b..58ea5ac7 100644 --- a/cli/menu/schema.go +++ b/cli/menu/schema.go @@ -2,9 +2,11 @@ package menu import ( "bufio" + "github.com/oullin/pkg" ) type Panel struct { - Reader *bufio.Reader - Choice *int + Reader *bufio.Reader + Choice *int + Validator *pkg.Validator } diff --git a/cli/posts/handler.go b/cli/posts/handler.go index f133d60e..cb9580ef 100644 --- a/cli/posts/handler.go +++ b/cli/posts/handler.go @@ -2,31 +2,29 @@ package posts import ( "fmt" + "github.com/oullin/pkg/cli" "github.com/oullin/pkg/markdown" ) -func Handle() error { - uri, err := markdown.ReadURL() - - if err != nil { - return fmt.Errorf("error reading the URL: %v", err) +func (i *Input) Parse() error { + file := markdown.Parser{ + Url: i.Url, } - file := markdown.Parser{Url: *uri} - response, err := file.Fetch() if err != nil { - return fmt.Errorf("error fetching the markdown content: %v", err) + return fmt.Errorf("%sError fetching the markdown content: %v %s", cli.Red, err, cli.Reset) } post, err := markdown.Parse(response) if err != nil { - return fmt.Errorf("error parsing markdown: %v", err) + return fmt.Errorf("%sEerror parsing markdown: %v %s", cli.Red, err, cli.Reset) } // --- All good! + // Todo: Save post in the DB. fmt.Printf("Title: %s\n", post.Title) fmt.Printf("Excerpt: %s\n", post.Excerpt) fmt.Printf("Slug: %s\n", post.Slug) diff --git a/cli/posts/schema.go b/cli/posts/schema.go new file mode 100644 index 00000000..300fe913 --- /dev/null +++ b/cli/posts/schema.go @@ -0,0 +1,5 @@ +package posts + +type Input struct { + Url string `validate:"required,min=10"` +} diff --git a/pkg/markdown/handler.go b/pkg/markdown/handler.go index d1934328..e6c6e71e 100644 --- a/pkg/markdown/handler.go +++ b/pkg/markdown/handler.go @@ -11,7 +11,7 @@ import ( ) func (p Parser) Fetch() (string, error) { - req, err := http.NewRequest("GET", p.GetUrl(), nil) + req, err := http.NewRequest("GET", p.Url, nil) if err != nil { return "", err diff --git a/pkg/markdown/input.go b/pkg/markdown/input.go deleted file mode 100644 index dcdcfd3e..00000000 --- a/pkg/markdown/input.go +++ /dev/null @@ -1,33 +0,0 @@ -package markdown - -import ( - "flag" - "fmt" - "net/http" - "net/url" -) - -func ReadURL() (*string, error) { - uri := flag.String("uri", "", "URL of the markdown file to parse. (required)") - flag.Parse() - - if *uri == "" { - return nil, fmt.Errorf("uri is required") - } - - if u, err := url.Parse(*uri); err != nil || u.Scheme != "https" || u.Host != "raw.githubusercontent.com" { - return nil, fmt.Errorf("invalid uri: %w", err) - } - - response, err := http.Head(*uri) - if err != nil { - return nil, err - } - defer response.Body.Close() - - if response.StatusCode != http.StatusOK { - return nil, fmt.Errorf("failed to fetch the markdown file cntent: status %d", response.StatusCode) - } - - return uri, nil -} diff --git a/pkg/validator.go b/pkg/validator.go index ba4656c5..8a47298c 100644 --- a/pkg/validator.go +++ b/pkg/validator.go @@ -13,6 +13,14 @@ type Validator struct { Errors map[string]interface{} } +func GetDefaultValidator() *Validator { + return MakeValidatorFrom( + validator.New( + validator.WithRequiredStructEnabled(), + ), + ) +} + func MakeValidatorFrom(abstract *validator.Validate) *Validator { return &Validator{ Errors: make(map[string]interface{}), From cc655f0f32e80a3a768a57014ce6c3662f9a59ec Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Mon, 16 Jun 2025 13:08:13 +0800 Subject: [PATCH 17/35] stract attrs --- cli/main.go | 4 +- database/attrs.go | 61 +++++++++++++++++++ database/{schema.go => model.go} | 0 database/seeder/seeds/categories.go | 7 +-- database/seeder/seeds/comments.go | 12 +--- .../seeder/seeds/{seeder.go => factory.go} | 34 +++++------ database/seeder/seeds/likes.go | 8 +-- database/seeder/seeds/newsletters.go | 11 +--- database/seeder/seeds/post_views.go | 9 +-- database/seeder/seeds/posts.go | 18 +----- database/seeder/seeds/users.go | 8 +-- 11 files changed, 87 insertions(+), 85 deletions(-) create mode 100644 database/attrs.go rename database/{schema.go => model.go} (100%) rename database/seeder/seeds/{seeder.go => factory.go} (88%) diff --git a/cli/main.go b/cli/main.go index a62fb7e0..24324993 100644 --- a/cli/main.go +++ b/cli/main.go @@ -28,14 +28,14 @@ func main() { switch panel.GetChoice() { case 1: - uri, err := panel.CapturePostURL() + input, err := panel.CapturePostURL() if err != nil { fmt.Println(err) continue } - err = uri.Parse() + err = input.Parse() if err != nil { fmt.Println(err) diff --git a/database/attrs.go b/database/attrs.go new file mode 100644 index 00000000..40e5abaa --- /dev/null +++ b/database/attrs.go @@ -0,0 +1,61 @@ +package database + +import ( + "time" +) + +type UsersAttrs struct { + Username string + Name string + IsAdmin bool +} + +type CategoriesAttrs struct { + Slug string + Description string +} + +type CommentsAttrs struct { + UUID string + PostID uint64 + AuthorID uint64 + ParentID *uint64 + Content string + ApprovedAt *time.Time +} + +type LikesAttrs struct { + UUID string `gorm:"type:uuid;unique;not null"` + PostID uint64 `gorm:"not null;index;uniqueIndex:idx_likes_post_user"` + UserID uint64 `gorm:"not null;index;uniqueIndex:idx_likes_post_user"` +} + +type NewsletterAttrs struct { + FirstName string + LastName string + Email string + SubscribedAt *time.Time + UnsubscribedAt *time.Time +} + +type PostViewsAttr struct { + Post Post + User User + IPAddress string + UserAgent string +} + +type PostsAttrs struct { + AuthorID uint64 + Slug string + Title string + Excerpt string + Content string + PublishedAt *time.Time + Author User + Categories []Category + Tags []Tag + PostViews []PostView + Comments []Comment + Likes []Like +} diff --git a/database/schema.go b/database/model.go similarity index 100% rename from database/schema.go rename to database/model.go diff --git a/database/seeder/seeds/categories.go b/database/seeder/seeds/categories.go index dfe4a026..89648eab 100644 --- a/database/seeder/seeds/categories.go +++ b/database/seeder/seeds/categories.go @@ -11,18 +11,13 @@ type CategoriesSeed struct { db *database.Connection } -type CategoriesAttrs struct { - Slug string - Description string -} - func MakeCategoriesSeed(db *database.Connection) *CategoriesSeed { return &CategoriesSeed{ db: db, } } -func (s CategoriesSeed) Create(attrs CategoriesAttrs) ([]database.Category, error) { +func (s CategoriesSeed) Create(attrs database.CategoriesAttrs) ([]database.Category, error) { var categories []database.Category seeds := []string{ diff --git a/database/seeder/seeds/comments.go b/database/seeder/seeds/comments.go index aeef2c34..24bd03af 100644 --- a/database/seeder/seeds/comments.go +++ b/database/seeder/seeds/comments.go @@ -4,29 +4,19 @@ import ( "fmt" "github.com/google/uuid" "github.com/oullin/database" - "time" ) type CommentsSeed struct { db *database.Connection } -type CommentsAttrs struct { - UUID string - PostID uint64 - AuthorID uint64 - ParentID *uint64 - Content string - ApprovedAt *time.Time -} - func MakeCommentsSeed(db *database.Connection) *CommentsSeed { return &CommentsSeed{ db: db, } } -func (s CommentsSeed) Create(attrs ...CommentsAttrs) ([]database.Comment, error) { +func (s CommentsSeed) Create(attrs ...database.CommentsAttrs) ([]database.Comment, error) { var comments []database.Comment for _, attr := range attrs { diff --git a/database/seeder/seeds/seeder.go b/database/seeder/seeds/factory.go similarity index 88% rename from database/seeder/seeds/seeder.go rename to database/seeder/seeds/factory.go index e90fbb45..3ec4b39c 100644 --- a/database/seeder/seeds/seeder.go +++ b/database/seeder/seeds/factory.go @@ -38,7 +38,7 @@ func (s *Seeder) TruncateDB() error { func (s *Seeder) SeedUsers() (database.User, database.User) { users := MakeUsersSeed(s.dbConn) - UserA, err := users.Create(UsersAttrs{ + UserA, err := users.Create(database.UsersAttrs{ Username: "gocanto", Name: "Gus", IsAdmin: true, @@ -48,7 +48,7 @@ func (s *Seeder) SeedUsers() (database.User, database.User) { panic(err) } - UserB, err := users.Create(UsersAttrs{ + UserB, err := users.Create(database.UsersAttrs{ Username: "li", Name: "liane", IsAdmin: false, @@ -65,7 +65,7 @@ func (s *Seeder) SeedPosts(UserA, UserB database.User) []database.Post { posts := MakePostsSeed(s.dbConn) timex := time.Now() - PostsA, err := posts.CreatePosts(PostsAttrs{ + PostsA, err := posts.CreatePosts(database.PostsAttrs{ AuthorID: UserA.ID, Slug: fmt.Sprintf("post-slug-%s", uuid.NewString()), Title: fmt.Sprintf("Post %s title", uuid.NewString()), @@ -79,7 +79,7 @@ func (s *Seeder) SeedPosts(UserA, UserB database.User) []database.Post { panic(err) } - PostsB, err := posts.CreatePosts(PostsAttrs{ + PostsB, err := posts.CreatePosts(database.PostsAttrs{ AuthorID: UserB.ID, Slug: fmt.Sprintf("post-slug-%s", uuid.NewString()), Title: fmt.Sprintf("Post %s title", uuid.NewString()), @@ -99,7 +99,7 @@ func (s *Seeder) SeedPosts(UserA, UserB database.User) []database.Post { func (s *Seeder) SeedCategories() []database.Category { categories := MakeCategoriesSeed(s.dbConn) - result, err := categories.Create(CategoriesAttrs{ + result, err := categories.Create(database.CategoriesAttrs{ Slug: fmt.Sprintf("category-slug-%s", uuid.NewString()), Description: fmt.Sprintf("[%s] Sed at risus vel nulla consequat fermentum. Donec et orci mauris", uuid.NewString()), }) @@ -127,10 +127,10 @@ func (s *Seeder) SeedComments(posts ...database.Post) { seed := MakeCommentsSeed(s.dbConn) timex := time.Now() - var attrs []CommentsAttrs + var values []database.CommentsAttrs for index, post := range posts { - attrs = append(attrs, CommentsAttrs{ + values = append(values, database.CommentsAttrs{ PostID: post.ID, AuthorID: post.AuthorID, ParentID: nil, @@ -139,23 +139,23 @@ func (s *Seeder) SeedComments(posts ...database.Post) { }) } - if _, err := seed.Create(attrs...); err != nil { + if _, err := seed.Create(values...); err != nil { panic(err) } } func (s *Seeder) SeedLikes(posts ...database.Post) { seed := MakeLikesSeed(s.dbConn) - var attrs []LikesAttrs + var values []database.LikesAttrs for _, post := range posts { - attrs = append(attrs, LikesAttrs{ + values = append(values, database.LikesAttrs{ PostID: post.ID, UserID: post.AuthorID, }) } - _, err := seed.Create(attrs...) + _, err := seed.Create(values...) if err != nil { panic(err) @@ -221,11 +221,11 @@ func (s *Seeder) SeedPostViews(posts []database.Post, users ...database.User) { seed := MakePostViewsSeed(s.dbConn) - var attrs []PostViewsAttr + var values []database.PostViewsAttr for pIndex, post := range posts { for uIndex, user := range users { - attrs = append(attrs, PostViewsAttr{ + values = append(values, database.PostViewsAttr{ Post: post, User: user, IPAddress: fmt.Sprintf("192.168.0.%d", pIndex+1), @@ -234,7 +234,7 @@ func (s *Seeder) SeedPostViews(posts []database.Post, users ...database.User) { } } - err := seed.Create(attrs) + err := seed.Create(values) if err != nil { panic(err) @@ -242,9 +242,9 @@ func (s *Seeder) SeedPostViews(posts []database.Post, users ...database.User) { } func (s *Seeder) SeedNewsLetters() error { - var newsletters []NewsletterAttrs + var newsletters []database.NewsletterAttrs - a := NewsletterAttrs{ + a := database.NewsletterAttrs{ FirstName: "John", LastName: "Smith", Email: "john.smith@gmail.com", @@ -254,7 +254,7 @@ func (s *Seeder) SeedNewsLetters() error { currentTime := time.Now() last3Month := currentTime.AddDate(0, -3, 0) - b := NewsletterAttrs{ + b := database.NewsletterAttrs{ FirstName: "Don", LastName: "Smith", Email: "Don.smith@gmail.com", diff --git a/database/seeder/seeds/likes.go b/database/seeder/seeds/likes.go index a9b34862..0f50c731 100644 --- a/database/seeder/seeds/likes.go +++ b/database/seeder/seeds/likes.go @@ -11,19 +11,13 @@ type LikesSeed struct { db *database.Connection } -type LikesAttrs struct { - UUID string `gorm:"type:uuid;unique;not null"` - PostID uint64 `gorm:"not null;index;uniqueIndex:idx_likes_post_user"` - UserID uint64 `gorm:"not null;index;uniqueIndex:idx_likes_post_user"` -} - func MakeLikesSeed(db *database.Connection) *LikesSeed { return &LikesSeed{ db: db, } } -func (s LikesSeed) Create(attrs ...LikesAttrs) ([]database.Like, error) { +func (s LikesSeed) Create(attrs ...database.LikesAttrs) ([]database.Like, error) { var likes []database.Like for _, attr := range attrs { diff --git a/database/seeder/seeds/newsletters.go b/database/seeder/seeds/newsletters.go index 5f99bb3b..a5934f59 100644 --- a/database/seeder/seeds/newsletters.go +++ b/database/seeder/seeds/newsletters.go @@ -4,28 +4,19 @@ import ( "fmt" "github.com/oullin/database" "github.com/oullin/pkg/gorm" - "time" ) type NewslettersSeed struct { db *database.Connection } -type NewsletterAttrs struct { - FirstName string - LastName string - Email string - SubscribedAt *time.Time - UnsubscribedAt *time.Time -} - func MakeNewslettersSeed(db *database.Connection) *NewslettersSeed { return &NewslettersSeed{ db: db, } } -func (s NewslettersSeed) Create(attrs []NewsletterAttrs) error { +func (s NewslettersSeed) Create(attrs []database.NewsletterAttrs) error { var newsletters []database.Newsletter for _, attr := range attrs { diff --git a/database/seeder/seeds/post_views.go b/database/seeder/seeds/post_views.go index 6a0b78ec..4562e3ce 100644 --- a/database/seeder/seeds/post_views.go +++ b/database/seeder/seeds/post_views.go @@ -10,20 +10,13 @@ type PostViewsSeed struct { db *database.Connection } -type PostViewsAttr struct { - Post database.Post - User database.User - IPAddress string - UserAgent string -} - func MakePostViewsSeed(db *database.Connection) *PostViewsSeed { return &PostViewsSeed{ db: db, } } -func (s PostViewsSeed) Create(attrs []PostViewsAttr) error { +func (s PostViewsSeed) Create(attrs []database.PostViewsAttr) error { for _, attr := range attrs { result := s.db.Sql().Create(&database.PostView{ PostID: attr.Post.ID, diff --git a/database/seeder/seeds/posts.go b/database/seeder/seeds/posts.go index e5a9a2b2..56f71b0c 100644 --- a/database/seeder/seeds/posts.go +++ b/database/seeder/seeds/posts.go @@ -5,35 +5,19 @@ import ( "github.com/google/uuid" "github.com/oullin/database" "github.com/oullin/pkg/gorm" - "time" ) type PostsSeed struct { db *database.Connection } -type PostsAttrs struct { - AuthorID uint64 - Slug string - Title string - Excerpt string - Content string - PublishedAt *time.Time - Author database.User - Categories []database.Category - Tags []database.Tag - PostViews []database.PostView - Comments []database.Comment - Likes []database.Like -} - func MakePostsSeed(db *database.Connection) *PostsSeed { return &PostsSeed{ db: db, } } -func (s PostsSeed) CreatePosts(attrs PostsAttrs, number int) ([]database.Post, error) { +func (s PostsSeed) CreatePosts(attrs database.PostsAttrs, number int) ([]database.Post, error) { var posts []database.Post for i := 1; i <= number; i++ { diff --git a/database/seeder/seeds/users.go b/database/seeder/seeds/users.go index d1f01dc8..6e1b6e7b 100644 --- a/database/seeder/seeds/users.go +++ b/database/seeder/seeds/users.go @@ -14,19 +14,13 @@ type UsersSeed struct { db *database.Connection } -type UsersAttrs struct { - Username string - Name string - IsAdmin bool -} - func MakeUsersSeed(db *database.Connection) *UsersSeed { return &UsersSeed{ db: db, } } -func (s UsersSeed) Create(attrs UsersAttrs) (database.User, error) { +func (s UsersSeed) Create(attrs database.UsersAttrs) (database.User, error) { pass, _ := pkg.MakePassword("password") fake := database.User{ From 305878f54859c05ee5da8ccedfec00bc779aab1e Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Mon, 16 Jun 2025 13:17:17 +0800 Subject: [PATCH 18/35] format --- database/attrs.go | 72 +++++++++++++++++++++++------------------------ 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/database/attrs.go b/database/attrs.go index 40e5abaa..9683378f 100644 --- a/database/attrs.go +++ b/database/attrs.go @@ -1,61 +1,61 @@ package database import ( - "time" + "time" ) type UsersAttrs struct { - Username string - Name string - IsAdmin bool + Username string + Name string + IsAdmin bool } type CategoriesAttrs struct { - Slug string - Description string + Slug string + Description string } type CommentsAttrs struct { - UUID string - PostID uint64 - AuthorID uint64 - ParentID *uint64 - Content string - ApprovedAt *time.Time + UUID string + PostID uint64 + AuthorID uint64 + ParentID *uint64 + Content string + ApprovedAt *time.Time } type LikesAttrs struct { - UUID string `gorm:"type:uuid;unique;not null"` - PostID uint64 `gorm:"not null;index;uniqueIndex:idx_likes_post_user"` - UserID uint64 `gorm:"not null;index;uniqueIndex:idx_likes_post_user"` + UUID string `gorm:"type:uuid;unique;not null"` + PostID uint64 `gorm:"not null;index;uniqueIndex:idx_likes_post_user"` + UserID uint64 `gorm:"not null;index;uniqueIndex:idx_likes_post_user"` } type NewsletterAttrs struct { - FirstName string - LastName string - Email string - SubscribedAt *time.Time - UnsubscribedAt *time.Time + FirstName string + LastName string + Email string + SubscribedAt *time.Time + UnsubscribedAt *time.Time } type PostViewsAttr struct { - Post Post - User User - IPAddress string - UserAgent string + Post Post + User User + IPAddress string + UserAgent string } type PostsAttrs struct { - AuthorID uint64 - Slug string - Title string - Excerpt string - Content string - PublishedAt *time.Time - Author User - Categories []Category - Tags []Tag - PostViews []PostView - Comments []Comment - Likes []Like + AuthorID uint64 + Slug string + Title string + Excerpt string + Content string + PublishedAt *time.Time + Author User + Categories []Category + Tags []Tag + PostViews []PostView + Comments []Comment + Likes []Like } From 9cf16b116ff83cdb7dd9fd70d59c073cfa84cecb Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Mon, 16 Jun 2025 17:05:13 +0800 Subject: [PATCH 19/35] extract handler --- cli/main.go | 18 ++++++++++++++- cli/posts/factory.go | 25 ++++++++++++++++++++ cli/posts/handler.go | 39 +++----------------------------- cli/posts/input.go | 44 ++++++++++++++++++++++++++++++++++++ cli/posts/schema.go | 5 ---- database/repository/posts.go | 11 +++++++++ 6 files changed, 100 insertions(+), 42 deletions(-) create mode 100644 cli/posts/factory.go create mode 100644 cli/posts/input.go delete mode 100644 cli/posts/schema.go create mode 100644 database/repository/posts.go diff --git a/cli/main.go b/cli/main.go index 24324993..4eb9c708 100644 --- a/cli/main.go +++ b/cli/main.go @@ -3,14 +3,27 @@ package main import ( "bufio" "fmt" + "github.com/oullin/boost" "github.com/oullin/cli/menu" + "github.com/oullin/cli/posts" + "github.com/oullin/env" "github.com/oullin/pkg" "github.com/oullin/pkg/cli" "os" "time" ) +var environment *env.Environment + +func init() { + secrets, _ := boost.Spark("./../.env") + + environment = secrets +} + func main() { + postsHandler := posts.MakePostsHandler(environment) + panel := menu.Panel{ Reader: bufio.NewReader(os.Stdin), Validator: pkg.GetDefaultValidator(), @@ -22,7 +35,7 @@ func main() { err := panel.CaptureInput() if err != nil { - cli.MakeTextColour(err.Error(), cli.Red).Println() + fmt.Println(cli.Red + err.Error() + cli.Reset) continue } @@ -42,6 +55,8 @@ func main() { continue } + (*postsHandler).HandlePost() + return case 2: showTime() @@ -59,6 +74,7 @@ func main() { } func showTime() { + fmt.Println("") now := time.Now().Format("2006-01-02 15:04:05") fmt.Println(cli.Green, "\nCurrent time is", now, cli.Reset) } diff --git a/cli/posts/factory.go b/cli/posts/factory.go new file mode 100644 index 00000000..04be6039 --- /dev/null +++ b/cli/posts/factory.go @@ -0,0 +1,25 @@ +package posts + +import ( + "github.com/oullin/boost" + "github.com/oullin/database/repository" + "github.com/oullin/env" +) + +type Handler struct { + Env *env.Environment + Repository repository.Posts +} + +func MakePostsHandler(env *env.Environment) *Handler { + cnn := boost.MakeDbConnection(env) + + repo := repository.Posts{ + Db: cnn, + Env: env, + } + + return &Handler{ + Repository: repo, + } +} diff --git a/cli/posts/handler.go b/cli/posts/handler.go index cb9580ef..f2e90f6d 100644 --- a/cli/posts/handler.go +++ b/cli/posts/handler.go @@ -1,40 +1,7 @@ package posts -import ( - "fmt" - "github.com/oullin/pkg/cli" - "github.com/oullin/pkg/markdown" -) +import "fmt" -func (i *Input) Parse() error { - file := markdown.Parser{ - Url: i.Url, - } - - response, err := file.Fetch() - - if err != nil { - return fmt.Errorf("%sError fetching the markdown content: %v %s", cli.Red, err, cli.Reset) - } - - post, err := markdown.Parse(response) - - if err != nil { - return fmt.Errorf("%sEerror parsing markdown: %v %s", cli.Red, err, cli.Reset) - } - - // --- All good! - // Todo: Save post in the DB. - fmt.Printf("Title: %s\n", post.Title) - fmt.Printf("Excerpt: %s\n", post.Excerpt) - fmt.Printf("Slug: %s\n", post.Slug) - fmt.Printf("Author: %s\n", post.Author) - fmt.Printf("Image URL: %s\n", post.ImageURL) - fmt.Printf("Image Alt: %s\n", post.ImageAlt) - fmt.Printf("Category Alt: %s\n", post.Category) - fmt.Printf("Tags Alt: %s\n", post.Tags) - fmt.Println("\n--- Content ---") - fmt.Println(post.Content) - - return nil +func (h *Handler) HandlePost() { + fmt.Println("HandlePost!") } diff --git a/cli/posts/input.go b/cli/posts/input.go new file mode 100644 index 00000000..5526ab39 --- /dev/null +++ b/cli/posts/input.go @@ -0,0 +1,44 @@ +package posts + +import ( + "fmt" + "github.com/oullin/pkg/cli" + "github.com/oullin/pkg/markdown" +) + +type Input struct { + Url string `validate:"required,min=10"` +} + +func (i *Input) Parse() error { + file := markdown.Parser{ + Url: i.Url, + } + + response, err := file.Fetch() + + if err != nil { + return fmt.Errorf("%sError fetching the markdown content: %v %s", cli.Red, err, cli.Reset) + } + + post, err := markdown.Parse(response) + + if err != nil { + return fmt.Errorf("%sEerror parsing markdown: %v %s", cli.Red, err, cli.Reset) + } + + // --- All good! + // Todo: Save post in the DB. + fmt.Printf("Title: %s\n", post.Title) + fmt.Printf("Excerpt: %s\n", post.Excerpt) + fmt.Printf("Slug: %s\n", post.Slug) + fmt.Printf("Author: %s\n", post.Author) + fmt.Printf("Image URL: %s\n", post.ImageURL) + fmt.Printf("Image Alt: %s\n", post.ImageAlt) + fmt.Printf("Category Alt: %s\n", post.Category) + fmt.Printf("Tags Alt: %s\n", post.Tags) + fmt.Println("\n--- Content ---") + fmt.Println(post.Content) + + return nil +} diff --git a/cli/posts/schema.go b/cli/posts/schema.go deleted file mode 100644 index 300fe913..00000000 --- a/cli/posts/schema.go +++ /dev/null @@ -1,5 +0,0 @@ -package posts - -type Input struct { - Url string `validate:"required,min=10"` -} diff --git a/database/repository/posts.go b/database/repository/posts.go new file mode 100644 index 00000000..66b35339 --- /dev/null +++ b/database/repository/posts.go @@ -0,0 +1,11 @@ +package repository + +import ( + "github.com/oullin/database" + "github.com/oullin/env" +) + +type Posts struct { + Db *database.Connection + Env *env.Environment +} From 5c6b92af4e057d275935d2927eaffa90c38970ec Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Mon, 16 Jun 2025 17:15:30 +0800 Subject: [PATCH 20/35] pass parsed post --- cli/main.go | 8 +++----- cli/posts/handler.go | 7 +++++-- cli/posts/input.go | 8 ++++---- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/cli/main.go b/cli/main.go index 4eb9c708..a8957e15 100644 --- a/cli/main.go +++ b/cli/main.go @@ -48,15 +48,13 @@ func main() { continue } - err = input.Parse() - - if err != nil { + if post, err := input.Parse(); err != nil { fmt.Println(err) continue + } else { + (*postsHandler).HandlePost(post) } - (*postsHandler).HandlePost() - return case 2: showTime() diff --git a/cli/posts/handler.go b/cli/posts/handler.go index f2e90f6d..2167e72e 100644 --- a/cli/posts/handler.go +++ b/cli/posts/handler.go @@ -1,7 +1,10 @@ package posts -import "fmt" +import ( + "fmt" + "github.com/oullin/pkg/markdown" +) -func (h *Handler) HandlePost() { +func (h *Handler) HandlePost(post *markdown.Post) { fmt.Println("HandlePost!") } diff --git a/cli/posts/input.go b/cli/posts/input.go index 5526ab39..4d64ee66 100644 --- a/cli/posts/input.go +++ b/cli/posts/input.go @@ -10,7 +10,7 @@ type Input struct { Url string `validate:"required,min=10"` } -func (i *Input) Parse() error { +func (i *Input) Parse() (*markdown.Post, error) { file := markdown.Parser{ Url: i.Url, } @@ -18,13 +18,13 @@ func (i *Input) Parse() error { response, err := file.Fetch() if err != nil { - return fmt.Errorf("%sError fetching the markdown content: %v %s", cli.Red, err, cli.Reset) + return nil, fmt.Errorf("%sError fetching the markdown content: %v %s", cli.Red, err, cli.Reset) } post, err := markdown.Parse(response) if err != nil { - return fmt.Errorf("%sEerror parsing markdown: %v %s", cli.Red, err, cli.Reset) + return nil, fmt.Errorf("%sEerror parsing markdown: %v %s", cli.Red, err, cli.Reset) } // --- All good! @@ -40,5 +40,5 @@ func (i *Input) Parse() error { fmt.Println("\n--- Content ---") fmt.Println(post.Content) - return nil + return &post, nil } From 90cf6284aadfdaa526fc8ed4c3d5e37595570cf8 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Tue, 17 Jun 2025 16:26:52 +0800 Subject: [PATCH 21/35] map post --- cli/main.go | 12 +++++++ cli/menu/panel.go | 1 + cli/posts/factory.go | 19 ++++++----- cli/posts/handler.go | 62 +++++++++++++++++++++++++++++++++--- cli/posts/input.go | 3 +- database/attrs.go | 14 +++++--- database/repository/posts.go | 8 +++-- database/repository/users.go | 31 ++++++++++++++++++ pkg/markdown/handler.go | 21 ++++++------ pkg/markdown/schema.go | 37 +++++++++++++++------ pkg/stringable.go | 26 ++++++++++++++- 11 files changed, 194 insertions(+), 40 deletions(-) create mode 100644 database/repository/users.go diff --git a/cli/main.go b/cli/main.go index a8957e15..f7877c6c 100644 --- a/cli/main.go +++ b/cli/main.go @@ -58,6 +58,8 @@ func main() { return case 2: showTime() + case 3: + timeParse() case 0: fmt.Println(cli.Green + "Goodbye!" + cli.Reset) return @@ -76,3 +78,13 @@ func showTime() { now := time.Now().Format("2006-01-02 15:04:05") fmt.Println(cli.Green, "\nCurrent time is", now, cli.Reset) } + +func timeParse() { + s := pkg.MakeStringable("2025-04-12") + + if seed, err := s.ToDatetime(); err != nil { + panic(err) + } else { + fmt.Println(seed.Format(time.DateTime)) + } +} diff --git a/cli/menu/panel.go b/cli/menu/panel.go index 5112e625..7edddc84 100644 --- a/cli/menu/panel.go +++ b/cli/menu/panel.go @@ -63,6 +63,7 @@ func (p *Panel) PrintMenu() { p.PrintOption("1) Parse Posts", inner) p.PrintOption("2) Show Time", inner) + p.PrintOption("3) Show Date", inner) p.PrintOption("0) Exit", inner) fmt.Println(footer + cli.Reset) diff --git a/cli/posts/factory.go b/cli/posts/factory.go index 04be6039..d2ead955 100644 --- a/cli/posts/factory.go +++ b/cli/posts/factory.go @@ -7,19 +7,22 @@ import ( ) type Handler struct { - Env *env.Environment - Repository repository.Posts + Env *env.Environment + PostsRepository *repository.Posts + UsersRepository *repository.Users } func MakePostsHandler(env *env.Environment) *Handler { cnn := boost.MakeDbConnection(env) - repo := repository.Posts{ - Db: cnn, - Env: env, - } - return &Handler{ - Repository: repo, + PostsRepository: &repository.Posts{ + Connection: cnn, + Env: env, + }, + UsersRepository: &repository.Users{ + Connection: cnn, + Env: env, + }, } } diff --git a/cli/posts/handler.go b/cli/posts/handler.go index 2167e72e..60e2fbc0 100644 --- a/cli/posts/handler.go +++ b/cli/posts/handler.go @@ -1,10 +1,64 @@ package posts import ( - "fmt" - "github.com/oullin/pkg/markdown" + "fmt" + "github.com/oullin/database" + "github.com/oullin/pkg/markdown" + "time" ) -func (h *Handler) HandlePost(post *markdown.Post) { - fmt.Println("HandlePost!") +func (h *Handler) HandlePost(payload *markdown.Post) error { + var err error + var publishedAt *time.Time + author := h.UsersRepository.FindBy(payload.Author) + + if author == nil { + return fmt.Errorf("the given author [%s] does not exist", payload.Author) + } + + if publishedAt, err = payload.GetPublishedAt(); err != nil { + return fmt.Errorf("the given published_at [%s] date is invalid", payload.PublishedAt) + } + + post := database.PostsAttrs{ + AuthorID: author.ID, + Slug: payload.Slug, + Title: payload.Title, + Excerpt: payload.Excerpt, + Content: payload.Content, + PublishedAt: publishedAt, + ImageURL: payload.ImageURL, + Author: *author, + Categories: h.ParseCategories(payload), + Tags: h.ParseTags(payload), + } + + fmt.Println("-----------------") + fmt.Println(post) + + return nil +} + +func (h *Handler) ParseCategories(payload *markdown.Post) []database.CategoriesAttrs { + var categories []database.CategoriesAttrs + + slice := append(categories, database.CategoriesAttrs{ + Slug: payload.CategorySlug, + Name: payload.Category, + }) + + return slice +} + +func (h *Handler) ParseTags(payload *markdown.Post) []database.TagAttrs { + var slice []database.TagAttrs + + for _, tag := range payload.Tags { + slice = append(slice, database.TagAttrs{ + Slug: tag, + Name: tag, + }) + } + + return slice } diff --git a/cli/posts/input.go b/cli/posts/input.go index 4d64ee66..5d359227 100644 --- a/cli/posts/input.go +++ b/cli/posts/input.go @@ -35,7 +35,8 @@ func (i *Input) Parse() (*markdown.Post, error) { fmt.Printf("Author: %s\n", post.Author) fmt.Printf("Image URL: %s\n", post.ImageURL) fmt.Printf("Image Alt: %s\n", post.ImageAlt) - fmt.Printf("Category Alt: %s\n", post.Category) + fmt.Printf("Category: %s\n", post.Category) + fmt.Printf("Category Slug: %s\n", post.CategorySlug) fmt.Printf("Tags Alt: %s\n", post.Tags) fmt.Println("\n--- Content ---") fmt.Println(post.Content) diff --git a/database/attrs.go b/database/attrs.go index 9683378f..a1c1285f 100644 --- a/database/attrs.go +++ b/database/attrs.go @@ -11,8 +11,13 @@ type UsersAttrs struct { } type CategoriesAttrs struct { - Slug string - Description string + Slug string + Name string +} + +type TagAttrs struct { + Slug string + Name string } type CommentsAttrs struct { @@ -51,10 +56,11 @@ type PostsAttrs struct { Title string Excerpt string Content string + ImageURL string PublishedAt *time.Time Author User - Categories []Category - Tags []Tag + Categories []CategoriesAttrs + Tags []TagAttrs PostViews []PostView Comments []Comment Likes []Like diff --git a/database/repository/posts.go b/database/repository/posts.go index 66b35339..8825584f 100644 --- a/database/repository/posts.go +++ b/database/repository/posts.go @@ -6,6 +6,10 @@ import ( ) type Posts struct { - Db *database.Connection - Env *env.Environment + Connection *database.Connection + Env *env.Environment +} + +func (r Posts) Create() (*database.Post, error) { + return nil, nil } diff --git a/database/repository/users.go b/database/repository/users.go new file mode 100644 index 00000000..f3ca9e1a --- /dev/null +++ b/database/repository/users.go @@ -0,0 +1,31 @@ +package repository + +import ( + "github.com/oullin/database" + "github.com/oullin/env" + "github.com/oullin/pkg/gorm" + "strings" +) + +type Users struct { + Connection *database.Connection + Env *env.Environment +} + +func (r Users) FindBy(username string) *database.User { + user := &database.User{} + + result := r.Connection.Sql(). + Where("username = ?", username). + First(&user) + + if gorm.HasDbIssues(result.Error) { + return nil + } + + if strings.Trim(user.UUID, " ") != "" { + return user + } + + return nil +} diff --git a/pkg/markdown/handler.go b/pkg/markdown/handler.go index e6c6e71e..a9f72b0c 100644 --- a/pkg/markdown/handler.go +++ b/pkg/markdown/handler.go @@ -7,7 +7,6 @@ import ( "net/http" "regexp" "strings" - "time" ) func (p Parser) Fetch() (string, error) { @@ -41,16 +40,6 @@ func (p Parser) Fetch() (string, error) { return string(body), nil } -func (p Parser) GetUrl() string { - sep := "?" - - if strings.Contains(p.Url, "?") { - sep = "&" - } - - return fmt.Sprintf("%s%sts=%d", p.Url, sep, time.Now().UnixNano()) -} - // Parse splits the document into front-matter and content, then parses YAML. // It also extracts a leading Parser image (header image) if present. func Parse(data string) (Post, error) { @@ -96,5 +85,15 @@ func Parse(data string) (Post, error) { post.Content = body } + parseCategory(&post) + return post, nil } + +func parseCategory(post *Post) { + category := post.FrontMatter.Category + parts := strings.Split(category, ":") + + post.Category = parts[1] + post.CategorySlug = parts[0] +} diff --git a/pkg/markdown/schema.go b/pkg/markdown/schema.go index 2be4a234..784b56c2 100644 --- a/pkg/markdown/schema.go +++ b/pkg/markdown/schema.go @@ -1,21 +1,40 @@ package markdown +import ( + "fmt" + "github.com/oullin/pkg" + "time" +) + type FrontMatter struct { - Title string `yaml:"title"` - Excerpt string `yaml:"excerpt"` - Slug string `yaml:"slug"` - Author string `yaml:"author"` - Category string `yaml:"category"` - Tags []string `yaml:"tags"` + Title string `yaml:"title"` + Excerpt string `yaml:"excerpt"` + Slug string `yaml:"slug"` + Author string `yaml:"author"` + Category string `yaml:"category"` + PublishedAt string `yaml:"published_at"` + Tags []string `yaml:"tags"` } type Post struct { FrontMatter - ImageURL string - ImageAlt string - Content string + ImageURL string + ImageAlt string + Content string + CategorySlug string } type Parser struct { Url string } + +func (f FrontMatter) GetPublishedAt() (*time.Time, error) { + stringable := pkg.MakeStringable(f.PublishedAt) + publishedAt, err := stringable.ToDatetime() + + if err != nil { + return nil, fmt.Errorf("error parsing published_at: %v", err) + } + + return publishedAt, nil +} diff --git a/pkg/stringable.go b/pkg/stringable.go index 4f81086d..6ee000ac 100644 --- a/pkg/stringable.go +++ b/pkg/stringable.go @@ -3,6 +3,7 @@ package pkg import ( "fmt" "strings" + "time" "unicode" ) @@ -12,7 +13,7 @@ type Stringable struct { func MakeStringable(value string) *Stringable { return &Stringable{ - value: value, + value: strings.TrimSpace(value), } } @@ -36,3 +37,26 @@ func (s Stringable) ToSnakeCase() string { func (s Stringable) Dd(abstract any) { fmt.Println(fmt.Sprintf("dd: %+v", abstract)) } + +func (s Stringable) ToDatetime() (*time.Time, error) { + parsed, err := time.Parse(time.DateOnly, s.value) + + if err != nil { + return nil, fmt.Errorf("error parsing date string: %v", err) + } + + now := time.Now() + + produce := time.Date( + parsed.Year(), + parsed.Month(), + parsed.Day(), + now.Hour(), + now.Minute(), + now.Second(), + now.Nanosecond(), + now.Location(), + ) + + return &produce, nil +} From a2508e982b69cf5ea6a09a5aa8aa5c1b1bbf607f Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Tue, 17 Jun 2025 16:27:12 +0800 Subject: [PATCH 22/35] format --- cli/posts/handler.go | 84 ++++++++++++++++++++++---------------------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/cli/posts/handler.go b/cli/posts/handler.go index 60e2fbc0..f8f9b699 100644 --- a/cli/posts/handler.go +++ b/cli/posts/handler.go @@ -1,64 +1,64 @@ package posts import ( - "fmt" - "github.com/oullin/database" - "github.com/oullin/pkg/markdown" - "time" + "fmt" + "github.com/oullin/database" + "github.com/oullin/pkg/markdown" + "time" ) func (h *Handler) HandlePost(payload *markdown.Post) error { - var err error - var publishedAt *time.Time - author := h.UsersRepository.FindBy(payload.Author) + var err error + var publishedAt *time.Time + author := h.UsersRepository.FindBy(payload.Author) - if author == nil { - return fmt.Errorf("the given author [%s] does not exist", payload.Author) - } + if author == nil { + return fmt.Errorf("the given author [%s] does not exist", payload.Author) + } - if publishedAt, err = payload.GetPublishedAt(); err != nil { - return fmt.Errorf("the given published_at [%s] date is invalid", payload.PublishedAt) - } + if publishedAt, err = payload.GetPublishedAt(); err != nil { + return fmt.Errorf("the given published_at [%s] date is invalid", payload.PublishedAt) + } - post := database.PostsAttrs{ - AuthorID: author.ID, - Slug: payload.Slug, - Title: payload.Title, - Excerpt: payload.Excerpt, - Content: payload.Content, - PublishedAt: publishedAt, - ImageURL: payload.ImageURL, - Author: *author, - Categories: h.ParseCategories(payload), - Tags: h.ParseTags(payload), - } + post := database.PostsAttrs{ + AuthorID: author.ID, + Slug: payload.Slug, + Title: payload.Title, + Excerpt: payload.Excerpt, + Content: payload.Content, + PublishedAt: publishedAt, + ImageURL: payload.ImageURL, + Author: *author, + Categories: h.ParseCategories(payload), + Tags: h.ParseTags(payload), + } - fmt.Println("-----------------") - fmt.Println(post) + fmt.Println("-----------------") + fmt.Println(post) - return nil + return nil } func (h *Handler) ParseCategories(payload *markdown.Post) []database.CategoriesAttrs { - var categories []database.CategoriesAttrs + var categories []database.CategoriesAttrs - slice := append(categories, database.CategoriesAttrs{ - Slug: payload.CategorySlug, - Name: payload.Category, - }) + slice := append(categories, database.CategoriesAttrs{ + Slug: payload.CategorySlug, + Name: payload.Category, + }) - return slice + return slice } func (h *Handler) ParseTags(payload *markdown.Post) []database.TagAttrs { - var slice []database.TagAttrs + var slice []database.TagAttrs - for _, tag := range payload.Tags { - slice = append(slice, database.TagAttrs{ - Slug: tag, - Name: tag, - }) - } + for _, tag := range payload.Tags { + slice = append(slice, database.TagAttrs{ + Slug: tag, + Name: tag, + }) + } - return slice + return slice } From d9695df4afc61e91af6d4a8b8026a8a4d37b7cfb Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Tue, 17 Jun 2025 16:36:53 +0800 Subject: [PATCH 23/35] fix seeder --- cli/posts/handler.go | 5 +++-- database/attrs.go | 5 +++-- database/seeder/seeds/factory.go | 1 + 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/cli/posts/handler.go b/cli/posts/handler.go index f8f9b699..305fbfe6 100644 --- a/cli/posts/handler.go +++ b/cli/posts/handler.go @@ -43,8 +43,9 @@ func (h *Handler) ParseCategories(payload *markdown.Post) []database.CategoriesA var categories []database.CategoriesAttrs slice := append(categories, database.CategoriesAttrs{ - Slug: payload.CategorySlug, - Name: payload.Category, + Slug: payload.CategorySlug, + Name: payload.Category, + Description: "", }) return slice diff --git a/database/attrs.go b/database/attrs.go index a1c1285f..03d07932 100644 --- a/database/attrs.go +++ b/database/attrs.go @@ -11,8 +11,9 @@ type UsersAttrs struct { } type CategoriesAttrs struct { - Slug string - Name string + Slug string + Name string + Description string } type TagAttrs struct { diff --git a/database/seeder/seeds/factory.go b/database/seeder/seeds/factory.go index 3ec4b39c..0b5210ba 100644 --- a/database/seeder/seeds/factory.go +++ b/database/seeder/seeds/factory.go @@ -101,6 +101,7 @@ func (s *Seeder) SeedCategories() []database.Category { result, err := categories.Create(database.CategoriesAttrs{ Slug: fmt.Sprintf("category-slug-%s", uuid.NewString()), + Name: fmt.Sprintf("category-slug-%s", uuid.NewString()), Description: fmt.Sprintf("[%s] Sed at risus vel nulla consequat fermentum. Donec et orci mauris", uuid.NewString()), }) From 907bd21c435abf010398d238e3f3c36dad055edc Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Tue, 17 Jun 2025 16:58:00 +0800 Subject: [PATCH 24/35] simpler cli colours --- cli/main.go | 8 +++--- cli/menu/panel.go | 20 ++++++------- cli/posts/input.go | 4 +-- database/seeder/main.go | 24 ++++++++-------- pkg/cli/colour.go | 62 ++++++----------------------------------- pkg/cli/message.go | 59 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 95 insertions(+), 82 deletions(-) create mode 100644 pkg/cli/message.go diff --git a/cli/main.go b/cli/main.go index f7877c6c..800007cd 100644 --- a/cli/main.go +++ b/cli/main.go @@ -35,7 +35,7 @@ func main() { err := panel.CaptureInput() if err != nil { - fmt.Println(cli.Red + err.Error() + cli.Reset) + fmt.Println(cli.RedColour + err.Error() + cli.Reset) continue } @@ -61,10 +61,10 @@ func main() { case 3: timeParse() case 0: - fmt.Println(cli.Green + "Goodbye!" + cli.Reset) + fmt.Println(cli.GreenColour + "Goodbye!" + cli.Reset) return default: - fmt.Println(cli.Red, "Unknown option. Try again.", cli.Reset) + fmt.Println(cli.RedColour, "Unknown option. Try again.", cli.Reset) } fmt.Print("\nPress Enter to continue...") @@ -76,7 +76,7 @@ func main() { func showTime() { fmt.Println("") now := time.Now().Format("2006-01-02 15:04:05") - fmt.Println(cli.Green, "\nCurrent time is", now, cli.Reset) + fmt.Println(cli.GreenColour, "\nCurrent time is", now, cli.Reset) } func timeParse() { diff --git a/cli/menu/panel.go b/cli/menu/panel.go index 7edddc84..340f890c 100644 --- a/cli/menu/panel.go +++ b/cli/menu/panel.go @@ -20,18 +20,18 @@ func (p *Panel) GetChoice() int { } func (p *Panel) CaptureInput() error { - fmt.Print(cli.Yellow + "Select an option: " + cli.Reset) + fmt.Print(cli.YellowColour + "Select an option: " + cli.Reset) input, err := p.Reader.ReadString('\n') if err != nil { - return fmt.Errorf("%s error reading input: %v %s", cli.Red, err, cli.Reset) + return fmt.Errorf("%s error reading input: %v %s", cli.RedColour, err, cli.Reset) } input = strings.TrimSpace(input) choice, err := strconv.Atoi(input) if err != nil { - return fmt.Errorf("%s Please enter a valid number. %s", cli.Red, cli.Reset) + return fmt.Errorf("%s Please enter a valid number. %s", cli.RedColour, cli.Reset) } p.Choice = &choice @@ -57,7 +57,7 @@ func (p *Panel) PrintMenu() { // Print in color fmt.Println() - fmt.Println(cli.Cyan + border) + fmt.Println(cli.CyanColour + border) fmt.Println(title) fmt.Println(divider) @@ -97,21 +97,21 @@ func (p *Panel) CapturePostURL() (*posts.Input, error) { uri, err := p.Reader.ReadString('\n') if err != nil { - return nil, fmt.Errorf("%sError reading the given post URL: %v %s", cli.Red, err, cli.Reset) + return nil, fmt.Errorf("%sError reading the given post URL: %v %s", cli.RedColour, err, cli.Reset) } uri = strings.TrimSpace(uri) if uri == "" { - return nil, fmt.Errorf("%sError: no URL provided: %s", cli.Red, cli.Reset) + return nil, fmt.Errorf("%sError: no URL provided: %s", cli.RedColour, cli.Reset) } parsedURL, err := url.Parse(uri) if err != nil { - return nil, fmt.Errorf("%sError: invalid URL: %v %s", cli.Red, err, cli.Reset) + return nil, fmt.Errorf("%sError: invalid URL: %v %s", cli.RedColour, err, cli.Reset) } if parsedURL.Scheme != "https" || parsedURL.Host != "raw.githubusercontent.com" { - return nil, fmt.Errorf("%sError: URL must begin with https://raw.githubusercontent.com: %v %s", cli.Red, err, cli.Reset) + return nil, fmt.Errorf("%sError: URL must begin with https://raw.githubusercontent.com: %v %s", cli.RedColour, err, cli.Reset) } input := posts.Input{Url: parsedURL.String()} @@ -120,10 +120,10 @@ func (p *Panel) CapturePostURL() (*posts.Input, error) { if _, err := validate.Rejects(input); err != nil { return nil, fmt.Errorf( "%sError validating the given post URL: %v %s \n%sViolations:%s %s", - cli.Red, + cli.RedColour, err, cli.Reset, - cli.Blue, + cli.BlueColour, cli.Reset, validate.GetErrorsAsJason(), ) diff --git a/cli/posts/input.go b/cli/posts/input.go index 5d359227..334c9c23 100644 --- a/cli/posts/input.go +++ b/cli/posts/input.go @@ -18,13 +18,13 @@ func (i *Input) Parse() (*markdown.Post, error) { response, err := file.Fetch() if err != nil { - return nil, fmt.Errorf("%sError fetching the markdown content: %v %s", cli.Red, err, cli.Reset) + return nil, fmt.Errorf("%sError fetching the markdown content: %v %s", cli.RedColour, err, cli.Reset) } post, err := markdown.Parse(response) if err != nil { - return nil, fmt.Errorf("%sEerror parsing markdown: %v %s", cli.Red, err, cli.Reset) + return nil, fmt.Errorf("%sEerror parsing markdown: %v %s", cli.RedColour, err, cli.Reset) } // --- All good! diff --git a/database/seeder/main.go b/database/seeder/main.go index fd3ba9da..ce2485a0 100644 --- a/database/seeder/main.go +++ b/database/seeder/main.go @@ -36,7 +36,7 @@ func main() { if err := seeder.TruncateDB(); err != nil { panic(err) } else { - cli.MakeTextColour("DB Truncated successfully ...", cli.Green).Print() + cli.Successln("DB Truncated successfully ...") time.Sleep(2 * time.Second) } @@ -50,14 +50,14 @@ func main() { go func() { defer close(categoriesChan) - cli.MakeTextColour("Seeding categories ...", cli.Yellow).Print() + cli.Warningln("Seeding categories ...") categoriesChan <- seeder.SeedCategories() }() go func() { defer close(tagsChan) - cli.MakeTextColour("Seeding tags ...", cli.Magenta).Print() + cli.Magentaln("Seeding tags ...") tagsChan <- seeder.SeedTags() }() @@ -72,50 +72,50 @@ func main() { go func() { defer wg.Done() - cli.MakeTextColour("Seeding comments ...", cli.Blue).Print() + cli.Blueln("Seeding comments ...") seeder.SeedComments(posts...) }() go func() { defer wg.Done() - cli.MakeTextColour("Seeding likes ...", cli.Cyan).Print() + cli.Cyanln("Seeding likes ...") seeder.SeedLikes(posts...) }() go func() { defer wg.Done() - cli.MakeTextColour("Seeding posts-categories ...", cli.Gray).Print() + cli.Grayln("Seeding posts-categories ...") seeder.SeedPostsCategories(categories, posts) }() go func() { defer wg.Done() - cli.MakeTextColour("Seeding posts-tags ...", cli.Magenta).Print() + cli.Grayln("Seeding posts-tags ...") seeder.SeedPostTags(tags, posts) }() go func() { defer wg.Done() - cli.MakeTextColour("Seeding views ...", cli.Yellow).Print() + cli.Warningln("Seeding views ...") seeder.SeedPostViews(posts, UserA, UserB) }() go func() { defer wg.Done() - cli.MakeTextColour("Seeding Newsletters ...", cli.Green).Print() + cli.Successln("Seeding Newsletters ...") if err := seeder.SeedNewsLetters(); err != nil { - cli.MakeTextColour(err.Error(), cli.Red).Print() + cli.Error(err.Error()) } }() wg.Wait() - cli.MakeTextColour("DB seeded as expected.", cli.Green).Print() + cli.Successln("DB seeded as expected.") } func clearScreen() { @@ -126,6 +126,6 @@ func clearScreen() { if err := cmd.Run(); err != nil { message := fmt.Sprintf("Could not clear screen. Error: %s", err.Error()) - cli.MakeTextColour(message, cli.Red).Print() + cli.Errorln(message) } } diff --git a/pkg/cli/colour.go b/pkg/cli/colour.go index d554793d..4642a26a 100644 --- a/pkg/cli/colour.go +++ b/pkg/cli/colour.go @@ -1,57 +1,11 @@ package cli -import ( - "fmt" - "os" - "slices" -) - var Reset = "\033[0m" -var Red = "\033[31m" -var Green = "\033[32m" -var Yellow = "\033[33m" -var Blue = "\033[34m" -var Magenta = "\033[35m" -var Cyan = "\033[36m" -var Gray = "\033[37m" -var White = "\033[97m" - -var colours = []string{Reset, Red, Green, Yellow, Blue, Magenta, Cyan, Gray, White} - -type TextColour struct { - text string - colour string - padding bool -} - -func MakeTextColour(text string, colour string) TextColour { - if !slices.Contains(colours, colour) { - colour = White - } - - return TextColour{ - text: text, - colour: colour, - padding: true, - } -} - -func (t TextColour) Print() { - _, err := fmt.Print(t.String()) - - if err != nil { - _, _ = fmt.Fprintf(os.Stderr, "%v\n", err) - } -} - -func (t TextColour) Println() { - _, err := fmt.Println(t.String()) - - if err != nil { - _, _ = fmt.Fprintf(os.Stderr, "%v\n", err) - } -} - -func (t TextColour) String() string { - return fmt.Sprintf("%s > %s %s\n", t.colour, t.text, Reset) -} +var RedColour = "\033[31m" +var GreenColour = "\033[32m" +var YellowColour = "\033[33m" +var BlueColour = "\033[34m" +var MagentaColour = "\033[35m" +var CyanColour = "\033[36m" +var GrayColour = "\033[37m" +var WhiteColour = "\033[97m" diff --git a/pkg/cli/message.go b/pkg/cli/message.go new file mode 100644 index 00000000..c8c81bb9 --- /dev/null +++ b/pkg/cli/message.go @@ -0,0 +1,59 @@ +package cli + +import "fmt" + +func Error(message string) { + fmt.Print(RedColour + message + Reset) +} + +func Errorln(message string) { + fmt.Println(RedColour + message + Reset) +} + +func Success(message string) { + fmt.Print(GreenColour + message + Reset) +} + +func Successln(message string) { + fmt.Println(GreenColour + message + Reset) +} + +func Warning(message string) { + fmt.Print(GreenColour + message + Reset) +} + +func Warningln(message string) { + fmt.Println(YellowColour + message + Reset) +} + +func Magenta(message string) { + fmt.Print(MagentaColour + message + Reset) +} + +func Magentaln(message string) { + fmt.Println(MagentaColour + message + Reset) +} + +func Blue(message string) { + fmt.Print(BlueColour + message + Reset) +} + +func Blueln(message string) { + fmt.Println(BlueColour + message + Reset) +} + +func Cyan(message string) { + fmt.Print(CyanColour + message + Reset) +} + +func Cyanln(message string) { + fmt.Println(CyanColour + message + Reset) +} + +func Gray(message string) { + fmt.Print(GrayColour + message + Reset) +} + +func Grayln(message string) { + fmt.Println(GrayColour + message + Reset) +} From f2cec41c367df3a0b43e131e990eef7cafa7b054 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Wed, 18 Jun 2025 12:22:05 +0800 Subject: [PATCH 25/35] start working on persisting posts --- database/repository/category.go | 90 +++++++++++++++++++++++++++++++++ database/repository/posts.go | 34 +++++++++++-- 2 files changed, 120 insertions(+), 4 deletions(-) create mode 100644 database/repository/category.go diff --git a/database/repository/category.go b/database/repository/category.go new file mode 100644 index 00000000..798474cc --- /dev/null +++ b/database/repository/category.go @@ -0,0 +1,90 @@ +package repository + +import ( + "fmt" + "github.com/google/uuid" + "github.com/oullin/database" + "github.com/oullin/env" + "github.com/oullin/pkg/gorm" + "strings" +) + +type Category struct { + Connection *database.Connection + Env *env.Environment +} + +func (r Category) FindBy(slug string) *database.Category { + category := &database.Category{} + + result := r.Connection.Sql(). + Where("slug = ?", slug). + First(&category) + + if gorm.HasDbIssues(result.Error) { + return nil + } + + if strings.Trim(category.UUID, " ") != "" { + return category + } + + return nil +} + +func (r Category) CreateOrUpdate(post database.Post, attrs database.PostsAttrs) (*[]database.Category, error) { + var output []database.Category + + for _, seed := range attrs.Categories { + exists, err := r.ExistOrUpdate(seed) + + if exists { + continue + } + + if err != nil { + return nil, fmt.Errorf("error creating/updating category [%s]: %s", seed.Name, err) + } + + category := database.Category{ + UUID: uuid.NewString(), + Name: seed.Name, + Slug: seed.Slug, + Description: seed.Description, + } + + if result := r.Connection.Sql().Create(&category); gorm.HasDbIssues(result.Error) { + return nil, fmt.Errorf("error creating category [%s]: %s", seed.Name, result.Error) + } + + trace := database.PostCategory{ + CategoryID: category.ID, + PostID: post.ID, + } + + if result := r.Connection.Sql().Create(&trace); gorm.HasDbIssues(result.Error) { + return nil, fmt.Errorf("error creating category trace [%s:%s]: %s", category.Name, post.Title, result.Error) + } + + output = append(output, category) + } + + return &output, nil +} + +func (r Category) ExistOrUpdate(seed database.CategoriesAttrs) (bool, error) { + var category *database.Category + + if category = r.FindBy(seed.Slug); category == nil { + return false, nil + } + + category.Name = seed.Name + category.Description = seed.Description + + if result := r.Connection.Sql().Save(&category); gorm.HasDbIssues(result.Error) { + return false, fmt.Errorf("error on exist or update category [%s]: %s", category.Name, result.Error) + } + + return true, nil +} diff --git a/database/repository/posts.go b/database/repository/posts.go index 8825584f..61c452d1 100644 --- a/database/repository/posts.go +++ b/database/repository/posts.go @@ -1,15 +1,41 @@ package repository import ( + "fmt" + "github.com/google/uuid" "github.com/oullin/database" "github.com/oullin/env" + "github.com/oullin/pkg/gorm" ) type Posts struct { - Connection *database.Connection - Env *env.Environment + Connection *database.Connection + Env *env.Environment + CategoriesRepository Category } -func (r Posts) Create() (*database.Post, error) { - return nil, nil +func (r Posts) Create(attrs database.PostsAttrs) (*database.Post, error) { + post := database.Post{ + UUID: uuid.NewString(), + AuthorID: attrs.AuthorID, + Slug: attrs.Slug, + Title: attrs.Title, + Excerpt: attrs.Excerpt, + Content: attrs.Content, + CoverImageURL: attrs.ImageURL, + PublishedAt: attrs.PublishedAt, + } + + // Todo: + // 1 - Encapsulate all these DB queries in a DB transaction. + // 2 - Make sure internal queries abort top level DB transactions. + if result := r.Connection.Sql().Create(&post); gorm.HasDbIssues(result.Error) { + return nil, fmt.Errorf("issue creating posts: %s", result.Error) + } + + if _, err := r.CategoriesRepository.CreateOrUpdate(post, attrs); err != nil { + return &post, fmt.Errorf("issue creating the given post [%s] category: %s", attrs.Slug, err.Error()) + } + + return &post, nil } From e11f801a0a55541d195a1104759e426a8f42d404 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Wed, 18 Jun 2025 13:38:06 +0800 Subject: [PATCH 26/35] add db transaction --- cli/posts/factory.go | 21 +++--- cli/posts/handler.go | 2 +- database/connection.go | 4 + .../repository/{category.go => categories.go} | 24 +++--- database/repository/posts.go | 73 +++++++++++-------- database/repository/users.go | 8 +- 6 files changed, 73 insertions(+), 59 deletions(-) rename database/repository/{category.go => categories.go} (65%) diff --git a/cli/posts/factory.go b/cli/posts/factory.go index d2ead955..fe44e597 100644 --- a/cli/posts/factory.go +++ b/cli/posts/factory.go @@ -7,22 +7,23 @@ import ( ) type Handler struct { - Env *env.Environment - PostsRepository *repository.Posts - UsersRepository *repository.Users + Env *env.Environment + Posts *repository.Posts + Users *repository.Users + Categories *repository.Categories } func MakePostsHandler(env *env.Environment) *Handler { - cnn := boost.MakeDbConnection(env) + db := boost.MakeDbConnection(env) return &Handler{ - PostsRepository: &repository.Posts{ - Connection: cnn, - Env: env, + Posts: &repository.Posts{ + DB: db, + Env: env, }, - UsersRepository: &repository.Users{ - Connection: cnn, - Env: env, + Users: &repository.Users{ + DB: db, + Env: env, }, } } diff --git a/cli/posts/handler.go b/cli/posts/handler.go index 305fbfe6..a1cea5ec 100644 --- a/cli/posts/handler.go +++ b/cli/posts/handler.go @@ -10,7 +10,7 @@ import ( func (h *Handler) HandlePost(payload *markdown.Post) error { var err error var publishedAt *time.Time - author := h.UsersRepository.FindBy(payload.Author) + author := h.Users.FindBy(payload.Author) if author == nil { return fmt.Errorf("the given author [%s] does not exist", payload.Author) diff --git a/database/connection.go b/database/connection.go index 10635b7c..5eeac376 100644 --- a/database/connection.go +++ b/database/connection.go @@ -73,3 +73,7 @@ func (c *Connection) Ping() { func (c *Connection) Sql() *gorm.DB { return c.driver } + +func (c *Connection) Transaction(callback func(db *gorm.DB) error) error { + return c.driver.Transaction(callback) +} diff --git a/database/repository/category.go b/database/repository/categories.go similarity index 65% rename from database/repository/category.go rename to database/repository/categories.go index 798474cc..9be5ea13 100644 --- a/database/repository/category.go +++ b/database/repository/categories.go @@ -9,15 +9,15 @@ import ( "strings" ) -type Category struct { - Connection *database.Connection - Env *env.Environment +type Categories struct { + DB *database.Connection + Env *env.Environment } -func (r Category) FindBy(slug string) *database.Category { +func (c Categories) FindBy(slug string) *database.Category { category := &database.Category{} - result := r.Connection.Sql(). + result := c.DB.Sql(). Where("slug = ?", slug). First(&category) @@ -32,11 +32,11 @@ func (r Category) FindBy(slug string) *database.Category { return nil } -func (r Category) CreateOrUpdate(post database.Post, attrs database.PostsAttrs) (*[]database.Category, error) { +func (c Categories) CreateOrUpdate(post database.Post, attrs database.PostsAttrs) (*[]database.Category, error) { var output []database.Category for _, seed := range attrs.Categories { - exists, err := r.ExistOrUpdate(seed) + exists, err := c.ExistOrUpdate(seed) if exists { continue @@ -53,7 +53,7 @@ func (r Category) CreateOrUpdate(post database.Post, attrs database.PostsAttrs) Description: seed.Description, } - if result := r.Connection.Sql().Create(&category); gorm.HasDbIssues(result.Error) { + if result := c.DB.Sql().Create(&category); gorm.HasDbIssues(result.Error) { return nil, fmt.Errorf("error creating category [%s]: %s", seed.Name, result.Error) } @@ -62,7 +62,7 @@ func (r Category) CreateOrUpdate(post database.Post, attrs database.PostsAttrs) PostID: post.ID, } - if result := r.Connection.Sql().Create(&trace); gorm.HasDbIssues(result.Error) { + if result := c.DB.Sql().Create(&trace); gorm.HasDbIssues(result.Error) { return nil, fmt.Errorf("error creating category trace [%s:%s]: %s", category.Name, post.Title, result.Error) } @@ -72,17 +72,17 @@ func (r Category) CreateOrUpdate(post database.Post, attrs database.PostsAttrs) return &output, nil } -func (r Category) ExistOrUpdate(seed database.CategoriesAttrs) (bool, error) { +func (c Categories) ExistOrUpdate(seed database.CategoriesAttrs) (bool, error) { var category *database.Category - if category = r.FindBy(seed.Slug); category == nil { + if category = c.FindBy(seed.Slug); category == nil { return false, nil } category.Name = seed.Name category.Description = seed.Description - if result := r.Connection.Sql().Save(&category); gorm.HasDbIssues(result.Error) { + if result := c.DB.Sql().Save(&category); gorm.HasDbIssues(result.Error) { return false, fmt.Errorf("error on exist or update category [%s]: %s", category.Name, result.Error) } diff --git a/database/repository/posts.go b/database/repository/posts.go index 61c452d1..75042282 100644 --- a/database/repository/posts.go +++ b/database/repository/posts.go @@ -1,41 +1,50 @@ package repository import ( - "fmt" - "github.com/google/uuid" - "github.com/oullin/database" - "github.com/oullin/env" - "github.com/oullin/pkg/gorm" + "fmt" + "github.com/google/uuid" + "github.com/oullin/database" + "github.com/oullin/env" + "github.com/oullin/pkg/gorm" + baseGorm "gorm.io/gorm" ) type Posts struct { - Connection *database.Connection - Env *env.Environment - CategoriesRepository Category + DB *database.Connection + Env *env.Environment + categories Categories } -func (r Posts) Create(attrs database.PostsAttrs) (*database.Post, error) { - post := database.Post{ - UUID: uuid.NewString(), - AuthorID: attrs.AuthorID, - Slug: attrs.Slug, - Title: attrs.Title, - Excerpt: attrs.Excerpt, - Content: attrs.Content, - CoverImageURL: attrs.ImageURL, - PublishedAt: attrs.PublishedAt, - } - - // Todo: - // 1 - Encapsulate all these DB queries in a DB transaction. - // 2 - Make sure internal queries abort top level DB transactions. - if result := r.Connection.Sql().Create(&post); gorm.HasDbIssues(result.Error) { - return nil, fmt.Errorf("issue creating posts: %s", result.Error) - } - - if _, err := r.CategoriesRepository.CreateOrUpdate(post, attrs); err != nil { - return &post, fmt.Errorf("issue creating the given post [%s] category: %s", attrs.Slug, err.Error()) - } - - return &post, nil +func (p Posts) Create(attrs database.PostsAttrs) (*database.Post, error) { + post := database.Post{ + UUID: uuid.NewString(), + AuthorID: attrs.AuthorID, + Slug: attrs.Slug, + Title: attrs.Title, + Excerpt: attrs.Excerpt, + Content: attrs.Content, + CoverImageURL: attrs.ImageURL, + PublishedAt: attrs.PublishedAt, + } + + err := p.DB.Transaction(func(db *baseGorm.DB) error { + // --- Post. + if result := db.Create(&post); gorm.HasDbIssues(result.Error) { + return fmt.Errorf("issue creating posts: %s", result.Error) + } + + // --- Categories. + if _, err := p.categories.CreateOrUpdate(post, attrs); err != nil { + return fmt.Errorf("issue creating the given post [%s] category: %s", attrs.Slug, err.Error()) + } + + // --- Returning [nil] commits the whole transaction. + return nil + }) + + if err != nil { + return nil, fmt.Errorf("error creating posts[%s]: %s", attrs.Title, err.Error()) + } + + return &post, nil } diff --git a/database/repository/users.go b/database/repository/users.go index f3ca9e1a..599e2431 100644 --- a/database/repository/users.go +++ b/database/repository/users.go @@ -8,14 +8,14 @@ import ( ) type Users struct { - Connection *database.Connection - Env *env.Environment + DB *database.Connection + Env *env.Environment } -func (r Users) FindBy(username string) *database.User { +func (u Users) FindBy(username string) *database.User { user := &database.User{} - result := r.Connection.Sql(). + result := u.DB.Sql(). Where("username = ?", username). First(&user) From f42921834a427de3c59efc8482d12e3d5378d351 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Wed, 18 Jun 2025 13:38:46 +0800 Subject: [PATCH 27/35] format --- database/repository/posts.go | 80 ++++++++++++++++++------------------ 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/database/repository/posts.go b/database/repository/posts.go index 75042282..e510857d 100644 --- a/database/repository/posts.go +++ b/database/repository/posts.go @@ -1,50 +1,50 @@ package repository import ( - "fmt" - "github.com/google/uuid" - "github.com/oullin/database" - "github.com/oullin/env" - "github.com/oullin/pkg/gorm" - baseGorm "gorm.io/gorm" + "fmt" + "github.com/google/uuid" + "github.com/oullin/database" + "github.com/oullin/env" + "github.com/oullin/pkg/gorm" + baseGorm "gorm.io/gorm" ) type Posts struct { - DB *database.Connection - Env *env.Environment - categories Categories + DB *database.Connection + Env *env.Environment + categories Categories } func (p Posts) Create(attrs database.PostsAttrs) (*database.Post, error) { - post := database.Post{ - UUID: uuid.NewString(), - AuthorID: attrs.AuthorID, - Slug: attrs.Slug, - Title: attrs.Title, - Excerpt: attrs.Excerpt, - Content: attrs.Content, - CoverImageURL: attrs.ImageURL, - PublishedAt: attrs.PublishedAt, - } - - err := p.DB.Transaction(func(db *baseGorm.DB) error { - // --- Post. - if result := db.Create(&post); gorm.HasDbIssues(result.Error) { - return fmt.Errorf("issue creating posts: %s", result.Error) - } - - // --- Categories. - if _, err := p.categories.CreateOrUpdate(post, attrs); err != nil { - return fmt.Errorf("issue creating the given post [%s] category: %s", attrs.Slug, err.Error()) - } - - // --- Returning [nil] commits the whole transaction. - return nil - }) - - if err != nil { - return nil, fmt.Errorf("error creating posts[%s]: %s", attrs.Title, err.Error()) - } - - return &post, nil + post := database.Post{ + UUID: uuid.NewString(), + AuthorID: attrs.AuthorID, + Slug: attrs.Slug, + Title: attrs.Title, + Excerpt: attrs.Excerpt, + Content: attrs.Content, + CoverImageURL: attrs.ImageURL, + PublishedAt: attrs.PublishedAt, + } + + err := p.DB.Transaction(func(db *baseGorm.DB) error { + // --- Post. + if result := db.Create(&post); gorm.HasDbIssues(result.Error) { + return fmt.Errorf("issue creating posts: %s", result.Error) + } + + // --- Categories. + if _, err := p.categories.CreateOrUpdate(post, attrs); err != nil { + return fmt.Errorf("issue creating the given post [%s] category: %s", attrs.Slug, err.Error()) + } + + // --- Returning [nil] commits the whole transaction. + return nil + }) + + if err != nil { + return nil, fmt.Errorf("error creating posts[%s]: %s", attrs.Title, err.Error()) + } + + return &post, nil } From fe37b9909f61430a4d76125188c9f875a55fee41 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Wed, 18 Jun 2025 13:48:31 +0800 Subject: [PATCH 28/35] wire process --- cli/posts/handler.go | 11 +++-- database/repository/posts.go | 80 ++++++++++++++++++------------------ 2 files changed, 48 insertions(+), 43 deletions(-) diff --git a/cli/posts/handler.go b/cli/posts/handler.go index a1cea5ec..35cf3adb 100644 --- a/cli/posts/handler.go +++ b/cli/posts/handler.go @@ -13,14 +13,14 @@ func (h *Handler) HandlePost(payload *markdown.Post) error { author := h.Users.FindBy(payload.Author) if author == nil { - return fmt.Errorf("the given author [%s] does not exist", payload.Author) + return fmt.Errorf("handler: the given author [%s] does not exist", payload.Author) } if publishedAt, err = payload.GetPublishedAt(); err != nil { - return fmt.Errorf("the given published_at [%s] date is invalid", payload.PublishedAt) + return fmt.Errorf("handler: the given published_at [%s] date is invalid", payload.PublishedAt) } - post := database.PostsAttrs{ + attrs := database.PostsAttrs{ AuthorID: author.ID, Slug: payload.Slug, Title: payload.Title, @@ -33,6 +33,11 @@ func (h *Handler) HandlePost(payload *markdown.Post) error { Tags: h.ParseTags(payload), } + var post *database.Post + if post, err = h.Posts.Create(attrs); err != nil { + return fmt.Errorf("handler: error persiting the post [%s]: %s", attrs.Title, err.Error()) + } + fmt.Println("-----------------") fmt.Println(post) diff --git a/database/repository/posts.go b/database/repository/posts.go index e510857d..f21e3e04 100644 --- a/database/repository/posts.go +++ b/database/repository/posts.go @@ -1,50 +1,50 @@ package repository import ( - "fmt" - "github.com/google/uuid" - "github.com/oullin/database" - "github.com/oullin/env" - "github.com/oullin/pkg/gorm" - baseGorm "gorm.io/gorm" + "fmt" + "github.com/google/uuid" + "github.com/oullin/database" + "github.com/oullin/env" + "github.com/oullin/pkg/gorm" + baseGorm "gorm.io/gorm" ) type Posts struct { - DB *database.Connection - Env *env.Environment - categories Categories + DB *database.Connection + Env *env.Environment + categories Categories } func (p Posts) Create(attrs database.PostsAttrs) (*database.Post, error) { - post := database.Post{ - UUID: uuid.NewString(), - AuthorID: attrs.AuthorID, - Slug: attrs.Slug, - Title: attrs.Title, - Excerpt: attrs.Excerpt, - Content: attrs.Content, - CoverImageURL: attrs.ImageURL, - PublishedAt: attrs.PublishedAt, - } - - err := p.DB.Transaction(func(db *baseGorm.DB) error { - // --- Post. - if result := db.Create(&post); gorm.HasDbIssues(result.Error) { - return fmt.Errorf("issue creating posts: %s", result.Error) - } - - // --- Categories. - if _, err := p.categories.CreateOrUpdate(post, attrs); err != nil { - return fmt.Errorf("issue creating the given post [%s] category: %s", attrs.Slug, err.Error()) - } - - // --- Returning [nil] commits the whole transaction. - return nil - }) - - if err != nil { - return nil, fmt.Errorf("error creating posts[%s]: %s", attrs.Title, err.Error()) - } - - return &post, nil + post := database.Post{ + UUID: uuid.NewString(), + AuthorID: attrs.AuthorID, + Slug: attrs.Slug, + Title: attrs.Title, + Excerpt: attrs.Excerpt, + Content: attrs.Content, + CoverImageURL: attrs.ImageURL, + PublishedAt: attrs.PublishedAt, + } + + err := p.DB.Transaction(func(db *baseGorm.DB) error { + // --- Post. + if result := db.Create(&post); gorm.HasDbIssues(result.Error) { + return fmt.Errorf("issue creating posts: %s", result.Error) + } + + // --- Categories. + if _, err := p.categories.CreateOrUpdate(post, attrs); err != nil { + return fmt.Errorf("issue creating the given post [%s] category: %s", attrs.Slug, err.Error()) + } + + // --- Returning [nil] commits the whole transaction. + return nil + }) + + if err != nil { + return nil, fmt.Errorf("error creating posts [%s]: %s", attrs.Title, err.Error()) + } + + return &post, nil } From 937e878c5aca462dee44362e2f3e65c1df160276 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Wed, 18 Jun 2025 14:39:25 +0800 Subject: [PATCH 29/35] fix DB insert --- cli/main.go | 5 +- cli/posts/factory.go | 11 ++-- cli/posts/handler.go | 7 ++- cli/posts/input.go | 33 ++++++------ database/repository/categories.go | 15 ++++-- database/repository/posts.go | 80 ++++++++++++++--------------- database/repository/users.go | 6 +-- database/seeder/seeds/categories.go | 5 +- database/seeder/seeds/tags.go | 5 +- 9 files changed, 91 insertions(+), 76 deletions(-) diff --git a/cli/main.go b/cli/main.go index 800007cd..3772e035 100644 --- a/cli/main.go +++ b/cli/main.go @@ -52,7 +52,10 @@ func main() { fmt.Println(err) continue } else { - (*postsHandler).HandlePost(post) + if err := (*postsHandler).HandlePost(post); err != nil { + fmt.Println(err) + continue + } } return diff --git a/cli/posts/factory.go b/cli/posts/factory.go index fe44e597..c1ce57d6 100644 --- a/cli/posts/factory.go +++ b/cli/posts/factory.go @@ -7,10 +7,9 @@ import ( ) type Handler struct { - Env *env.Environment - Posts *repository.Posts - Users *repository.Users - Categories *repository.Categories + Env *env.Environment + Posts *repository.Posts + Users *repository.Users } func MakePostsHandler(env *env.Environment) *Handler { @@ -20,6 +19,10 @@ func MakePostsHandler(env *env.Environment) *Handler { Posts: &repository.Posts{ DB: db, Env: env, + Categories: &repository.Categories{ + DB: db, + Env: env, + }, }, Users: &repository.Users{ DB: db, diff --git a/cli/posts/handler.go b/cli/posts/handler.go index 35cf3adb..eba58fc4 100644 --- a/cli/posts/handler.go +++ b/cli/posts/handler.go @@ -3,6 +3,7 @@ package posts import ( "fmt" "github.com/oullin/database" + "github.com/oullin/pkg/cli" "github.com/oullin/pkg/markdown" "time" ) @@ -33,13 +34,11 @@ func (h *Handler) HandlePost(payload *markdown.Post) error { Tags: h.ParseTags(payload), } - var post *database.Post - if post, err = h.Posts.Create(attrs); err != nil { + if _, err = h.Posts.Create(attrs); err != nil { return fmt.Errorf("handler: error persiting the post [%s]: %s", attrs.Title, err.Error()) } - fmt.Println("-----------------") - fmt.Println(post) + cli.Successln(fmt.Sprintf("Post [%s] created successfully.", attrs.Title)) return nil } diff --git a/cli/posts/input.go b/cli/posts/input.go index 334c9c23..4862edbc 100644 --- a/cli/posts/input.go +++ b/cli/posts/input.go @@ -7,7 +7,9 @@ import ( ) type Input struct { - Url string `validate:"required,min=10"` + Url string `validate:"required,min=10"` + Debug bool + MarkdownPost *markdown.Post } func (i *Input) Parse() (*markdown.Post, error) { @@ -22,24 +24,25 @@ func (i *Input) Parse() (*markdown.Post, error) { } post, err := markdown.Parse(response) + i.MarkdownPost = &post if err != nil { return nil, fmt.Errorf("%sEerror parsing markdown: %v %s", cli.RedColour, err, cli.Reset) } - // --- All good! - // Todo: Save post in the DB. - fmt.Printf("Title: %s\n", post.Title) - fmt.Printf("Excerpt: %s\n", post.Excerpt) - fmt.Printf("Slug: %s\n", post.Slug) - fmt.Printf("Author: %s\n", post.Author) - fmt.Printf("Image URL: %s\n", post.ImageURL) - fmt.Printf("Image Alt: %s\n", post.ImageAlt) - fmt.Printf("Category: %s\n", post.Category) - fmt.Printf("Category Slug: %s\n", post.CategorySlug) - fmt.Printf("Tags Alt: %s\n", post.Tags) - fmt.Println("\n--- Content ---") - fmt.Println(post.Content) - return &post, nil } + +func (i *Input) Render() { + fmt.Printf("Title: %s\n", i.MarkdownPost.Title) + fmt.Printf("Excerpt: %s\n", i.MarkdownPost.Excerpt) + fmt.Printf("Slug: %s\n", i.MarkdownPost.Slug) + fmt.Printf("Author: %s\n", i.MarkdownPost.Author) + fmt.Printf("Image URL: %s\n", i.MarkdownPost.ImageURL) + fmt.Printf("Image Alt: %s\n", i.MarkdownPost.ImageAlt) + fmt.Printf("Category: %s\n", i.MarkdownPost.Category) + fmt.Printf("Category Slug: %s\n", i.MarkdownPost.CategorySlug) + fmt.Printf("Tags Alt: %s\n", i.MarkdownPost.Tags) + fmt.Println("\n--- Content ---") + fmt.Println(i.MarkdownPost.Content) +} diff --git a/database/repository/categories.go b/database/repository/categories.go index 9be5ea13..75490b52 100644 --- a/database/repository/categories.go +++ b/database/repository/categories.go @@ -15,10 +15,10 @@ type Categories struct { } func (c Categories) FindBy(slug string) *database.Category { - category := &database.Category{} + category := database.Category{} result := c.DB.Sql(). - Where("slug = ?", slug). + Where("LOWER(slug) = ?", strings.ToLower(slug)). First(&category) if gorm.HasDbIssues(result.Error) { @@ -26,7 +26,7 @@ func (c Categories) FindBy(slug string) *database.Category { } if strings.Trim(category.UUID, " ") != "" { - return category + return &category } return nil @@ -79,8 +79,13 @@ func (c Categories) ExistOrUpdate(seed database.CategoriesAttrs) (bool, error) { return false, nil } - category.Name = seed.Name - category.Description = seed.Description + if strings.Trim(seed.Name, " ") != "" { + category.Name = seed.Name + } + + if strings.Trim(seed.Description, " ") != "" { + category.Description = seed.Description + } if result := c.DB.Sql().Save(&category); gorm.HasDbIssues(result.Error) { return false, fmt.Errorf("error on exist or update category [%s]: %s", category.Name, result.Error) diff --git a/database/repository/posts.go b/database/repository/posts.go index f21e3e04..d05ea95c 100644 --- a/database/repository/posts.go +++ b/database/repository/posts.go @@ -1,50 +1,50 @@ package repository import ( - "fmt" - "github.com/google/uuid" - "github.com/oullin/database" - "github.com/oullin/env" - "github.com/oullin/pkg/gorm" - baseGorm "gorm.io/gorm" + "fmt" + "github.com/google/uuid" + "github.com/oullin/database" + "github.com/oullin/env" + "github.com/oullin/pkg/gorm" + baseGorm "gorm.io/gorm" ) type Posts struct { - DB *database.Connection - Env *env.Environment - categories Categories + DB *database.Connection + Env *env.Environment + Categories *Categories } func (p Posts) Create(attrs database.PostsAttrs) (*database.Post, error) { - post := database.Post{ - UUID: uuid.NewString(), - AuthorID: attrs.AuthorID, - Slug: attrs.Slug, - Title: attrs.Title, - Excerpt: attrs.Excerpt, - Content: attrs.Content, - CoverImageURL: attrs.ImageURL, - PublishedAt: attrs.PublishedAt, - } - - err := p.DB.Transaction(func(db *baseGorm.DB) error { - // --- Post. - if result := db.Create(&post); gorm.HasDbIssues(result.Error) { - return fmt.Errorf("issue creating posts: %s", result.Error) - } - - // --- Categories. - if _, err := p.categories.CreateOrUpdate(post, attrs); err != nil { - return fmt.Errorf("issue creating the given post [%s] category: %s", attrs.Slug, err.Error()) - } - - // --- Returning [nil] commits the whole transaction. - return nil - }) - - if err != nil { - return nil, fmt.Errorf("error creating posts [%s]: %s", attrs.Title, err.Error()) - } - - return &post, nil + post := database.Post{ + UUID: uuid.NewString(), + AuthorID: attrs.AuthorID, + Slug: attrs.Slug, + Title: attrs.Title, + Excerpt: attrs.Excerpt, + Content: attrs.Content, + CoverImageURL: attrs.ImageURL, + PublishedAt: attrs.PublishedAt, + } + + err := p.DB.Transaction(func(db *baseGorm.DB) error { + // --- Post. + if result := db.Create(&post); gorm.HasDbIssues(result.Error) { + return fmt.Errorf("issue creating posts: %s", result.Error) + } + + // --- Categories. + if _, err := p.Categories.CreateOrUpdate(post, attrs); err != nil { + return fmt.Errorf("issue creating the given post [%s] category: %s", attrs.Slug, err.Error()) + } + + // --- Returning [nil] commits the whole transaction. + return nil + }) + + if err != nil { + return nil, fmt.Errorf("error creating posts [%s]: %s", attrs.Title, err.Error()) + } + + return &post, nil } diff --git a/database/repository/users.go b/database/repository/users.go index 599e2431..f4143c40 100644 --- a/database/repository/users.go +++ b/database/repository/users.go @@ -13,10 +13,10 @@ type Users struct { } func (u Users) FindBy(username string) *database.User { - user := &database.User{} + user := database.User{} result := u.DB.Sql(). - Where("username = ?", username). + Where("LOWER(username) = ?", strings.ToLower(username)). First(&user) if gorm.HasDbIssues(result.Error) { @@ -24,7 +24,7 @@ func (u Users) FindBy(username string) *database.User { } if strings.Trim(user.UUID, " ") != "" { - return user + return &user } return nil diff --git a/database/seeder/seeds/categories.go b/database/seeder/seeds/categories.go index 89648eab..d56d265d 100644 --- a/database/seeder/seeds/categories.go +++ b/database/seeder/seeds/categories.go @@ -5,6 +5,7 @@ import ( "github.com/google/uuid" "github.com/oullin/database" "github.com/oullin/pkg/gorm" + "strings" ) type CategoriesSeed struct { @@ -25,11 +26,11 @@ func (s CategoriesSeed) Create(attrs database.CategoriesAttrs) ([]database.Categ "Cloud", "Data", "DevOps", "ML", "Startups", "Engineering", } - for index, seed := range seeds { + for _, seed := range seeds { categories = append(categories, database.Category{ UUID: uuid.NewString(), Name: seed, - Slug: fmt.Sprintf("[%d]: slug-%s", index+1, attrs.Slug), + Slug: strings.ToLower(seed), Description: attrs.Description, }) } diff --git a/database/seeder/seeds/tags.go b/database/seeder/seeds/tags.go index 119ed883..8acae213 100644 --- a/database/seeder/seeds/tags.go +++ b/database/seeder/seeds/tags.go @@ -5,6 +5,7 @@ import ( "github.com/google/uuid" "github.com/oullin/database" "github.com/oullin/pkg/gorm" + "strings" ) type TagsSeed struct { @@ -24,11 +25,11 @@ func (s TagsSeed) Create() ([]database.Tag, error) { "Automation", "Teamwork", "Agile", "OpenAI", "Scaling", "Future", } - for index, name := range allowed { + for _, name := range allowed { tag := database.Tag{ UUID: uuid.NewString(), Name: name, - Slug: fmt.Sprintf("tag[%d]-slug-%s", index, name), + Slug: strings.ToLower(name), } tags = append(tags, tag) From dd162b0a8f230fdb087f2bbfdc435cea64583d58 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Wed, 18 Jun 2025 15:17:26 +0800 Subject: [PATCH 30/35] tweaks --- cli/posts/factory.go | 1 + cli/posts/input.go | 10 ++++++++-- database/seeder/main.go | 3 ++- pkg/cli/message.go | 2 +- pkg/markdown/handler.go | 10 ++++++++++ pkg/validator.go | 18 +++++++++++++----- 6 files changed, 35 insertions(+), 9 deletions(-) diff --git a/cli/posts/factory.go b/cli/posts/factory.go index c1ce57d6..caf1ac15 100644 --- a/cli/posts/factory.go +++ b/cli/posts/factory.go @@ -16,6 +16,7 @@ func MakePostsHandler(env *env.Environment) *Handler { db := boost.MakeDbConnection(env) return &Handler{ + Env: env, Posts: &repository.Posts{ DB: db, Env: env, diff --git a/cli/posts/input.go b/cli/posts/input.go index 4862edbc..d7f14d88 100644 --- a/cli/posts/input.go +++ b/cli/posts/input.go @@ -24,16 +24,22 @@ func (i *Input) Parse() (*markdown.Post, error) { } post, err := markdown.Parse(response) - i.MarkdownPost = &post if err != nil { return nil, fmt.Errorf("%sEerror parsing markdown: %v %s", cli.RedColour, err, cli.Reset) } - return &post, nil + i.MarkdownPost = &post + + return i.MarkdownPost, nil } func (i *Input) Render() { + if i.MarkdownPost == nil { + cli.Errorln("No markdown post found or initialised. Called Parse() first.") + return + } + fmt.Printf("Title: %s\n", i.MarkdownPost.Title) fmt.Printf("Excerpt: %s\n", i.MarkdownPost.Excerpt) fmt.Printf("Slug: %s\n", i.MarkdownPost.Slug) diff --git a/database/seeder/main.go b/database/seeder/main.go index ce2485a0..b31039f7 100644 --- a/database/seeder/main.go +++ b/database/seeder/main.go @@ -107,9 +107,10 @@ func main() { go func() { defer wg.Done() - cli.Successln("Seeding Newsletters ...") if err := seeder.SeedNewsLetters(); err != nil { cli.Error(err.Error()) + } else { + cli.Successln("Seeding Newsletters ...") } }() diff --git a/pkg/cli/message.go b/pkg/cli/message.go index c8c81bb9..2b8c6887 100644 --- a/pkg/cli/message.go +++ b/pkg/cli/message.go @@ -19,7 +19,7 @@ func Successln(message string) { } func Warning(message string) { - fmt.Print(GreenColour + message + Reset) + fmt.Print(YellowColour + message + Reset) } func Warningln(message string) { diff --git a/pkg/markdown/handler.go b/pkg/markdown/handler.go index a9f72b0c..77c52e0e 100644 --- a/pkg/markdown/handler.go +++ b/pkg/markdown/handler.go @@ -96,4 +96,14 @@ func parseCategory(post *Post) { post.Category = parts[1] post.CategorySlug = parts[0] + + if len(parts) >= 2 { + post.Category = parts[1] + post.CategorySlug = parts[0] + + return + } + + post.Category = category + post.Slug = strings.ToLower(category) } diff --git a/pkg/validator.go b/pkg/validator.go index 8a47298c..e5ba8a6f 100644 --- a/pkg/validator.go +++ b/pkg/validator.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/go-playground/validator/v10" "strings" + "sync" ) type Validator struct { @@ -14,11 +15,18 @@ type Validator struct { } func GetDefaultValidator() *Validator { - return MakeValidatorFrom( - validator.New( - validator.WithRequiredStructEnabled(), - ), - ) + var once sync.Once + var defaultValidator *Validator + + once.Do(func() { + defaultValidator = MakeValidatorFrom( + validator.New( + validator.WithRequiredStructEnabled(), + ), + ) + }) + + return defaultValidator } func MakeValidatorFrom(abstract *validator.Validate) *Validator { From 0550083e17f84cc82c6accb37da5fe23f0050787 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Wed, 18 Jun 2025 16:28:10 +0800 Subject: [PATCH 31/35] better abstraction --- database/attrs.go | 4 ---- database/seeder/seeds/factory.go | 2 -- database/seeder/seeds/posts.go | 4 ++-- 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/database/attrs.go b/database/attrs.go index 03d07932..8dc884fa 100644 --- a/database/attrs.go +++ b/database/attrs.go @@ -59,10 +59,6 @@ type PostsAttrs struct { Content string ImageURL string PublishedAt *time.Time - Author User Categories []CategoriesAttrs Tags []TagAttrs - PostViews []PostView - Comments []Comment - Likes []Like } diff --git a/database/seeder/seeds/factory.go b/database/seeder/seeds/factory.go index 0b5210ba..c1ebcee4 100644 --- a/database/seeder/seeds/factory.go +++ b/database/seeder/seeds/factory.go @@ -72,7 +72,6 @@ func (s *Seeder) SeedPosts(UserA, UserB database.User) []database.Post { Excerpt: fmt.Sprintf("[%s] Sed at risus vel nulla consequat fermentum. Donec et orci mauris", uuid.NewString()), Content: fmt.Sprintf("[%s] Sed at risus vel nulla consequat fermentum. Donec et orci mauris. Nullam tempor velit id mi luctus, a scelerisque libero accumsan. In hac habitasse platea dictumst. Cras ac nunc nec massa tristique fringilla.", uuid.NewString()), PublishedAt: &timex, - Author: UserA, }, 1) if err != nil { @@ -86,7 +85,6 @@ func (s *Seeder) SeedPosts(UserA, UserB database.User) []database.Post { Excerpt: fmt.Sprintf("[%s] Sed at risus vel nulla consequat fermentum. Donec et orci mauris", uuid.NewString()), Content: fmt.Sprintf("[%s] Sed at risus vel nulla consequat fermentum. Donec et orci mauris. Nullam tempor velit id mi luctus, a scelerisque libero accumsan. In hac habitasse platea dictumst. Cras ac nunc nec massa tristique fringilla.", uuid.NewString()), PublishedAt: &timex, - Author: UserB, }, 1) if err != nil { diff --git a/database/seeder/seeds/posts.go b/database/seeder/seeds/posts.go index 56f71b0c..5dc897d9 100644 --- a/database/seeder/seeds/posts.go +++ b/database/seeder/seeds/posts.go @@ -24,8 +24,8 @@ func (s PostsSeed) CreatePosts(attrs database.PostsAttrs, number int) ([]databas post := database.Post{ UUID: uuid.NewString(), AuthorID: attrs.AuthorID, - Slug: fmt.Sprintf("%s-post-%s-%d", attrs.Author.Username, attrs.Slug, i), - Title: fmt.Sprintf("Post: [%d] by %s", i, attrs.Author.Username), + Slug: attrs.Slug, + Title: attrs.Title, Excerpt: "This is an excerpt.", Content: "This is the full content of the post.", CoverImageURL: "", From dbe7064ee9d75942a0c7198f67d445a01a67c884 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Wed, 18 Jun 2025 16:34:28 +0800 Subject: [PATCH 32/35] fix panel nil pointers --- cli/menu/panel.go | 6 +++++- cli/posts/handler.go | 1 - 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/cli/menu/panel.go b/cli/menu/panel.go index 340f890c..ead97f6b 100644 --- a/cli/menu/panel.go +++ b/cli/menu/panel.go @@ -16,6 +16,10 @@ func (p *Panel) PrintLine() { } func (p *Panel) GetChoice() int { + if p.Choice == nil { + return 0 + } + return *p.Choice } @@ -111,7 +115,7 @@ func (p *Panel) CapturePostURL() (*posts.Input, error) { } if parsedURL.Scheme != "https" || parsedURL.Host != "raw.githubusercontent.com" { - return nil, fmt.Errorf("%sError: URL must begin with https://raw.githubusercontent.com: %v %s", cli.RedColour, err, cli.Reset) + return nil, fmt.Errorf("%sError: URL must begin with https://raw.githubusercontent.com %s", cli.RedColour, cli.Reset) } input := posts.Input{Url: parsedURL.String()} diff --git a/cli/posts/handler.go b/cli/posts/handler.go index eba58fc4..2ed90def 100644 --- a/cli/posts/handler.go +++ b/cli/posts/handler.go @@ -29,7 +29,6 @@ func (h *Handler) HandlePost(payload *markdown.Post) error { Content: payload.Content, PublishedAt: publishedAt, ImageURL: payload.ImageURL, - Author: *author, Categories: h.ParseCategories(payload), Tags: h.ParseTags(payload), } From cd24aa033a10ba0bba98a9334218624a1656a66e Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Wed, 18 Jun 2025 17:23:21 +0800 Subject: [PATCH 33/35] start working on client --- cli/main.go | 15 +++++---- cli/posts/factory.go | 66 ++++++++++++++++++++++++-------------- cli/posts/handler.go | 6 ++-- pkg/Client.go | 75 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 127 insertions(+), 35 deletions(-) create mode 100644 pkg/Client.go diff --git a/cli/main.go b/cli/main.go index 3772e035..e2a41775 100644 --- a/cli/main.go +++ b/cli/main.go @@ -22,8 +22,6 @@ func init() { } func main() { - postsHandler := posts.MakePostsHandler(environment) - panel := menu.Panel{ Reader: bufio.NewReader(os.Stdin), Validator: pkg.GetDefaultValidator(), @@ -48,14 +46,15 @@ func main() { continue } - if post, err := input.Parse(); err != nil { + handler := posts.MakeHandler( + input, + pkg.MakeDefaultClient(pkg.GetDefaultTransport()), + environment, + ) + + if _, err := handler.NotParsed(); err != nil { fmt.Println(err) continue - } else { - if err := (*postsHandler).HandlePost(post); err != nil { - fmt.Println(err) - continue - } } return diff --git a/cli/posts/factory.go b/cli/posts/factory.go index caf1ac15..0136be8e 100644 --- a/cli/posts/factory.go +++ b/cli/posts/factory.go @@ -1,33 +1,51 @@ package posts import ( - "github.com/oullin/boost" - "github.com/oullin/database/repository" - "github.com/oullin/env" + "github.com/oullin/boost" + "github.com/oullin/database/repository" + "github.com/oullin/env" + "github.com/oullin/pkg" + "github.com/oullin/pkg/markdown" ) type Handler struct { - Env *env.Environment - Posts *repository.Posts - Users *repository.Users + Input *Input + Client *pkg.Client + Posts *repository.Posts + Users *repository.Users } -func MakePostsHandler(env *env.Environment) *Handler { - db := boost.MakeDbConnection(env) - - return &Handler{ - Env: env, - Posts: &repository.Posts{ - DB: db, - Env: env, - Categories: &repository.Categories{ - DB: db, - Env: env, - }, - }, - Users: &repository.Users{ - DB: db, - Env: env, - }, - } +func MakeHandler(input *Input, client *pkg.Client, env *env.Environment) Handler { + db := boost.MakeDbConnection(env) + + return Handler{ + Input: input, + Client: client, + Posts: &repository.Posts{ + DB: db, + Categories: &repository.Categories{ + DB: db, + }, + }, + Users: &repository.Users{ + DB: db, + }, + } +} + +func (h Handler) NotParsed() (bool, error) { + input := h.Input + + var err error + var entity *markdown.Post + + if entity, err = input.Parse(); err != nil { + return true, err + } + + if err = h.HandlePost(entity); err != nil { + return true, err + } + + return true, nil } diff --git a/cli/posts/handler.go b/cli/posts/handler.go index 2ed90def..577142d8 100644 --- a/cli/posts/handler.go +++ b/cli/posts/handler.go @@ -8,7 +8,7 @@ import ( "time" ) -func (h *Handler) HandlePost(payload *markdown.Post) error { +func (h Handler) HandlePost(payload *markdown.Post) error { var err error var publishedAt *time.Time author := h.Users.FindBy(payload.Author) @@ -42,7 +42,7 @@ func (h *Handler) HandlePost(payload *markdown.Post) error { return nil } -func (h *Handler) ParseCategories(payload *markdown.Post) []database.CategoriesAttrs { +func (h Handler) ParseCategories(payload *markdown.Post) []database.CategoriesAttrs { var categories []database.CategoriesAttrs slice := append(categories, database.CategoriesAttrs{ @@ -54,7 +54,7 @@ func (h *Handler) ParseCategories(payload *markdown.Post) []database.CategoriesA return slice } -func (h *Handler) ParseTags(payload *markdown.Post) []database.TagAttrs { +func (h Handler) ParseTags(payload *markdown.Post) []database.TagAttrs { var slice []database.TagAttrs for _, tag := range payload.Tags { diff --git a/pkg/Client.go b/pkg/Client.go new file mode 100644 index 00000000..7a5cd84b --- /dev/null +++ b/pkg/Client.go @@ -0,0 +1,75 @@ +package pkg + +import ( + "context" + "fmt" + "io" + "net/http" + "time" +) + +type Client struct { + UserAgent string + client *http.Client + transport *http.Transport + WithHeaders *func(*http.Request) + AbortOnNone2xx bool +} + +func GetDefaultTransport() *http.Transport { + return &http.Transport{ + MaxIdleConns: 100, + MaxIdleConnsPerHost: 10, + IdleConnTimeout: 90 * time.Second, + } +} + +func MakeDefaultClient(transport *http.Transport) *Client { + client := &http.Client{ + Transport: transport, + Timeout: 15 * time.Second, + } + + return &Client{ + client: client, + transport: transport, + UserAgent: "gocanto.dev", + WithHeaders: nil, + AbortOnNone2xx: false, + } +} + +func (f *Client) Get(ctx context.Context, url string) (string, error) { + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + + if err != nil { + return "", fmt.Errorf("failed to create request: %w", err) + } + + if f.WithHeaders != nil { + callback := *f.WithHeaders + callback(req) + } + + req.Header.Set("User-Agent", f.UserAgent) + + resp, err := f.client.Do(req) + if err != nil { + return "", fmt.Errorf("http request failed: %w", err) + } + + defer resp.Body.Close() + + if f.AbortOnNone2xx && (resp.StatusCode < 200 || resp.StatusCode >= 300) { + return "", fmt.Errorf("received non-2xx status code: %d", resp.StatusCode) + } + + // To avoid allocating a massive buffer for a potentially huge response, we could use io.Copy with a limited reader + // if we need to process the body. However, if we must return a string, reading all is necessary. + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("failed to read response body: %w", err) + } + + return string(body), nil +} From 27baa5112e9809216816a4dd381f5314c136c6f6 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Wed, 18 Jun 2025 17:30:18 +0800 Subject: [PATCH 34/35] guard against nil --- cli/posts/factory.go | 68 ++++++++++++++++++++++---------------------- pkg/Client.go | 12 ++++++-- 2 files changed, 44 insertions(+), 36 deletions(-) diff --git a/cli/posts/factory.go b/cli/posts/factory.go index 0136be8e..efb1de67 100644 --- a/cli/posts/factory.go +++ b/cli/posts/factory.go @@ -1,51 +1,51 @@ package posts import ( - "github.com/oullin/boost" - "github.com/oullin/database/repository" - "github.com/oullin/env" - "github.com/oullin/pkg" - "github.com/oullin/pkg/markdown" + "github.com/oullin/boost" + "github.com/oullin/database/repository" + "github.com/oullin/env" + "github.com/oullin/pkg" + "github.com/oullin/pkg/markdown" ) type Handler struct { - Input *Input - Client *pkg.Client - Posts *repository.Posts - Users *repository.Users + Input *Input + Client *pkg.Client + Posts *repository.Posts + Users *repository.Users } func MakeHandler(input *Input, client *pkg.Client, env *env.Environment) Handler { - db := boost.MakeDbConnection(env) - - return Handler{ - Input: input, - Client: client, - Posts: &repository.Posts{ - DB: db, - Categories: &repository.Categories{ - DB: db, - }, - }, - Users: &repository.Users{ - DB: db, - }, - } + db := boost.MakeDbConnection(env) + + return Handler{ + Input: input, + Client: client, + Posts: &repository.Posts{ + DB: db, + Categories: &repository.Categories{ + DB: db, + }, + }, + Users: &repository.Users{ + DB: db, + }, + } } func (h Handler) NotParsed() (bool, error) { - input := h.Input + input := h.Input - var err error - var entity *markdown.Post + var err error + var entity *markdown.Post - if entity, err = input.Parse(); err != nil { - return true, err - } + if entity, err = input.Parse(); err != nil { + return true, err + } - if err = h.HandlePost(entity); err != nil { - return true, err - } + if err = h.HandlePost(entity); err != nil { + return true, err + } - return true, nil + return true, nil } diff --git a/pkg/Client.go b/pkg/Client.go index 7a5cd84b..4a052d15 100644 --- a/pkg/Client.go +++ b/pkg/Client.go @@ -12,7 +12,7 @@ type Client struct { UserAgent string client *http.Client transport *http.Transport - WithHeaders *func(*http.Request) + WithHeaders func(*http.Request) AbortOnNone2xx bool } @@ -25,6 +25,10 @@ func GetDefaultTransport() *http.Transport { } func MakeDefaultClient(transport *http.Transport) *Client { + if transport == nil { + transport = GetDefaultTransport() + } + client := &http.Client{ Transport: transport, Timeout: 15 * time.Second, @@ -40,6 +44,10 @@ func MakeDefaultClient(transport *http.Transport) *Client { } func (f *Client) Get(ctx context.Context, url string) (string, error) { + if f == nil || f.client == nil { + return "", fmt.Errorf("client is nil") + } + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { @@ -47,7 +55,7 @@ func (f *Client) Get(ctx context.Context, url string) (string, error) { } if f.WithHeaders != nil { - callback := *f.WithHeaders + callback := f.WithHeaders callback(req) } From a42e99fb496cb5a0cc9e5ab39b451db6d75baf6c Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Thu, 19 Jun 2025 14:08:57 +0800 Subject: [PATCH 35/35] extract http client --- cli/main.go | 45 ++++++++------------ cli/menu/schema.go | 12 ------ cli/{menu/panel.go => panel/menu.go} | 43 +++++++++++++++---- cli/posts/factory.go | 62 ++++++++++++++++++++++------ cli/posts/handler.go | 9 ++-- cli/posts/input.go | 51 +---------------------- database/seeder/main.go | 2 +- pkg/Client.go | 9 ++-- pkg/markdown/handler.go | 8 ++-- 9 files changed, 117 insertions(+), 124 deletions(-) delete mode 100644 cli/menu/schema.go rename cli/{menu/panel.go => panel/menu.go} (82%) diff --git a/cli/main.go b/cli/main.go index e2a41775..3b7f6cd3 100644 --- a/cli/main.go +++ b/cli/main.go @@ -1,15 +1,12 @@ package main import ( - "bufio" - "fmt" "github.com/oullin/boost" - "github.com/oullin/cli/menu" + "github.com/oullin/cli/panel" "github.com/oullin/cli/posts" "github.com/oullin/env" "github.com/oullin/pkg" "github.com/oullin/pkg/cli" - "os" "time" ) @@ -22,38 +19,30 @@ func init() { } func main() { - panel := menu.Panel{ - Reader: bufio.NewReader(os.Stdin), - Validator: pkg.GetDefaultValidator(), - } - - panel.PrintMenu() + menu := panel.MakeMenu() for { - err := panel.CaptureInput() + err := menu.CaptureInput() if err != nil { - fmt.Println(cli.RedColour + err.Error() + cli.Reset) + cli.Errorln(err.Error()) continue } - switch panel.GetChoice() { + switch menu.GetChoice() { case 1: - input, err := panel.CapturePostURL() + input, err := menu.CapturePostURL() if err != nil { - fmt.Println(err) + cli.Errorln(err.Error()) continue } - handler := posts.MakeHandler( - input, - pkg.MakeDefaultClient(pkg.GetDefaultTransport()), - environment, - ) + httpClient := pkg.MakeDefaultClient(nil) + handler := posts.MakeHandler(input, httpClient, environment) if _, err := handler.NotParsed(); err != nil { - fmt.Println(err) + cli.Errorln(err.Error()) continue } @@ -63,22 +52,22 @@ func main() { case 3: timeParse() case 0: - fmt.Println(cli.GreenColour + "Goodbye!" + cli.Reset) + cli.Successln("Goodbye!") return default: - fmt.Println(cli.RedColour, "Unknown option. Try again.", cli.Reset) + cli.Errorln("Unknown option. Try again.") } - fmt.Print("\nPress Enter to continue...") + cli.Blueln("Press Enter to continue...") - panel.PrintLine() + menu.PrintLine() } } func showTime() { - fmt.Println("") now := time.Now().Format("2006-01-02 15:04:05") - fmt.Println(cli.GreenColour, "\nCurrent time is", now, cli.Reset) + + cli.Cyanln("\nThe current time is: " + now) } func timeParse() { @@ -87,6 +76,6 @@ func timeParse() { if seed, err := s.ToDatetime(); err != nil { panic(err) } else { - fmt.Println(seed.Format(time.DateTime)) + cli.Magentaln(seed.Format(time.DateTime)) } } diff --git a/cli/menu/schema.go b/cli/menu/schema.go deleted file mode 100644 index 58ea5ac7..00000000 --- a/cli/menu/schema.go +++ /dev/null @@ -1,12 +0,0 @@ -package menu - -import ( - "bufio" - "github.com/oullin/pkg" -) - -type Panel struct { - Reader *bufio.Reader - Choice *int - Validator *pkg.Validator -} diff --git a/cli/menu/panel.go b/cli/panel/menu.go similarity index 82% rename from cli/menu/panel.go rename to cli/panel/menu.go index ead97f6b..2b1c03a9 100644 --- a/cli/menu/panel.go +++ b/cli/panel/menu.go @@ -1,8 +1,10 @@ -package menu +package panel import ( + "bufio" "fmt" "github.com/oullin/cli/posts" + "github.com/oullin/pkg" "github.com/oullin/pkg/cli" "golang.org/x/term" "net/url" @@ -11,11 +13,28 @@ import ( "strings" ) -func (p *Panel) PrintLine() { +type Menu struct { + Choice *int + Reader *bufio.Reader + Validator *pkg.Validator +} + +func MakeMenu() Menu { + menu := Menu{ + Reader: bufio.NewReader(os.Stdin), + Validator: pkg.GetDefaultValidator(), + } + + menu.Print() + + return menu +} + +func (p *Menu) PrintLine() { _, _ = p.Reader.ReadString('\n') } -func (p *Panel) GetChoice() int { +func (p *Menu) GetChoice() int { if p.Choice == nil { return 0 } @@ -23,7 +42,7 @@ func (p *Panel) GetChoice() int { return *p.Choice } -func (p *Panel) CaptureInput() error { +func (p *Menu) CaptureInput() error { fmt.Print(cli.YellowColour + "Select an option: " + cli.Reset) input, err := p.Reader.ReadString('\n') @@ -43,7 +62,7 @@ func (p *Panel) CaptureInput() error { return nil } -func (p *Panel) PrintMenu() { +func (p *Menu) Print() { // Try to get the terminal width; default to 80 if it fails width, _, err := term.GetSize(int(os.Stdout.Fd())) @@ -74,17 +93,19 @@ func (p *Panel) PrintMenu() { } // PrintOption left-pads a space, writes the text, then fills to the full inner width. -func (p *Panel) PrintOption(text string, inner int) { +func (p *Menu) PrintOption(text string, inner int) { content := " " + text + if len(content) > inner { content = content[:inner] } + padding := inner - len(content) fmt.Printf("║%s%s║\n", content, strings.Repeat(" ", padding)) } // CenterText centers s within width, padding with spaces. -func (p *Panel) CenterText(s string, width int) string { +func (p *Menu) CenterText(s string, width int) string { if len(s) >= width { return s[:width] } @@ -96,8 +117,9 @@ func (p *Panel) CenterText(s string, width int) string { return strings.Repeat(" ", left) + s + strings.Repeat(" ", right) } -func (p *Panel) CapturePostURL() (*posts.Input, error) { +func (p *Menu) CapturePostURL() (*posts.Input, error) { fmt.Print("Enter the post markdown file URL: ") + uri, err := p.Reader.ReadString('\n') if err != nil { @@ -118,7 +140,10 @@ func (p *Panel) CapturePostURL() (*posts.Input, error) { return nil, fmt.Errorf("%sError: URL must begin with https://raw.githubusercontent.com %s", cli.RedColour, cli.Reset) } - input := posts.Input{Url: parsedURL.String()} + input := posts.Input{ + Url: parsedURL.String(), + } + validate := p.Validator if _, err := validate.Rejects(input); err != nil { diff --git a/cli/posts/factory.go b/cli/posts/factory.go index efb1de67..822c4dbe 100644 --- a/cli/posts/factory.go +++ b/cli/posts/factory.go @@ -1,26 +1,32 @@ package posts import ( + "context" + "fmt" "github.com/oullin/boost" "github.com/oullin/database/repository" "github.com/oullin/env" "github.com/oullin/pkg" "github.com/oullin/pkg/markdown" + "net/http" + "time" ) type Handler struct { - Input *Input - Client *pkg.Client - Posts *repository.Posts - Users *repository.Users + Input *Input + Client *pkg.Client + Posts *repository.Posts + Users *repository.Users + IsDebugging bool } func MakeHandler(input *Input, client *pkg.Client, env *env.Environment) Handler { db := boost.MakeDbConnection(env) return Handler{ - Input: input, - Client: client, + Input: input, + Client: client, + IsDebugging: false, Posts: &repository.Posts{ DB: db, Categories: &repository.Categories{ @@ -34,18 +40,50 @@ func MakeHandler(input *Input, client *pkg.Client, env *env.Environment) Handler } func (h Handler) NotParsed() (bool, error) { - input := h.Input - var err error - var entity *markdown.Post + var content string + uri := h.Input.Url - if entity, err = input.Parse(); err != nil { - return true, err + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + h.Client.OnHeaders = func(req *http.Request) { + req.Header.Set("Cache-Control", "no-cache") + req.Header.Set("Pragma", "no-cache") + } + + content, err = h.Client.Get(ctx, uri) + + if err != nil { + return false, fmt.Errorf("error fetching url [%s]: %w", uri, err) } - if err = h.HandlePost(entity); err != nil { + var article *markdown.Post + if article, err = markdown.Parse(content); err != nil || article == nil { + return false, fmt.Errorf("error parsing url [%s]: %w", uri, err) + } + + if h.IsDebugging { + h.RenderArticle(article) + } + + if err = h.HandlePost(article); err != nil { return true, err } return true, nil } + +func (h Handler) RenderArticle(post *markdown.Post) { + fmt.Printf("Title: %s\n", post.Title) + fmt.Printf("Excerpt: %s\n", post.Excerpt) + fmt.Printf("Slug: %s\n", post.Slug) + fmt.Printf("Author: %s\n", post.Author) + fmt.Printf("Image URL: %s\n", post.ImageURL) + fmt.Printf("Image Alt: %s\n", post.ImageAlt) + fmt.Printf("Category: %s\n", post.Category) + fmt.Printf("Category Slug: %s\n", post.CategorySlug) + fmt.Printf("Tags Alt: %s\n", post.Tags) + fmt.Println("\n--- Content ---") + fmt.Println(post.Content) +} diff --git a/cli/posts/handler.go b/cli/posts/handler.go index 577142d8..f3767fee 100644 --- a/cli/posts/handler.go +++ b/cli/posts/handler.go @@ -11,7 +11,10 @@ import ( func (h Handler) HandlePost(payload *markdown.Post) error { var err error var publishedAt *time.Time - author := h.Users.FindBy(payload.Author) + + author := h.Users.FindBy( + payload.Author, + ) if author == nil { return fmt.Errorf("handler: the given author [%s] does not exist", payload.Author) @@ -23,11 +26,11 @@ func (h Handler) HandlePost(payload *markdown.Post) error { attrs := database.PostsAttrs{ AuthorID: author.ID, + PublishedAt: publishedAt, Slug: payload.Slug, Title: payload.Title, Excerpt: payload.Excerpt, Content: payload.Content, - PublishedAt: publishedAt, ImageURL: payload.ImageURL, Categories: h.ParseCategories(payload), Tags: h.ParseTags(payload), @@ -37,7 +40,7 @@ func (h Handler) HandlePost(payload *markdown.Post) error { return fmt.Errorf("handler: error persiting the post [%s]: %s", attrs.Title, err.Error()) } - cli.Successln(fmt.Sprintf("Post [%s] created successfully.", attrs.Title)) + cli.Successln("\n" + fmt.Sprintf("Post [%s] created successfully.", attrs.Title)) return nil } diff --git a/cli/posts/input.go b/cli/posts/input.go index d7f14d88..300fe913 100644 --- a/cli/posts/input.go +++ b/cli/posts/input.go @@ -1,54 +1,5 @@ package posts -import ( - "fmt" - "github.com/oullin/pkg/cli" - "github.com/oullin/pkg/markdown" -) - type Input struct { - Url string `validate:"required,min=10"` - Debug bool - MarkdownPost *markdown.Post -} - -func (i *Input) Parse() (*markdown.Post, error) { - file := markdown.Parser{ - Url: i.Url, - } - - response, err := file.Fetch() - - if err != nil { - return nil, fmt.Errorf("%sError fetching the markdown content: %v %s", cli.RedColour, err, cli.Reset) - } - - post, err := markdown.Parse(response) - - if err != nil { - return nil, fmt.Errorf("%sEerror parsing markdown: %v %s", cli.RedColour, err, cli.Reset) - } - - i.MarkdownPost = &post - - return i.MarkdownPost, nil -} - -func (i *Input) Render() { - if i.MarkdownPost == nil { - cli.Errorln("No markdown post found or initialised. Called Parse() first.") - return - } - - fmt.Printf("Title: %s\n", i.MarkdownPost.Title) - fmt.Printf("Excerpt: %s\n", i.MarkdownPost.Excerpt) - fmt.Printf("Slug: %s\n", i.MarkdownPost.Slug) - fmt.Printf("Author: %s\n", i.MarkdownPost.Author) - fmt.Printf("Image URL: %s\n", i.MarkdownPost.ImageURL) - fmt.Printf("Image Alt: %s\n", i.MarkdownPost.ImageAlt) - fmt.Printf("Category: %s\n", i.MarkdownPost.Category) - fmt.Printf("Category Slug: %s\n", i.MarkdownPost.CategorySlug) - fmt.Printf("Tags Alt: %s\n", i.MarkdownPost.Tags) - fmt.Println("\n--- Content ---") - fmt.Println(i.MarkdownPost.Content) + Url string `validate:"required,min=10"` } diff --git a/database/seeder/main.go b/database/seeder/main.go index b31039f7..7413ae37 100644 --- a/database/seeder/main.go +++ b/database/seeder/main.go @@ -116,7 +116,7 @@ func main() { wg.Wait() - cli.Successln("DB seeded as expected.") + cli.Magentaln("DB seeded as expected ....") } func clearScreen() { diff --git a/pkg/Client.go b/pkg/Client.go index 4a052d15..4c0bd722 100644 --- a/pkg/Client.go +++ b/pkg/Client.go @@ -12,7 +12,7 @@ type Client struct { UserAgent string client *http.Client transport *http.Transport - WithHeaders func(*http.Request) + OnHeaders func(req *http.Request) AbortOnNone2xx bool } @@ -38,7 +38,7 @@ func MakeDefaultClient(transport *http.Transport) *Client { client: client, transport: transport, UserAgent: "gocanto.dev", - WithHeaders: nil, + OnHeaders: nil, AbortOnNone2xx: false, } } @@ -54,9 +54,8 @@ func (f *Client) Get(ctx context.Context, url string) (string, error) { return "", fmt.Errorf("failed to create request: %w", err) } - if f.WithHeaders != nil { - callback := f.WithHeaders - callback(req) + if f.OnHeaders != nil { + f.OnHeaders(req) } req.Header.Set("User-Agent", f.UserAgent) diff --git a/pkg/markdown/handler.go b/pkg/markdown/handler.go index 77c52e0e..29ef074c 100644 --- a/pkg/markdown/handler.go +++ b/pkg/markdown/handler.go @@ -42,13 +42,13 @@ func (p Parser) Fetch() (string, error) { // Parse splits the document into front-matter and content, then parses YAML. // It also extracts a leading Parser image (header image) if present. -func Parse(data string) (Post, error) { +func Parse(data string) (*Post, error) { var post Post // Expecting format: ---\n---\n sections := strings.SplitN(data, "---", 3) if len(sections) < 3 { - return post, fmt.Errorf("invalid front-matter format") + return nil, fmt.Errorf("invalid front-matter format") } fm := strings.TrimSpace(sections[1]) @@ -57,7 +57,7 @@ func Parse(data string) (Post, error) { // Unmarshal YAML into FrontMatter err := yaml.Unmarshal([]byte(fm), &post.FrontMatter) if err != nil { - return post, err + return nil, err } // Look for a header image at the top of the content @@ -87,7 +87,7 @@ func Parse(data string) (Post, error) { parseCategory(&post) - return post, nil + return &post, nil } func parseCategory(post *Post) {