586 lines
16 KiB
Go
586 lines
16 KiB
Go
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
|
|
|
package folio
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"gopkg.in/yaml.v3"
|
|
"io"
|
|
"io/fs"
|
|
_ "os"
|
|
"log/slog"
|
|
"net/url"
|
|
"github.com/sters/yaml-diff/yamldiff"
|
|
"strings"
|
|
"decl/internal/codec"
|
|
_ "decl/internal/types"
|
|
"decl/internal/mapper"
|
|
"decl/internal/data"
|
|
"decl/internal/schema"
|
|
"context"
|
|
)
|
|
|
|
type DocumentType struct {
|
|
Schema URI `json:"schema,omitempty" yaml:"schema,omitempty"`
|
|
URI URI `json:"source,omitempty" yaml:"source,omitempty"`
|
|
Format codec.Format `json:"format,omitempty" yaml:"format,omitempty"`
|
|
Requires Dependencies `json:"requires,omitempty" yaml:"requires,omitempty"`
|
|
Imports []URI `json:"imports,omitempty" yaml:"imports,omitempty"`
|
|
Errors []string `json:"error,omitempty" yaml:"error,omitempty"`
|
|
}
|
|
|
|
type Document struct {
|
|
Schema URI `json:"schema,omitempty" yaml:"schema,omitempty"`
|
|
URI URI `json:"source,omitempty" yaml:"source,omitempty"`
|
|
Format codec.Format `json:"format,omitempty" yaml:"format,omitempty"`
|
|
Requires Dependencies `json:"requires,omitempty" yaml:"requires,omitempty"`
|
|
Imports []URI `json:"imports,omitempty" yaml:"imports,omitempty"`
|
|
Errors []string `json:"error,omitempty" yaml:"error,omitempty"`
|
|
uris mapper.Store[string, data.Declaration]
|
|
ResourceDeclarations []*Declaration `json:"resources,omitempty" yaml:"resources,omitempty"`
|
|
configNames mapper.Store[string, data.Block] `json:"-" yaml:"-"`
|
|
Configurations []*Block `json:"configurations,omitempty" yaml:"configurations,omitempty"`
|
|
config data.Document
|
|
Registry *Registry `json:"-" yaml:"-"`
|
|
failedResources int `json:"-" yaml:"-"`
|
|
}
|
|
|
|
func NewDocument(r *Registry) *Document {
|
|
if r == nil {
|
|
r = DocumentRegistry
|
|
}
|
|
return &Document{ Registry: r, Format: codec.FormatYaml, uris: mapper.New[string, data.Declaration](), configNames: mapper.New[string, data.Block]() }
|
|
}
|
|
|
|
func (d *Document) GetURI() string {
|
|
return string(d.URI)
|
|
}
|
|
|
|
func (d *Document) SetURI(uri string) {
|
|
d.URI = URI(uri)
|
|
}
|
|
|
|
func (d *Document) Types() data.TypesRegistry[data.Resource] {
|
|
return d.Registry.ResourceTypes
|
|
}
|
|
|
|
func (d *Document) ConfigFilter(filter data.BlockSelector) []data.Block {
|
|
configurations := make([]data.Block, 0, len(d.Configurations))
|
|
for i := range d.Configurations {
|
|
filterConfiguration := d.Configurations[i]
|
|
if filter == nil || filter(filterConfiguration) {
|
|
configurations = append(configurations, d.Configurations[i])
|
|
}
|
|
}
|
|
return configurations
|
|
}
|
|
|
|
func (d *Document) Filter(filter data.DeclarationSelector) []data.Declaration {
|
|
resources := make([]data.Declaration, 0, len(d.ResourceDeclarations))
|
|
for i := range d.ResourceDeclarations {
|
|
filterResource := d.ResourceDeclarations[i]
|
|
if filter == nil || filter(filterResource) {
|
|
resources = append(resources, d.ResourceDeclarations[i])
|
|
}
|
|
}
|
|
return resources
|
|
}
|
|
|
|
func (d *Document) Has(key string) bool {
|
|
return d.uris.Has(key)
|
|
}
|
|
|
|
func (d *Document) Get(key string) (any, bool) {
|
|
return d.uris.Get(key)
|
|
}
|
|
|
|
func (d *Document) Set(key string, value any) {
|
|
d.uris.Set(key, value.(data.Declaration))
|
|
}
|
|
|
|
func (d *Document) Delete(key string) {
|
|
d.uris.Delete(key)
|
|
}
|
|
|
|
func (d *Document) GetResource(uri string) *Declaration {
|
|
if decl, ok := d.uris[uri]; ok {
|
|
return decl.(*Declaration)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (d *Document) Failures() int {
|
|
return d.failedResources
|
|
}
|
|
|
|
func (d *Document) Clone() data.Document {
|
|
clone := NewDocument(d.Registry)
|
|
clone.config = d.config
|
|
|
|
clone.Configurations = make([]*Block, len(d.Configurations))
|
|
for i, res := range d.Configurations {
|
|
clone.Configurations[i] = res.Clone().(*Block)
|
|
}
|
|
|
|
clone.ResourceDeclarations = make([]*Declaration, len(d.ResourceDeclarations))
|
|
for i, res := range d.ResourceDeclarations {
|
|
clone.ResourceDeclarations[i] = res.Clone().(*Declaration)
|
|
clone.ResourceDeclarations[i].SetDocument(clone)
|
|
clone.ResourceDeclarations[i].SetConfig(d.config)
|
|
}
|
|
clone.Requires = d.Requires
|
|
return clone
|
|
}
|
|
|
|
func (d *Document) ImportedDocuments() (documents []data.Document) {
|
|
documents = make([]data.Document, 0, len(d.Imports))
|
|
for _, uri := range d.Imports {
|
|
if doc, ok := DocumentRegistry.GetDocument(uri); ok {
|
|
documents = append(documents, doc)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (d *Document) loadImports() (err error) {
|
|
for _, uri := range d.Imports {
|
|
if ! DocumentRegistry.HasDocument(uri) {
|
|
if _, err = DocumentRegistry.Load(uri); err != nil {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (d *Document) assignResourcesDocument() {
|
|
slog.Info("Document.assignResourcesDocument()", "declarations", d.ResourceDeclarations, "len", len(d.ResourceDeclarations))
|
|
for i := range d.ResourceDeclarations {
|
|
if d.ResourceDeclarations[i] == nil {
|
|
d.ResourceDeclarations[i] = NewDeclaration()
|
|
}
|
|
slog.Info("Document.assignResourcesDocument()", "declaration", d.ResourceDeclarations[i])
|
|
d.ResourceDeclarations[i].SetDocument(d)
|
|
slog.Info("Document.assignResourcesDocument()", "declaration", d.ResourceDeclarations[i])
|
|
d.MapResourceURI(d.ResourceDeclarations[i].Attributes.URI(), d.ResourceDeclarations[i])
|
|
d.Registry.DeclarationMap[d.ResourceDeclarations[i]] = d
|
|
}
|
|
}
|
|
|
|
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) {
|
|
err = f.StringDecoder(docData).Decode(d)
|
|
return
|
|
}
|
|
|
|
func (d *Document) Load(docData []byte, f codec.Format) (err error) {
|
|
err = f.StringDecoder(string(docData)).Decode(d)
|
|
return
|
|
}
|
|
|
|
func (d *Document) LoadReader(r io.ReadCloser, f codec.Format) (err error) {
|
|
err = f.Decoder(r).Decode(d)
|
|
return
|
|
}
|
|
|
|
func (d *Document) GetSchemaFiles() (schemaFs fs.FS) {
|
|
var ok bool
|
|
if schemaFs, ok = d.Registry.Schemas.Get(d.Schema); ok {
|
|
return
|
|
}
|
|
schemaFs, _ = d.Registry.Schemas.Get(d.Registry.DefaultSchema)
|
|
return
|
|
}
|
|
|
|
func (d *Document) Validate() error {
|
|
jsonDocument, jsonErr := d.JSON()
|
|
slog.Info("document.Validate() json", "err", jsonErr)
|
|
if jsonErr == nil {
|
|
s := schema.New("document", d.GetSchemaFiles())
|
|
err := s.Validate(string(jsonDocument))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
/*
|
|
for i := range d.ResourceDeclarations {
|
|
if e := d.ResourceDeclarations[i].Resource().Validate(); e != nil {
|
|
return fmt.Errorf("failed to validate resource %s; %w", d.ResourceDeclarations[i].Resource().URI(), e)
|
|
}
|
|
}
|
|
*/
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (d *Document) SetConfig(config data.Document) {
|
|
d.config = config
|
|
}
|
|
|
|
func (d *Document) ConfigDoc() data.Document {
|
|
return d.config
|
|
}
|
|
|
|
func (d *Document) Resources() []*Declaration {
|
|
return d.ResourceDeclarations
|
|
}
|
|
|
|
func (d *Document) Declarations() (declarations []data.Declaration) {
|
|
for _, v := range d.ResourceDeclarations {
|
|
declarations = append(declarations, data.Declaration(v))
|
|
}
|
|
return
|
|
}
|
|
|
|
func (d *Document) Len() int {
|
|
return len(d.ResourceDeclarations)
|
|
}
|
|
|
|
func (d *Document) CheckConstraints() bool {
|
|
return d.Requires.Check()
|
|
}
|
|
|
|
func (d *Document) ResolveIds(ctx context.Context) {
|
|
for i := range d.ResourceDeclarations {
|
|
d.ResourceDeclarations[i].ResolveId(ctx)
|
|
}
|
|
}
|
|
|
|
func (d *Document) Apply(state string) error {
|
|
if d == nil {
|
|
panic("Undefined Document")
|
|
}
|
|
slog.Info("Document.Apply()", "declarations", d, "override", state)
|
|
var start, i int = 0, 0
|
|
if state == "delete" {
|
|
start = len(d.ResourceDeclarations) - 1
|
|
}
|
|
if len(d.ResourceDeclarations) > 0 {
|
|
for {
|
|
idx := i - start
|
|
if idx < 0 { idx = - idx }
|
|
|
|
d.ResourceDeclarations[idx].SetConfig(d.config)
|
|
|
|
slog.Info("Document.Apply() applying resource", "index", idx, "uri", d.ResourceDeclarations[idx].Resource().URI(), "state", state, "resource", d.ResourceDeclarations[idx].Resource())
|
|
|
|
if d.ResourceDeclarations[idx].Requires.Check() {
|
|
if e := d.ResourceDeclarations[idx].Apply(state); e != nil {
|
|
d.ResourceDeclarations[idx].Error = e.Error()
|
|
slog.Info("Document.Apply() ERROR", "index", idx, "uri", d.ResourceDeclarations[idx].Resource().URI(), "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 {
|
|
break
|
|
}
|
|
i++
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (d *Document) Generate(w io.Writer) (err error) {
|
|
err = d.Format.Validate()
|
|
if err == nil {
|
|
if e := d.Format.Encoder(w); e != nil {
|
|
defer func() {
|
|
if closeErr := e.Close(); closeErr != nil && err == nil {
|
|
err = closeErr
|
|
}
|
|
}()
|
|
err = e.Encode(d);
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
/*
|
|
func (d *Document) MapConfigurationURI(uri string, block data.Block) {
|
|
d.configUris[uri] = block
|
|
}
|
|
*/
|
|
|
|
func (d *Document) MapResourceURI(uri string, declaration data.Declaration) {
|
|
d.uris[uri] = declaration
|
|
}
|
|
|
|
func (d *Document) UnMapResourceURI(uri string) {
|
|
d.uris.Delete(uri)
|
|
}
|
|
|
|
func (d *Document) AddDeclaration(declaration data.Declaration) {
|
|
uri := declaration.URI()
|
|
decl := declaration.(*Declaration)
|
|
if decl.Requires.Check() {
|
|
|
|
d.ResourceDeclarations = append(d.ResourceDeclarations, decl)
|
|
|
|
d.MapResourceURI(uri, declaration)
|
|
decl.SetDocument(d)
|
|
d.Registry.DeclarationMap[decl] = d
|
|
}
|
|
}
|
|
|
|
func (d *Document) AddResourceDeclaration(resourceType string, resourceDeclaration data.Resource) {
|
|
slog.Info("Document.AddResourceDeclaration()", "type", resourceType, "resource", resourceDeclaration)
|
|
decl := NewDeclarationFromDocument(d)
|
|
decl.Type = TypeName(resourceType)
|
|
if decl.Requires.Check() {
|
|
decl.Attributes = resourceDeclaration
|
|
d.ResourceDeclarations = append(d.ResourceDeclarations, decl)
|
|
d.MapResourceURI(decl.Attributes.URI(), decl)
|
|
decl.SetDocument(d)
|
|
d.Registry.DeclarationMap[decl] = d
|
|
}
|
|
}
|
|
|
|
func (d *Document) NewResource(uri string) (newResource data.Resource, err error) {
|
|
|
|
decl := NewDeclarationFromDocument(d)
|
|
|
|
if err = decl.NewResource(&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() {
|
|
slog.Info("Document.NewResource()", "type", decl.Type, "declaration", decl)
|
|
|
|
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
|
|
}
|
|
|
|
func (d *Document) AddResource(uri string) error {
|
|
decl := NewDeclarationFromDocument(d)
|
|
if e := decl.SetURI(uri); e != nil {
|
|
return e
|
|
}
|
|
|
|
if decl.Requires.Check() {
|
|
d.ResourceDeclarations = append(d.ResourceDeclarations, decl)
|
|
d.MapResourceURI(decl.Attributes.URI(), decl)
|
|
decl.SetDocument(d)
|
|
d.Registry.DeclarationMap[decl] = d
|
|
} else {
|
|
return fmt.Errorf("%w: %s", ErrConstraintFailure, uri)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (d *Document) JSON() ([]byte, error) {
|
|
var buf strings.Builder
|
|
err := codec.FormatJson.Serialize(d, &buf)
|
|
return []byte(buf.String()), err
|
|
}
|
|
|
|
func (d *Document) YAML() ([]byte, error) {
|
|
var buf strings.Builder
|
|
err := codec.FormatYaml.Serialize(d, &buf)
|
|
return []byte(buf.String()), err
|
|
}
|
|
|
|
func (d *Document) PB() ([]byte, error) {
|
|
var buf strings.Builder
|
|
err := codec.FormatProtoBuf.Serialize(d, &buf)
|
|
return []byte(buf.String()), err
|
|
}
|
|
|
|
func (d *Document) AddConfigurationBlock(configurationName string, configurationType TypeName, configuration data.Configuration) {
|
|
cfg := NewBlock()
|
|
cfg.Name = configurationName
|
|
cfg.Type = configurationType
|
|
cfg.Values = configuration
|
|
d.configNames[cfg.Name] = cfg
|
|
d.Configurations = append(d.Configurations, cfg)
|
|
}
|
|
|
|
func (d *Document) AddConfiguration(uri string) error {
|
|
cfg := NewBlock()
|
|
if e := cfg.SetURI(uri); e != nil {
|
|
return e
|
|
}
|
|
if cfg.Name == "" {
|
|
return data.ErrConfigUndefinedName
|
|
}
|
|
d.configNames[cfg.Name] = cfg
|
|
d.Configurations = append(d.Configurations, cfg)
|
|
return nil
|
|
}
|
|
|
|
func (d *Document) HasConfig(name string) bool {
|
|
_, ok := d.configNames[name]
|
|
return ok
|
|
}
|
|
|
|
func (d *Document) GetConfig(name string) data.Block {
|
|
return d.configNames[name]
|
|
}
|
|
|
|
func (d *Document) AppendConfigurations(docs []data.Document) {
|
|
for _, doc := range docs {
|
|
for _, config := range doc.(*Document).Configurations {
|
|
d.AddConfigurationBlock(config.Name, config.Type, config.Values)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Generate a diff of the loaded document against the current resource state
|
|
func (d *Document) DiffState(output io.Writer) (returnOutput string, diffErr error) {
|
|
clone := d.Clone()
|
|
diffErr = clone.Apply("read")
|
|
if diffErr != nil {
|
|
return "", diffErr
|
|
}
|
|
return d.Diff(clone, output)
|
|
}
|
|
|
|
func (d *Document) Diff(with data.Document, output io.Writer) (returnOutput string, diffErr error) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
returnOutput = ""
|
|
diffErr = fmt.Errorf("%s", r)
|
|
}
|
|
}()
|
|
slog.Info("Document.Diff()")
|
|
opts := []yamldiff.DoOptionFunc{}
|
|
if output == nil {
|
|
output = &strings.Builder{}
|
|
}
|
|
ydata, yerr := d.YAML()
|
|
if yerr != nil {
|
|
return "", yerr
|
|
}
|
|
yamlDiff,yamlDiffErr := yamldiff.Load(string(ydata))
|
|
if yamlDiffErr != nil {
|
|
return "", yamlDiffErr
|
|
}
|
|
|
|
wdata,werr := with.YAML()
|
|
if werr != nil {
|
|
return "", werr
|
|
}
|
|
withDiff,withDiffErr := yamldiff.Load(string(wdata))
|
|
if withDiffErr != nil {
|
|
return "", withDiffErr
|
|
}
|
|
|
|
for _,docDiffResults := range yamldiff.Do(yamlDiff, withDiff, opts...) {
|
|
slog.Info("Diff()", "diff", docDiffResults, "dump", docDiffResults.Dump())
|
|
_,e := output.Write([]byte(docDiffResults.Dump()))
|
|
if e != nil {
|
|
return "", e
|
|
}
|
|
}
|
|
slog.Info("Document.Diff() ", "document.yaml", ydata, "with.yaml", wdata)
|
|
if stringOutput, ok := output.(*strings.Builder); ok {
|
|
return stringOutput.String(), nil
|
|
}
|
|
return "", nil
|
|
}
|
|
|
|
/*
|
|
func (d *Document) UnmarshalValue(value *DocumentType) error {
|
|
d.Requires = value.Requires
|
|
}
|
|
*/
|
|
|
|
func (d *Document) UnmarshalYAML(value *yaml.Node) error {
|
|
type decodeDocument Document
|
|
t := &DocumentType{}
|
|
if unmarshalDocumentErr := value.Decode(t); unmarshalDocumentErr != nil {
|
|
return unmarshalDocumentErr
|
|
}
|
|
|
|
if unmarshalResourcesErr := value.Decode((*decodeDocument)(d)); unmarshalResourcesErr != nil {
|
|
return unmarshalResourcesErr
|
|
}
|
|
d.assignConfigurationsDocument()
|
|
d.assignResourcesDocument()
|
|
return d.loadImports()
|
|
}
|
|
|
|
func (d *Document) UnmarshalJSON(data []byte) error {
|
|
type decodeDocument Document
|
|
t := (*decodeDocument)(d)
|
|
if unmarshalDocumentErr := json.Unmarshal(data, t); unmarshalDocumentErr != nil {
|
|
return unmarshalDocumentErr
|
|
}
|
|
d.assignConfigurationsDocument()
|
|
d.assignResourcesDocument()
|
|
return d.loadImports()
|
|
}
|
|
|
|
func (d *Document) AddError(e error) {
|
|
d.Errors = append(d.Errors, e.Error())
|
|
}
|