yaclint

package module
v0.0.0-...-3dbd443 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 5, 2024 License: MIT Imports: 10 Imported by: 2

README

YACLINT

yaclint is a simple linter for configurations loaded by using the yacl library.

the concept is simple. you have a struct that represents your configuration. you load the configuration from a file and then you can use the linter to check if the configuration contains all the fields that are needed, or if there are unexpected fields.

it is then up to you, how critical the issues are. because of different type and value checks (depending on different config file types), it is often not possible to check if the configuration is invalid, just because the struct expects an integer, but it is a string in the config file. so this is tracked, but not ment as an error.

NOTE: invalid types and values, and any other source related issues are not checked by this linter. there is also no need for, because this would already throw an error while reading the config file.

Happy Path Example

simple example of how to use the linter and just make sure there is no Bigger issue. that means it might be not all fields are set, but there are no unexpected fields.

package main

import (
	"fmt"

	"github.com/swaros/contxt/module/yacl"
	"github.com/swaros/contxt/module/yaclint"
	"github.com/swaros/contxt/module/yamc"
)

type Config struct {
	Name string `yaml:"name"`
	Age  int    `yaml:"age"`
}

func main() {
	// create a new yacl instance
	config := &Config{}
	cfgApp := yacl.New(
		config,
		yamc.NewYamlReader(),
	)
	// load the config file. must be done before the linter can be used
	if err := cfgApp.LoadFile("config.yaml"); err != nil {
		panic(err)
	}

	// create a new linter instance
	linter := yaclint.NewLinter(*cfgApp)
	// error if remapping is not possible. so no linting error
	if err := linter.Verify(); err != nil {
		panic(err)
	}

	// anything that seems totally wrong is an error
	if linter.HasError() {
		// just print the issues. right now this should not be the case
		fmt.Println(`Error! 
	Sorry!
		but it seems you did something wrong
		in the config file`)
		fmt.Println(linter.PrintIssues())
	} else {
		// now we can use the config
		fmt.Println(config.Name)
		fmt.Println(config.Age)
	}
}
Usage

first to understand what is happens in the background, here is a short overview of the process.

  1. a configuration is loaded and mapped to a struct (using the yacl library)
  2. the linter now reloads the configuration wthout any struct mapping
  3. the linter now checks what fields are missing and what fields are unexpected, because they are not defined in the struct

it would not help to panic out if there are any differences. a difference is expected. so the linter just reports the issues and it is up to the application to decide what to do.

but there are of course some types of issues that are more critical than others. so the linter has 3 different levels of issues.

information level

in the best case, there is no different between the struct and the map. so the linter will not find any issues. most of the time, and depending on the config file type, there are some differences "by design". so while recreate the map, some values maybe converted to a different type.

because the linter don't know about any reader based issues, it will still reported at lower level, as information.

for example:

type Config struct {
	Name string `yaml:"name"`
	Age  int    `yaml:"age"`
}

related yaml

name: "John Doe"
age: 42000000

is reported as issue in info level.

[-]ValuesNotMatching: level[2] @age (Age) vs @age (Age) ['42000000' != '4.2e+07']
[+]ValuesNotMatching: level[2] @age (Age) vs @age (Age) ['4.2e+07' != '42000000']
warning level

the usual case is, that there are some fields they are defined in the struct, but not in the config file. this is not an issue, because this is most of the time the expected behavior, and some default values are used. this will be an warning and the application could decide if this is an issue or not.

for example:

type Config struct {
	Name    string `yaml:"name"`
	Contact struct {
		Email string `yaml:"email"`
		Phone string `yaml:"phone"`
	} `yaml:"contact"`
	LastName string `yaml:"lastname"`
	Age      int    `yaml:"age"`
}

related yaml

name: john
lastname: doe
age: 60
contact:
#  phone: 123456789 # <---- this is missing
  email: jdoe@example.com

is reported as issue in warning level.

[+]MissingEntry: level[5] @phone (Contact.Phone)
error level

the other case is, there are fields in the config file, they are not defined in the struct. this is an issue, because the configuration is not used as expected.

name: john
lastname: doe
age: 60
phone: 123456789 # <---- expected in contact section but not here
contact:
  email: jdoe@example.com

is reported as issue in error level.

[-]UnknownEntry: level[12] @phone (phone)
[+]MissingEntry: level[5] @phone (Contact.Phone)

and worst, the user may have misspelled a field name. this can be a time consuming issue to find, because a typo is not always obvious.

name: john
lastname: doe
age: 60
contact:
  phone: 123456789
  emil: jdoe@example.com

is reported as issue in error level.

[-]UnknownEntry: level[12] @    emil (    emil)
[+]MissingEntry: level[5] @email (Contact.Email)

none of these issues can be seen just by using the yacl library (or by using the yaml or json libs). so the linter is used to check if the configuration is used as expected. so the linter will find this issues and print it out.

implementation
yaclint.NewLinter()

the linter is created by using the yaclint.NewLinter() function. it takes a yacl instance as parameter. this is used to get the configuration and the reader.

func NewLinter(yacl yacl.Yacl) *Linter
linter.Verify()

the linter is used by calling the Verify() function. this will reload the configuration and check for any issues.

func (l *Linter) Verify() error

the error that might be returned is related to the yacl library. so if there is an issue with the reader, this will be reported as error.

the issues from the Verify itself are reported in different ways.

linter.HasError()

checks if any of the issues at least has an error level.

func (l *Linter) HasError() bool
linter.HasWarning()

checks if any of the issues at least has an warning level.

func (l *Linter) HasWarning() bool
linter.HasInfo()

checks if any of the issues at least has an info level.

func (l *Linter) HasInfo() bool
linter.GetIssue()

the linter has a function to get all the issues equal or higher a specific level. this is ment to react on the issues programmatically.

func (l *Linter) GetIssue(level int, reportFn func(token *MatchToken))

for example:

cantIgnore := linter.HasError() // error can not (or should not) be ignored at all
linter.GetIssue(yaclint.IssueLevelWarn, func(token *yaclint.MatchToken) {	 
	switch token.KeyPath {
	case "Contact":
		fmt.Println("The contact section is required.")
		cantIgnore = true
	case "Contact.Email":
		fmt.Println("The email is required. this is used for the authentication.")
		cantIgnore = true
	case "Contact.Phone":
		fmt.Println("no phone?. okay, fine...we can ignore this")
	}

})
// getting out if we found an issue that we can not ignore
if cantIgnore {
	fmt.Println("sorry, can't ignore this issue.")
	exit(1)
}
linter.PrintIssues()

the linter has a function to print out the issues. this is used to print out all the issues if there are any.

func (l *Linter) PrintIssues() string

this is ment for report the issue to the user. the output is a string conversation of the issues.

[-]UnknownEntry: level[12] @    emil (    emil)
[+]MissingEntry: level[5] @email (Contact.Email)

for each level, we have also a function to get these outputs as string slices depending on the level. (see below)

linter.Errors()

the linter has a function to get all the issues with an error level. this is used to print out the issues if there are any. these are string conversations of the issues. it is ment for report the issue to the user.

func (l *Linter) Errors() []String
linter.Warnings()

the linter has a function to get all the issues with an warning level. this is used to print out the issues if there are any. similar to the GetErrors() function, these are string conversations of the issues. it is ment for report the issue to the user.

func (l *Linter) Warnings() []String
linter.Infos()

the linter has a function to get all the issues with an info level. this is used to print out the issues if there are any. similar to the GetErrors() function, these are string conversations of the issues. it is ment for report the issue to the user.

func (l *Linter) Infos() []String

Documentation

Index

Constants

