Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Global json schema (auto versioning) #412

Merged
merged 7 commits into from
Oct 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
"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 @@

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 @@

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 @@
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 @@
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 @@
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 @@
}
}

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)")
creativeprojects marked this conversation as resolved.
Show resolved Hide resolved
}

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)

Check warning on line 186 in commands_generate.go

View check run for this annotation

Codecov / codecov/patch

commands_generate.go#L186

Added line #L186 was not covered by tests
}
creativeprojects marked this conversation as resolved.
Show resolved Hide resolved
tpl, err = tpl.ParseFS(templates, "config-schema.gojson")
if err != nil {
return fmt.Errorf("parsing failed: %w", err)

Check warning on line 190 in commands_generate.go

View check run for this annotation

Codecov / codecov/patch

commands_generate.go#L190

Added line #L190 was not covered by tests
}
creativeprojects marked this conversation as resolved.
Show resolved Hide resolved
err = tpl.ExecuteTemplate(output, "config-schema.gojson", data)
if err != nil {
return fmt.Errorf("cannot execute template: %w", err)

Check warning on line 194 in commands_generate.go

View check run for this annotation

Codecov / codecov/patch

commands_generate.go#L194

Added line #L194 was not covered by tests
}
creativeprojects marked this conversation as resolved.
Show resolved Hide resolved
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])
}
creativeprojects marked this conversation as resolved.
Show resolved Hide resolved
}

// 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 @@

type schemaObject struct {
schemaTypeBase
AdditionalProperties bool `json:"additionalProperties"`
AdditionalProperties any `json:"additionalProperties,omitempty"`
creativeprojects marked this conversation as resolved.
Show resolved Hide resolved
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 @@

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 @@
err = fmt.Errorf("type of %q in properties is undefined", name)
}
}
if err == nil {
switch s.AdditionalProperties.(type) {
case nil:

Check warning on line 303 in config/jsonschema/model.go

View check run for this annotation

Codecov / codecov/patch

config/jsonschema/model.go#L303

Added line #L303 was not covered by tests
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 @@
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)
}
creativeprojects marked this conversation as resolved.
Show resolved Hide resolved
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)

creativeprojects marked this conversation as resolved.
Show resolved Hide resolved
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
Loading