feat: cli client with spec schema validation
This commit is contained in:
@ -1,5 +1,36 @@
|
||||
package spec
|
||||
|
||||
import "errors"
|
||||
import (
|
||||
"strings"
|
||||
|
||||
var ErrSchemaUnknown = errors.New("schema unknown")
|
||||
"github.com/pkg/errors"
|
||||
"github.com/qri-io/jsonschema"
|
||||
)
|
||||
|
||||
var ErrUnknownSchema = errors.New("unknown schema")
|
||||
|
||||
type ValidationError struct {
|
||||
keyErrors []jsonschema.KeyError
|
||||
}
|
||||
|
||||
func (e *ValidationError) Error() string {
|
||||
var sb strings.Builder
|
||||
|
||||
if _, err := sb.WriteString("validation error: "); err != nil {
|
||||
panic(errors.WithStack(err))
|
||||
}
|
||||
|
||||
for i, err := range e.keyErrors {
|
||||
if i != 0 {
|
||||
if _, err := sb.WriteString(", "); err != nil {
|
||||
panic(errors.WithStack(err))
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := sb.WriteString(err.Error()); err != nil {
|
||||
panic(errors.WithStack(err))
|
||||
}
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
@ -1,35 +0,0 @@
|
||||
package spec
|
||||
|
||||
const NameGateway Name = "gateway.emissary.cadoles.com"
|
||||
|
||||
type GatewayID string
|
||||
|
||||
type Gateway struct {
|
||||
Revision int `json:"revision"`
|
||||
Gateways map[GatewayID]GatewayEntry `json:"gateways"`
|
||||
}
|
||||
|
||||
type GatewayEntry struct {
|
||||
Address string `json:"address"`
|
||||
Target string `json:"target"`
|
||||
}
|
||||
|
||||
func (g *Gateway) SpecName() Name {
|
||||
return NameGateway
|
||||
}
|
||||
|
||||
func (g *Gateway) SpecRevision() int {
|
||||
return g.Revision
|
||||
}
|
||||
|
||||
func (g *Gateway) SpecData() any {
|
||||
return struct {
|
||||
Gateways map[GatewayID]GatewayEntry
|
||||
}{Gateways: g.Gateways}
|
||||
}
|
||||
|
||||
func NewGatewaySpec() *Gateway {
|
||||
return &Gateway{
|
||||
Gateways: make(map[GatewayID]GatewayEntry),
|
||||
}
|
||||
}
|
17
internal/spec/gateway/init.go
Normal file
17
internal/spec/gateway/init.go
Normal file
@ -0,0 +1,17 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
//go:embed schema.json
|
||||
var schema []byte
|
||||
|
||||
func init() {
|
||||
if err := spec.Register(NameGateway, schema); err != nil {
|
||||
panic(errors.WithStack(err))
|
||||
}
|
||||
}
|
29
internal/spec/gateway/schema.json
Normal file
29
internal/spec/gateway/schema.json
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://gateway.emissary.cadoles.com/spec.json",
|
||||
"title": "GatewaySpec",
|
||||
"description": "Emissary 'Gateway' specification",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"gateways": {
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
".*": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"address": {
|
||||
"type": "string"
|
||||
},
|
||||
"target": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["address", "target"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["gateways"],
|
||||
"additionalProperties": false
|
||||
}
|
37
internal/spec/gateway/spec.go
Normal file
37
internal/spec/gateway/spec.go
Normal file
@ -0,0 +1,37 @@
|
||||
package gateway
|
||||
|
||||
import "forge.cadoles.com/Cadoles/emissary/internal/spec"
|
||||
|
||||
const NameGateway spec.Name = "gateway.emissary.cadoles.com"
|
||||
|
||||
type ID string
|
||||
|
||||
type Spec struct {
|
||||
Revision int `json:"revision"`
|
||||
Gateways map[ID]GatewayEntry `json:"gateways"`
|
||||
}
|
||||
|
||||
type GatewayEntry struct {
|
||||
Address string `json:"address"`
|
||||
Target string `json:"target"`
|
||||
}
|
||||
|
||||
func (s *Spec) SpecName() spec.Name {
|
||||
return NameGateway
|
||||
}
|
||||
|
||||
func (s *Spec) SpecRevision() int {
|
||||
return s.Revision
|
||||
}
|
||||
|
||||
func (s *Spec) SpecData() any {
|
||||
return struct {
|
||||
Gateways map[ID]GatewayEntry
|
||||
}{Gateways: s.Gateways}
|
||||
}
|
||||
|
||||
func NewSpec() *Spec {
|
||||
return &Spec{
|
||||
Gateways: make(map[ID]GatewayEntry),
|
||||
}
|
||||
}
|
13
internal/spec/gateway/testdata/spec-additional-prop.json
vendored
Normal file
13
internal/spec/gateway/testdata/spec-additional-prop.json
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "gateway.emissary.cadoles.com",
|
||||
"data": {
|
||||
"gateways": {
|
||||
"cadoles.com": {
|
||||
"address": ":3003",
|
||||
"target": "https://www.cadoles.com",
|
||||
"foo": "bar"
|
||||
}
|
||||
}
|
||||
},
|
||||
"revision": 0
|
||||
}
|
11
internal/spec/gateway/testdata/spec-missing-prop.json
vendored
Normal file
11
internal/spec/gateway/testdata/spec-missing-prop.json
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "gateway.emissary.cadoles.com",
|
||||
"data": {
|
||||
"gateways": {
|
||||
"cadoles.com": {
|
||||
"address": ":3003"
|
||||
}
|
||||
}
|
||||
},
|
||||
"revision": 0
|
||||
}
|
12
internal/spec/gateway/testdata/spec-ok.json
vendored
Normal file
12
internal/spec/gateway/testdata/spec-ok.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "gateway.emissary.cadoles.com",
|
||||
"data": {
|
||||
"gateways": {
|
||||
"cadoles.com": {
|
||||
"address": ":3003",
|
||||
"target": "https://www.cadoles.com"
|
||||
}
|
||||
}
|
||||
},
|
||||
"revision": 0
|
||||
}
|
75
internal/spec/gateway/validator_test.go
Normal file
75
internal/spec/gateway/validator_test.go
Normal file
@ -0,0 +1,75 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type validatorTestCase struct {
|
||||
Name string
|
||||
Source string
|
||||
ExpectedResult bool
|
||||
}
|
||||
|
||||
var validatorTestCases = []validatorTestCase{
|
||||
{
|
||||
Name: "SpecOK",
|
||||
Source: "testdata/spec-ok.json",
|
||||
ExpectedResult: true,
|
||||
},
|
||||
{
|
||||
Name: "SpecMissingProp",
|
||||
Source: "testdata/spec-missing-prop.json",
|
||||
ExpectedResult: false,
|
||||
},
|
||||
{
|
||||
Name: "SpecAdditionalProp",
|
||||
Source: "testdata/spec-additional-prop.json",
|
||||
ExpectedResult: false,
|
||||
},
|
||||
}
|
||||
|
||||
func TestValidator(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
validator := spec.NewValidator()
|
||||
if err := validator.Register(NameGateway, schema); err != nil {
|
||||
t.Fatalf("+%v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
for _, tc := range validatorTestCases {
|
||||
func(tc *validatorTestCase) {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rawSpec, err := ioutil.ReadFile(tc.Source)
|
||||
if err != nil {
|
||||
t.Fatalf("+%v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
var spec spec.RawSpec
|
||||
|
||||
if err := json.Unmarshal(rawSpec, &spec); err != nil {
|
||||
t.Fatalf("+%v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
result, err := validator.Validate(ctx, &spec)
|
||||
|
||||
if e, g := tc.ExpectedResult, result; e != g {
|
||||
t.Errorf("result: expected '%v', got '%v'", e, g)
|
||||
}
|
||||
|
||||
if tc.ExpectedResult && err != nil {
|
||||
t.Errorf("+%v", errors.WithStack(err))
|
||||
}
|
||||
})
|
||||
}(&tc)
|
||||
}
|
||||
}
|
@ -3,13 +3,13 @@ package spec
|
||||
type Spec interface {
|
||||
SpecName() Name
|
||||
SpecRevision() int
|
||||
SpecData() any
|
||||
SpecData() map[string]any
|
||||
}
|
||||
|
||||
type RawSpec struct {
|
||||
Name Name `json:"name"`
|
||||
Revision int `json:"revision"`
|
||||
Data any `json:"data"`
|
||||
Name Name `json:"name"`
|
||||
Revision int `json:"revision"`
|
||||
Data map[string]any `json:"data"`
|
||||
}
|
||||
|
||||
func (s *RawSpec) SpecName() Name {
|
||||
@ -20,6 +20,6 @@ func (s *RawSpec) SpecRevision() int {
|
||||
return s.Revision
|
||||
}
|
||||
|
||||
func (s *RawSpec) SpecData() any {
|
||||
func (s *RawSpec) SpecData() map[string]any {
|
||||
return s.Data
|
||||
}
|
||||
|
17
internal/spec/uci/init.go
Normal file
17
internal/spec/uci/init.go
Normal file
@ -0,0 +1,17 @@
|
||||
package uci
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
//go:embed schema.json
|
||||
var schema []byte
|
||||
|
||||
func init() {
|
||||
if err := spec.Register(NameUCI, schema); err != nil {
|
||||
panic(errors.WithStack(err))
|
||||
}
|
||||
}
|
97
internal/spec/uci/schema.json
Normal file
97
internal/spec/uci/schema.json
Normal file
@ -0,0 +1,97 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://uci.emissary.cadoles.com/spec.json",
|
||||
"title": "UCISpec",
|
||||
"description": "Emissary 'UCI' specification",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"config": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"packages": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/package"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["packages"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"postImportCommands": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command": {
|
||||
"type": "string"
|
||||
},
|
||||
"args": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["command", "args"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["config", "postImportCommands"],
|
||||
"additionalProperties": false,
|
||||
"$defs": {
|
||||
"package": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"configs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/config"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["name", "configs"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"config": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"section": {
|
||||
"type": "string"
|
||||
},
|
||||
"options": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/option"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["name", "section", "options"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"option": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["list", "option"]
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["type", "name", "value"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
@ -1,10 +1,13 @@
|
||||
package spec
|
||||
package uci
|
||||
|
||||
import "forge.cadoles.com/Cadoles/emissary/internal/openwrt/uci"
|
||||
import (
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/openwrt/uci"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
||||
)
|
||||
|
||||
const NameUCI Name = "uci.emissary.cadoles.com"
|
||||
const NameUCI spec.Name = "uci.emissary.cadoles.com"
|
||||
|
||||
type UCI struct {
|
||||
type Spec struct {
|
||||
Revision int `json:"revisions"`
|
||||
Config *uci.UCI `json:"config"`
|
||||
PostImportCommands []*UCIPostImportCommand `json:"postImportCommands"`
|
||||
@ -15,23 +18,23 @@ type UCIPostImportCommand struct {
|
||||
Args []string `json:"args"`
|
||||
}
|
||||
|
||||
func (u *UCI) SpecName() Name {
|
||||
func (s *Spec) SpecName() spec.Name {
|
||||
return NameUCI
|
||||
}
|
||||
|
||||
func (u *UCI) SpecRevision() int {
|
||||
return u.Revision
|
||||
func (s *Spec) SpecRevision() int {
|
||||
return s.Revision
|
||||
}
|
||||
|
||||
func (u *UCI) SpecData() any {
|
||||
func (s *Spec) SpecData() any {
|
||||
return struct {
|
||||
Config *uci.UCI `json:"config"`
|
||||
PostImportCommands []*UCIPostImportCommand `json:"postImportCommands"`
|
||||
}{Config: u.Config, PostImportCommands: u.PostImportCommands}
|
||||
}{Config: s.Config, PostImportCommands: s.PostImportCommands}
|
||||
}
|
||||
|
||||
func NewUCISpec() *UCI {
|
||||
return &UCI{
|
||||
func NewSpec() *Spec {
|
||||
return &Spec{
|
||||
PostImportCommands: make([]*UCIPostImportCommand, 0),
|
||||
}
|
||||
}
|
162
internal/spec/uci/testdata/spec-missing-prop.json
vendored
Normal file
162
internal/spec/uci/testdata/spec-missing-prop.json
vendored
Normal file
@ -0,0 +1,162 @@
|
||||
{
|
||||
"name": "uci.emissary.cadoles.com",
|
||||
"data": {
|
||||
"config": {
|
||||
"packages": [
|
||||
{
|
||||
"name": "uhttpd",
|
||||
"configs": [
|
||||
{
|
||||
"name": "uhttpd",
|
||||
"section": "main",
|
||||
"options": [
|
||||
{
|
||||
"type": "list",
|
||||
"name": "listen_http",
|
||||
"value": "0.0.0.0:8080"
|
||||
},
|
||||
{
|
||||
"type": "list",
|
||||
"name": "listen_http",
|
||||
"value": "[::]:8080"
|
||||
},
|
||||
{
|
||||
"type": "list",
|
||||
"name": "listen_https",
|
||||
"value": "0.0.0.0:8443"
|
||||
},
|
||||
{
|
||||
"type": "list",
|
||||
"name": "listen_https",
|
||||
"value": "[::]:8443"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "redirect_https",
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "home",
|
||||
"value": "/www"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "rfc1918_filter",
|
||||
"value": "1"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "max_requests",
|
||||
"value": "3"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "max_connections",
|
||||
"value": "100"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "cert",
|
||||
"value": "/etc/uhttpd.crt"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "key",
|
||||
"value": "/etc/uhttpd.key"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "cgi_prefix",
|
||||
"value": "/cgi-bin"
|
||||
},
|
||||
{
|
||||
"type": "list",
|
||||
"name": "lua_prefix",
|
||||
"value": "/cgi-bin/luci=/usr/lib/lua/luci/sgi/uhttpd.lua"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "script_timeout",
|
||||
"value": "60"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "network_timeout",
|
||||
"value": "30"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "http_keepalive",
|
||||
"value": "20"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "tcp_keepalive",
|
||||
"value": "1"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "ubus_prefix"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "cert",
|
||||
"section": "defaults",
|
||||
"options": [
|
||||
{
|
||||
"type": "option",
|
||||
"name": "days",
|
||||
"value": "730"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "key_type",
|
||||
"value": "ec"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "bits",
|
||||
"value": "2048"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "ec_curve",
|
||||
"value": "P-256"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "country",
|
||||
"value": "ZZ"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "state",
|
||||
"value": "Somewhere"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "location",
|
||||
"value": "Unknown"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "commonname",
|
||||
"value": "OpenWrt"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"postImportCommands": [
|
||||
{
|
||||
"command": "reload_config",
|
||||
"args": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"revision": 0
|
||||
}
|
163
internal/spec/uci/testdata/spec-ok.json
vendored
Normal file
163
internal/spec/uci/testdata/spec-ok.json
vendored
Normal file
@ -0,0 +1,163 @@
|
||||
{
|
||||
"name": "uci.emissary.cadoles.com",
|
||||
"data": {
|
||||
"config": {
|
||||
"packages": [
|
||||
{
|
||||
"name": "uhttpd",
|
||||
"configs": [
|
||||
{
|
||||
"name": "uhttpd",
|
||||
"section": "main",
|
||||
"options": [
|
||||
{
|
||||
"type": "list",
|
||||
"name": "listen_http",
|
||||
"value": "0.0.0.0:8080"
|
||||
},
|
||||
{
|
||||
"type": "list",
|
||||
"name": "listen_http",
|
||||
"value": "[::]:8080"
|
||||
},
|
||||
{
|
||||
"type": "list",
|
||||
"name": "listen_https",
|
||||
"value": "0.0.0.0:8443"
|
||||
},
|
||||
{
|
||||
"type": "list",
|
||||
"name": "listen_https",
|
||||
"value": "[::]:8443"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "redirect_https",
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "home",
|
||||
"value": "/www"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "rfc1918_filter",
|
||||
"value": "1"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "max_requests",
|
||||
"value": "3"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "max_connections",
|
||||
"value": "100"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "cert",
|
||||
"value": "/etc/uhttpd.crt"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "key",
|
||||
"value": "/etc/uhttpd.key"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "cgi_prefix",
|
||||
"value": "/cgi-bin"
|
||||
},
|
||||
{
|
||||
"type": "list",
|
||||
"name": "lua_prefix",
|
||||
"value": "/cgi-bin/luci=/usr/lib/lua/luci/sgi/uhttpd.lua"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "script_timeout",
|
||||
"value": "60"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "network_timeout",
|
||||
"value": "30"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "http_keepalive",
|
||||
"value": "20"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "tcp_keepalive",
|
||||
"value": "1"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "ubus_prefix",
|
||||
"value": "/ubus"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "cert",
|
||||
"section": "defaults",
|
||||
"options": [
|
||||
{
|
||||
"type": "option",
|
||||
"name": "days",
|
||||
"value": "730"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "key_type",
|
||||
"value": "ec"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "bits",
|
||||
"value": "2048"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "ec_curve",
|
||||
"value": "P-256"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "country",
|
||||
"value": "ZZ"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "state",
|
||||
"value": "Somewhere"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "location",
|
||||
"value": "Unknown"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "commonname",
|
||||
"value": "OpenWrt"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"postImportCommands": [
|
||||
{
|
||||
"command": "reload_config",
|
||||
"args": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"revision": 0
|
||||
}
|
70
internal/spec/uci/validator_test.go
Normal file
70
internal/spec/uci/validator_test.go
Normal file
@ -0,0 +1,70 @@
|
||||
package uci
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type validatorTestCase struct {
|
||||
Name string
|
||||
Source string
|
||||
ExpectedResult bool
|
||||
}
|
||||
|
||||
var validatorTestCases = []validatorTestCase{
|
||||
{
|
||||
Name: "SpecOK",
|
||||
Source: "testdata/spec-ok.json",
|
||||
ExpectedResult: true,
|
||||
},
|
||||
{
|
||||
Name: "SpecMissingProp",
|
||||
Source: "testdata/spec-missing-prop.json",
|
||||
ExpectedResult: false,
|
||||
},
|
||||
}
|
||||
|
||||
func TestValidator(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
validator := spec.NewValidator()
|
||||
if err := validator.Register(NameUCI, schema); err != nil {
|
||||
t.Fatalf("+%v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
for _, tc := range validatorTestCases {
|
||||
func(tc *validatorTestCase) {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rawSpec, err := ioutil.ReadFile(tc.Source)
|
||||
if err != nil {
|
||||
t.Fatalf("+%v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
var spec spec.RawSpec
|
||||
|
||||
if err := json.Unmarshal(rawSpec, &spec); err != nil {
|
||||
t.Fatalf("+%v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
result, err := validator.Validate(ctx, &spec)
|
||||
|
||||
if e, g := tc.ExpectedResult, result; e != g {
|
||||
t.Errorf("result: expected '%v', got '%v'", e, g)
|
||||
}
|
||||
|
||||
if tc.ExpectedResult && err != nil {
|
||||
t.Errorf("+%v", errors.WithStack(err))
|
||||
}
|
||||
})
|
||||
}(&tc)
|
||||
}
|
||||
}
|
63
internal/spec/validator.go
Normal file
63
internal/spec/validator.go
Normal file
@ -0,0 +1,63 @@
|
||||
package spec
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/qri-io/jsonschema"
|
||||
)
|
||||
|
||||
type Validator struct {
|
||||
schemas map[Name]*jsonschema.Schema
|
||||
}
|
||||
|
||||
func (v *Validator) Register(name Name, rawSchema []byte) error {
|
||||
schema := &jsonschema.Schema{}
|
||||
if err := json.Unmarshal(rawSchema, schema); err != nil {
|
||||
return errors.Wrapf(err, "could not register spec shema '%s'", name)
|
||||
}
|
||||
|
||||
v.schemas[name] = schema
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *Validator) Validate(ctx context.Context, spec Spec) (bool, error) {
|
||||
schema, exists := v.schemas[spec.SpecName()]
|
||||
if !exists {
|
||||
return false, errors.WithStack(ErrUnknownSchema)
|
||||
}
|
||||
|
||||
state := schema.Validate(ctx, spec.SpecData())
|
||||
if !state.IsValid() {
|
||||
return false, errors.WithStack(&ValidationError{*state.Errs})
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func NewValidator() *Validator {
|
||||
return &Validator{
|
||||
schemas: make(map[Name]*jsonschema.Schema),
|
||||
}
|
||||
}
|
||||
|
||||
var defaultValidator = NewValidator()
|
||||
|
||||
func Register(name Name, rawSchema []byte) error {
|
||||
if err := defaultValidator.Register(name, rawSchema); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Validate(ctx context.Context, spec Spec) (bool, error) {
|
||||
ok, err := defaultValidator.Validate(ctx, spec)
|
||||
if err != nil {
|
||||
return ok, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return ok, nil
|
||||
}
|
Reference in New Issue
Block a user