View Source
const (
	IssueLevelInfo  = 2
	IssueLevelWarn  = 5
	IssueLevelError = 10
)
View Source
const (
	// -- info level --
	Unset                    = -1 // the value is not set. the initial value. nothing was tryed to match
	PerfectMatch             = 0  // the value and type matches. should most times not happen, because we compare the default values with actual values. so a diff is common
	ValueMatchButTypeDiffers = 1  // the value matches but in different type like "1.4" and 1.4 (valid because of yaml parser type conversion)
	ValueNotMatch            = 2  // the value is not matching (still valid because of yaml parser type conversion. we compare the default values with actual values. so a diff is common)

	// -- warning level --
	MissingEntry = 5 // the entry is missing. this entry is defined in struct but not in config. depends on the implementation if this is an issue.
	// -- error level --
	WrongType    = 11 // the type is wrong. different from the strct definition, and also no type conversion is possible
	UnknownEntry = 12 // the entry is is in the config but not in the struct

	IdentObject = 1 // the token is an object
	IdentArray  = 2 // the token is an array
	IdentValue  = 3 // the token is a value

	ValueString     = 0 // the token is not Open or Close Object or Array and not KeyValue
	KeyValue        = 1 // the token is a key value pair like "key: value"
	OpenObject      = 2 // the token is an object like "{"
	CloseObject     = 3 // the token is an object like "}"
	OpenArray       = 4 // the token is an object like "["
	CloseArray      = 5 // the token is an object like "]"
	KeyedOpenObject = 6 // the token is an object like "key: {"
	KeyedOpenArray  = 7 // the token is an object like "key: ["

)
View Source
const (
	DirtyLogMaxLen = 500
)

Variables

This section is empty.

Functions

func DetectedValueFromString

func DetectedValueFromString(str string) interface{}

Types

type DirtyLogger

type DirtyLogger struct {
	// contains filtered or unexported fields
}

func NewDirtyLogger

func NewDirtyLogger() *DirtyLogger

func (*DirtyLogger) CreateCtxoutTracer

func (dl *DirtyLogger) CreateCtxoutTracer() *DirtyLogger

func (*DirtyLogger) CreateSimpleTracer

func (dl *DirtyLogger) CreateSimpleTracer() *DirtyLogger

func (*DirtyLogger) GetTrace

func (dl *DirtyLogger) GetTrace(find ...string) []string

func (*DirtyLogger) Print

func (dl *DirtyLogger) Print(pattern ...string) string

func (*DirtyLogger) SetAddTime

func (dl *DirtyLogger) SetAddTime(addTime bool) *DirtyLogger

func (*DirtyLogger) SetTraceFn

func (dl *DirtyLogger) SetTraceFn(traceFn TraceFn)

func (*DirtyLogger) Trace

func (dl *DirtyLogger) Trace(args ...interface{})

type DirtyLoggerDef

type DirtyLoggerDef interface {
	Trace(args ...interface{})
	SetTraceFn(traceFn TraceFn)
	GetTrace(find ...string) []string
	Print(patter ...string) string
}

type LintChunk

type LintChunk struct {
	ChunkNr int
	Removed []*MatchToken
	Added   []*MatchToken
}

type LintMap

type LintMap struct {
	Chunks []*LintChunk
}

func (*LintMap) GetTokensByTokenPath

func (l *LintMap) GetTokensByTokenPath(fromToken *MatchToken) []*MatchToken

find tokens by keypath over all chunks

func (*LintMap) GetTokensFromSequence

func (l *LintMap) GetTokensFromSequence(seq int) []*MatchToken

GetTokensFromSequence returns all tokens from the given sequence

func (*LintMap) GetTokensFromSequenceAndIndex

func (l *LintMap) GetTokensFromSequenceAndIndex(seq int, index int) []*MatchToken

GetTokensFromSequenceAndIndex returns all tokens from the given sequence and index

type Linter

type Linter struct {
	// contains filtered or unexported fields
}

func NewLinter

func NewLinter(config yacl.ConfigModel) *Linter

Create a new Linter. The config is the config model that we need to verify it must be loaded before, and it must be injected as pointer

func (*Linter) Errors

