425 lines
12 KiB
Go
425 lines
12 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"
|
|
"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"`
|
|
}
|
|
|
|
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"`
|
|
uris mapper.Store[string, data.Declaration]
|
|
ResourceDeclarations []*Declaration `json:"resources" yaml:"resources"`
|
|
configNames mapper.Store[string, data.Block]
|
|
Configurations []*Block `json:"configurations,omitempty" yaml:"configurations,omitempty"`
|
|
config data.Document
|
|
Registry *Registry `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) 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) GetResource(uri string) *Declaration {
|
|
if decl, ok := d.uris[uri]; ok {
|
|
return decl.(*Declaration)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (d *Document) Clone() *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)
|
|
}
|
|
return clone
|
|
}
|
|
|
|
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) 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", "json", jsonDocument, "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) 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
|
|
}
|
|
for {
|
|
idx := i - start
|
|
if idx < 0 { idx = - idx }
|
|
|
|
slog.Info("Document.Apply() applying resource", "index", idx, "uri", d.ResourceDeclarations[idx].Resource().URI(), "resource", d.ResourceDeclarations[idx].Resource())
|
|
if state != "" {
|
|
d.ResourceDeclarations[idx].Transition = state
|
|
}
|
|
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)
|
|
return e
|
|
}
|
|
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) MapResourceURI(uri string, declaration data.Declaration) {
|
|
d.uris[uri] = declaration
|
|
}
|
|
|
|
func (d *Document) AddDeclaration(declaration data.Declaration) {
|
|
uri := declaration.URI()
|
|
decl := declaration.(*Declaration)
|
|
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)
|
|
decl.Attributes = resourceDeclaration
|
|
d.ResourceDeclarations = append(d.ResourceDeclarations, decl)
|
|
d.MapResourceURI(decl.Attributes.URI(), decl)
|
|
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
|
|
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())
|
|
d.ResourceDeclarations = append(d.ResourceDeclarations, decl)
|
|
d.MapResourceURI(decl.Attributes.URI(), decl)
|
|
decl.SetDocument(d)
|
|
d.Registry.DeclarationMap[decl] = d
|
|
newResource = decl.Attributes
|
|
return
|
|
}
|
|
|
|
func (d *Document) AddResource(uri string) error {
|
|
decl := NewDeclarationFromDocument(d)
|
|
if e := decl.SetURI(uri); e != nil {
|
|
return e
|
|
}
|
|
d.ResourceDeclarations = append(d.ResourceDeclarations, decl)
|
|
d.MapResourceURI(decl.Attributes.URI(), decl)
|
|
decl.SetDocument(d)
|
|
d.Registry.DeclarationMap[decl] = d
|
|
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) *Block {
|
|
return d.configNames[name].(*Block)
|
|
}
|
|
|
|
func (d *Document) AppendConfigurations(docs []data.Document) {
|
|
if docs != nil {
|
|
for _, doc := range docs {
|
|
for _, config := range doc.(*Document).Configurations {
|
|
d.AddConfigurationBlock(config.Name, config.Type, config.Values)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (d *Document) Diff(with *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) 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.assignResourcesDocument()
|
|
return nil
|
|
}
|
|
|
|
func (d *Document) UnmarshalJSON(data []byte) error {
|
|
type decodeDocument Document
|
|
t := (*decodeDocument)(d)
|
|
if unmarshalDocumentErr := json.Unmarshal(data, t); unmarshalDocumentErr != nil {
|
|
return unmarshalDocumentErr
|
|
}
|
|
d.assignResourcesDocument()
|
|
return nil
|
|
}
|
|
|