From 695c9fc35325d3bec3ec81bdce59f780acd74e8d Mon Sep 17 00:00:00 2001 From: bodqhrohro Date: Fri, 25 Oct 2019 21:12:38 +0300 Subject: [PATCH] Add config validation --- config/config.go | 84 +++++++++++++++++++++++++++++++++++++- config/config_test.go | 12 ++++-- config_schema.json | 94 +++++++++++++++++++++++++++++++++++++++++++ go.mod | 1 + go.sum | 2 + telegabber.go | 3 +- 6 files changed, 190 insertions(+), 6 deletions(-) create mode 100644 config_schema.json diff --git a/config/config.go b/config/config.go index 2ec6706..cd2883b 100644 --- a/config/config.go +++ b/config/config.go @@ -1,9 +1,11 @@ package config import ( + "fmt" "github.com/pkg/errors" "io/ioutil" + "github.com/santhosh-tekuri/jsonschema" "gopkg.in/yaml.v2" ) @@ -47,7 +49,7 @@ type TelegramTdlibClientConfig struct { 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 file, err := ioutil.ReadFile(path) @@ -60,5 +62,85 @@ func ReadConfig(path string) (Config, error) { 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 } + +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) +} diff --git a/config/config_test.go b/config/config_test.go index 34009d7..6fce23b 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -4,23 +4,27 @@ import ( "testing" ) +const SCHEMA_PATH string = "../config_schema.json" + func TestNoConfig(t *testing.T) { - _, err := ReadConfig("../test/sfklase.yml") + _, err := ReadConfig("../test/sfklase.yml", SCHEMA_PATH) if err == nil { t.Errorf("Non-existent config was successfully read") } } func TestGoodConfig(t *testing.T) { - _, err := ReadConfig("../test/good_config.yml") + _, err := ReadConfig("../test/good_config.yml", SCHEMA_PATH) if err != nil { t.Errorf("Good config is not accepted: %v", err) } } func TestBadConfig(t *testing.T) { - _, err := ReadConfig("../test/bad_config.yml") + _, err := ReadConfig("../test/bad_config.yml", SCHEMA_PATH) 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) } } diff --git a/config_schema.json b/config_schema.json new file mode 100644 index 0000000..434bde5 --- /dev/null +++ b/config_schema.json @@ -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 + } + } +} diff --git a/go.mod b/go.mod index 0193f42..c6f2878 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.13 require ( github.com/pkg/errors v0.8.1 + github.com/santhosh-tekuri/jsonschema v1.2.4 gopkg.in/yaml.v2 v2.2.4 gosrc.io/xmpp v0.1.3 ) diff --git a/go.sum b/go.sum index e273364..4734020 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,8 @@ 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/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/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/telegabber.go b/telegabber.go index 6c17c91..d924822 100644 --- a/telegabber.go +++ b/telegabber.go @@ -8,9 +8,10 @@ import ( ) const CONFIG_PATH string = "config.yml" +const SCHEMA_PATH string = "./config_schema.json" func main() { - config, err := config.ReadConfig(CONFIG_PATH) + config, err := config.ReadConfig(CONFIG_PATH, SCHEMA_PATH) if err != nil { log.Fatal(err) }