Add config validation
This commit is contained in:
parent
72c9dac62c
commit
695c9fc353
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -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
94
config_schema.json
Normal 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
1
go.mod
|
@ -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
2
go.sum
|
@ -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=
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue