Skip to content

Commit

Permalink
First explorations with TEMPL (#4)
Browse files Browse the repository at this point in the history
* Testing out some templ handlers

* Updating readme with templ instructions

* refactoring the account and transactions handlers

* refactoring the account and transactions handlers

* Revert "refactoring the account and transactions handlers"

This reverts commit d107f8a.

* Trying a new setup

* No changes from gofmt?

* go mod tidy
  • Loading branch information
ESteanes authored Jul 22, 2024
1 parent 6d6485a commit 45d99d5
Show file tree
Hide file tree
Showing 13 changed files with 516 additions and 58 deletions.
52 changes: 32 additions & 20 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,30 +10,42 @@ on:
branches: [ "develop" ]

jobs:

build:
run:
name: Build
runs-on: ubuntu-latest
timeout-minutes: 5
strategy:
fail-fast: true
matrix:
go: ['stable', 'oldstable']

steps:
- uses: actions/checkout@v4
- name: Check out code
uses: actions/checkout@v3

- name: Install Go
uses: actions/setup-go@v4
with:
go-version: ${{ matrix.go }}
check-latest: true

- name: Go Format
run: gofmt -s -w . && git diff --exit-code

- name: Go Vet
run: go vet ./...

- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.21.5'
- name: Go Tidy
run: go mod tidy && git diff --exit-code

- name: Cache Go modules
uses: actions/cache@v2
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Go Mod
run: go mod download

- name: Install dependencies
run: go mod download
- name: Go Mod Verify
run: go mod verify

- name: Build
run: go build -v ./...
- name: Go Generate
run: go generate ./... && git diff --exit-code

- name: Test
run: go test -v ./...
- name: Go Build
run: go build -o /dev/null ./...
16 changes: 5 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,7 @@ Phase 2:
Phase 3:
* Interactable UI
* Have a UI in which you can interact with the transactions - give them additional metadata such as tags, photos (receipts/invoices)
* Mutate the graphs - apply filters to change what the graphs output


## Notes

* To generate the open-api interface run the following command:
```
sudo docker run --rm -v ${PWD}:/local openapitools/openapi-generator-cli generate -i /local/openapi.json -g go -o /local/expense-manager/up-bank-interface
```


* Mutate the graphs - apply filters to change what the graphs output

## Modules

Expand Down Expand Up @@ -67,6 +57,10 @@ LICENSE
```
This will stop the generator from generating those files (which will mess up the Go compilation)

4. Generate the TEMPL components
`templ generate`

* Note if you're finding it hard to run `templ`, it might be because `$GOPATH` is not on your `$PATH` so your computer can't find the executable. In that case either update your path or run `./$GOPATH/templ`

4. Format code
`gofmt -s -w .`
Expand Down
42 changes: 42 additions & 0 deletions datafetcher/handlers/accounts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package handlers

import (
"context"
"fmt"
"log"
"net/http"

"github.com/esteanes/expense-manager/datafetcher/upclient"
)

type AccountHandler struct {
*BaseHandler
}

func NewAccountHandler(log *log.Logger, upclient *upclient.APIClient, auth context.Context) *AccountHandler {
handler := &AccountHandler{}
handler.BaseHandler = &BaseHandler{
Uri: "/accounts",
Log: log,
UpClient: upclient,
UpAuth: auth,
Handler: handler, // Set the Handler interface to the specific handler
}
return handler
}

func (h *AccountHandler) Post(w http.ResponseWriter, r *http.Request) {}
func (h *AccountHandler) Get(w http.ResponseWriter, r *http.Request) {
pageSize := int32(30)
filterAccountType := upclient.AccountTypeEnum("SAVER")
filterOwnershipType := upclient.OwnershipTypeEnum("INDIVIDUAL")
resp, r2, err := h.UpClient.AccountsAPI.AccountsGet(h.UpAuth).PageSize(pageSize).FilterAccountType(filterAccountType).FilterOwnershipType(filterOwnershipType).Execute()

if err != nil {
fmt.Fprintf(w, "Error when calling `AccountsAPI.AccountsGet``: %v\n", err)
fmt.Fprintf(w, "Full HTTP response: %v\n", r2)
h.Log.Println("Unable to get account information")
}

