add resource and document constraints
Some checks are pending
Lint / golangci-lint (push) Waiting to run
Declarative Tests / test (push) Waiting to run
Declarative Tests / build-fedora (push) Waiting to run
Declarative Tests / build-ubuntu-focal (push) Waiting to run

This commit is contained in:
Matthew Rich 2024-09-19 07:57:26 +00:00
parent a38bd8a4d7
commit 6e0049c4d2
28 changed files with 1002 additions and 70 deletions

View File

@ -7,6 +7,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"net/url"
"strings" "strings"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"decl/internal/codec" "decl/internal/codec"
@ -95,6 +96,15 @@ func (b *Block) NewConfiguration(uri *string) (err error) {
return return
} }
func (b *Block) NewConfigurationFromParsedURI(uri *url.URL) (err error) {
if uri == nil {
b.Values, err = b.ConfigurationTypes.New(fmt.Sprintf("%s://", b.Type))
} else {
b.Values, err = b.ConfigurationTypes.NewFromParsedURI(uri)
}
return
}
func (b *Block) GetValue(key string) (any, error) { func (b *Block) GetValue(key string) (any, error) {
return b.Values.GetValue(key) return b.Values.GetValue(key)
} }
@ -125,6 +135,20 @@ func (b *Block) SetURI(uri string) (err error) {
return return
} }
func (b *Block) SetParsedURI(uri *url.URL) (err error) {
if b.Values == nil {
if err = b.NewConfigurationFromParsedURI(uri); err != nil {
return
}
}
if b.Values == nil {
return fmt.Errorf("%w - %s", data.ErrUnknownConfigurationType, uri)
}
b.Type = TypeName(b.Values.Type())
_,err = b.Values.Read(context.Background())
return
}
func (b *Block) UnmarshalValue(value *BlockType) error { func (b *Block) UnmarshalValue(value *BlockType) error {
if b.ConfigurationTypes == nil { if b.ConfigurationTypes == nil {
panic(fmt.Errorf("Undefined type registry: unable to create new configuration %s", value.Type)) panic(fmt.Errorf("Undefined type registry: unable to create new configuration %s", value.Type))
@ -145,6 +169,9 @@ func (b *Block) UnmarshalValue(value *BlockType) error {
} }
func (b *Block) UnmarshalYAML(value *yaml.Node) error { func (b *Block) UnmarshalYAML(value *yaml.Node) error {
if b.ConfigurationTypes == nil {
b.ConfigurationTypes = DocumentRegistry.ConfigurationTypes
}
t := &BlockType{} t := &BlockType{}
if unmarshalConfigurationTypeErr := value.Decode(t); unmarshalConfigurationTypeErr != nil { if unmarshalConfigurationTypeErr := value.Decode(t); unmarshalConfigurationTypeErr != nil {
return unmarshalConfigurationTypeErr return unmarshalConfigurationTypeErr
@ -168,6 +195,9 @@ func (b *Block) UnmarshalYAML(value *yaml.Node) error {
} }
func (b *Block) UnmarshalJSON(jsonData []byte) error { func (b *Block) UnmarshalJSON(jsonData []byte) error {
if b.ConfigurationTypes == nil {
b.ConfigurationTypes = DocumentRegistry.ConfigurationTypes
}
t := &BlockType{} t := &BlockType{}
if unmarshalConfigurationTypeErr := json.Unmarshal(jsonData, t); unmarshalConfigurationTypeErr != nil { if unmarshalConfigurationTypeErr := json.Unmarshal(jsonData, t); unmarshalConfigurationTypeErr != nil {
return unmarshalConfigurationTypeErr return unmarshalConfigurationTypeErr
@ -186,7 +216,3 @@ func (b *Block) UnmarshalJSON(jsonData []byte) error {
_, readErr := b.Values.Read(context.Background()) _, readErr := b.Values.Read(context.Background())
return readErr return readErr
} }
func (b *Block) MarshalYAML() (any, error) {
return b, nil
}

View File

@ -5,27 +5,14 @@ package folio
import ( import (
_ "fmt" _ "fmt"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"log"
"os"
"testing" "testing"
"strings" "strings"
"decl/internal/data"
"decl/internal/codec"
"io"
"log/slog"
) )
var TempDir string
func TestMain(m *testing.M) {
var err error
TempDir, err = os.MkdirTemp("", "testconfig")
if err != nil || TempDir == "" {
log.Fatal(err)
}
rc := m.Run()
os.RemoveAll(TempDir)
os.Exit(rc)
}
func TestNewBlock(t *testing.T) { func TestNewBlock(t *testing.T) {
configYaml := ` configYaml := `
name: "foo" name: "foo"
@ -36,14 +23,21 @@ values:
docReader := strings.NewReader(configYaml) docReader := strings.NewReader(configYaml)
block := NewBlock() block := NewBlock()
block.ConfigurationTypes = TestConfigurationTypes
slog.Info("TestNewBlock()", "block", block, "types", block.ConfigurationTypes)
assert.NotNil(t, block) assert.NotNil(t, block)
assert.Nil(t, block.Load(docReader)) assert.Nil(t, block.LoadReader(io.NopCloser(docReader), codec.FormatYaml))
assert.Equal(t, "foo", block.Name) assert.Equal(t, "foo", block.Name)
block.Values.(*MockQuuz).InjectGetValue = func(key string) (any, error) { return "test", nil }
val, err := block.GetValue("http_user") val, err := block.GetValue("http_user")
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, "test", val) assert.Equal(t, "test", val)
block.Values.(*MockQuuz).InjectGetValue = func(key string) (any, error) { return nil, data.ErrUnknownConfigurationKey }
missingVal, missingErr := block.GetValue("content") missingVal, missingErr := block.GetValue("content")
assert.ErrorIs(t, missingErr, ErrUnknownConfigurationKey) assert.ErrorIs(t, missingErr, data.ErrUnknownConfigurationKey)
assert.Nil(t, missingVal) assert.Nil(t, missingVal)
} }

View File

@ -0,0 +1,83 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package folio
import (
"decl/internal/data"
"errors"
"log/slog"
)
// Dependencies describe a requirement on a given system property
// system properties:
// loaded from config
//
// deps assigned to decl
// match values in document configurations
// match values in all configurations?
//
// documents/facter.jx.yaml -> facts -> Get(key)
//
// ConfigMapper? -> DocumentRegistry
// system:
// arch: amd64
// foo: bar
var (
ErrConstraintFailure = errors.New("Constraint failure")
)
type Constraint map[string]any
//func Compare[aV, bV map[K]V, K, V comparable](a aV, b bV) (matched bool) {
func CompareMap(a, b any) (matched bool) {
for k,v := range a.(Constraint) {
if bv, exists := b.(map[string]any)[k]; exists && bv == v {
matched = true
} else {
matched = false
}
}
return
}
func (c Constraint) CompareValues(a, b any) (matched bool) {
matched = true
slog.Info("Constraint.CompareValues()", "a", a, "b", b)
switch btype := b.(type) {
case map[string]string:
return CompareMap(a, b)
case map[string]any:
return CompareMap(a, b)
case []string:
acmp := a.([]string)
if len(acmp) > len(btype) {
return false
}
for i,v := range acmp {
if v != btype[i] {
return false
}
}
default:
return a == b
}
return
}
func (c Constraint) Check(configs data.ConfigurationValueGetter) (matched bool) {
matched = true
for k,v := range c {
slog.Info("Constraint.Check()", "constraint", c, "config", configs)
if configValue, err := configs.GetValue(k); err != nil || ! c.CompareValues(v, configValue) {
slog.Info("Constraint.Check() - FAILURE", "configvalue", configValue, "constraint", v, "error", err)
matched = false
}
}
return
}

View File

@ -15,6 +15,8 @@ _ "gitea.rosskeen.house/rosskeen.house/machine"
"decl/internal/codec" "decl/internal/codec"
"decl/internal/data" "decl/internal/data"
"decl/internal/schema" "decl/internal/schema"
"net/url"
"runtime/debug"
) )
type ConfigName string type ConfigName string
@ -23,6 +25,9 @@ type DeclarationType struct {
Type TypeName `json:"type" yaml:"type"` Type TypeName `json:"type" yaml:"type"`
Transition string `json:"transition,omitempty" yaml:"transition,omitempty"` Transition string `json:"transition,omitempty" yaml:"transition,omitempty"`
Config ConfigName `json:"config,omitempty" yaml:"config,omitempty"` Config ConfigName `json:"config,omitempty" yaml:"config,omitempty"`
OnError OnError `json:"onerror,omitempty" yaml:"onerror,omitempty"`
Error string `json:"error,omitempty" yaml:"error,omitempty"`
Requires Dependencies `json:"requires,omitempty" yaml:"requires,omitempty"`
} }
type Declaration struct { type Declaration struct {
@ -30,6 +35,9 @@ type Declaration struct {
Transition string `json:"transition,omitempty" yaml:"transition,omitempty"` Transition string `json:"transition,omitempty" yaml:"transition,omitempty"`
Attributes data.Resource `json:"attributes" yaml:"attributes"` Attributes data.Resource `json:"attributes" yaml:"attributes"`
Config ConfigName `json:"config,omitempty" yaml:"config,omitempty"` Config ConfigName `json:"config,omitempty" yaml:"config,omitempty"`
OnError OnError `json:"onerror,omitempty" yaml:"onerror,omitempty"`
Error string `json:"error,omitempty" yaml:"error,omitempty"`
Requires Dependencies `json:"requires,omitempty" yaml:"requires,omitempty"`
// runtime luaruntime.LuaRunner // runtime luaruntime.LuaRunner
document *Document document *Document
configBlock data.Block configBlock data.Block
@ -69,10 +77,11 @@ func (d *Declaration) ResolveId(ctx context.Context) string {
func (d *Declaration) Clone() data.Declaration { func (d *Declaration) Clone() data.Declaration {
return &Declaration { return &Declaration {
Type: d.Type, Type: d.Type,
Transition: d.Transition, Transition: d.Transition,
Attributes: d.Attributes.Clone(), Attributes: d.Attributes.Clone(),
//runtime: luaruntime.New(), //runtime: luaruntime.New(),
Config: d.Config, Config: d.Config,
Requires: d.Requires,
} }
} }
@ -112,6 +121,17 @@ func (d *Declaration) Validate() (err error) {
return err return err
} }
func (d *Declaration) NewResourceFromParsedURI(u *url.URL) (err error) {
if u == nil {
d.Attributes, err = d.ResourceTypes.NewFromParsedURI(&url.URL{ Scheme: string(d.Type) })
} else {
if d.Attributes, err = d.ResourceTypes.NewFromParsedURI(u); err == nil {
err = d.Attributes.SetParsedURI(u)
}
}
return
}
func (d *Declaration) NewResource(uri *string) (err error) { func (d *Declaration) NewResource(uri *string) (err error) {
if d.ResourceTypes == nil { if d.ResourceTypes == nil {
panic(fmt.Errorf("Undefined type registry: unable to create new resource %s", *uri)) panic(fmt.Errorf("Undefined type registry: unable to create new resource %s", *uri))
@ -119,7 +139,9 @@ func (d *Declaration) NewResource(uri *string) (err error) {
if uri == nil { if uri == nil {
d.Attributes, err = d.ResourceTypes.New(fmt.Sprintf("%s://", d.Type)) d.Attributes, err = d.ResourceTypes.New(fmt.Sprintf("%s://", d.Type))
} else { } else {
d.Attributes, err = d.ResourceTypes.New(*uri) if d.Attributes, err = d.ResourceTypes.New(*uri); err == nil {
err = d.Attributes.SetURI(*uri)
}
} }
return return
} }
@ -131,8 +153,12 @@ func (d *Declaration) Resource() data.Resource {
func (d *Declaration) Apply() (result error) { func (d *Declaration) Apply() (result error) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
slog.Info("Declaration.Apply()", "error", r, "stacktrace", string(debug.Stack()))
result = fmt.Errorf("%s", r) result = fmt.Errorf("%s", r)
} }
if result != nil {
d.Error = result.Error()
}
}() }()
stater := d.Attributes.StateMachine() stater := d.Attributes.StateMachine()
@ -158,10 +184,17 @@ func (d *Declaration) Apply() (result error) {
case "create", "present": case "create", "present":
if stater.CurrentState() == "absent" || stater.CurrentState() == "unknown" { if stater.CurrentState() == "absent" || stater.CurrentState() == "unknown" {
if result = stater.Trigger("create"); result != nil { if result = stater.Trigger("create"); result != nil {
slog.Info("Declaration.Apply()", "trigger", "create", "state", stater.CurrentState(), "error", result, "declaration", d)
return result return result
} }
} }
result = stater.Trigger("read") result = stater.Trigger("read")
currentState := stater.CurrentState()
switch currentState {
case "create", "present":
default:
return fmt.Errorf("Failed to create resource: %s - state: %s, err: %s", d.URI(), currentState, result)
}
} }
return result return result
} }
@ -179,9 +212,12 @@ func (d *Declaration) SetConfig(configDoc data.Document) {
func (d *Declaration) SetURI(uri string) (err error) { func (d *Declaration) SetURI(uri string) (err error) {
slog.Info("Declaration.SetURI()", "uri", uri, "declaration", d) slog.Info("Declaration.SetURI()", "uri", uri, "declaration", d)
if d.Attributes == nil { if d.Attributes == nil {
if err = d.NewResource(&uri); err != nil { err = d.NewResource(&uri)
return } else {
} err = d.Attributes.SetURI(uri)
}
if err != nil {
return err
} }
if d.Attributes == nil { if d.Attributes == nil {
return fmt.Errorf("%w: %s", ErrUnknownResourceType, uri) return fmt.Errorf("%w: %s", ErrUnknownResourceType, uri)
@ -192,6 +228,24 @@ func (d *Declaration) SetURI(uri string) (err error) {
return return
} }
func (d *Declaration) SetParsedURI(uri *url.URL) (err error) {
slog.Info("Declaration.SetParsedURI()", "uri", uri, "declaration", d)
if d.Attributes == nil {
err = d.NewResourceFromParsedURI(uri)
} else {
err = d.Attributes.SetParsedURI(uri)
}
if err != nil {
return err
}
if d.Attributes == nil {
return fmt.Errorf("%w: %s", ErrUnknownResourceType, uri)
}
d.Type = TypeName(d.Attributes.Type())
_, err = d.Attributes.Read(context.Background()) // fix context
slog.Info("Declaration.SetURI() - read", "error", err)
return
}
func (d *Declaration) UnmarshalValue(value *DeclarationType) error { func (d *Declaration) UnmarshalValue(value *DeclarationType) error {
slog.Info("Declaration.UnmarshalValue", "declaration", d, "value", value, "addr", d) slog.Info("Declaration.UnmarshalValue", "declaration", d, "value", value, "addr", d)
@ -201,6 +255,9 @@ func (d *Declaration) UnmarshalValue(value *DeclarationType) error {
d.Type = value.Type d.Type = value.Type
d.Transition = value.Transition d.Transition = value.Transition
d.Config = value.Config d.Config = value.Config
d.OnError = value.OnError
d.Error = value.Error
d.Requires = value.Requires
newResource, resourceErr := d.ResourceTypes.New(fmt.Sprintf("%s://", value.Type)) newResource, resourceErr := d.ResourceTypes.New(fmt.Sprintf("%s://", value.Type))
if resourceErr != nil { if resourceErr != nil {
slog.Info("Declaration.UnmarshalValue", "value", value, "error", resourceErr) slog.Info("Declaration.UnmarshalValue", "value", value, "error", resourceErr)

View File

@ -0,0 +1,45 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package folio
import (
"log/slog"
)
// Dependencies describe a requirement on a given system property
// system properties:
// loaded from config
//
// deps assigned to decl
// match values in document configurations
// match values in all configurations?
//
// documents/facter.jx.yaml -> facts -> Get(key)
//
// ConfigMapper? -> DocumentRegistry
// requires:
// arch: amd64
// foo: bar
type Dependencies map[string]Constraint
func (d Dependencies) Check() (matched bool) {
matched = true
for k,v := range d {
b, z := DocumentRegistry.ConfigNameMap.Get(k)
slog.Info("Dependencies.Check()", "key", k, "value", v, "block", b, "ok", z)
if configBlock, ok := DocumentRegistry.ConfigNameMap.Get(k); ok {
if ! v.Check(configBlock) {
matched = false
}
} else {
matched = false
return
}
}
return
}

View File

@ -9,7 +9,7 @@ import (
"io" "io"
"io/fs" "io/fs"
"log/slog" "log/slog"
_ "net/url" "net/url"
"github.com/sters/yaml-diff/yamldiff" "github.com/sters/yaml-diff/yamldiff"
"strings" "strings"
"decl/internal/codec" "decl/internal/codec"
@ -24,18 +24,21 @@ type DocumentType struct {
Schema URI `json:"schema,omitempty" yaml:"schema,omitempty"` Schema URI `json:"schema,omitempty" yaml:"schema,omitempty"`
URI URI `json:"source,omitempty" yaml:"source,omitempty"` URI URI `json:"source,omitempty" yaml:"source,omitempty"`
Format codec.Format `json:"format,omitempty" yaml:"format,omitempty"` Format codec.Format `json:"format,omitempty" yaml:"format,omitempty"`
Requires Dependencies `json:"requires,omitempty" yaml:"requires,omitempty"`
} }
type Document struct { type Document struct {
Schema URI `json:"schema,omitempty" yaml:"schema,omitempty"` Schema URI `json:"schema,omitempty" yaml:"schema,omitempty"`
URI URI `json:"source,omitempty" yaml:"source,omitempty"` URI URI `json:"source,omitempty" yaml:"source,omitempty"`
Format codec.Format `json:"format,omitempty" yaml:"format,omitempty"` Format codec.Format `json:"format,omitempty" yaml:"format,omitempty"`
Requires Dependencies `json:"requires,omitempty" yaml:"requires,omitempty"`
uris mapper.Store[string, data.Declaration] uris mapper.Store[string, data.Declaration]
ResourceDeclarations []*Declaration `json:"resources" yaml:"resources"` ResourceDeclarations []*Declaration `json:"resources,omitempty" yaml:"resources,omitempty"`
configNames mapper.Store[string, data.Block] configNames mapper.Store[string, data.Block] `json:"-" yaml:"-"`
Configurations []*Block `json:"configurations,omitempty" yaml:"configurations,omitempty"` Configurations []*Block `json:"configurations,omitempty" yaml:"configurations,omitempty"`
config data.Document config data.Document
Registry *Registry `json:"-" yaml:"-"` Registry *Registry `json:"-" yaml:"-"`
failedResources int `json:"-" yaml:"-"`
} }
func NewDocument(r *Registry) *Document { func NewDocument(r *Registry) *Document {
@ -90,7 +93,11 @@ func (d *Document) GetResource(uri string) *Declaration {
return nil return nil
} }
func (d *Document) Clone() *Document { func (d *Document) Failures() int {
return d.failedResources
}
func (d *Document) Clone() data.Document {
clone := NewDocument(d.Registry) clone := NewDocument(d.Registry)
clone.config = d.config clone.config = d.config
@ -105,6 +112,7 @@ func (d *Document) Clone() *Document {
clone.ResourceDeclarations[i].SetDocument(clone) clone.ResourceDeclarations[i].SetDocument(clone)
clone.ResourceDeclarations[i].SetConfig(d.config) clone.ResourceDeclarations[i].SetConfig(d.config)
} }
clone.Requires = d.Requires
return clone return clone
} }
@ -122,6 +130,22 @@ func (d *Document) assignResourcesDocument() {
} }
} }
func (d *Document) assignConfigurationsDocument() {
slog.Info("Document.assignConfigurationsDocument()", "configurations", d.Configurations, "len", len(d.Configurations))
for i := range d.Configurations {
if d.Configurations[i] == nil {
d.Configurations[i] = NewBlock()
}
slog.Info("Document.assignConfigurationsDocument()", "configuration", d.Configurations[i])
//d.Configurations[i].SetDocument(d)
slog.Info("Document.assignConfigurationsDocument()", "configuration", d.Configurations[i])
//d.MapConfigurationURI(d.Configurations[i].URI(), d.Configurations[i])
d.configNames[d.Configurations[i].Name] = d.Configurations[i]
d.Registry.ConfigurationMap[d.Configurations[i]] = d
d.Registry.ConfigNameMap[d.Configurations[i].Name] = d.Configurations[i]
}
}
func (d *Document) LoadString(docData string, f codec.Format) (err error) { func (d *Document) LoadString(docData string, f codec.Format) (err error) {
err = f.StringDecoder(docData).Decode(d) err = f.StringDecoder(docData).Decode(d)
return return
@ -148,7 +172,7 @@ func (d *Document) GetSchemaFiles() (schemaFs fs.FS) {
func (d *Document) Validate() error { func (d *Document) Validate() error {
jsonDocument, jsonErr := d.JSON() jsonDocument, jsonErr := d.JSON()
slog.Info("document.Validate() json", "json", jsonDocument, "err", jsonErr) slog.Info("document.Validate() json", "err", jsonErr)
if jsonErr == nil { if jsonErr == nil {
s := schema.New("document", d.GetSchemaFiles()) s := schema.New("document", d.GetSchemaFiles())
err := s.Validate(string(jsonDocument)) err := s.Validate(string(jsonDocument))
@ -189,6 +213,10 @@ func (d *Document) Len() int {
return len(d.ResourceDeclarations) return len(d.ResourceDeclarations)
} }
func (d *Document) CheckConstraints() bool {
return d.Requires.Check()
}
func (d *Document) ResolveIds(ctx context.Context) { func (d *Document) ResolveIds(ctx context.Context) {
for i := range d.ResourceDeclarations { for i := range d.ResourceDeclarations {
d.ResourceDeclarations[i].ResolveId(ctx) d.ResourceDeclarations[i].ResolveId(ctx)
@ -213,9 +241,24 @@ func (d *Document) Apply(state string) error {
d.ResourceDeclarations[idx].Transition = state d.ResourceDeclarations[idx].Transition = state
} }
d.ResourceDeclarations[idx].SetConfig(d.config) d.ResourceDeclarations[idx].SetConfig(d.config)
if e := d.ResourceDeclarations[idx].Apply(); e != nil {
slog.Error("Document.Apply() error applying resource", "index", idx, "uri", d.ResourceDeclarations[idx].Resource().URI(), "resource", d.ResourceDeclarations[idx].Resource(), "error", e) slog.Info("Document.Apply() applying resource", "index", idx, "uri", d.ResourceDeclarations[idx].Resource().URI(), "resource", d.ResourceDeclarations[idx].Resource())
return e
if d.ResourceDeclarations[idx].Requires.Check() {
if e := d.ResourceDeclarations[idx].Apply(); e != nil {
slog.Error("Document.Apply() error applying resource", "index", idx, "uri", d.ResourceDeclarations[idx].Resource().URI())
d.ResourceDeclarations[idx].Error = e.Error()
switch d.ResourceDeclarations[idx].OnError.GetStrategy() {
case OnErrorStop:
return e
case OnErrorFail:
d.failedResources++
}
}
} else {
d.ResourceDeclarations[idx].Error = fmt.Sprintf("Constraint failure: %s", d.ResourceDeclarations[idx].Requires)
} }
if i >= len(d.ResourceDeclarations) - 1 { if i >= len(d.ResourceDeclarations) - 1 {
break break
@ -240,6 +283,12 @@ func (d *Document) Generate(w io.Writer) (err error) {
return return
} }
/*
func (d *Document) MapConfigurationURI(uri string, block data.Block) {
d.configUris[uri] = block
}
*/
func (d *Document) MapResourceURI(uri string, declaration data.Declaration) { func (d *Document) MapResourceURI(uri string, declaration data.Declaration) {
d.uris[uri] = declaration d.uris[uri] = declaration
} }
@ -247,39 +296,90 @@ func (d *Document) MapResourceURI(uri string, declaration data.Declaration) {
func (d *Document) AddDeclaration(declaration data.Declaration) { func (d *Document) AddDeclaration(declaration data.Declaration) {
uri := declaration.URI() uri := declaration.URI()
decl := declaration.(*Declaration) decl := declaration.(*Declaration)
d.ResourceDeclarations = append(d.ResourceDeclarations, decl) if decl.Requires.Check() {
d.MapResourceURI(uri, declaration)
decl.SetDocument(d) d.ResourceDeclarations = append(d.ResourceDeclarations, decl)
d.Registry.DeclarationMap[decl] = d
d.MapResourceURI(uri, declaration)
decl.SetDocument(d)
d.Registry.DeclarationMap[decl] = d
}
} }
func (d *Document) AddResourceDeclaration(resourceType string, resourceDeclaration data.Resource) { func (d *Document) AddResourceDeclaration(resourceType string, resourceDeclaration data.Resource) {
slog.Info("Document.AddResourceDeclaration()", "type", resourceType, "resource", resourceDeclaration) slog.Info("Document.AddResourceDeclaration()", "type", resourceType, "resource", resourceDeclaration)
decl := NewDeclarationFromDocument(d) decl := NewDeclarationFromDocument(d)
decl.Type = TypeName(resourceType) decl.Type = TypeName(resourceType)
decl.Attributes = resourceDeclaration if decl.Requires.Check() {
d.ResourceDeclarations = append(d.ResourceDeclarations, decl) decl.Attributes = resourceDeclaration
d.MapResourceURI(decl.Attributes.URI(), decl) d.ResourceDeclarations = append(d.ResourceDeclarations, decl)
decl.SetDocument(d) d.MapResourceURI(decl.Attributes.URI(), decl)
d.Registry.DeclarationMap[decl] = d decl.SetDocument(d)
d.Registry.DeclarationMap[decl] = d
}
} }
// XXX NewResource is not commonly used by the underlying resource Read() is no longer called so it needs more testing // XXX NewResource is not commonly used by the underlying resource Read() is no longer called so it needs more testing
func (d *Document) NewResource(uri string) (newResource data.Resource, err error) { func (d *Document) NewResource(uri string) (newResource data.Resource, err error) {
decl := NewDeclarationFromDocument(d) decl := NewDeclarationFromDocument(d)
if err = decl.NewResource(&uri); err != nil { if err = decl.NewResource(&uri); err != nil {
return return
} }
if decl.Attributes == nil { if decl.Attributes == nil {
err = fmt.Errorf("%w: %s", ErrUnknownResourceType, uri) err = fmt.Errorf("%w: %s", ErrUnknownResourceType, uri)
return return
} }
decl.Type = TypeName(decl.Attributes.Type()) decl.Type = TypeName(decl.Attributes.Type())
d.ResourceDeclarations = append(d.ResourceDeclarations, decl)
d.MapResourceURI(decl.Attributes.URI(), decl) if decl.Requires.Check() {
decl.SetDocument(d) slog.Info("Document.NewResource()", "type", decl.Type, "declaration", decl)
d.Registry.DeclarationMap[decl] = d
newResource = decl.Attributes d.ResourceDeclarations = append(d.ResourceDeclarations, decl)
d.MapResourceURI(decl.Attributes.URI(), decl)
decl.SetDocument(d)
d.Registry.DeclarationMap[decl] = d
newResource = decl.Attributes
} else {
err = fmt.Errorf("%w: %s", ErrConstraintFailure, uri)
}
return
}
func (d *Document) NewResourceFromURI(uri URI) (newResource data.Resource, err error) {
return d.NewResourceFromParsedURI(uri.Parse())
}
func (d *Document) NewResourceFromParsedURI(uri *url.URL) (newResource data.Resource, err error) {
if uri == nil {
return nil, fmt.Errorf("%w: %s", ErrUnknownResourceType, uri)
}
decl := NewDeclarationFromDocument(d)
if err = decl.NewResourceFromParsedURI(uri); err != nil {
return
}
if decl.Attributes == nil {
err = fmt.Errorf("%w: %s", ErrUnknownResourceType, uri)
return
}
decl.Type = TypeName(decl.Attributes.Type())
if decl.Requires.Check() {
d.ResourceDeclarations = append(d.ResourceDeclarations, decl)
d.MapResourceURI(decl.Attributes.URI(), decl)
decl.SetDocument(d)
d.Registry.DeclarationMap[decl] = d
newResource = decl.Attributes
} else {
err = fmt.Errorf("%w: %s", ErrConstraintFailure, uri)
}
return return
} }
@ -288,10 +388,15 @@ func (d *Document) AddResource(uri string) error {
if e := decl.SetURI(uri); e != nil { if e := decl.SetURI(uri); e != nil {
return e return e
} }
d.ResourceDeclarations = append(d.ResourceDeclarations, decl)
d.MapResourceURI(decl.Attributes.URI(), decl) if decl.Requires.Check() {
decl.SetDocument(d) d.ResourceDeclarations = append(d.ResourceDeclarations, decl)
d.Registry.DeclarationMap[decl] = d d.MapResourceURI(decl.Attributes.URI(), decl)
decl.SetDocument(d)
d.Registry.DeclarationMap[decl] = d
} else {
return fmt.Errorf("%w: %s", ErrConstraintFailure, uri)
}
return nil return nil
} }
@ -340,21 +445,19 @@ func (d *Document) HasConfig(name string) bool {
return ok return ok
} }
func (d *Document) GetConfig(name string) *Block { func (d *Document) GetConfig(name string) data.Block {
return d.configNames[name].(*Block) return d.configNames[name]
} }
func (d *Document) AppendConfigurations(docs []data.Document) { func (d *Document) AppendConfigurations(docs []data.Document) {
if docs != nil { for _, doc := range docs {
for _, doc := range docs { for _, config := range doc.(*Document).Configurations {
for _, config := range doc.(*Document).Configurations { d.AddConfigurationBlock(config.Name, config.Type, config.Values)
d.AddConfigurationBlock(config.Name, config.Type, config.Values)
}
} }
} }
} }
func (d *Document) Diff(with *Document, output io.Writer) (returnOutput string, diffErr error) { func (d *Document) Diff(with data.Document, output io.Writer) (returnOutput string, diffErr error) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
returnOutput = "" returnOutput = ""
@ -408,6 +511,7 @@ func (d *Document) UnmarshalYAML(value *yaml.Node) error {
if unmarshalResourcesErr := value.Decode((*decodeDocument)(d)); unmarshalResourcesErr != nil { if unmarshalResourcesErr := value.Decode((*decodeDocument)(d)); unmarshalResourcesErr != nil {
return unmarshalResourcesErr return unmarshalResourcesErr
} }
d.assignConfigurationsDocument()
d.assignResourcesDocument() d.assignResourcesDocument()
return nil return nil
} }
@ -418,6 +522,7 @@ func (d *Document) UnmarshalJSON(data []byte) error {
if unmarshalDocumentErr := json.Unmarshal(data, t); unmarshalDocumentErr != nil { if unmarshalDocumentErr := json.Unmarshal(data, t); unmarshalDocumentErr != nil {
return unmarshalDocumentErr return unmarshalDocumentErr
} }
d.assignConfigurationsDocument()
d.assignResourcesDocument() d.assignResourcesDocument()
return nil return nil
} }

View File

@ -17,6 +17,7 @@ import (
var ( var (
TestResourceTypes *types.Types[data.Resource] = types.New[data.Resource]() TestResourceTypes *types.Types[data.Resource] = types.New[data.Resource]()
TestConfigurationTypes *types.Types[data.Configuration] = types.New[data.Configuration]()
) )
func TestNewDocument(t *testing.T) { func TestNewDocument(t *testing.T) {

View File

@ -28,6 +28,7 @@ func TestMain(m *testing.M) {
ProcessTestUserName, ProcessTestGroupName = ProcessUserName() ProcessTestUserName, ProcessTestGroupName = ProcessUserName()
RegisterMocks() RegisterMocks()
RegisterConfigurationMocks()
rc := m.Run() rc := m.Run()

View File

@ -0,0 +1,106 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package folio
import (
"context"
_ "gopkg.in/yaml.v3"
"encoding/json"
_ "fmt"
"decl/internal/data"
"decl/internal/codec"
"io"
"net/url"
)
type MockConfiguration struct {
InjectURI func() string `json:"-" yaml:"-"`
InjectType func() string `json:"-" yaml:"-"`
InjectResolveId func(ctx context.Context) string `json:"-" yaml:"-"`
InjectValidate func() error `json:"-" yaml:"-"`
InjectJSON func() ([]byte, error) `json:"-" yaml:"-"`
InjectYAML func() ([]byte, error) `json:"-" yaml:"-"`
InjectPB func() ([]byte, error) `json:"-" yaml:"-"`
InjectGenerate func(w io.Writer) (error) `json:"-" yaml:"-"`
InjectLoadString func(string, codec.Format) (error) `json:"-" yaml:"-"`
InjectLoad func([]byte, codec.Format) (error) `json:"-" yaml:"-"`
InjectLoadReader func(io.ReadCloser, codec.Format) (error) `json:"-" yaml:"-"`
InjectRead func(context.Context) ([]byte, error) `json:"-" yaml:"-"`
InjectGetValue func(key string) (any, error) `json:"-" yaml:"-"`
InjectHas func(key string) (bool) `json:"-" yaml:"-"`
}
func (m *MockConfiguration) Clone() data.Configuration {
return nil
}
func (m *MockConfiguration) SetURI(uri string) error {
return nil
}
func (m *MockConfiguration) SetParsedURI(uri *url.URL) error {
return nil
}
func (m *MockConfiguration) URI() string {
return m.InjectURI()
}
func (m *MockConfiguration) ResolveId(ctx context.Context) string {
return m.InjectResolveId(ctx)
}
func (m *MockConfiguration) JSON() ([]byte, error) {
return m.InjectJSON()
}
func (m *MockConfiguration) YAML() ([]byte, error) {
return m.InjectYAML()
}
func (m *MockConfiguration) PB() ([]byte, error) {
return m.InjectPB()
}
func (m *MockConfiguration) Generate(w io.Writer) (error) {
return m.InjectGenerate(w)
}
func (m *MockConfiguration) LoadString(docData string, format codec.Format) (error) {
return m.InjectLoadString(docData, format)
}
func (m *MockConfiguration) Load(docData []byte, format codec.Format) (error) {
return m.InjectLoad(docData, format)
}
func (m *MockConfiguration) LoadReader(r io.ReadCloser, format codec.Format) (error) {
return m.InjectLoadReader(r, format)
}
func (m *MockConfiguration) Read(ctx context.Context) ([]byte, error) {
return m.InjectRead(ctx)
}
func (m *MockConfiguration) Validate() error {
return m.InjectValidate()
}
func (m *MockConfiguration) Type() string {
return m.InjectType()
}
func (m *MockConfiguration) UnmarshalJSON(data []byte) error {
if err := json.Unmarshal(data, m); err != nil {
return err
}
return nil
}
func (m *MockConfiguration) GetValue(key string) (any, error) {
return m.InjectGetValue(key)
}
func (m *MockConfiguration) Has(key string) (bool) {
return m.InjectHas(key)
}

View File

@ -0,0 +1,48 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package folio
import (
"context"
_ "gopkg.in/yaml.v3"
"decl/internal/codec"
"decl/internal/data"
"io"
"net/url"
"path/filepath"
"fmt"
)
func RegisterConfigurationMocks() {
TestConfigurationTypes.Register([]string{"generic"}, func(u *url.URL) data.Configuration {
q := NewQuuzConfiguration()
q.Name = filepath.Join(u.Hostname(), u.Path)
return q
})
}
type MockQuuz struct {
*MockConfiguration `json:"-" yaml:"-"`
Name string `json:"name" yaml:"name"`
Value string `json:"value" yaml:"value"`
}
func NewMockConfiguration(typename string) *MockConfiguration {
return &MockConfiguration {
InjectType: func() string { return typename },
InjectResolveId: func(ctx context.Context) string { return "bar" },
InjectLoadString: func(string, codec.Format) (error) { return nil },
InjectLoad: func([]byte, codec.Format) (error) { return nil },
InjectLoadReader: func(io.ReadCloser, codec.Format) (error) { return nil },
InjectValidate: func() error { return nil },
InjectRead: func(context.Context) ([]byte, error) { return nil, nil },
InjectGetValue: func(key string) (any, error) { return nil, nil },
InjectURI: func() string { return fmt.Sprintf("%s://bar", typename) },
}
}
func NewQuuzConfiguration() *MockQuuz {
q := &MockQuuz {}
q.MockConfiguration = NewMockConfiguration("generic")
return q
}

View File

@ -11,6 +11,7 @@ _ "fmt"
"decl/internal/data" "decl/internal/data"
"decl/internal/codec" "decl/internal/codec"
"io" "io"
"net/url"
) )
type MockResource struct { type MockResource struct {
@ -53,6 +54,10 @@ func (m *MockResource) SetURI(uri string) error {
return nil return nil
} }
func (m *MockResource) SetParsedURI(uri *url.URL) error {
return nil
}
func (m *MockResource) URI() string { func (m *MockResource) URI() string {
return m.InjectURI() return m.InjectURI()
} }

76
internal/folio/onerror.go Normal file
View File

@ -0,0 +1,76 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package folio
import (
"errors"
"encoding/json"
"gopkg.in/yaml.v3"
"log/slog"
)
var (
ErrInvalidOnErrorStrategy = errors.New("Invalid OnError strategy")
)
type OnError string
const (
OnErrorStop = "stop"
OnErrorFail = "fail" // set error value and return the error
OnErrorSkip = "skip" // set error value and continue processing resources
)
func NewOnError() OnError {
return OnErrorFail
}
func (o OnError) Strategy() string {
switch o {
case OnErrorStop, OnErrorFail, OnErrorSkip:
return string(o)
}
return ""
}
func (o OnError) GetStrategy() OnError {
switch o {
case OnErrorStop, OnErrorFail, OnErrorSkip:
return o
default:
slog.Warn("OnError.GetStrategy() - invalid value, defaulting to OnErrorFail", "strategy", o)
return OnErrorFail
}
}
func (o OnError) Validate() error {
switch o {
case OnErrorStop, OnErrorFail, OnErrorSkip:
return nil
default:
return ErrInvalidOnErrorStrategy
}
}
func (o *OnError) UnmarshalValue(value string) (err error) {
if err = OnError(value).Validate(); err == nil {
*o = OnError(value)
}
return
}
func (o *OnError) UnmarshalJSON(jsonData []byte) error {
var s string
if unmarshalErr := json.Unmarshal(jsonData, &s); unmarshalErr != nil {
return unmarshalErr
}
return o.UnmarshalValue(s)
}
func (o *OnError) UnmarshalYAML(value *yaml.Node) error {
var s string
if err := value.Decode(&s); err != nil {
return err
}
return o.UnmarshalValue(s)
}

View File

@ -0,0 +1,21 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package folio
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestOnErrorStrategies(t *testing.T) {
for _, v := range []struct{ strategy OnError; expected OnError; validate error }{
{ strategy: OnErrorFail, expected: "fail", validate: nil },
{ strategy: OnErrorSkip, expected: "skip", validate: nil },
{ strategy: OnError("unknown"), expected: "", validate: ErrInvalidOnErrorStrategy },
}{
o := v.strategy
assert.Equal(t, v.expected, OnError(o.Strategy()))
assert.ErrorIs(t, o.Validate(), v.validate)
}
}

View File

@ -5,12 +5,13 @@ package folio
import ( import (
_ "errors" _ "errors"
_ "fmt" _ "fmt"
_ "net/url" "net/url"
_ "strings" _ "strings"
"decl/internal/types" "decl/internal/types"
"decl/internal/data" "decl/internal/data"
"decl/internal/mapper" "decl/internal/mapper"
"io/fs" "io/fs"
"log/slog"
) )
var ( var (
@ -24,6 +25,7 @@ type Registry struct {
Documents []*Document Documents []*Document
UriMap mapper.Store[URI, *Document] UriMap mapper.Store[URI, *Document]
DeclarationMap mapper.Store[*Declaration, *Document] DeclarationMap mapper.Store[*Declaration, *Document]
ConfigNameMap mapper.Store[string, *Block]
ConfigurationMap mapper.Store[*Block, *Document] ConfigurationMap mapper.Store[*Block, *Document]
DefaultSchema URI DefaultSchema URI
} }
@ -36,6 +38,7 @@ func NewRegistry() *Registry {
Documents: make([]*Document, 0, 10), Documents: make([]*Document, 0, 10),
UriMap: mapper.New[URI, *Document](), UriMap: mapper.New[URI, *Document](),
DeclarationMap: mapper.New[*Declaration, *Document](), DeclarationMap: mapper.New[*Declaration, *Document](),
ConfigNameMap: mapper.New[string, *Block](),
ConfigurationMap: mapper.New[*Block, *Document](), ConfigurationMap: mapper.New[*Block, *Document](),
Schemas: mapper.New[URI, fs.FS](), Schemas: mapper.New[URI, fs.FS](),
DefaultSchema: schemaFilesUri, DefaultSchema: schemaFilesUri,
@ -52,8 +55,21 @@ func (r *Registry) Has(key *Declaration) (bool) {
return r.DeclarationMap.Has(key) return r.DeclarationMap.Has(key)
} }
func (r *Registry) HasDocument(key URI) bool {
return r.UriMap.Has(key)
}
func (r *Registry) GetDocument(key URI) (*Document, bool) {
return r.UriMap.Get(key)
}
func (r *Registry) SetDocument(key URI, value *Document) {
r.UriMap.Set(key, value)
}
func (r *Registry) NewDocument(uri URI) (doc *Document) { func (r *Registry) NewDocument(uri URI) (doc *Document) {
doc = NewDocument(r) doc = NewDocument(r)
doc.URI = uri
r.Documents = append(r.Documents, doc) r.Documents = append(r.Documents, doc)
if uri != "" { if uri != "" {
r.UriMap[uri] = doc r.UriMap[uri] = doc
@ -61,15 +77,64 @@ func (r *Registry) NewDocument(uri URI) (doc *Document) {
return return
} }
func (r *Registry) Load(uri URI) (documents []data.Document, err error) { func (r *Registry) AppendParsedURI(uri *url.URL, documents []data.Document) (addedDocuments []data.Document, err error) {
var extractor data.Converter var convertUri data.Converter
var sourceResource data.Resource var sourceResource data.Resource
if extractor, err = r.ConverterTypes.New(string(uri)); err == nil {
if sourceResource, err = uri.NewResource(nil); err == nil { slog.Info("folio.Registry.AppendParsedURI()", "uri", uri, "converter", r.ConverterTypes)
documents, err = extractor.(data.ManyExtractor).ExtractMany(sourceResource, nil) if convertUri, err = r.ConverterTypes.NewFromParsedURI(uri); err == nil {
if sourceResource, err = NewResourceFromParsedURI(uri, nil); err == nil {
switch extractor := convertUri.(type) {
case data.ManyExtractor:
var docs []data.Document
docs, err = extractor.ExtractMany(sourceResource, nil)
slog.Info("folio.Registry.Append() - ExtractMany", "uri", uri, "source", sourceResource, "docs", docs, "error", err)
documents = append(documents, docs...)
case data.Extractor:
var singleDocument data.Document
singleDocument, err = extractor.Extract(sourceResource, nil)
slog.Info("folio.Registry.Append() - Extract", "uri", uri, "source", sourceResource, "doc", singleDocument, "error", err)
documents = append(documents, singleDocument)
}
} }
} }
slog.Info("folio.Registry.Append()", "uri", uri, "converter", r.ConverterTypes, "error", err)
addedDocuments = documents
return return
} }
func (r *Registry) Append(uri URI, documents []data.Document) (addedDocuments []data.Document, err error) {
var convertUri data.Converter
var sourceResource data.Resource
slog.Info("folio.Registry.Append()", "uri", uri, "converter", r.ConverterTypes)
if convertUri, err = r.ConverterTypes.New(string(uri)); err == nil {
if sourceResource, err = uri.NewResource(nil); err == nil {
switch extractor := convertUri.(type) {
case data.ManyExtractor:
var docs []data.Document
docs, err = extractor.ExtractMany(sourceResource, nil)
slog.Info("folio.Registry.Append() - ExtractMany", "uri", uri, "source", sourceResource, "docs", docs, "error", err)
documents = append(documents, docs...)
case data.Extractor:
var singleDocument data.Document
singleDocument, err = extractor.Extract(sourceResource, nil)
slog.Info("folio.Registry.Append() - Extract", "uri", uri, "source", sourceResource, "doc", singleDocument, "error", err)
documents = append(documents, singleDocument)
}
}
}
slog.Info("folio.Registry.Append()", "uri", uri, "converter", r.ConverterTypes, "error", err)
addedDocuments = documents
return
}
func (r *Registry) Load(uri URI) (documents []data.Document, err error) {
documents = make([]data.Document, 0, 10)
return r.Append(uri, documents)
}
func (r *Registry) LoadFromURL(uri *url.URL) (documents []data.Document, err error) {
documents = make([]data.Document, 0, 10)
return r.AppendParsedURI(uri, documents)
}

View File

@ -0,0 +1,26 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package folio
import (
"net/url"
"decl/internal/data"
)
var (
)
/* Create a new resource using a parsed URI */
func NewResourceFromParsedURI(u *url.URL, document data.Document) (newResource data.Resource, err error) {
if document == nil {
declaration := NewDeclaration()
if err = declaration.NewResourceFromParsedURI(u); err == nil {
return declaration.Attributes, err
}
} else {
newResource, err = document.NewResourceFromParsedURI(u)
return
}
return
}

View File

@ -32,6 +32,9 @@ type ResourceReference URI
// Return a Content ReadWriter for the resource referred to. // Return a Content ReadWriter for the resource referred to.
func (r ResourceReference) Lookup(look data.ResourceMapper) ContentReadWriter { func (r ResourceReference) Lookup(look data.ResourceMapper) ContentReadWriter {
if string(r) == "" {
return nil
}
slog.Info("ResourceReference.Lookup()", "resourcereference", r, "resourcemapper", look) slog.Info("ResourceReference.Lookup()", "resourcereference", r, "resourcemapper", look)
if look != nil { if look != nil {
if v,ok := look.Get(string(r)); ok { if v,ok := look.Get(string(r)); ok {

View File

@ -8,5 +8,6 @@ import (
var schemaFilesUri URI = "file://folio/schemas/*.schema.json" var schemaFilesUri URI = "file://folio/schemas/*.schema.json"
//go:embed schemas/config/*.schema.json
//go:embed schemas/*.schema.json //go:embed schemas/*.schema.json
var schemaFiles embed.FS var schemaFiles embed.FS

View File

@ -14,6 +14,15 @@
"type": "string", "type": "string",
"description": "Config name" "description": "Config name"
}, },
"onerror": {
"type": "string",
"description": "error handling strategy",
"enum": [
"stop",
"fail",
"skip"
]
},
"attributes": { "attributes": {
"oneOf": [ "oneOf": [
{ "$ref": "bar.schema.json" } { "$ref": "bar.schema.json" }

View File

@ -0,0 +1,8 @@
{
"$id": "codec.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "codec",
"type": "string",
"description": "Supported serialization encode/decode formats",
"enum": [ "yaml", "json", "protobuf" ]
}

View File

@ -0,0 +1,25 @@
{
"$id": "block.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "block",
"type": "object",
"required": [ "name", "values" ],
"properties": {
"name": {
"type": "string",
"description": "Config block name",
"minLength": 2
},
"type": {
"type": "string",
"description": "Config type name.",
"enum": [ "system", "generic", "exec", "certificate" ]
},
"values": {
"oneOf": [
{ "type": "object" },
{ "$ref": "certificate.schema.json" }
]
}
}
}

View File

@ -0,0 +1,62 @@
{
"$id": "certificate.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "certificate",
"type": "object",
"required": [ "path", "filetype" ],
"properties": {
"SerialNumber": {
"type": "integer",
"description": "Serial number",
"minLength": 1
},
"Issuer": {
"$ref": "pkixname.schema.json"
},
"Subject": {
"$ref": "pkixname.schema.json"
},
"NotBefore": {
"type": "string",
"format": "date-time",
"description": "Cert is not valid before time in YYYY-MM-DDTHH:MM:SS.sssssssssZ format."
},
"NotAfter": {
"type": "string",
"format": "date-time",
"description": "Cert is not valid after time in YYYY-MM-DDTHH:MM:SS.sssssssssZ format."
},
"KeyUsage": {
"type": "integer",
"enum": [
1,
2,
3,
4,
5,
6,
7,
8,
9
],
"description": "Actions valid for a key. E.g. 1 = KeyUsageDigitalSignature"
},
"ExtKeyUsage": {
"type": "array",
"items": {
"type": "integer",
"minimum": 0,
"maximum": 13
},
"description": "Extended set of actions valid for a key"
},
"BasicConstraintsValid": {
"type": "boolean",
"description": "BasicConstraintsValid indicates whether IsCA, MaxPathLen, and MaxPathLenZero are valid"
},
"IsCA": {
"type": "boolean",
"description": ""
}
}
}

View File

@ -0,0 +1,65 @@
{
"$id": "pkixname.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "pkixname",
"type": "object",
"properties": {
"Country": {
"type": "array",
"description": "Country name",
"items": {
"type": "string"
}
},
"Organization": {
"type": "array",
"description": "Organization name",
"items": {
"type": "string"
}
},
"OrganizationalUnit": {
"type": "array",
"description": "Organizational Unit name",
"items": {
"type": "string"
}
},
"Locality": {
"type": "array",
"description": "Locality name",
"items": {
"type": "string"
}
},
"Province": {
"type": "array",
"description": "Province name",
"items": {
"type": "string"
}
},
"StreetAddress": {
"type": "array",
"description": "Street address",
"items": {
"type": "string"
}
},
"PostalCode": {
"type": "array",
"description": "Postal Code",
"items": {
"type": "string"
}
},
"SerialNumber": {
"type": "string",
"description": ""
},
"CommonName": {
"type": "string",
"description": "Name"
}
}
}

View File

@ -0,0 +1,9 @@
{
"$id": "constraints.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "constraints",
"type": "object",
"additionalProperties": {
"type": ["string", "number", "object", "array", "boolean", "null"]
}
}

View File

@ -14,6 +14,15 @@
"type": "string", "type": "string",
"description": "Config name" "description": "Config name"
}, },
"onerror": {
"type": "string",
"description": "error handling strategy",
"enum": [
"stop",
"fail",
"skip"
]
},
"attributes": { "attributes": {
"oneOf": [ "oneOf": [
{ "$ref": "bar.schema.json" } { "$ref": "bar.schema.json" }

View File

@ -0,0 +1,9 @@
{
"$id": "dependencies.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "dependencies",
"type": "object",
"additionalProperties": {
"$ref": "constraints.schema.json"
}
}

View File

@ -3,8 +3,27 @@
"$schema": "http://json-schema.org/draft-07/schema#", "$schema": "http://json-schema.org/draft-07/schema#",
"title": "document", "title": "document",
"type": "object", "type": "object",
"required": [ "resources" ], "required": [],
"properties": { "properties": {
"source": {
"type": "string",
"description": "Document URI"
},
"format": {
"$ref": "codec.schema.json"
},
"requires": {
"$ref": "dependencies.schema.json"
},
"configurations": {
"type": "array",
"description": "Configurations list",
"items": {
"oneOf": [
{ "$ref": "config/block.schema.json" }
]
}
},
"resources": { "resources": {
"type": "array", "type": "array",
"description": "Resources list", "description": "Resources list",
@ -17,4 +36,3 @@
} }
} }
} }

View File

@ -0,0 +1,35 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package folio
import (
"decl/internal/data"
"decl/internal/signature"
"errors"
"encoding/hex"
)
var (
ErrInvalidSignature error = errors.New("Invalid signature")
)
type Signature []byte
func (s Signature) Verify(res data.ContentHasher) error {
i := &signature.Ident{}
return i.VerifySum(res.Hash(), s)
}
func (s *Signature) SetHexString(sig string) error {
if v, e := hex.DecodeString(sig); e == nil {
*s = v
} else {
return e
}
return nil
}
func (s *Signature) String() string {
return hex.EncodeToString(*s)
}

View File

@ -8,6 +8,7 @@ import (
"decl/internal/data" "decl/internal/data"
"decl/internal/identifier" "decl/internal/identifier"
"errors" "errors"
"strings"
) )
@ -17,6 +18,10 @@ var (
type URI identifier.ID type URI identifier.ID
func NewURI(u *url.URL) URI {
return URI(u.String())
}
func (u URI) NewResource(document data.Document) (newResource data.Resource, err error) { func (u URI) NewResource(document data.Document) (newResource data.Resource, err error) {
if document == nil { if document == nil {
declaration := NewDeclaration() declaration := NewDeclaration()
@ -62,3 +67,18 @@ func (u URI) Extension() (string, string) {
return (identifier.ID)(u).Extension() return (identifier.ID)(u).Extension()
} }
func (u URI) ContentType() string {
var ext strings.Builder
exttype, fileext := u.Extension()
if fileext == "" {
return exttype
}
ext.WriteString(exttype)
ext.WriteRune('.')
ext.WriteString(fileext)
return ext.String()
}
func (u URI) IsEmpty() bool {
return (identifier.ID)(u).IsEmpty()
}