// Copyright 2024 Matthew Rich . All rights reserved. package client import ( "decl/internal/data" "decl/internal/folio" _ "decl/internal/fan" _ "decl/internal/config" _ "decl/internal/resource" "decl/internal/fs" "decl/internal/builtin" "errors" "fmt" "context" "log/slog" "os" ) var ( ErrFailedResources error = errors.New("Failed Resources") ErrFailedDocuments error = errors.New("Document errors") ) type App struct { Target folio.URI ImportedMap map[folio.URI]data.Document Documents []data.Document emitter data.Converter merged data.Document Config data.Document } func NewClient() *App { a := &App{ ImportedMap: make(map[folio.URI]data.Document), Documents: make([]data.Document, 0, 100) } return a } // Load compiled-in config documents. func (a *App) BuiltInConfiguration() (err error) { var defaultConfigurations []data.Document if defaultConfigurations, err = builtin.BuiltInDocuments(); len(defaultConfigurations) > 0 { slog.Info("Client.BuiltInConfiguration()", "documents", defaultConfigurations, "error", err) a.Config.AppendConfigurations(defaultConfigurations) } return } // Load config documents from default system config path. Ignore if missing. func (a *App) SystemConfiguration(configPath string) (err error) { var extractor data.Converter var sourceResource data.Resource if a.Config == nil { a.Config = folio.DocumentRegistry.NewDocument("file:///etc/jx/runtimeconfig.jx.yaml") } if configPath != "" { //configURI := folio.URI(configPath) var loaded []data.Document docFs := fs.NewWalkDir(os.DirFS(configPath), configPath, func(fsys fs.FS, path string, file fs.DirEntry) (loadErr error) { u := folio.URI(fmt.Sprintf("file://%s", path)) if ! file.IsDir() { slog.Info("Client.SystemConfiguration()", "uri", u) if extractor, loadErr = folio.DocumentRegistry.ConverterTypes.New(string(u)); loadErr == nil { if sourceResource, loadErr = u.NewResource(nil); loadErr == nil { if loaded, loadErr = extractor.(data.ManyExtractor).ExtractMany(sourceResource, nil); loadErr == nil { a.Config.AppendConfigurations(loaded) } } } } return }) err = docFs.Walk(nil) } return } func (a *App) MergeDocuments() { a.merged = folio.DocumentRegistry.NewDocument("file://-") for _, d := range a.Documents { for _, declaration := range d.(*folio.Document).ResourceDeclarations { a.merged.AddDeclaration((data.Declaration)(declaration)) } } } func (a *App) SetOutput(uri string) (err error) { if uri == "-" { uri = "jx://-" } a.Target = folio.URI(uri) if a.emitter, err = folio.DocumentRegistry.ConverterTypes.New(uri); err != nil { return fmt.Errorf("Failed opening target: %s, %w", uri, err) } slog.Info("Client.SetOutput()", "uri", uri, "emitter", a.emitter) return } // Each document has an `imports` keyword which can be used to load dependencies func (a *App) LoadDocumentImports() error { slog.Info("Client.LoadDocumentImports()", "documents", a.Documents) for i, d := range a.Documents { importedDocs := d.ImportedDocuments() slog.Info("Client.LoadDocumentImports()", "imported", importedDocs) for _, importedDocument := range importedDocs { docURI := folio.URI(importedDocument.GetURI()) if _, ok := a.ImportedMap[docURI]; !ok { a.ImportedMap[docURI] = importedDocument a.Documents = append(a.Documents, nil) copy(a.Documents[i+1:], a.Documents[i:]) a.Documents[i] = importedDocument /* if _, outputErr := a.emitter.Emit(importedDocument, nil); outputErr != nil { return outputErr } */ } } } return nil } func (a *App) ImportResource(ctx context.Context, uri string) (err error) { if len(a.Documents) < 1 { a.Documents = append(a.Documents, folio.DocumentRegistry.NewDocument("")) } resourceURI := folio.URI(uri) u := resourceURI.Parse().URL() if u == nil { return fmt.Errorf("Failed adding resource: %s", uri) } if u.Scheme == "" { u.Scheme = "file" } for _, d := range a.Documents { if newResource, newResourceErr := d.NewResource(uri); newResourceErr == nil { if _, err = newResource.Read(ctx); err != nil { return } } else { return newResourceErr } } return } func (a *App) ImportSource(uri string) (loadedDocuments []data.Document, err error) { if source := folio.URI(uri).Parse().URL(); source != nil { if source.Scheme == "" { source.Scheme = "file" } slog.Info("Client.ImportSource()", "uri", uri, "source", source, "error", err) if loadedDocuments, err = folio.DocumentRegistry.LoadFromParsedURI(source); err == nil && loadedDocuments != nil { a.Documents = append(a.Documents, loadedDocuments...) } } else { err = folio.ErrInvalidURI } slog.Info("Client.ImportSource()", "uri", uri, "error", err) return } func (a *App) Import(docs []string) (err error) { for _, source := range docs { if _, err = a.ImportSource(source); err != nil { return } } return } func (a *App) Apply(ctx context.Context, deleteResources bool) (err error) { var errorsCount int = 0 for _, d := range a.Documents { d.SetConfig(a.Config) var overrideState string = "" if deleteResources { overrideState = "delete" } d.ResolveIds(ctx) _ = d.Apply("stat") if ! d.CheckConstraints() { slog.Info("Client.Apply() document constraints failed", "requires", d) d.AddError(fmt.Errorf("%w: %s", folio.ErrConstraintFailure, d.GetURI())) errorsCount++ continue } slog.Info("Client.Apply()", "uri", d.GetURI(), "document", d, "state", overrideState, "error", err) if e := d.(*folio.Document).Apply(overrideState); e != nil { slog.Info("Client.Apply() error", "error", e) return e } if d.Failures() > 0 { d.AddError(fmt.Errorf("%w: %d, %w", ErrFailedResources, d.Failures(), err)) errorsCount++ } } if errorsCount > 0 { return fmt.Errorf("%w: %d", ErrFailedDocuments, errorsCount) } return } func (a *App) ImportCmd(ctx context.Context, docs []string, resourceURI string, quiet bool, merge bool) (err error) { defer a.Close() if err = a.Import(docs); err != nil { return } if err = a.LoadDocumentImports(); err != nil { return } if len(resourceURI) > 0 { if err = a.ImportResource(ctx, resourceURI); err != nil { return } } if quiet { err = a.Quiet() } else { if merge { a.MergeDocuments() } err = a.Emit() if err != nil { return } } return } func (a *App) ApplyCmd(ctx context.Context, docs []string, quiet bool, deleteResources bool) (err error) { defer a.Close() var failedResources error if err = a.Import(docs); err != nil { return } if err = a.LoadDocumentImports(); err != nil { return } if failedResources = a.Apply(ctx, deleteResources); failedResources != nil { slog.Info("Client.ApplyCmd()", "client", a, "error", failedResources) if ! errors.Is(failedResources, ErrFailedResources) && ! errors.Is(failedResources, ErrFailedDocuments) { return failedResources } } if quiet { err = a.Quiet() } else { err = a.Emit() } if failedResources != nil { if err != nil { return fmt.Errorf("%w %w", failedResources, err) } else { return failedResources } } return } func (a *App) Diff(left []data.Document, right []data.Document) (err error) { output := os.Stdout slog.Info("jx diff ", "right", right, "left", left) index := 0 for { if index >= len(right) && index >= len(left) { break } if index >= len(right) { if _, err = left[index].Diff(folio.DocumentRegistry.NewDocument(""), output); err != nil { return } index++ continue } if index >= len(left) { if _, err = folio.DocumentRegistry.NewDocument("").Diff(right[index], output); err != nil { return } index++ continue } if _, err = left[index].Diff(right[index], output); err != nil { return } index++ } return } func (a *App) DiffCmd(docs []string) (err error) { output := os.Stdout var leftDocuments, rightDocuments []data.Document var rightSource folio.URI //leftSource := folio.URI(docs[0]) if len(docs) > 1 { rightSource = folio.URI(docs[1]) } if leftDocuments, err = a.ImportSource(docs[0]); err == nil { if rightSource.IsEmpty() { for _, doc := range leftDocuments { _, err = doc.DiffState(output) } } else { if rightDocuments, err = a.ImportSource(docs[1]); err == nil { err = a.Diff(leftDocuments, rightDocuments) } } } return err } func (a *App) ConfigCmd(docs []string, includeSystemConfig bool) (err error) { defer a.Close() if err = a.BuiltInConfiguration(); err != nil { slog.Warn("BuiltInConfiguration()", "error", err) } if err = a.Import(docs); err != nil { return } if err = a.LoadDocumentImports(); err != nil { return } if includeSystemConfig { if _, err = a.emitter.Emit(a.Config, nil); err != nil { return } } _, err = a.emitter.(data.ManyEmitter).EmitMany(a.Documents, nil) return } func (a *App) Quiet() (err error) { output := os.Stdout for _, d := range a.Documents { for _, dr := range d.Declarations() { if _, err = output.Write([]byte(fmt.Sprintf("%s\n", dr.Resource().URI()))); err != nil { return } } } return } func (a *App) Emit() (err error) { if a.merged == nil { for _, d := range a.Documents { slog.Info("Client.Emit() document", "document", d) if _, err = a.emitter.Emit(d, nil); err != nil { return } } } else { if _, err = a.emitter.Emit(a.merged, nil); err != nil { return } } return } func (a *App) Close() (err error) { if a.emitter != nil { slog.Info("Client.Close() emitter", "emitter", a.emitter) return a.emitter.Close() } return }