fmt.Fprintf(w, "Response from `AccountsAPI.AccountsGet`: %v\n", resp)
}
33 changes: 33 additions & 0 deletions datafetcher/handlers/basehandler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package handlers

import (
"context"
"log"
"net/http"

"github.com/esteanes/expense-manager/datafetcher/upclient"
)

type Handler interface {
Post(w http.ResponseWriter, r *http.Request)
Get(w http.ResponseWriter, r *http.Request)
}

type BaseHandler struct {
Uri string
Log *log.Logger
UpClient *upclient.APIClient
UpAuth context.Context
Handler Handler // Embed the Handler interface
}

func (h *BaseHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost:
h.Handler.Post(w, r)
case http.MethodGet:
h.Handler.Get(w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
39 changes: 39 additions & 0 deletions datafetcher/handlers/transactions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package handlers

import (
"context"
"fmt"
"log"
"net/http"
"os"

"github.com/esteanes/expense-manager/datafetcher/upclient"
)

type TransactionsHandler struct {
*BaseHandler
}

func NewTransactionHandler(log *log.Logger, upclient *upclient.APIClient, auth context.Context) *AccountHandler {
handler := &AccountHandler{}
handler.BaseHandler = &BaseHandler{
Uri: "/transactions",
Log: log,
UpClient: upclient,
UpAuth: auth,
Handler: handler, // Set the Handler interface to the specific handler
}
return handler
}

func (h *TransactionsHandler) Post(w http.ResponseWriter, r *http.Request) {}
func (h *TransactionsHandler) Get(w http.ResponseWriter, r *http.Request) {
pageSize := int32(30) // int32 | The number of records to return in each page. (optional)
resp2, r2, err := h.UpClient.TransactionsAPI.TransactionsGet(h.UpAuth).PageSize(pageSize).Execute()
if err != nil {
fmt.Fprintf(os.Stderr, "Error when calling `TransactionsAPI.TransactionsGet``: %v\n", err)
fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r2.Body)
}
// response from `TransactionsGet`: ListTransactionsResponse
fmt.Fprintf(os.Stdout, "Response from `TransactionsAPI.TransactionsGet`: %v\n", resp2)
}
103 changes: 78 additions & 25 deletions datafetcher/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,14 @@ import (
"fmt"
"log"
"net/http"
"os"
"time"

"github.com/a-h/templ"
"github.com/esteanes/expense-manager/datafetcher/handlers"
templates "github.com/esteanes/expense-manager/datafetcher/templ"
"github.com/esteanes/expense-manager/datafetcher/upclient"

"github.com/alexedwards/scs/v2"
)

// homePage function to handle requests to the root URL
Expand All @@ -17,36 +22,84 @@ func homePage(w http.ResponseWriter, r *http.Request) {
}

func getInfo(w http.ResponseWriter, r *http.Request) {
pageSize := int32(30) // int32 | The number of records to return in each page. (optional)
filterAccountType := upclient.AccountTypeEnum("SAVER") // AccountTypeEnum | The type of account for which to return records. This can be used to filter Savers from spending accounts. (optional)
filterOwnershipType := upclient.OwnershipTypeEnum("INDIVIDUAL") // OwnershipTypeEnum | The account ownership structure for which to return records. This can be used to filter 2Up accounts from Up accounts. (optional)
auth := context.WithValue(context.Background(), upclient.ContextAccessToken, os.Getenv("up-bank-bearer-token"))

configuration := upclient.NewConfiguration()
apiClient := upclient.NewAPIClient(configuration)
resp, r2, err := apiClient.AccountsAPI.AccountsGet(auth).PageSize(pageSize).FilterAccountType(filterAccountType).FilterOwnershipType(filterOwnershipType).Execute()
if err != nil {
fmt.Fprintf(os.Stderr, "Error when calling `AccountsAPI.AccountsGet``: %v\n", err)
fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r2)
}
// response from `AccountsGet`: ListAccountsResponse
fmt.Fprintf(os.Stdout, "Response from `AccountsAPI.AccountsGet`: %v\n", resp)
}

