jx/internal/folio/document.go
Matthew Rich 35899c86a5
Some checks failed
Lint / golangci-lint (push) Failing after 9m51s
Declarative Tests / test (push) Failing after 14s
add openpgp resources
2024-11-10 10:27:31 -08:00

634 lines
17 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"
"github.com/sters/yaml-diff/yamldiff"
"strings"
"decl/internal/codec"
_ "decl/internal/types"
"decl/internal/mapper"
"decl/internal/data"
"decl/internal/schema"
"context"
"path/filepath"
)
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:"-"`
importPaths *SearchPath `json:"-" yaml:"-"`
}
func NewDocument(r *Registry) *Document {
if r == nil {
r = DocumentRegistry
}
var configImportPath ConfigKey = "system.importpath"
return &Document{
Registry: r,
Format: codec.FormatYaml,
uris: mapper.New[string, data.Declaration](),
configNames: mapper.New[string, data.Block](),
importPaths: NewSearchPath(configImportPath.GetStringSlice()),
}
}
func (d *Document) GetURI() string {
return string(d.URI)
}
func (d *Document) SetURI(uri string) {
d.URI = URI(uri)
d.AddProjectPath()
}
func (d *Document) AddProjectPath() {
exists := d.URI.Exists()
if exists {
if u := d.URI.Parse().(*ParsedURI); u != nil {
projectPath := filepath.Dir(filepath.Join(u.Hostname(), u.Path))
if err := d.importPaths.AddPath(projectPath); err != nil {
panic(err)
}
}
}
}
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) {
var load URI = uri
if ! load.Exists() {
foundURI := d.importPaths.FindURI(load)
if foundURI != "" {
load = foundURI
}
}
slog.Info("Document.loadImports()", "load", load, "uri", uri, "importpaths", d.importPaths, "doc", d.URI)
if _, err = DocumentRegistry.Load(load); 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)
slog.Info("Document.GetSchemaFiles()", "schemaFs", schemaFs)
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))
slog.Info("Document.Validate()", "error", err)
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 data.URIParser) (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) YamlDiff(with data.Document) (diffs []*yamldiff.YamlDiff, diffErr error) {
defer func() {
if r := recover(); r != nil {
diffErr = fmt.Errorf("%s", r)
}
}()
opts := []yamldiff.DoOptionFunc{}
ydata, yerr := d.YAML()
if yerr != nil {
return nil, yerr
}
yamlDiff,yamlDiffErr := yamldiff.Load(string(ydata))
if yamlDiffErr != nil {
return nil, yamlDiffErr
}
wdata,werr := with.YAML()
if werr != nil {
return nil, werr
}
withDiff,withDiffErr := yamldiff.Load(string(wdata))
if withDiffErr != nil {
return nil, withDiffErr
}
slog.Info("Document.Diff() ", "document.yaml", ydata, "with.yaml", wdata)
return yamldiff.Do(yamlDiff, withDiff, opts...), nil
}
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)
}
}()
if output == nil {
output = &strings.Builder{}
}
var diffs []*yamldiff.YamlDiff
diffs, diffErr = d.YamlDiff(with)
for _,docDiffResults := range diffs {
slog.Info("Diff()", "diff", docDiffResults, "dump", docDiffResults.Dump())
_,e := output.Write([]byte(docDiffResults.Dump()))
if e != nil {
return "", e
}
}
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) (err 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
}
err = d.loadImports()
d.assignConfigurationsDocument()
d.assignResourcesDocument()
return
}
func (d *Document) UnmarshalJSON(data []byte) (err error) {
type decodeDocument Document
t := (*decodeDocument)(d)
if unmarshalDocumentErr := json.Unmarshal(data, t); unmarshalDocumentErr != nil {
return unmarshalDocumentErr
}
err = d.loadImports()
d.assignConfigurationsDocument()
d.assignResourcesDocument()
return
}
func (d *Document) AddError(e error) {
d.Errors = append(d.Errors, e.Error())
}