Skip to content

Commit

Permalink
global json schema (auto versioning)
Browse files Browse the repository at this point in the history
  • Loading branch information
creativeprojects committed Oct 6, 2024
1 parent 830d0fd commit 05dc6d6
Show file tree
Hide file tree
Showing 22 changed files with 298 additions and 58 deletions.
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ RESTIC_GEN=$(BUILD)restic-generator
RESTIC_DIR=$(BUILD)restic-
RESTIC_CMD=$(BUILD)restic-commands.json

CONTRIB_DIR=contrib
JSONSCHEMA_DIR=docs/static/jsonschema
CONFIG_REFERENCE_DIR=docs/content/reference

Expand Down Expand Up @@ -243,6 +244,8 @@ generate-jsonschema: build

mkdir -p $(JSONSCHEMA_DIR) || echo "$(JSONSCHEMA_DIR) exists"

$(abspath $(BINARY)) generate --config-reference $(CONTRIB_DIR)/templates/config-schema.gojson > $(JSONSCHEMA_DIR)/config.json

for config_version in 1 2 ; do \
$(abspath $(BINARY)) generate --json-schema v$$config_version > $(JSONSCHEMA_DIR)/config-$$config_version.json ; \
for restic_version in 0.9 0.10 0.11 0.12 0.13 0.14 0.15 0.16 0.17 ; do \
Expand Down
6 changes: 5 additions & 1 deletion commands_generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,11 @@ func generateConfigReference(output io.Writer, args []string) error {
}

data := config.NewTemplateInfoData(resticVersion)
tpl := templates.New("config-reference", data.GetFuncs())
name := "config-reference"
if len(args) > 0 {
name = filepath.Base(args[0])
}
tpl := templates.New(name, data.GetFuncs())
templates, err := fs.Sub(configReferenceTemplates, "contrib/templates")
if err != nil {
return fmt.Errorf("cannot load templates: %w", err)
Expand Down
15 changes: 15 additions & 0 deletions commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"bytes"
"encoding/json"
"fmt"
"os"
"sort"
Expand Down Expand Up @@ -234,6 +235,20 @@ func TestGenerateCommand(t *testing.T) {
assert.Contains(t, ref, "generating nested section")
})

t.Run("--config-reference config-schema.gojson", func(t *testing.T) {
buffer.Reset()
assert.NoError(t, generateCommand(buffer, contextWithArguments([]string{"--config-reference", "contrib/templates/config-schema.gojson"})))
ref := buffer.String()
assert.Contains(t, ref, `"$schema"`)
assert.Contains(t, ref, "/jsonschema/config-1.json")
assert.Contains(t, ref, "/jsonschema/config-2.json")

decoder := json.NewDecoder(strings.NewReader(ref))
content := make(map[string]any)
assert.NoError(t, decoder.Decode(&content))
assert.Contains(t, content, `$schema`)
})