func NewNowHandler(now func() time.Time) NowHandler {
return NowHandler{Now: now}
}

type NowHandler struct {
Now func() time.Time
}

func (nh NowHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
templates.TimeComponent(nh.Now()).Render(r.Context(), w)
}

type GlobalState struct {
Count int
}

resp2, r2, err := apiClient.TransactionsAPI.TransactionsGet(auth).PageSize(pageSize).Execute()
if err != nil {
fmt.Fprintf(os.Stderr, "Error when calling `TransactionsAPI.TransactionsGet``: %v\n", err)
fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r2.Body)
var global GlobalState
var sessionManager *scs.SessionManager

func getHandler(w http.ResponseWriter, r *http.Request) {
userCount := sessionManager.GetInt(r.Context(), "count")
component := templates.Page(global.Count, userCount)
component.Render(r.Context(), w)
}

func postHandler(w http.ResponseWriter, r *http.Request) {
// Update state.
r.ParseForm()

// Check to see if the global button was pressed.
if r.Form.Has("global") {
global.Count++
}
// response from `TransactionsGet`: ListTransactionsResponse
fmt.Fprintf(os.Stdout, "Response from `TransactionsAPI.TransactionsGet`: %v\n", resp2)
//TODO: Update session.
if r.Form.Has("user") {
currentCount := sessionManager.GetInt(r.Context(), "count")
sessionManager.Put(r.Context(), "count", currentCount+1)
}
// Display the form.
getHandler(w, r)
}

func handleInfo(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost {
postHandler(w, r)
return
}
getHandler(w, r)
}

// HandleRequests function to define the routes and start the server
func HandleRequests() {
http.HandleFunc("/", homePage) // Set the root URL to call homePage function
http.HandleFunc("/info", getInfo)
log.Default().Println("Serving request at localhost:8080")
log.Fatal(http.ListenAndServe(":8080", nil)) // Start the server on port 8080
func HandleRequests(upBankToken string, log *log.Logger) {
sessionManager = scs.New()
sessionManager.Lifetime = 24 * time.Hour

auth := context.WithValue(context.Background(), upclient.ContextAccessToken, upBankToken)

configuration := upclient.NewConfiguration()
apiClient := upclient.NewAPIClient(configuration)

accountHandler := handlers.NewAccountHandler(log, apiClient, auth)
transactionsHandler := handlers.NewTransactionHandler(log, apiClient, auth)
component := templates.Hello("its ya boi")
mux := http.NewServeMux()
mux.HandleFunc(accountHandler.Uri, accountHandler.ServeHTTP)
mux.HandleFunc(transactionsHandler.Uri, transactionsHandler.ServeHTTP)

mux.HandleFunc("/", homePage)
mux.HandleFunc("/info", getInfo)
mux.Handle("/time", NewNowHandler(time.Now))
mux.Handle("/hello", templ.Handler(component))
mux.HandleFunc("/counter", handleInfo)
log.Println("Serving request at localhost:8080")
muxWithSessionMiddleware := sessionManager.LoadAndSave(mux)
if err := http.ListenAndServe("localhost:8080", muxWithSessionMiddleware); err != nil {
log.Printf("error listening: %v", err)
}
}
31 changes: 31 additions & 0 deletions datafetcher/templ/components.templ
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package templ

import (
"strconv"
"time"
)
templ TimeComponent(d time.Time) {
<div>{ d.String() }</div>
}

templ NotFoundComponent() {
<div>404 - Not found</div>
}


templ counts(global, user int) {
<div>Global: { strconv.Itoa(global) }</div>
<div>User: { strconv.Itoa(user) }</div>
}

templ form() {
<form action="/counter" method="POST">
<div><button type="submit" name="global" value="global">Global</button></div>
<div><button type="submit" name="user" value="user">User</button></div>
</form>
}

templ Page(global, user int) {
@counts(global, user)
@form()
}
Loading

0 comments on commit 45d99d5

Please sign in to comment.