-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
83861d2
commit bfe8252
Showing
12 changed files
with
350 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
package config | ||
|
||
type Config struct { | ||
Firestore struct { | ||
ProjectID string `mapstructure:"project_id"` | ||
CredentialsFilePath string `mapstructure:"credentials_file"` | ||
CollectionID string `mapstructure:"collection_id"` | ||
} `mapstructure:"firestore"` | ||
Twilio struct { | ||
AccountSID string `mapstructure:"account_sid"` | ||
AuthToken string `mapstructure:"auth_token"` | ||
PhoneNumber string `mapstructure:"phone_number"` | ||
} `mapstructure:"twilio"` | ||
Smtp struct { | ||
Host string `mapstructure:"host"` | ||
Port int `mapstructure:"port"` | ||
Username string `mapstructure:"username"` | ||
Password string `mapstructure:"password"` | ||
From string `mapstructure:"from"` | ||
} `mapstructure:"smtp"` | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,90 @@ | ||
package coursesense | ||
|
||
// | ||
import ( | ||
"context" | ||
"errors" | ||
"fmt" | ||
) | ||
|
||
// Domain types are defined in this file | ||
|
||
type Course struct { | ||
Department string `json:"department"` | ||
Code int `json:"code"` | ||
} | ||
|
||
func (c Course) Valid() error { | ||
if c.Department == "" { | ||
return errors.New("Department cannot be empty") | ||
} | ||
if c.Code <= 999 && c.Code >= 9999 { | ||
return errors.New("Course code must be 4 digits") | ||
} | ||
|
||
return nil | ||
} | ||
|
||
type Section struct { | ||
Course Course `json:"course"` | ||
Code string `json:"code"` | ||
Term string `json:"term"` | ||
} | ||
|
||
func (s Section) Valid() error { | ||
if s.Code == "" { | ||
return errors.New("Section code cannot be empty") | ||
} | ||
if s.Term == "" { | ||
return errors.New("Term cannot be empty") | ||
} | ||
return s.Course.Valid() | ||
} | ||
|
||
func (s Section) String() string { | ||
return fmt.Sprintf("%s*%d*%s*%s", s.Course.Department, s.Course.Code, s.Code, s.Term) | ||
} | ||
|
||
// Service that gets information on course sections | ||
type SectionService interface { | ||
Exists(context.Context, Section) (bool, error) | ||
GetAvailableSeats(context.Context, Section) (uint, error) | ||
} | ||
|
||
// A user registered for notifications on a Section | ||
type Watcher struct { | ||
Email string `json:"email"` | ||
Phone string `json:"phone"` | ||
} | ||
|
||
func (w Watcher) Valid() error { | ||
if w.Email == "" && w.Phone == "" { | ||
return errors.New("At least one contact method needs to be present") | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (w Watcher) String() string { | ||
return fmt.Sprintf("%s:%s", w.Email, w.Phone) | ||
} | ||
|
||
// Service that manages Watchers | ||
type WatcherService interface { | ||
AddWatcher(context.Context, Section, Watcher) error | ||
GetWatchedSections(context.Context) ([]Section, error) | ||
GetWatchers(context.Context, Section) ([]Watcher, error) | ||
RemoveWatchers(context.Context, Section) error | ||
} | ||
|
||
// A type that sends can send notifications to Watchers | ||
type Notifier interface { | ||
Notify(context.Context, Section, ...Watcher) error | ||
} | ||
|
||
type TriggerService interface { | ||
Trigger(context.Context) error | ||
} | ||
|
||
type RegistrationService interface { | ||
Register(context.Context, Section, Watcher) error | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
package firestore |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
module github.com/jacobmichels/Course-Sense-Go | ||
|
||
go 1.19 | ||
|
||
require github.com/julienschmidt/httprouter v1.3.0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= | ||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
package notifier |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
package notifier |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
package register | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
|
||
coursesense "github.com/jacobmichels/Course-Sense-Go" | ||
) | ||
|
||
// Register implements RegistrationService | ||
var _ coursesense.RegistrationService = Register{} | ||
|
||
type Register struct { | ||
sectionService coursesense.SectionService | ||
watcherService coursesense.WatcherService | ||
} | ||
|
||
func NewRegister(s coursesense.SectionService, w coursesense.WatcherService) Register { | ||
return Register{s, w} | ||
} | ||
|
||
func (r Register) Register(ctx context.Context, section coursesense.Section, watcher coursesense.Watcher) error { | ||
// Registration steps | ||
// 1. Ensure the section exists | ||
// 2. Use the watcher service to persist the watcher to the section | ||
|
||
exists, err := r.sectionService.Exists(ctx, section) | ||
if err != nil { | ||
return fmt.Errorf("failed to check if section exists: %w", err) | ||
} | ||
|
||
if !exists { | ||
return fmt.Errorf("section %s does not exist", section) | ||
} | ||
|
||
if err := r.watcherService.AddWatcher(ctx, section, watcher); err != nil { | ||
return fmt.Errorf("failed to persist %s to %s", watcher, section) | ||
} | ||
|
||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
package server | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"errors" | ||
"fmt" | ||
"log" | ||
"net/http" | ||
"time" | ||
|
||
coursesense "github.com/jacobmichels/Course-Sense-Go" | ||
"github.com/julienschmidt/httprouter" | ||
) | ||
|
||
type Server struct { | ||
registrationService coursesense.RegistrationService | ||
triggerService coursesense.TriggerService | ||
addr string | ||
} | ||
|
||
func NewServer(addr string, r coursesense.RegistrationService, t coursesense.TriggerService) Server { | ||
return Server{r, t, addr} | ||
} | ||
|
||
func (s Server) Start(ctx context.Context) error { | ||
r := httprouter.New() | ||
|
||
// register routes | ||
r.GET("/ping", s.pingHandler()) | ||
r.GET("/trigger", s.triggerHandler()) | ||
r.PUT("/register", s.registerHandler()) | ||
|
||
srv := http.Server{Addr: s.addr, Handler: r} | ||
log.Printf("listening on addr %s", s.addr) | ||
|
||
// start server, respecting context cancelation | ||
errChan := make(chan error) | ||
go func() { errChan <- srv.ListenAndServe() }() | ||
select { | ||
case err := <-errChan: | ||
if !errors.Is(err, http.ErrServerClosed) { | ||
return fmt.Errorf("server error: %w", err) | ||
} | ||
case <-ctx.Done(): | ||
log.Println("gracefully shutting down") | ||
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) | ||
defer cancel() | ||
|
||
if err := srv.Shutdown(shutdownCtx); err != nil { | ||
return fmt.Errorf("server shutdown error: %w", err) | ||
} | ||
log.Println("server shutdown complete") | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (s Server) pingHandler() httprouter.Handle { | ||
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { | ||
log.Println("Ping request received") | ||
|
||
if _, err := w.Write([]byte("OK")); err != nil { | ||
log.Printf("Error writing ping response: %s", err) | ||
} | ||
} | ||
} | ||
|
||
func (s Server) triggerHandler() httprouter.Handle { | ||
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { | ||
log.Println("Trigger request receieved") | ||
|
||
if err := s.triggerService.Trigger(r.Context()); err != nil { | ||
log.Printf("trigger service failed: %s", err) | ||
http.Error(w, "Trigger failed internally, please try again later. If error persists please contact service owner", http.StatusInternalServerError) | ||
return | ||
} | ||
|
||
log.Println("Trigger request succeeded") | ||
} | ||
} | ||
|
||
type RegisterRequest struct { | ||
Section coursesense.Section `json:"section"` | ||
Watcher coursesense.Watcher `json:"watcher"` | ||
} | ||
|
||
func (r RegisterRequest) Valid() error { | ||
err := r.Section.Valid() | ||
if err != nil { | ||
return err | ||
} | ||
|
||
return r.Watcher.Valid() | ||
} | ||
|
||
func (s Server) registerHandler() httprouter.Handle { | ||
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { | ||
log.Println("Register request receieved") | ||
|
||
var req RegisterRequest | ||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { | ||
log.Printf("error decoding register request: %s", err) | ||
http.Error(w, "Failed to parse request", http.StatusBadRequest) | ||
return | ||
} | ||
|
||
if err := req.Valid(); err != nil { | ||
log.Printf("register request invalid: %s", err) | ||
http.Error(w, "Invalid request body", http.StatusBadRequest) | ||
return | ||
} | ||
|
||
if err := s.registrationService.Register(r.Context(), req.Section, req.Watcher); err != nil { | ||
log.Printf("registration failed: %s", err) | ||
http.Error(w, "Registration failed internally, please try again later. If error persists please contact service owner", http.StatusInternalServerError) | ||
return | ||
} | ||
|
||
w.WriteHeader(http.StatusCreated) | ||
log.Println("Register request succeeded") | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
package trigger | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"log" | ||
|
||
coursesense "github.com/jacobmichels/Course-Sense-Go" | ||
) | ||
|
||
var _ coursesense.TriggerService = Trigger{} | ||
|
||
type Trigger struct { | ||
sectionService coursesense.SectionService | ||
watcherService coursesense.WatcherService | ||
notifiers []coursesense.Notifier | ||
} | ||
|
||
func NewTrigger(s coursesense.SectionService, w coursesense.WatcherService, n ...coursesense.Notifier) Trigger { | ||
return Trigger{s, w, n} | ||
} | ||
|
||
func (t Trigger) Trigger(ctx context.Context) error { | ||
// Trigger steps | ||
// 1. Get all watched sections from the watcher service | ||
// 2. Loop over the sections, checking the available capacity on each | ||
// 3. If availability is found, use the notifiers to notify the watchers for that section, then remove said watchers once successfully notified | ||
|
||
sections, err := t.watcherService.GetWatchedSections(ctx) | ||
if err != nil { | ||
return fmt.Errorf("failed to get watched sections: %w", err) | ||
} | ||
|
||
for _, section := range sections { | ||
available, err := t.sectionService.GetAvailableSeats(ctx, section) | ||
if err != nil { | ||
return fmt.Errorf("failed to get available seats for %s: %w", section, err) | ||
} | ||
|
||
if available == 0 { | ||
continue | ||
} | ||
|
||
log.Printf("%d available seats found for %s", available, section) | ||
|
||
watchers, err := t.watcherService.GetWatchers(ctx, section) | ||
if err != nil { | ||
return fmt.Errorf("failed to get watchers for %s: %w", section, err) | ||
} | ||
|
||
for _, notifier := range t.notifiers { | ||
err := notifier.Notify(ctx, section, watchers...) | ||
if err != nil { | ||
return fmt.Errorf("failed to notify watchers for %s: %w", section, err) | ||
} | ||
} | ||
|
||
if err := t.watcherService.RemoveWatchers(ctx, section); err != nil { | ||
return fmt.Errorf("failed to remove watchers from %s: %w", section, err) | ||
} | ||
} | ||
|
||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
package webadvisor |