func (l *Linter) Errors() []string

Errors will return all errors found in the diff as a string array

func (*Linter) GetDiff

func (l *Linter) GetDiff() (string, error)

GetDiff returns the diff between the config file and the structed config file. The diff is returned as string.

func (*Linter) GetHighestIssueLevel

func (l *Linter) GetHighestIssueLevel() int

GetHighestIssueLevel returns the highest issue level found.

func (*Linter) GetIssue

func (l *Linter) GetIssue(level int, reportFn func(token *MatchToken))

GetIssue will execute the reportFn for all tokens that have the same or higher level as the given level.

func (*Linter) GetTrace

func (l *Linter) GetTrace(orFind ...string) string

func (*Linter) HasError

func (l *Linter) HasError() bool

report if we found an issue with the config file, that should not be ignored.

func (*Linter) HasInfo

func (l *Linter) HasInfo() bool

report if we found an issue with the config file, that is most usual, like type conversion. (what is difficult to avoid)

func (*Linter) HasWarning

func (l *Linter) HasWarning() bool

report if we found an issue with the config file, that is not so important, but be warned.

func (*Linter) HaveParsingError

func (l *Linter) HaveParsingError() (string, bool)

if the lint fails and do not report any error, the config could be just invalid for structure parsing. this can happen while the config file is tryed to be loaded, but it is not readable. this can be the case if the config is injected with an reference of an pointer, or it is an array, a map[string]string or an interface{}. if this was happens, this function will return the reason why the parsing failed.

func (*Linter) Infos

func (l *Linter) Infos() []string

Infos will return all infos found in the diff as a string array

func (*Linter) PrintIssues

func (l *Linter) PrintIssues() string

PrintIssues will print the issues found in the diff. This is an report function and show any issue greater than 0.

func (*Linter) SetDirtyLogger

func (l *Linter) SetDirtyLogger(logger DirtyLoggerDef)

func (*Linter) Trace

func (l *Linter) Trace(arg ...interface{})

Trace is a helper function to trace the linter workflow. this might help to debug the linter.

func (*Linter) Verify

func (l *Linter) Verify() error

Verify is the main function of the linter. It will verify the config file against the structed config file. It will return an error if the config file is not valid.

func (*Linter) WalkIssues

func (l *Linter) WalkIssues(hndlFn func(token *MatchToken, added bool))

WalkIssues will execute the reportFn for all tokens that have the same or higher level as the given level.

func (*Linter) Warnings

func (l *Linter) Warnings() []string

Warnings will return all warnings found in the diff as a string array

type MatchToken

type MatchToken struct {
	KeyWord    string
	OrginKey   string
	KeyPath    string
	Value      interface{}
	Type       string
	Added      bool
	SequenceNr int
	IndexNr    int
	Status     int
	PairToken  *MatchToken
	ParentLint *LintMap
	TraceFunc  func(args ...interface{})
}

func NewMatchToken

func NewMatchToken(structDef yamc.StructDef, traceFn func(args ...interface{}), parent *LintMap, line string, indexNr int, seqNr int, added bool) MatchToken

func (*MatchToken) IsPair

func (m *MatchToken) IsPair(token *MatchToken) bool

IsPair checks if the given token is a pair to this token it checks if the keyword is the same and the added flag is different if so it sets the pair token property and returns true

func (*MatchToken) IsValid

func (m *MatchToken) IsValid() bool

IsValid checks if the token is valid and can be used for further processing

func (*MatchToken) ToIssueString

func (m *MatchToken) ToIssueString() string

ToIssueString returns a string representation of the issue

func (*MatchToken) ToString

func (m *MatchToken) ToString() string

func (*MatchToken) VerifyValue

func (m *MatchToken) VerifyValue() int

VerifyValue checks if the value is matching the pair token if so it sets the status property and returns the status the status represents the issue level

type TraceFn

type TraceFn func(args ...interface{}) // thats all we need right now

Directories

Path Synopsis
example

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL