add client pkg
This commit is contained in:
parent
4caed45f96
commit
486281525a
305
internal/client/client.go
Normal file
305
internal/client/client.go
Normal file
@ -0,0 +1,305 @@
|
|||||||
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"decl/internal/data"
|
||||||
|
"decl/internal/folio"
|
||||||
|
_ "decl/internal/fan"
|
||||||
|
_ "decl/internal/config"
|
||||||
|
_ "decl/internal/resource"
|
||||||
|
"decl/internal/fs"
|
||||||
|
_ "errors"
|
||||||
|
"fmt"
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
var (
|
||||||
|
)
|
||||||
|
|
||||||
|
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 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() {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Each document has an `imports` keyword which can be used to load dependencies
|
||||||
|
func (a *App) LoadDocumentImports() error {
|
||||||
|
for _, d := range a.Documents {
|
||||||
|
for _, importedDocument := range d.ImportedDocuments() {
|
||||||
|
docURI := folio.URI(importedDocument.GetURI())
|
||||||
|
if _, ok := a.ImportedMap[docURI]; !ok {
|
||||||
|
a.ImportedMap[docURI] = 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()
|
||||||
|
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 loadedDocuments, err = folio.DocumentRegistry.Load(folio.URI(uri)); err == nil && loadedDocuments != nil {
|
||||||
|
a.Documents = append(a.Documents, loadedDocuments...)
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
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) {
|
||||||
|
for _, d := range a.Documents {
|
||||||
|
d.SetConfig(a.Config)
|
||||||
|
|
||||||
|
var overrideState string = ""
|
||||||
|
if deleteResources {
|
||||||
|
overrideState = "delete"
|
||||||
|
}
|
||||||
|
d.ResolveIds(ctx)
|
||||||
|
|
||||||
|
if ! d.CheckConstraints() {
|
||||||
|
slog.Info("Client.Apply() document constrains failed", "requires", d)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if e := d.(*folio.Document).Apply(overrideState); e != nil {
|
||||||
|
slog.Info("Client.Apply() error", "error", e)
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
if d.Failures() > 0 {
|
||||||
|
err = fmt.Errorf("Failed resources: %d, %w", d.Failures(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) ImportCmd(ctx context.Context, docs []string, resourceURI string, quiet bool, merge bool) (err error) {
|
||||||
|
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) {
|
||||||
|
if err = a.Import(docs); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = a.LoadDocumentImports(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = a.Apply(ctx, deleteResources); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if quiet {
|
||||||
|
err = a.Quiet()
|
||||||
|
} else {
|
||||||
|
err = a.Emit()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
||||||
|
|
||||||
|
//leftSource := folio.URI(docs[0])
|
||||||
|
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 {
|
||||||
|
rightDocuments, err = a.ImportSource(docs[1])
|
||||||
|
err = a.Diff(leftDocuments, rightDocuments)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
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) {
|
||||||
|
return a.emitter.Close()
|
||||||
|
}
|
211
internal/client/client_test.go
Normal file
211
internal/client/client_test.go
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"os"
|
||||||
|
"os/user"
|
||||||
|
"os/exec"
|
||||||
|
"testing"
|
||||||
|
"decl/internal/tempdir"
|
||||||
|
"log"
|
||||||
|
"decl/internal/folio"
|
||||||
|
_ "decl/internal/fan"
|
||||||
|
"decl/internal/codec"
|
||||||
|
"decl/internal/data"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
)
|
||||||
|
|
||||||
|
var programLevel = new(slog.LevelVar)
|
||||||
|
|
||||||
|
var TempDir tempdir.Path = "jx_client"
|
||||||
|
|
||||||
|
var ProcessTestUserName string
|
||||||
|
var ProcessTestGroupName string
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
LoggerConfig()
|
||||||
|
var err error
|
||||||
|
err = TempDir.Create()
|
||||||
|
if err != nil || TempDir == "" {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ProcessTestUserName, ProcessTestGroupName = ProcessUserName()
|
||||||
|
rc := m.Run()
|
||||||
|
|
||||||
|
TempDir.Remove()
|
||||||
|
os.Exit(rc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoggerConfig() {
|
||||||
|
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: programLevel}))
|
||||||
|
slog.SetDefault(logger)
|
||||||
|
programLevel.Set(slog.LevelDebug)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ProcessUserName() (string, string) {
|
||||||
|
processUser, userErr := user.Current()
|
||||||
|
if userErr != nil {
|
||||||
|
panic(userErr)
|
||||||
|
}
|
||||||
|
processGroup, groupErr := user.LookupGroupId(processUser.Gid)
|
||||||
|
if groupErr != nil {
|
||||||
|
panic(groupErr)
|
||||||
|
}
|
||||||
|
return processUser.Username, processGroup.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExitError(e error) string {
|
||||||
|
if e != nil {
|
||||||
|
switch v := e.(type) {
|
||||||
|
case *exec.ExitError:
|
||||||
|
return string(v.Stderr)
|
||||||
|
default:
|
||||||
|
return e.Error()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// jx import <docuri>...
|
||||||
|
func TestClientImport(t *testing.T) {
|
||||||
|
c := NewClient()
|
||||||
|
assert.NotNil(t, c)
|
||||||
|
|
||||||
|
importDocuments := []string{
|
||||||
|
"file://../../examples/file.jx.yaml",
|
||||||
|
"file://../../examples/user.jx.yaml",
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Nil(t, c.Import(importDocuments))
|
||||||
|
|
||||||
|
for index, uri := range importDocuments {
|
||||||
|
u := folio.URI(uri)
|
||||||
|
r, readerErr := u.ContentReaderStream()
|
||||||
|
assert.Nil(t, readerErr)
|
||||||
|
assert.NotNil(t, r)
|
||||||
|
|
||||||
|
doc := folio.DocumentRegistry.NewDocument(folio.URI(uri))
|
||||||
|
assert.Nil(t, doc.LoadReader(r, codec.FormatYaml))
|
||||||
|
|
||||||
|
imported := c.Documents[index]
|
||||||
|
assert.NotNil(t, imported)
|
||||||
|
assert.Equal(t, uri, imported.GetURI())
|
||||||
|
assert.Equal(t, doc.Len(), imported.Len())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// jx import --resource <resource>
|
||||||
|
func TestClientImportResource(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
c := NewClient()
|
||||||
|
assert.NotNil(t, c)
|
||||||
|
|
||||||
|
importResources := []string{
|
||||||
|
"file://../../COPYRIGHT",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, uri := range importResources {
|
||||||
|
assert.Nil(t, c.ImportResource(ctx, uri))
|
||||||
|
}
|
||||||
|
|
||||||
|
imported := c.Documents[0]
|
||||||
|
assert.NotNil(t, imported)
|
||||||
|
for _, uri := range importResources {
|
||||||
|
assert.NotNil(t, imported.(*folio.Document).GetResource(uri))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClientEmit(t *testing.T) {
|
||||||
|
//ctx := context.Background()
|
||||||
|
c := NewClient()
|
||||||
|
assert.NotNil(t, c)
|
||||||
|
|
||||||
|
importDocuments := []string{
|
||||||
|
"file://../../examples/file.jx.yaml",
|
||||||
|
"file://../../examples/user.jx.yaml",
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Nil(t, c.Import(importDocuments))
|
||||||
|
targetFile := TempDir.FilePath("jx_emit_output.jx.yaml")
|
||||||
|
targetFileURI := fmt.Sprintf("file://%s", targetFile)
|
||||||
|
assert.Nil(t, c.SetOutput(targetFile))
|
||||||
|
assert.Nil(t, c.Emit())
|
||||||
|
|
||||||
|
assert.FileExists(t, targetFile)
|
||||||
|
|
||||||
|
|
||||||
|
u := folio.URI(targetFileURI)
|
||||||
|
r, readerErr := u.ContentReaderStream()
|
||||||
|
assert.Nil(t, readerErr)
|
||||||
|
assert.NotNil(t, r)
|
||||||
|
|
||||||
|
extractor, err := folio.DocumentRegistry.ConverterTypes.New(targetFileURI)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.NotNil(t, extractor)
|
||||||
|
|
||||||
|
targetResource, resErr := u.NewResource(nil)
|
||||||
|
assert.Nil(t, resErr)
|
||||||
|
docs, exErr := extractor.(data.ManyExtractor).ExtractMany(targetResource, nil)
|
||||||
|
assert.Nil(t, exErr)
|
||||||
|
|
||||||
|
assert.Equal(t, 2, len(docs))
|
||||||
|
|
||||||
|
assert.Equal(t, 1, docs[1].Len())
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkClientSystemConfigurations(b *testing.B) {
|
||||||
|
assert.Nil(b, TempDir.Mkdir("benchconfig", 0700))
|
||||||
|
ConfDir := tempdir.Path(TempDir.FilePath("benchconfig"))
|
||||||
|
ConfDir.CreateFile("cfg.jx.yaml", `
|
||||||
|
configurations:
|
||||||
|
- name: files
|
||||||
|
values:
|
||||||
|
prefix: /usr
|
||||||
|
`)
|
||||||
|
|
||||||
|
configDirURI := fmt.Sprintf("file://%s", ConfDir)
|
||||||
|
|
||||||
|
programLevel.Set(slog.LevelError)
|
||||||
|
b.Run("systemconfiguration", func(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
c := NewClient()
|
||||||
|
c.SystemConfiguration(configDirURI)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
programLevel.Set(slog.LevelDebug)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClientSystemConfiguration(t *testing.T) {
|
||||||
|
c := NewClient()
|
||||||
|
assert.NotNil(t, c)
|
||||||
|
|
||||||
|
assert.Nil(t, TempDir.Mkdir("config", 0700))
|
||||||
|
|
||||||
|
ConfDir := tempdir.Path(TempDir.FilePath("config"))
|
||||||
|
ConfDir.CreateFile("cfg.jx.yaml", `
|
||||||
|
configurations:
|
||||||
|
- name: files
|
||||||
|
values:
|
||||||
|
prefix: /usr
|
||||||
|
`)
|
||||||
|
|
||||||
|
//configDirURI := fmt.Sprintf("file://%s", ConfDir)
|
||||||
|
configErr := c.SystemConfiguration(string(ConfDir))
|
||||||
|
assert.Nil(t, configErr)
|
||||||
|
|
||||||
|
assert.NotNil(t, c.Config)
|
||||||
|
|
||||||
|
slog.Info("TestClientSystemConfiguration", "config", c.Config)
|
||||||
|
cfg := c.Config.GetConfig("files")
|
||||||
|
assert.NotNil(t, cfg)
|
||||||
|
|
||||||
|
value, valueErr := cfg.GetValue("prefix")
|
||||||
|
assert.Nil(t, valueErr)
|
||||||
|
assert.Equal(t, "/usr", value.(string))
|
||||||
|
return
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user