Add config validation

This commit is contained in:
bodqhrohro 2019-10-25 21:12:38 +03:00
parent 72c9dac62c
commit 695c9fc353
6 changed files with 190 additions and 6 deletions

View file

@ -1,9 +1,11 @@
package config package config
import ( import (
"fmt"
"github.com/pkg/errors" "github.com/pkg/errors"
"io/ioutil" "io/ioutil"
"github.com/santhosh-tekuri/jsonschema"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
) )
@ -47,7 +49,7 @@ type TelegramTdlibClientConfig struct {
UseChatInfoDatabase bool `yaml:":use_chat_info_database"` UseChatInfoDatabase bool `yaml:":use_chat_info_database"`
} }
func ReadConfig(path string) (Config, error) { func ReadConfig(path string, schema_path string) (Config, error) {
var config Config var config Config
file, err := ioutil.ReadFile(path) file, err := ioutil.ReadFile(path)
@ -60,5 +62,85 @@ func ReadConfig(path string) (Config, error) {
return config, errors.Wrap(err, "Error parsing config") return config, errors.Wrap(err, "Error parsing config")
} }
err = validateConfig(file, schema_path)
if err != nil {
return config, errors.Wrap(err, "Validation error")
}
return config, nil return config, nil
} }
func validateConfig(file []byte, schema_path string) error {
schema, err := jsonschema.Compile(schema_path)
if err != nil {
return errors.Wrap(err, "Corrupted JSON schema")
}
var config_generic interface{}
err = yaml.Unmarshal(file, &config_generic)
if err != nil {
return errors.Wrap(err, "Error re-parsing config")
}
config_generic, err = convertToStringKeysRecursive(config_generic, "")
if err != nil {
return errors.Wrap(err, "Config conversion error")
}
err = schema.ValidateInterface(config_generic)
if err != nil {
return errors.Wrap(err, "Config validation error")
}
return nil
}
// copied and adapted from https://github.com/docker/docker-ce/blob/de14285fad39e215ea9763b8b404a37686811b3f/components/cli/cli/compose/loader/loader.go#L330
func convertToStringKeysRecursive(value interface{}, keyPrefix string) (interface{}, error) {
if mapping, ok := value.(map[interface{}]interface{}); ok {
dict := make(map[string]interface{})
for key, entry := range mapping {
str, ok := key.(string)
if !ok {
return nil, formatInvalidKeyError(keyPrefix, key)
}
var newKeyPrefix string
if keyPrefix == "" {
newKeyPrefix = str
} else {
newKeyPrefix = fmt.Sprintf("%s.%s", keyPrefix, str)
}
convertedEntry, err := convertToStringKeysRecursive(entry, newKeyPrefix)
if err != nil {
return nil, err
}
dict[str] = convertedEntry
}
return dict, nil
}
if list, ok := value.([]interface{}); ok {
var convertedList []interface{}
for index, entry := range list {
newKeyPrefix := fmt.Sprintf("%s[%d]", keyPrefix, index)
convertedEntry, err := convertToStringKeysRecursive(entry, newKeyPrefix)
if err != nil {
return nil, err
}
convertedList = append(convertedList, convertedEntry)
}
return convertedList, nil
}
return value, nil
}
func formatInvalidKeyError(keyPrefix string, key interface{}) error {
var location string
if keyPrefix == "" {
location = "at top level"
} else {
location = fmt.Sprintf("in %s", keyPrefix)
}
return errors.Errorf("Non-string key %s: %#v", location, key)
}

View file

@ -4,23 +4,27 @@ import (
"testing" "testing"
) )
const SCHEMA_PATH string = "../config_schema.json"
func TestNoConfig(t *testing.T) { func TestNoConfig(t *testing.T) {
_, err := ReadConfig("../test/sfklase.yml") _, err := ReadConfig("../test/sfklase.yml", SCHEMA_PATH)
if err == nil { if err == nil {
t.Errorf("Non-existent config was successfully read") t.Errorf("Non-existent config was successfully read")
} }
} }
func TestGoodConfig(t *testing.T) { func TestGoodConfig(t *testing.T) {
_, err := ReadConfig("../test/good_config.yml") _, err := ReadConfig("../test/good_config.yml", SCHEMA_PATH)
if err != nil { if err != nil {
t.Errorf("Good config is not accepted: %v", err) t.Errorf("Good config is not accepted: %v", err)
} }
} }
func TestBadConfig(t *testing.T) { func TestBadConfig(t *testing.T) {
_, err := ReadConfig("../test/bad_config.yml") _, err := ReadConfig("../test/bad_config.yml", SCHEMA_PATH)
if err == nil { if err == nil {
t.Errorf("Bad config is accepted but it shoudn't!") t.Errorf("Bad config is accepted but it shouldn't!")
} else {
t.Log(err)
} }
} }

94
config_schema.json Normal file
View file

@ -0,0 +1,94 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"required": [":telegram", ":xmpp"],
"properties": {
":telegram": {
"type": "object",
"required": [":loglevel", ":content", ":tdlib"],
"properties": {
":loglevel": {
"$ref": "#/definitions/non-empty-string"
},
":content": {
"type": "object",
"properties": {
":path": {
"type": "string"
},
":link": {
"type": "string"
},
":upload": {
"type": "string"
}
}
},
":tdlib_verbosity": {
"type": "integer"
},
":tdlib": {
"required": [":lib_path", ":client"],
"type": "object",
"properties": {
":lib_path": {
"$ref": "#/definitions/non-empty-string"
},
":client": {
"type": "object",
"required": [":api_id", ":api_hash"],
"properties": {
":api_id": {
"$ref": "#/definitions/non-empty-string"
},
":api_hash": {
"$ref": "#/definitions/non-empty-string"
},
":device_model": {
"type": "string"
},
":application_version": {
"type": "string"
},
":use_chat_info_database": {
"type": "boolean"
}
}
}
}
}
}
},
":xmpp": {
"type": "object",
"required": [":loglevel", ":jid", ":host", ":port", ":password", ":db"],
"properties": {
":loglevel": {
"$ref": "#/definitions/non-empty-string"
},
":jid": {
"$ref": "#/definitions/non-empty-string"
},
":host": {
"$ref": "#/definitions/non-empty-string"
},
":port": {
"type": "integer",
"minimum": 1
},
":password": {
"$ref": "#/definitions/non-empty-string"
},
":db": {
"$ref": "#/definitions/non-empty-string"
}
}
}
},
"definitions": {
"non-empty-string": {
"type": "string",
"minLength": 1
}
}
}

1
go.mod
View file

@ -4,6 +4,7 @@ go 1.13
require ( require (
github.com/pkg/errors v0.8.1 github.com/pkg/errors v0.8.1
github.com/santhosh-tekuri/jsonschema v1.2.4
gopkg.in/yaml.v2 v2.2.4 gopkg.in/yaml.v2 v2.2.4
gosrc.io/xmpp v0.1.3 gosrc.io/xmpp v0.1.3
) )

2
go.sum
View file

@ -1,6 +1,8 @@
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/santhosh-tekuri/jsonschema v1.2.4 h1:hNhW8e7t+H1vgY+1QeEQpveR6D4+OwKPXCfD2aieJis=
github.com/santhosh-tekuri/jsonschema v1.2.4/go.mod h1:TEAUOeZSmIxTTuHatJzrvARHiuO9LYd+cIxzgEHCQI4=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View file

@ -8,9 +8,10 @@ import (
) )
const CONFIG_PATH string = "config.yml" const CONFIG_PATH string = "config.yml"
const SCHEMA_PATH string = "./config_schema.json"
func main() { func main() {
config, err := config.ReadConfig(CONFIG_PATH) config, err := config.ReadConfig(CONFIG_PATH, SCHEMA_PATH)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }