Skip to content

Commit

Permalink
Merge pull request #1 from jacobmichels/clean-layout
Browse files Browse the repository at this point in the history
Clean layout
  • Loading branch information
jacobmichels authored Sep 18, 2022
2 parents 94b3092 + 6c4d704 commit 58bbdc1
Show file tree
Hide file tree
Showing 20 changed files with 774 additions and 635 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ RUN go mod download

COPY . .

RUN go build -o /usr/bin/course-sense-go
RUN go build -o /usr/bin/course-sense-go cmd/coursesense/main.go

FROM alpine:3.16.2

Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@
The University of Guelph updated their course selection system, so Course Sense must evolve!

This is a stateless Go service intended to be run in a serverless environment (I use Cloud Run) but a VM would work too.

## Project layout

This project follows [Ben Johnson's Standard Project Layout](https://www.gobeyond.dev/standard-package-layout/)
60 changes: 60 additions & 0 deletions cmd/coursesense/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package main

import (
"context"
"fmt"
"log"
"os"
"os/signal"

"github.com/jacobmichels/Course-Sense-Go/config"
"github.com/jacobmichels/Course-Sense-Go/firestore"
"github.com/jacobmichels/Course-Sense-Go/notifier"
"github.com/jacobmichels/Course-Sense-Go/register"
"github.com/jacobmichels/Course-Sense-Go/server"
"github.com/jacobmichels/Course-Sense-Go/trigger"
"github.com/jacobmichels/Course-Sense-Go/webadvisor"
)

func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt)
go func() {
<-sig
log.Println("received interrupt, shutting down")
cancel()
}()

cfg, err := config.ReadConfig()
if err != nil {
log.Panicf("failed to get config: %s", err)
}

webadvisorService, err := webadvisor.NewWebAdvisorSectionService()
if err != nil {
log.Panicf("failed to create WebAdvisorSectionService: %s", err)
}

firestoreService, err := firestore.NewFirestoreWatcherService(ctx, cfg.Firestore.ProjectID, cfg.Firestore.SectionCollectionID, cfg.Firestore.WatcherCollectionID, cfg.Firestore.CredentialsFilePath)
if err != nil {
log.Panicf("failed to create FirestoreWatcherService: %s", err)
}

emailNotifier := notifier.NewEmail(cfg.Smtp.Host, cfg.Smtp.Username, cfg.Smtp.Password, cfg.Smtp.From, cfg.Smtp.Port)

register := register.NewRegister(webadvisorService, firestoreService)
trigger := trigger.NewTrigger(webadvisorService, firestoreService, emailNotifier)

port := os.Getenv("PORT")
if port == "" {
port = "8080"
}

srv := server.NewServer(fmt.Sprintf(":%s", port), register, trigger)
if err = srv.Start(ctx); err != nil {
log.Panicf("Server failure: %s", err)
}
}
36 changes: 19 additions & 17 deletions internal/server/config.go → config/config.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package server
package config

import (
"fmt"
Expand All @@ -8,16 +8,12 @@ import (
"github.com/spf13/viper"
)

type AppConfig struct {
type Config struct {
Firestore struct {
ProjectID string `mapstructure:"project_id"`
CredentialsFilePath string `mapstructure:"credentials_file"`
CollectionID string `mapstructure:"collection_id"`
}
Twilio struct {
AccountSID string `mapstructure:"account_sid"`
AuthToken string `mapstructure:"auth_token"`
PhoneNumber string `mapstructure:"phone_number"`
SectionCollectionID string `mapstructure:"section_collection_id"`
WatcherCollectionID string `mapstructure:"watcher_collection_id"`
}
Smtp struct {
Host string `mapstructure:"host"`
Expand All @@ -28,31 +24,37 @@ type AppConfig struct {
}
}

func readAppConfig() (*AppConfig, error) {
func ReadConfig() (Config, error) {
viper.AddConfigPath(".")
viper.SetConfigName("config")
viper.SetConfigType("yaml")

viper.AutomaticEnv()
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))

viper.SetDefault("firestore.project_id", "")
viper.SetDefault("firestore.credentials_file", "")
viper.SetDefault("firestore.collection_id", "sections")
viper.SetDefault("firestore.section_collection_id", "sections")
viper.SetDefault("firestore.watcher_collection_id", "watchers")
viper.SetDefault("smtp.port", 0)
viper.SetDefault("smtp.host", "")
viper.SetDefault("smtp.username", "")
viper.SetDefault("smtp.password", "")
viper.SetDefault("smtp.from", "")

viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
viper.AutomaticEnv()

if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
log.Println("No config file found, continuing with env and defaults")
} else {
// Config file was found but another error was produced
return nil, fmt.Errorf("failed to read config file: %s", err)
return Config{}, fmt.Errorf("failed to read config file: %s", err)
}
}

var config AppConfig
var config Config
if err := viper.Unmarshal(&config); err != nil {
return nil, fmt.Errorf("failed to unmarshal config: %s", err)
return Config{}, fmt.Errorf("failed to unmarshal config: %s", err)
}

return &config, nil
return config, nil
}
90 changes: 90 additions & 0 deletions coursesense.go
Original file line number Diff line number Diff line change
@@ -0,0 +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
}
Loading

0 comments on commit 58bbdc1

Please sign in to comment.