197fd0097d4f35a57435cdcc853ffb055f5bd9a1 — Hakan Bayindir 4 months ago 16c456d
refactor: A general polishing run with some fixes.

- proj: Bump project version to 0.4.0a
- feat: Add notification priority sanity checking support.
- fix: Call defaults from the correct structure (runTimeConfiguration) in initFlags function.
- fix: Store user supplied configuration file path in runTimeConfiguration.
- refactor: Polish and check comments.
- refactor: Update a log entry formatting in applyDefaultConfiguration, about hostname.
2 files changed, 44 insertions(+), 17 deletions(-)

M src/nudge.go
@@ 2,6 2,15 @@

**Note:** Please add newest entries on top. Use ISO date format YYYY-MM-DD and markdown formatting.

## 2023-10-19

- proj: Bump project version to 0.4.0a
- feat: Add notification priority sanity checking support.
- fix: Call defaults from the correct structure (`runTimeConfiguration`) in `initFlags` function.
- fix: Store user supplied configuration file path in `runTimeConfiguration`.
- refactor: Polish and check comments.
- refactor: Update a log entry formatting in `applyDefaultConfiguration`, about hostname.

## 2023-10-16

- proj: Release version 0.3.0.

M src/nudge.go => src/nudge.go +35 -17
@@ 54,7 54,7 @@ type RuntimeConfiguration struct {
	applicationName string // Stores the sanitized version of the application name.
	version         string // Stores the version of the application.
	dryrun          bool   // Setting to true will make code switch to "simulation" mode.
	configFilePath  string // This variable stores our configuration file's path.
	configFilePath  string // Stores configuration file path.
	apiKey          string // Secret key we use for sending messages.
	userKey         string // Secret key of the user, used as a recipient address.
	logLevel        string // Contains the logging level the application starts with.

@@ 78,13 78,14 @@ type FlagStorage struct {
	// Next block contains options about program execution and behavior.
	versionRequested bool   // Setting to true will make nudge to write version and exit.
	dryrun           bool   // Setting to true will make code switch to "simulation" mode.
	logLevel         string // Contains the logging level the application starts with.
	logLevel         string // Contains the logging level provided at the CLI.

// Following struct contains the set of acceptable configuration option values.
// checkConfigurationSanity() function consulsts this structure while verifying the final configuration.
type ValidConfigurationOptions struct {
	possibleLogLevels []string
	possiblePriorityLevelRange []int

// This function initializes the ValidConfigurationOptions data structure, which will be

@@ 92,18 93,19 @@ type ValidConfigurationOptions struct {
// is nice and shiny.
func initializeValidConfigurationOptions(ValidConfigurationOptions *ValidConfigurationOptions) {
	ValidConfigurationOptions.possibleLogLevels = []string{"debug", "info", "warn", "error", "fatal", "panic"}
	ValidConfigurationOptions.possiblePriorityLevelRange = []int{-2, 2}

// This function stores and applies the defaults of the application.
// It's called first to initialize the defaults, then config file and the lastly the flags
// Can change these defaults.
// access same structures to update these structures to the final configuration.
func applyDefaultConfiguration(runtimeConfiguration *RuntimeConfiguration, notificationToSend *Notification) {
	// Get the notificationTitle first. We will use it for message titles, as default
	notificationTitle, err := os.Hostname()

	if err != nil {
		// Since logger is not up when we call this function, we'll use Println here.
		fmt.Println("Encountered an error while obtaining hostname, will use \"Nudge\" instead.")
		fmt.Println("WARN: Encountered an error while obtaining hostname, will use \"Nudge\" instead.")
		notificationTitle = "Nudge"

@@ 115,9 117,10 @@ func applyDefaultConfiguration(runtimeConfiguration *RuntimeConfiguration, notif
	notificationToSend.title = notificationTitle

	// Let's set defaults for the application itself, again where it makes sense.
	// Some applications do not sanitize the name like that, but I prefer to do.
	applicationName := strings.Split(os.Args[0], "/")
	runtimeConfiguration.applicationName = applicationName[len(applicationName)-1]
	runtimeConfiguration.version = "0.3.0"
	runtimeConfiguration.version = "0.4.0a1"
	runtimeConfiguration.dryrun = false
	runtimeConfiguration.logLevel = "warn"

@@ 126,8 129,9 @@ func applyDefaultConfiguration(runtimeConfiguration *RuntimeConfiguration, notif
// It's not designed as a method, since it might touch multiple data structures at once.
func readAndApplyConfiguration(configurationFilePath *string, runtimeConfiguration *RuntimeConfiguration, notificationToSend *Notification, flagStorage *FlagStorage, logger *zap.SugaredLogger) {
	if flagStorage.configFilePath != "" {
		logger.Debugf("Configuration file is set to %s, via command line.", flagStorage.configFilePath)
		runtimeConfiguration.configFilePath = flagStorage.configFilePath // Copy desired config file path to runtime configuration.
		logger.Debugf("Configuration file is set to %s, via command line.", runtimeConfiguration.configFilePath)
		viper.SetConfigType("toml") // Because we use ".conf" extension, not ".toml".

	} else {

@@ 190,9 194,8 @@ func readAndApplyConfiguration(configurationFilePath *string, runtimeConfigurati

// This functions defines the flags, binds them and parses them.
// Note: Code defaults are also baked into this function as a start, they will be moved out later.
// Cue Lord of the Rings from Blind Guardian.
func initFlags(notificationToSend *Notification, flagStorage *FlagStorage, logger *zap.SugaredLogger) {
func initFlags(notificationToSend *Notification, runtimeConfiguration *RuntimeConfiguration, flagStorage *FlagStorage, logger *zap.SugaredLogger) {
	// In this initial version, we only handle some of the features. Rest will be handled later.
	// Values are sent to flagStorage, because we'll apply them after parsing the config file.
	flag.StringVar(&flagStorage.device, "devices", notificationToSend.device, "List of devices to be notified. Separate multiple devices with ','.")

@@ 203,13 206,13 @@ func initFlags(notificationToSend *Notification, flagStorage *FlagStorage, logge
	flag.StringVar(&flagStorage.urlTitle, "url_title", notificationToSend.urlTitle, "An optional title for URL to send.")

	// Below this point we handle the runtime configuration of Nudge.
	flag.BoolVar(&flagStorage.dryrun, "dryrun", flagStorage.dryrun, "Simulate sending a notification.")
	flag.BoolVar(&flagStorage.dryrun, "dryrun", runtimeConfiguration.dryrun, "Simulate sending a notification.")

	// Get logging level and save it.
	flag.StringVar(&flagStorage.logLevel, "log_level", flagStorage.logLevel, "Change the logging level.")
	flag.StringVar(&flagStorage.logLevel, "log_level", runtimeConfiguration.logLevel, "Change the logging level.")

	// Following flag is for controlling the place of the configuration file.
	flag.StringVar(&flagStorage.configFilePath, "config_path", flagStorage.configFilePath, "Define or override configuration file path.")
	flag.StringVar(&flagStorage.configFilePath, "config_path", runtimeConfiguration.configFilePath, "Define or override configuration file path.")

	// Always implement a version string.
	flag.BoolVar(&flagStorage.versionRequested, "version", false, "Print version and exit.")

@@ 237,9 240,14 @@ func applyFlags(setFlags *[]string, flagStorage *FlagStorage, runtimeConfigurati

	logger.Debugf("The set flags are %s.", *setFlags)

	// There's no way to make it in a single pass. So it's done in old GNU Getopt way.
	// It's fast enough, so I'm fine with this.
	for _, flagValue := range *setFlags {
		logger.Debugf("Working on flag %s.", flagValue)

		// The for loop above visits the set flags one by one, then we handle it here by looking up what it is.
		// Go doesn't provide JMP tables for switches, but I don't think this slows down that much.
		// ref: https://github.com/golang/go/issues/5496
		switch flagValue {
		case "devices":
			notificationToSend.device = flagStorage.device

@@ 294,6 302,15 @@ func checkConfigurationSanity(notificationToSend *Notification, runtimeConfigura

	logger.Debugf("Provided log level (%s) is valid.", runtimeConfiguration.logLevel)
	// Check message notification is at acceptable levels and clamp the value if necessary.
	if notificationToSend.priority < validConfigurationOptions.possiblePriorityLevelRange[0] {
		logger.Warnf("Supplied notification priority %d is too low. Clamping to %d.", notificationToSend.priority, validConfigurationOptions.possiblePriorityLevelRange[0])
		notificationToSend.priority = validConfigurationOptions.possiblePriorityLevelRange[0]
	}else if notificationToSend.priority > validConfigurationOptions.possiblePriorityLevelRange[1] {
		logger.Warnf("Supplied notification priority %d is too high. Clamping to %d.", notificationToSend.priority, validConfigurationOptions.possiblePriorityLevelRange[1])
		notificationToSend.priority = validConfigurationOptions.possiblePriorityLevelRange[1]

	// Check whether we have the API & User keys.
	// Let's do some magic:

@@ 344,7 361,8 @@ func printState(runtimeConfiguration *RuntimeConfiguration, notification *Notifi

// This function sends the actual notification via PUSH request, via Pushover.
func sendNotification(notificationToSend *Notification, runtimeConfiguration *RuntimeConfiguration) (body []byte) {
// TODO: Add logging directives into this function.
func sendNotification(notificationToSend *Notification, runtimeConfiguration *RuntimeConfiguration, logger *zap.SugaredLogger) (body []byte) {
	response, err := http.PostForm("https://api.pushover.net/1/messages.json",
			"token":     {runtimeConfiguration.apiKey},

@@ 359,7 377,7 @@ func sendNotification(notificationToSend *Notification, runtimeConfiguration *Ru
			"html":      {fmt.Sprint(notificationToSend.html)}})

	if err != nil {
		logger.Panicf("Something went wrong, exiting (error is %s).", err)

	defer response.Body.Close() // Automatically close response.Body when the function exits.

@@ 430,7 448,7 @@ func main() {
	// Then let's see what we have at hand (options, parameters, flags).
	// Flags are parsed first, stored in a secondary config area.
	// This allows us to override config file with flags more gracefully.
	initFlags(&notificationToSend, &flagStorage, sugaredLogger)
	initFlags(&notificationToSend, &runtimeConfiguration, &flagStorage, sugaredLogger)

	// Start with reading the config, if present.
	// If file is not present, fallback to defaults.

@@ 484,7 502,7 @@ func main() {

	// Let's send the notification:
	if runtimeConfiguration.dryrun == false {
		sendNotification(&notificationToSend, &runtimeConfiguration)
		sendNotification(&notificationToSend, &runtimeConfiguration, sugaredLogger)
	} else {
		sugaredLogger.Warnf("Not sending the notification since in dry run mode.")