t.Run("--json-schema", func(t *testing.T) {
buffer.Reset()
assert.NoError(t, generateCommand(buffer, contextWithArguments([]string{"--json-schema"})))
Expand Down
6 changes: 3 additions & 3 deletions config/checkdoc/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ const (
configTag = "```"
checkdocIgnore = "<!-- checkdoc-ignore -->"
goTemplate = "{{"
replaceURL = "http://localhost:1313/resticprofile/jsonschema/config-$1.json"
replaceURL = "http://localhost:1313/resticprofile/jsonschema/config$1.json"
)

var (
urlPattern = regexp.MustCompile(`{{< [^>}]+config-(\d)\.json"[^>}]+ >}}`)
_ = regexp.MustCompile(`{{< [^>}]+config-(\d)\.json"[^>}]+ >}}`) // Remove this when VS Code fixed the syntax highlighting issues
urlPattern = regexp.MustCompile(`{{< [^>}]+config(-\d)?\.json"[^>}]+ >}}`)
_ = regexp.MustCompile(`{{< [^>}]+config(-\d)?\.json"[^>}]+ >}}`) // Remove this when VS Code fixed the syntax highlighting issues
)

var (
Expand Down
2 changes: 1 addition & 1 deletion config/global.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ type Global struct {
Initialize bool `mapstructure:"initialize" default:"false" description:"Initialize a repository if missing"`
NoAutoRepositoryFile maybe.Bool `mapstructure:"prevent-auto-repository-file" default:"false" description:"Prevents using a repository file for repository definitions containing a password"`
ResticBinary string `mapstructure:"restic-binary" description:"Full path of the restic executable (detected if not set)"`
ResticVersion string // not configurable at the moment. To be set after ResticBinary is known.
ResticVersion string `mapstructure:"restic-version" pattern:"^(|[0-9]+\\.[0-9]+(\\.[0-9]+)?)$" description:"Sets the restic version (detected if not set)"`
FilterResticFlags bool `mapstructure:"restic-arguments-filter" default:"true" description:"Remove unknown flags instead of passing all configured flags to restic"`
ResticLockRetryAfter time.Duration `mapstructure:"restic-lock-retry-after" default:"1m" description:"Time to wait before trying to get a lock on a restic repository - see https://creativeprojects.github.io/resticprofile/usage/locks/"`
ResticStaleLockAge time.Duration `mapstructure:"restic-stale-lock-age" default:"1h" description:"The age an unused lock on a restic repository must have at least before resticprofile attempts to unlock - see https://creativeprojects.github.io/resticprofile/usage/locks/"`
Expand Down
21 changes: 17 additions & 4 deletions config/jsonschema/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ func newSchemaBool() *schemaTypeBase {

type schemaObject struct {
schemaTypeBase
AdditionalProperties bool `json:"additionalProperties"`
AdditionalProperties any `json:"additionalProperties,omitempty"`
PatternProperties map[string]SchemaType `json:"patternProperties,omitempty"`
Properties map[string]SchemaType `json:"properties,omitempty"`
Required []string `json:"required,omitempty"`
Expand All @@ -274,9 +274,10 @@ type schemaObject struct {

func newSchemaObject() *schemaObject {
return withBaseType(&schemaObject{
PatternProperties: make(map[string]SchemaType),
Properties: make(map[string]SchemaType),
DependentRequired: make(map[string][]string),
AdditionalProperties: false,
PatternProperties: make(map[string]SchemaType),
Properties: make(map[string]SchemaType),
DependentRequired: make(map[string][]string),
}, "object")
}

Expand All @@ -297,6 +298,15 @@ func (s *schemaObject) verify() (err error) {
err = fmt.Errorf("type of %q in properties is undefined", name)
}
}
if err == nil {
switch s.AdditionalProperties.(type) {
case nil:
case bool:
case SchemaType:
default:
err = fmt.Errorf("additionalProperties must be nil, boolean or SchemaType")
}
}
if err == nil {
err = s.schemaTypeBase.verify()
}
Expand Down Expand Up @@ -438,6 +448,9 @@ func internalWalkTypes(into map[SchemaType]bool, current SchemaType, callback fu
for name, property := range t.PatternProperties {
t.PatternProperties[name] = internalWalkTypes(into, property, callback)
}
if item, ok := t.AdditionalProperties.(SchemaType); ok {
t.AdditionalProperties = internalWalkTypes(into, item, callback)
}
case *schemaArray:
t.Items = internalWalkTypes(into, t.Items, callback)
case *schemaTypeList:
Expand Down
6 changes: 6 additions & 0 deletions config/jsonschema/model_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,12 @@ func TestVerify(t *testing.T) {
obj.Properties["first"] = newSchemaString()
assert.NoError(t, obj.verify())

assert.Equal(t, false, obj.AdditionalProperties)
obj.AdditionalProperties = "-"
assert.ErrorContains(t, obj.verify(), `additionalProperties must be nil, boolean or SchemaType`)
obj.AdditionalProperties = newSchemaString()
assert.NoError(t, obj.verify())

testBase(t, obj.base())
})

Expand Down
9 changes: 1 addition & 8 deletions config/jsonschema/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -432,14 +432,7 @@ func applyListAppendSchema(target SchemaType) {
func schemaForConfigV1(profileInfo config.ProfileInfo) (object *schemaObject) {
object = schemaForProfile(profileInfo)

// exclude non-profile properties from profile-schema
profilesPattern := fmt.Sprintf(`^(?!%s).*$`, strings.Join([]string{
constants.SectionConfigurationGlobal,
constants.SectionConfigurationGroups,
constants.SectionConfigurationIncludes,
constants.ParameterVersion,
}, "|"))
object.PatternProperties[profilesPattern] = object.PatternProperties[matchAll]
object.AdditionalProperties = object.PatternProperties[matchAll]
delete(object.PatternProperties, matchAll)

object.Description = "resticprofile configuration v1"
Expand Down
6 changes: 3 additions & 3 deletions config/jsonschema/schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -357,9 +357,9 @@ func TestSchemaForPropertySet(t *testing.T) {

t.Run("AdditionalProperties", func(t *testing.T) {
s := schemaForPropertySet(newMock(func(m *mocks.NamedPropertySet) { m.EXPECT().IsClosed().Return(false) }))
assert.True(t, s.AdditionalProperties)
assert.Equal(t, true, s.AdditionalProperties)
s = schemaForPropertySet(newMock(func(m *mocks.NamedPropertySet) { m.EXPECT().IsClosed().Return(true) }))
assert.False(t, s.AdditionalProperties)
assert.Equal(t, false, s.AdditionalProperties)
})

t.Run("TypedAdditionalProperty", func(t *testing.T) {
Expand All @@ -374,7 +374,7 @@ func TestSchemaForPropertySet(t *testing.T) {
m.EXPECT().OtherPropertyInfo().Return(pi)
}))

assert.False(t, s.AdditionalProperties)
assert.Equal(t, false, s.AdditionalProperties)
assert.Equal(t, newSchemaString(), s.PatternProperties[matchAll])
})

Expand Down
2 changes: 1 addition & 1 deletion config/mocks/NamedPropertySet.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion config/mocks/ProfileInfo.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion config/mocks/PropertyInfo.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion config/mocks/SectionInfo.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

184 changes: 184 additions & 0 deletions contrib/templates/config-schema.gojson
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
{{- /* ------------------------------------------------------------------------------

Template that generates a global json schema which redirects to versioned URLs
depending on "version" and "restic-version" properties

Usage: resticprofile generate \
--config-reference contrib/templates/config-schema.gojson

------------------------------------------------------------------------------ */ -}}
{{- /*gotype: github.com/creativeprojects/resticprofile/config.TemplateInfoData*/ -}}
{{- $baseUrl := "https://creativeprojects.github.io/resticprofile/jsonschema" -}}
{{- if .Env.SCHEMA_BASE_URL -}}
{{- $baseUrl = .Env.SCHEMA_BASE_URL -}}
{{- end -}}
{{- $refBaseUrl := $baseUrl -}}
{{- if .Env.SCHEMA_REF_BASE_URL -}}
{{- $refBaseUrl = .Env.SCHEMA_REF_BASE_URL -}}
{{- end -}}
{
"$id": "{{ $baseUrl | js }}/config.json",
"$schema": "https://json-schema.org/draft-07/schema",
"$defs": {
"version-1": {
"oneOf": [
{
"type": "object",
"properties": {
"version": {
"type": "string",
"const": "1"
}
},
"required": [
"version"
]
},
{
"type": "object",
"properties": {
"version": {
"type": "string",
"maxLength": 0
}
},
"required": [
"version"
]
}
]
},
"version-2": {
"type": "object",
"properties": {
"version": {
"type": "string",
"const": "2"
}
},
"required": [
"version"
]
},
"no-version": {
"not": {
"type": "object",
"properties": {
"version": {
}
},
"required": [
"version"
]
}
},
"no-restic-version": {
"type": "object",
"properties": {
"global": {
"not": {
"type": "object",
"properties": {
"restic-version": {
}
},
"required": [
"restic-version"
]
}
}
}
}
},
"if": {
"$ref": "#/$defs/no-version"
},
"then": {
"$ref": "{{ $refBaseUrl | js }}/config-1.json"
},
"else": {
"if": {
"$ref": "#/$defs/no-restic-version"
},
"then": {
"oneOf": [
{
{{ block "noResticVersion" (list $refBaseUrl "2") -}}
{{- $base := index . 0 -}}
{{- $config := index . 1 -}}
"allOf": [
{
"$ref": "#/$defs/version-{{ $config }}"
},
{
"$ref": "{{ $base | js }}/config-{{ $config }}.json"
}
]
{{ end }}
},
{
{{ template "noResticVersion" (list $refBaseUrl "1") }}
}
]
},
"else": {
"oneOf": [
{{ define "schemaWithResticVersion" }}
{{- $base := index . 0 -}}
{{- $config := index . 1 -}}
{{- $version := index . 2 -}}
{
"allOf": [
{
"$ref": "#/$defs/version-{{ $config | js }}"
},
{
"type": "object",
"properties": {
"global": {
"type": "object",
"properties": {
"restic-version": {
"anyOf": [
{
"type": "string",
"pattern": "{{ $version | replace "." "\\." | js }}.*",
"default": "{{ $version | js }}",
"minLength": 3
},
{
"type": "string",
"const": "{{ $version | js }}"
}
]
}
},
"required": [
"restic-version"
]
}
},
"required": [
"global"
]
},
{
"$ref": "{{ $base | js }}/config-{{ $config | js }}-restic-{{ $version | replace "." "-" | js }}.json"
}
]
}
{{ end -}}
{{- /* restic major version ( assuming it is "0.$major", may need to change when restic reaches v1 ) */ -}}
{{ range $index, $version := .KnownResticVersions -}}
{{- $version = slice ($version | split ".") 0 2 | join "." -}}
{{- if gt $index 0 -}}{{ "," }}{{- end -}}
{{- template "schemaWithResticVersion" (list $refBaseUrl "2" $version) -}}
{{ "," }}
{{- template "schemaWithResticVersion" (list $refBaseUrl "1" $version) -}}
{{- end }}
]
}
},
"title": "resticprofile configuration",
"type": "object"
}
Loading

0 comments on commit 05dc6d6

Please sign in to comment.