// Copyright 2024 Matthew Rich . 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()) }