From 701c3e8ac00655f9cc1bc56d85362130c31a6a9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Roos?= Date: Thu, 26 Jan 2023 17:00:15 +0100 Subject: [PATCH] Improve README --- README.md | 221 +++++++++++--------- docs/normalization/normalization.md | 21 -- docs/using_it_for_real/conf/verdeterapp.yml | 1 - docs/using_it_for_real/main.go | 37 ---- docs/using_it_for_real/using_it_for_real.md | 145 ------------- 5 files changed, 125 insertions(+), 300 deletions(-) delete mode 100644 docs/normalization/normalization.md delete mode 100644 docs/using_it_for_real/conf/verdeterapp.yml delete mode 100644 docs/using_it_for_real/main.go delete mode 100644 docs/using_it_for_real/using_it_for_real.md diff --git a/README.md b/README.md index aae5a34..cad32eb 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,43 @@ # Verdeter -Verdeter is a library to write configuration easily with cobra and viper for distributed applications. Verdeter bring the power of cobra and viper in a single library. +[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/ditrit/verdeter/CI.yml?branch=main&style=flat-square)](https://github.com/spf13/viper/actions?query=workflow%3ACI) +[![Go Report Card](https://goreportcard.com/badge/github.com/ditrit/verdeter?style=flat-square)](https://goreportcard.com/report/github.com/ditrit/verdeter) +![Go Version](https://img.shields.io/badge/go%20version-%3E=1.18-61CFDD.svg?style=flat-square) +[![PkgGoDev](https://pkg.go.dev/badge/mod/github.com/ditrit/verdeter)](https://pkg.go.dev/mod/github.com/ditrit/verdeter) -It should be consider as a wrapper for cobra and viper that allow developers to code apps that are POSIX compliant by default. +Verdeter is a library to write CLIs and configuration easily by bringing the power of [cobra](https://github.com/spf13/cobra) and [viper](https://github.com/spf13/viper) in a single library. -> The api is susceptible to change at any point in time until the v1 is released. - -Verdeter allow developers to bind a posix compliant flag, an environment variable and a variable in a config file to a viper key with a single line of code. +Verdeter allow developers to bind a posix compliant flag, an environment variable and a variable in a config file to a viper key with a single line of code. +Verdeter provide a consistent precedence order by extending [viper precedence order](https://github.com/spf13/viper#why-viper). Verdeter also comes with extra features such as: -- support for [normalize function](https://github.com/ditrit/verdeter/blob/main/docs/normalization/normalization.md), ex: `LowerString` (lower the input string) -- support for [key specific checks](https://github.com/ditrit/verdeter/blob/main/docs/using_it_for_real/using_it_for_real.md), ex: `StringNotEmpty`(check if the input string is empty), `CheckIsHighPort`(check is the input integer is a high tcp port), or `AuthorizedValues`(check if the value of a config key is contained in a defined array of authorized values)) + +- support for [normalize function](#normalization) to normalize user inputs (ex: [`LowerString`] lower the input string) +- support for [key specific validation](#validation), ex: `StringNotEmpty`(check if the input string is empty), `CheckIsHighPort`(check is the input integer is a high tcp port), or `AuthorizedValues`(check if the value of a config key is contained in a defined array of authorized values)) - support for constraints, ex: check for specific arch -- support for dynamic default values (named *Computed values*), ex: set `time.Now().Unix()` as a default for a "time" key +- support for dynamic default values (named *Computed values*), ex: set `time.Now().Unix()` as a default for a "time" key. +Table of contents: -## How Verdeter differ from viper in handling configuration value +- [How Verdeter differ from viper in handling configuration values](#how-verdeter-differ-from-viper-in-handling-configuration-values) +- [Get Started](#get-started) +- [Normalization](#normalization) +- [Validation](#validation) +- [Licence](#licence) +- [Contributing Guidelines](#contributing-guidelines) + +## How Verdeter differ from viper in handling configuration values Verdeter uses the following precedence order. Each item takes precedence over the item below it: -1. Explicit call to `viper.Set`: +1. Explicit call to `viper.Set` + + `viper.Set(key)` set the key to a fixed value. - `viper.Set(key)` set the key to a fixed value - *Example: `viper.Set("age", 25)` will set the key "**age**" to `25`* 2. POSIX flags - Cli flags are handled by cobra using [pflag](https://github.com/spf13/pflag) + Cli flags are handled by cobra using [pflag](https://github.com/spf13/pflag). *Example: appending the flag `--age 25` will set the key "**age**" to `25`* @@ -35,7 +46,6 @@ Verdeter uses the following precedence order. Each item takes precedence over th Environment Variable are handled by viper (read more [here](https://github.com/spf13/viper#working-with-environment-variables)) *Example: running `export _age` will export an environment variable (the `` is set by verdeter). Verdeter will bind automatically the environment variable name to a viper key when the developer will define the key he needs. Then, when the developer retreive a value for the "**age**" key with a call to `viper.Get("age)`, viper get all the environment variable and find the value of `_age`.* - 4. Value in a config file @@ -43,12 +53,14 @@ Verdeter uses the following precedence order. Each item takes precedence over th *Example:* Let's say the "**config_path**" is set to `./conf.yml` and the file looks like below + ```yml # conf.yml author: name: bob age: 25 ``` + Then you would use `viper.Get("author.name")` to access the value `bob` and `viper.Get("age")` to access the value `25`. 5. Dynamic default values (*computed values*) @@ -70,21 +82,16 @@ Verdeter uses the following precedence order. Each item takes precedence over th (*VerdeterCommand).SetComputedValue("time", defaultTime) ``` - Then the value can be retreived easily using `viper.Get("time")` as usual - + Then the value can be retreived easily using `viper.Get("time")` as usual. 6. static default - Static defaults can be set using verdeter + Static defaults can be set using verdeter. + ```go // of course here the value is static (*VerdeterCommand).SetDefault("time", 1661957668) ``` - Alternatively you can use viper directly to do exactly the same thing (please note that we will use `(*VerdeterCommand).SetDefault` in the rest of the documentation). - ```go - viper.SetDefault("time", 1661957668) - ``` - 7. type default (0 for an integer) @@ -93,102 +100,57 @@ Verdeter uses the following precedence order. Each item takes precedence over th *Example:* let's say thay we **did not** call `(*VerdeterCommand).SetRequired("time")` to set the key "time" as required. Then a call to `viper.GetInt("time")` will return `0`. (please note that a call to `viper.Get()` returns an `interface{}` wich has no "defaut value"). +## Get Started -## Basic Example +Get the library. -Let's create a rootCommand named "myApp" -```go - -var rootCommand = verdeter.NewConfigCmd( - // Name of the app - "myApp", - - // A short description - "myApp is an amazing piece of software", - - // A longer description - `myApp is an amazing piece of software, -that everyone can use thanks to verdeter`, - - // Callback - func(cfg *verdeter.VerdeterCommand, args []string) { - key := "author.name" - fmt.Printf("value for %q is %q\n", key, viper.GetString(key)) - }) +```shell +go get github.com/ditrit/verdeter ``` -You might to receive args on the command line, set the number of args you want. -If more are provided, Cobra will throw an error. +Let's create an app that print stuff to the terminal. Let's start with the classic "hello world!" ```go -// only 2 args please -rootCommand.SetNbArgs(2) -``` -Then I want to add configuration to this command, for example to bind an address and a port to myApp. +import "github.com/ditrit/verdeter" + +var helloCommand = verdeter.BuildVerdeterCommand(verdeter.VerdeterConfig{ + Use: "hello", + Long: "hello is an app that says hello", + Run: func(cmd *cobra.Command, args []string) { + fmt.Printf("Hello World! (from: %q)\n", viper.GetString("from")) + }, +}) +``` + +If you take a look at [verdeter.VerdeterConfig's documentation]() you will observe that it's quite similar to the the original cobra.Command type. That's done on purpose to help you transition easily from your cobra app. + +Then I want to add configuration to this command: let's create a config key named "from" that will represent the name of the sender. ```go // Adding a local key. -rootCommand.LKey("addr", verdeter.IsStr, "a", "bind to IPV4 addr") -rootCommand.LKey("port", verdeter.IsInt, "p", "bind to TCP port") +helloCommand.GKey("from", verdeter.IsStr, "f", "the sender name") -/* if you want sub commands to inherit this flag, +/* if you want sub commands to inherit this flag/config key, use (*verdeter.VerdeterCommand).GKey instead */ ``` > The config types availables are `verdeter.IsStr`, `verdeter.IsInt`, `verdeter.IsUint` and `verdeter.IsBool`. -A default value can be set for each config key +A default value can be set for that config key. See [.SetDefault() doc](). ```go -rootCommand.SetDefault("addr", "127.0.0.1") -rootCommand.SetDefault("port", 7070) +// The default value of the config key "from" is nom "earth". +rootCommand.SetDefault("from", "earth") ``` -A validator can be bound to a config key. - -```go -// creating a validator from scratch -addrValidator := models.Validator{ - // the name of the validator - Name: "IPV4 validator", - - // the actual validation function - Func: func (input interface{}) error { - valueStr, ok := input.(string) - if !ok { - return fmt.Error("wrong input type") - } - parts := strings.Split(".") - if len(parts)!=4 { - return fmt.Errorf("An IPv4 is composed of four 8bit integers, fount %d", len(parts)) - } - for _,p := parts { - intVal, err := strconv.Atoi(p) - if err != nil { - return err - } - if intVal<0 || intVal >255 { - return fmt.Error("one of the part in the string is not a byte") - } - - } - }, -} - -// using the validator we just created -rootCommand.SetValidator("addr", addrValidator) +Config key can be marked as required. The cobra function [(* cobra.Command).PreRunE](https://pkg.go.dev/github.com/spf13/cobra#Command) will fail if the designated config key is not provided, preventing your `Run` or `RunE` function to be called. -// verdeter comes with some predefined validators -rootCommand.SetValidator("port", verdeter.validators.CheckTCPHighPort) -``` - -Config key can be marked as required. The cobra function [(* cobra.Command).PreRunE](https://pkg.go.dev/github.com/spf13/cobra#Command) will fail if the designated config key is not provided, preventing the callback to run. ```go -rootCommand.SetRequired("addr") +rootCommand.SetRequired("from") ``` -To actually run the command, use this code in your main.go +To actually run the command, use the `Execute()` method. ```go func main() { @@ -199,11 +161,78 @@ func main() { */ // Launch the command - rootCommand.Execute() + helloCommand.Execute() +} +``` + +## Normalization + +Let's say you are building an app that take strings as config values. Instead of asking your user to use only lowercase strings you could set a normalizer with verdeter that will ensure that the string value you will retrieve is actually a lowercase value. + +Please note that normalization functions use a specific signature +```go +import "github.com/ditrit/verdeter/models" + +var LowerString models.NormalizationFunction = func(val interface{}) interface{} { + strVal, ok := val.(string) + if !ok { + return val + } + return strings.ToLower(strVal) } + +verdeterCommand.SetNormalize("keyname", LowerString) +``` + +--- + +*The `LowerString` normalization function is actually available at `verdeter.normalization.LowerString`* + +## Validation + +Let's say you are building an app that serve content over http. You will need to bind your app to a port on the server. You will likely put that in a config key. In order to prevent configuration mistakes that would prevent the application from running, you want to make sure the port number is a TCP high port. Note that Verdeter introduce a validation step that run before the `PreRun` or `PreRunE` function, depending on wich you are using. + +First write your validator using verdeter model. + +```go +import "github.com/ditrit/verdeter/models" + +var validatorTCPHighPort = models.Validator{ + + // Give your validator a name + Name: "TCP High Port Check" + + // Then provide the validation function. + // (Please note that you need that exact signature) + Func: func (input interface{}) error { + // First make sure this is an integer + portNumber, ok := input.(int) + if !ok { // seems that is not an integer + return fmt.Errorf("should be an integer") + } + + // Check if the port is in the correct interval + if intVal >= 1024 && intVal <= 65535 { + return nil + } + return fmt.Errorf("value (%d) is not a TCP high port ", port) + } +} + +// Then register the validator for the config key +verdeterCommand.AddValidator("port", validatorTCPHighPort) ``` +--- + +*The `CheckTCPHighPort` validator is actually available in verdeter at `verdeter.validators.CheckTCPHighPort`* + + +## Licence + +Verdeter is licenced under the Mozilla Public License Version 2.0: see [LICENSE](LICENSE). + ## Contributing Guidelines -See [CONTRIBUTING](CONTRIBUTING.md) \ No newline at end of file +See [CONTRIBUTING](CONTRIBUTING.md). diff --git a/docs/normalization/normalization.md b/docs/normalization/normalization.md deleted file mode 100644 index c3087e1..0000000 --- a/docs/normalization/normalization.md +++ /dev/null @@ -1,21 +0,0 @@ -# Normalization - -Verdeter support normalization functions. - -Let's say you are building an app that take strings as config values. Instead of asking you user to use only lowercase strings you could set a normalizer with verdeter that will ensure that the string value you will retrieve is actually a lowercase value. - -```go -var LowerString models.NormalizationFunction = func(val interface{}) interface{} { - strVal, ok := val.(string) - if !ok { - return val - } - return strings.ToLower(strVal) -} - -verdeterCommand.SetNormalize("keyname", LowerString) -``` - ---- - -*The `LowerString` normalization function is actually available at `verdeter.normalization.LowerString`* \ No newline at end of file diff --git a/docs/using_it_for_real/conf/verdeterapp.yml b/docs/using_it_for_real/conf/verdeterapp.yml deleted file mode 100644 index 35ad839..0000000 --- a/docs/using_it_for_real/conf/verdeterapp.yml +++ /dev/null @@ -1 +0,0 @@ -time: 1661865582 \ No newline at end of file diff --git a/docs/using_it_for_real/main.go b/docs/using_it_for_real/main.go deleted file mode 100644 index 64d1156..0000000 --- a/docs/using_it_for_real/main.go +++ /dev/null @@ -1,37 +0,0 @@ -package main - -import ( - "fmt" - "time" - - "github.com/ditrit/verdeter" - "github.com/spf13/viper" -) - -var verdeterRootCmd = verdeter.NewVerdeterCommand( - "verdeterapp", - "verdeterapp print formated time to the terminal", - "/* Insert a longer description here*/", - func(cfg *verdeter.VerdeterCommand, args []string) error { - timeStamp := viper.GetInt("time") - t := time.Unix(int64(timeStamp), 0) - fmt.Println(t) - - // no error to return - return nil - }) - -func main() { - - viper.Set("config_path", "./conf/") - - // Set a new key named "time" with a shortcut named "t" - verdeterRootCmd.GKey("time", verdeter.IsInt, "t", "the time") - - // If the value of time is not set, run this function and set "time" to it's output - verdeterRootCmd.SetComputedValue("time", func() interface{} { - return time.Now().Unix() - }) - - verdeterRootCmd.Execute() -} diff --git a/docs/using_it_for_real/using_it_for_real.md b/docs/using_it_for_real/using_it_for_real.md deleted file mode 100644 index 71d4f8e..0000000 --- a/docs/using_it_for_real/using_it_for_real.md +++ /dev/null @@ -1,145 +0,0 @@ -# Using Verdeter, for real - -We will create an app that will take a unix timestamp as an input for a config key and print a formated version to the standard output. - -Let's define our root command with the callback -```go -var verdeterRootCmd = verdeter.NewVerdeterCommand( - "verdeterapp", - "verdeterapp print formated time to the terminal", - "/* Insert a longer description here*/", - func(cfg *verdeter.VerdeterCommand, args []string) error { - timeStamp := viper.GetInt("time") - t := time.Unix(int64(timeStamp), 0) - fmt.Println(t) - - // no error to return - return nil -}) -``` - -Then set the config_path to `./conf/vertederapp.yml` -```go -verdeterRootCmd.SetDefault("config_path", "./conf/") -``` - -Then add a consig key - -```go -// Set a new key named "time" with a shortcut named "t" -verdeterRootCmd.GKey("time", verdeter.IsInt, "t", "the input unix time") -``` - -The we set a dynamic default value to `time.Now().Unix()`. That way if the config key 'time" is not present in the flag, in the environment variables or in the config file, "time" will take the value of the current time. - - -```go -// If the value of time is not set, run this function and set "time" to it's output -verdeterRootCmd.SetComputedValue("time", func() interface{} { - return time.Now().Unix() -}) -``` - -The we set a dynamic default value to `time.Now().Unix()`. That way if the config key 'time" is not present in the flag, in the environment variables or in the config file, "time" will take the value of the current time. - - -```go -// If the value of time is not set, run this function and set "time" to it's output -verdeterRootCmd.SetComputedValue("time", func() interface{} { - return time.Now().Unix() -}) -``` -Let's write a config file - -```yml -# ./conf/verdeterapp.yml -time: 1661865582 -``` -***The code in full:*** -```go -package main - -import ( - "fmt" - "time" - - "github.com/ditrit/verdeter" - "github.com/spf13/viper" -) - -var verdeterRootCmd = verdeter.NewVerdeterCommand( - "verdeterapp", - "verdeterapp print formated time to the terminal", - "/* Insert a longer description here*/", - func(cfg *verdeter.VerdeterCommand, args []string) error { - timeStamp := viper.GetInt("time") - t := time.Unix(int64(timeStamp), 0) - fmt.Println(t) - - // no error to return - return nil - }) - -func main() { - // Initialize the command - viper.Set("config_path", "./conf/") - - // Set a new key named "time" with a shortcut named "t" - verdeterRootCmd.GKey("time", verdeter.IsInt, "t", "the time") - - // If the value of time is not set, run this function and set "time" to it's output - verdeterRootCmd.SetComputedValue("time", func() interface{} { - return time.Now().Unix() - }) - - verdeterRootCmd.Execute() -} - -``` - - -## Executing the program - -> the "help" command is available - ``` -$ verdeterapp help -Usage: - verdeterapp [flags] - -Flags: - -h, --help help for verdeterapp - -t, --time int the time - ``` - -**`time` can now be set:** -- with an environment variable `VERDETERAPP_TIME` -- with a flag `--time` (or `-t`) -- with a value set in a config - - - -"time" is set from the flag - -```bash -> go build -o verdeterapp -> ./verdeterapp --time 1661865571 -Tue Aug 30 2022 13:19:31 GMT+0000 -``` - -"time" is set from the ENV variable - -```bash -> go build -o verdeterapp -> VERDETERAPP_TIME=1661865555 ./verdeterapp -Tue Aug 30 2022 13:19:15 GMT+0000 -``` - -"time" is set from the config file - -```bash -> go build -o verdeterapp -> ./verdeterapp -Tue Aug 30 2022 13:19:42 GMT+0000 -``` - -In this example, verdeter allowed us to build an app that can use configuration from 3 main sources (*flags, environment variables and config file*) easily. \ No newline at end of file