Skip to content

Commit

Permalink
Global json schema (auto versioning) (#412)
Browse files Browse the repository at this point in the history
* global json schema (auto versioning)

* update schema on yaml config examples

* add tests
  • Loading branch information
creativeprojects authored Oct 6, 2024
1 parent 830d0fd commit 62c5c64
Show file tree
Hide file tree
Showing 28 changed files with 344 additions and 75 deletions.
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,8 @@ generate-jsonschema: build

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

$(abspath $(BINARY)) generate --json-schema global > $(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
42 changes: 33 additions & 9 deletions commands_generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import (
"github.com/creativeprojects/resticprofile/util/templates"
)

const pathTemplates = "contrib/templates"

//go:embed contrib/completion/bash-completion.sh
var bashCompletionScript string

Expand Down Expand Up @@ -75,7 +77,7 @@ func generateConfigReference(output io.Writer, args []string) error {

data := config.NewTemplateInfoData(resticVersion)
tpl := templates.New("config-reference", data.GetFuncs())
templates, err := fs.Sub(configReferenceTemplates, "contrib/templates")
templates, err := fs.Sub(configReferenceTemplates, pathTemplates)
if err != nil {
return fmt.Errorf("cannot load templates: %w", err)
}
Expand Down Expand Up @@ -105,7 +107,7 @@ func generateConfigReference(output io.Writer, args []string) error {

for _, staticPage := range staticPages {
fmt.Fprintf(output, "generating %s...\n", staticPage.templateName)
err = generatePage(tpl, data, filepath.Join(destination, staticPage.fileName), staticPage.templateName)
err = generateFileFromTemplate(tpl, data, filepath.Join(destination, staticPage.fileName), staticPage.templateName)
if err != nil {
return fmt.Errorf("unable to generate page %s: %w", staticPage.fileName, err)
}
Expand All @@ -119,7 +121,7 @@ func generateConfigReference(output io.Writer, args []string) error {
Section: profileSection,
Weight: weight,
}
err = generatePage(tpl, sectionData, filepath.Join(destination, "profile", profileSection.Name()+".md"), "profile.sub-section.gomd")
err = generateFileFromTemplate(tpl, sectionData, filepath.Join(destination, "profile", profileSection.Name()+".md"), "profile.sub-section.gomd")
if err != nil {
return fmt.Errorf("unable to generate profile section %s: %w", profileSection.Name(), err)
}
Expand All @@ -134,7 +136,7 @@ func generateConfigReference(output io.Writer, args []string) error {
Section: nestedSection,
Weight: weight,
}
err = generatePage(tpl, sectionData, filepath.Join(destination, "nested", nestedSection.Name()+".md"), "profile.nested-section.gomd")
err = generateFileFromTemplate(tpl, sectionData, filepath.Join(destination, "nested", nestedSection.Name()+".md"), "profile.nested-section.gomd")
if err != nil {
return fmt.Errorf("unable to generate nested section %s: %w", nestedSection.Name(), err)
}
Expand All @@ -143,7 +145,7 @@ func generateConfigReference(output io.Writer, args []string) error {
return nil
}

func generatePage(tpl *template.Template, data any, fileName, templateName string) error {
func generateFileFromTemplate(tpl *template.Template, data any, fileName, templateName string) error {
err := os.MkdirAll(filepath.Dir(fileName), 0o755)
if err != nil {
return fmt.Errorf("cannot create directory: %w", err)
Expand Down Expand Up @@ -171,12 +173,34 @@ func generateJsonSchema(output io.Writer, args []string) (err error) {
}
}

version := config.Version02
if len(args) > 0 && args[0] == "v1" {
version = config.Version01
if len(args) == 0 {
return fmt.Errorf("missing type of json schema to generate (global, v1, v2)")
}

return jsonschema.WriteJsonSchema(version, resticVersion, output)
switch args[0] {
case "global":
data := config.NewTemplateInfoData(resticVersion)
tpl := templates.New("", data.GetFuncs())
templates, err := fs.Sub(configReferenceTemplates, pathTemplates)
if err != nil {
return fmt.Errorf("cannot load templates: %w", err)
}
tpl, err = tpl.ParseFS(templates, "config-schema.gojson")
if err != nil {
return fmt.Errorf("parsing failed: %w", err)
}
err = tpl.ExecuteTemplate(output, "config-schema.gojson", data)
if err != nil {
return fmt.Errorf("cannot execute template: %w", err)
}
return nil
case "v1":
return jsonschema.WriteJsonSchema(config.Version01, resticVersion, output)
case "v2":
return jsonschema.WriteJsonSchema(config.Version02, resticVersion, output)
default:
return fmt.Errorf("unknown json schema type: %s", args[0])
}
}

// SectionInfoData is used as data for go templates that render profile section references
Expand Down
31 changes: 28 additions & 3 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,12 +235,28 @@ func TestGenerateCommand(t *testing.T) {
assert.Contains(t, ref, "generating nested section")
})

t.Run("--json-schema", func(t *testing.T) {
t.Run("--json-schema global", func(t *testing.T) {
buffer.Reset()
assert.NoError(t, generateCommand(buffer, contextWithArguments([]string{"--json-schema"})))
assert.NoError(t, generateCommand(buffer, contextWithArguments([]string{"--json-schema", "global"})))
ref := buffer.String()
assert.Contains(t, ref, "\"profiles\":")
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 no-option", func(t *testing.T) {
buffer.Reset()
assert.Error(t, generateCommand(buffer, contextWithArguments([]string{"--json-schema"})))
})

t.Run("--json-schema invalid-option", func(t *testing.T) {
buffer.Reset()
assert.Error(t, generateCommand(buffer, contextWithArguments([]string{"--json-schema", "_invalid_"})))
})

t.Run("--json-schema v1", func(t *testing.T) {
Expand All @@ -249,6 +266,14 @@ func TestGenerateCommand(t *testing.T) {
assert.Contains(t, ref, "/jsonschema/config-1.json")
})

t.Run("--json-schema v2", func(t *testing.T) {
buffer.Reset()
assert.NoError(t, generateCommand(buffer, contextWithArguments([]string{"--json-schema", "v2"})))
ref := buffer.String()
assert.Contains(t, ref, "\"profiles\":")
assert.Contains(t, ref, "/jsonschema/config-2.json")
})

t.Run("--json-schema --version 0.13 v1", func(t *testing.T) {
buffer.Reset()
assert.NoError(t, generateCommand(buffer, contextWithArguments([]string{"--json-schema", "--version", "0.13", "v1"})))
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.

Loading

0 comments on commit 62c5c64

Please sign in to comment.