From 04b27dc5df181cd1940dca6474360f32b05b00b3 Mon Sep 17 00:00:00 2001 From: Matthew Rich Date: Sat, 17 Aug 2024 18:19:56 -0700 Subject: [PATCH] add folio package --- internal/folio/block.go | 192 ++++++++ internal/folio/block_test.go | 49 ++ internal/folio/declaration.go | 311 +++++++++++++ internal/folio/declaration_test.go | 135 ++++++ internal/folio/document.go | 424 ++++++++++++++++++ internal/folio/document_test.go | 208 +++++++++ internal/folio/folio_test.go | 60 +++ internal/folio/mock_foo_resource_test.go | 101 +++++ internal/folio/mock_resource_test.go | 137 ++++++ internal/folio/registry.go | 75 ++++ internal/folio/registry_test.go | 54 +++ internal/folio/resourcereference.go | 69 +++ internal/folio/resourcereference_test.go | 30 ++ internal/folio/schema.go | 12 + .../folio/schemas/bar-declaration.schema.json | 23 + internal/folio/schemas/bar.schema.json | 22 + .../folio/schemas/declaration.schema.json | 23 + internal/folio/schemas/document.schema.json | 20 + .../folio/schemas/foo-declaration.schema.json | 23 + internal/folio/schemas/foo.schema.json | 18 + internal/folio/types.go | 29 ++ internal/folio/uri.go | 2 +- internal/folio/uri_test.go | 23 + 23 files changed, 2039 insertions(+), 1 deletion(-) create mode 100644 internal/folio/block.go create mode 100644 internal/folio/block_test.go create mode 100644 internal/folio/declaration.go create mode 100644 internal/folio/declaration_test.go create mode 100644 internal/folio/document.go create mode 100644 internal/folio/document_test.go create mode 100644 internal/folio/folio_test.go create mode 100644 internal/folio/mock_foo_resource_test.go create mode 100644 internal/folio/mock_resource_test.go create mode 100644 internal/folio/registry.go create mode 100644 internal/folio/registry_test.go create mode 100644 internal/folio/resourcereference.go create mode 100644 internal/folio/resourcereference_test.go create mode 100644 internal/folio/schema.go create mode 100644 internal/folio/schemas/bar-declaration.schema.json create mode 100644 internal/folio/schemas/bar.schema.json create mode 100644 internal/folio/schemas/declaration.schema.json create mode 100644 internal/folio/schemas/document.schema.json create mode 100644 internal/folio/schemas/foo-declaration.schema.json create mode 100644 internal/folio/schemas/foo.schema.json create mode 100644 internal/folio/types.go diff --git a/internal/folio/block.go b/internal/folio/block.go new file mode 100644 index 0000000..b94febb --- /dev/null +++ b/internal/folio/block.go @@ -0,0 +1,192 @@ +// Copyright 2024 Matthew Rich . All rights reserved. + +package folio + +import ( + "context" + "encoding/json" + "fmt" + "io" + "strings" + "gopkg.in/yaml.v3" + "decl/internal/codec" + "decl/internal/data" + "decl/internal/schema" +) + +type BlockType struct { + Name string `json:"name" yaml:"name"` + Type TypeName `json:"type" yaml:"type"` +} + +type Block struct { + Name string `json:"name" yaml:"name"` + Type TypeName `json:"type" yaml:"type"` + Values data.Configuration `json:"values" yaml:"values"` + ConfigurationTypes data.TypesRegistry[data.Configuration] `json:"-" yaml:"-"` +} + +func NewBlock() *Block { + return &Block{ Type: "generic", ConfigurationTypes: DocumentRegistry.ConfigurationTypes } +} + +func (b *Block) Clone() data.Block { + return &Block { + Type: b.Type, + Values: b.Values.Clone(), + } +} + +func (b *Block) Load(docData []byte, f codec.Format) (err error) { + err = f.StringDecoder(string(docData)).Decode(b) + return +} + +func (b *Block) LoadReader(r io.ReadCloser, f codec.Format) (err error) { + err = f.Decoder(r).Decode(b) + return +} + +func (b *Block) LoadString(docData string, f codec.Format) (err error) { + err = f.StringDecoder(docData).Decode(b) + return +} + +func (b *Block) LoadBlock(yamlBlock string) (err error) { + return b.LoadString(yamlBlock, codec.FormatYaml) +} + +func (b *Block) JSON() ([]byte, error) { + var buf strings.Builder + err := codec.FormatJson.Serialize(b, &buf) + return []byte(buf.String()), err +} + +func (b *Block) YAML() ([]byte, error) { + var buf strings.Builder + err := codec.FormatYaml.Serialize(b, &buf) + return []byte(buf.String()), err +} + +func (b *Block) PB() ([]byte, error) { + var buf strings.Builder + err := codec.FormatProtoBuf.Serialize(b, &buf) + return []byte(buf.String()), err +} + +func (b *Block) Validate() (err error) { + var blockJson []byte + if blockJson, err = b.JSON(); err == nil { + s := schema.New(fmt.Sprintf("%s-block", b.Type), schemaFiles) // XXX wrong schemaFiles + err = s.Validate(string(blockJson)) + } + return +} + +func (b *Block) NewConfiguration(uri *string) (err error) { + if b.ConfigurationTypes == nil { + panic(fmt.Errorf("Undefined type registry: unable to create new configuration %s", *uri)) + } + if uri == nil { + b.Values, err = b.ConfigurationTypes.New(fmt.Sprintf("%s://", b.Type)) + } else { + b.Values, err = b.ConfigurationTypes.New(*uri) + } + return +} + +func (b *Block) GetValue(key string) (any, error) { + return b.Values.GetValue(key) +} + +func (b *Block) Configuration() data.Configuration { + return b.Values +} + +func (b *Block) ConfigurationType() data.TypeName { + return data.TypeName(b.Type) +} + +func (b *Block) URI() string { + return b.Values.URI() +} + +func (b *Block) SetURI(uri string) (err error) { + if b.Values == nil { + if err = b.NewConfiguration(&uri); err != nil { + return + } + } + if b.Values == nil { + return fmt.Errorf("%w - %s", data.ErrUnknownConfigurationType, uri) + } + b.Type = TypeName(b.Values.Type()) + _,err = b.Values.Read(context.Background()) + return +} + +func (b *Block) UnmarshalValue(value *BlockType) error { + if b.ConfigurationTypes == nil { + panic(fmt.Errorf("Undefined type registry: unable to create new configuration %s", value.Type)) + } + b.Name = value.Name + if value.Type == "" { + b.Type = "generic" + } else { + b.Type = value.Type + } + + newConfig, configErr := b.ConfigurationTypes.New(fmt.Sprintf("%s://", b.Type)) + if configErr != nil { + return configErr + } + b.Values = newConfig + return nil +} + +func (b *Block) UnmarshalYAML(value *yaml.Node) error { + t := &BlockType{} + if unmarshalConfigurationTypeErr := value.Decode(t); unmarshalConfigurationTypeErr != nil { + return unmarshalConfigurationTypeErr + } + + if err := b.UnmarshalValue(t); err != nil { + return err + } + + configurationVals := struct { + Values yaml.Node `json:"values"` + }{} + if unmarshalValuesErr := value.Decode(&configurationVals); unmarshalValuesErr != nil { + return unmarshalValuesErr + } + if unmarshalConfigurationErr := configurationVals.Values.Decode(b.Values); unmarshalConfigurationErr != nil { + return unmarshalConfigurationErr + } + _, readErr := b.Values.Read(context.Background()) + return readErr +} + +func (b *Block) UnmarshalJSON(jsonData []byte) error { + t := &BlockType{} + if unmarshalConfigurationTypeErr := json.Unmarshal(jsonData, t); unmarshalConfigurationTypeErr != nil { + return unmarshalConfigurationTypeErr + } + + if err := b.UnmarshalValue(t); err != nil { + return err + } + + configurationVals := struct { + Values data.Configuration `json:"values"` + }{Values: b.Values} + if unmarshalValuesErr := json.Unmarshal(jsonData, &configurationVals); unmarshalValuesErr != nil { + return unmarshalValuesErr + } + _, readErr := b.Values.Read(context.Background()) + return readErr +} + +func (b *Block) MarshalYAML() (any, error) { + return b, nil +} diff --git a/internal/folio/block_test.go b/internal/folio/block_test.go new file mode 100644 index 0000000..00e5ae6 --- /dev/null +++ b/internal/folio/block_test.go @@ -0,0 +1,49 @@ +// Copyright 2024 Matthew Rich . All rights reserved. + +package folio + +import ( +_ "fmt" + "github.com/stretchr/testify/assert" + "log" + "os" + "testing" + "strings" +) + +var TempDir string + +func TestMain(m *testing.M) { + var err error + TempDir, err = os.MkdirTemp("", "testconfig") + if err != nil || TempDir == "" { + log.Fatal(err) + } + + rc := m.Run() + + os.RemoveAll(TempDir) + os.Exit(rc) +} + +func TestNewBlock(t *testing.T) { + configYaml := ` +name: "foo" +values: + http_user: "test" + http_pass: "password" +` + docReader := strings.NewReader(configYaml) + + block := NewBlock() + assert.NotNil(t, block) + assert.Nil(t, block.Load(docReader)) + assert.Equal(t, "foo", block.Name) + val, err := block.GetValue("http_user") + assert.Nil(t, err) + assert.Equal(t, "test", val) + + missingVal, missingErr := block.GetValue("content") + assert.ErrorIs(t, missingErr, ErrUnknownConfigurationKey) + assert.Nil(t, missingVal) +} diff --git a/internal/folio/declaration.go b/internal/folio/declaration.go new file mode 100644 index 0000000..8162757 --- /dev/null +++ b/internal/folio/declaration.go @@ -0,0 +1,311 @@ +// Copyright 2024 Matthew Rich . All rights reserved. + +package folio + +import ( +_ "errors" + "context" + "encoding/json" + "fmt" + "io" + "gopkg.in/yaml.v3" + "log/slog" +_ "gitea.rosskeen.house/rosskeen.house/machine" +//_ "gitea.rosskeen.house/pylon/luaruntime" + "decl/internal/codec" + "decl/internal/data" + "decl/internal/schema" +) + +type ConfigName string + +type DeclarationType struct { + Type TypeName `json:"type" yaml:"type"` + Transition string `json:"transition,omitempty" yaml:"transition,omitempty"` + Config ConfigName `json:"config,omitempty" yaml:"config,omitempty"` +} + +type Declaration struct { + Type TypeName `json:"type" yaml:"type"` + Transition string `json:"transition,omitempty" yaml:"transition,omitempty"` + Attributes data.Resource `json:"attributes" yaml:"attributes"` + Config ConfigName `json:"config,omitempty" yaml:"config,omitempty"` +// runtime luaruntime.LuaRunner + document *Document + configBlock data.Block + ResourceTypes data.TypesRegistry[data.Resource] `json:"-" yaml:"-"` +} + +func NewDeclaration() *Declaration { + return &Declaration{ ResourceTypes: DocumentRegistry.ResourceTypes } +} + +func NewDeclarationFromDocument(document *Document) *Declaration { + return &Declaration{ document: document, ResourceTypes: document.Types() } +} + +func (d *Declaration) SetDocument(newDocument *Document) { + slog.Info("Declaration.SetDocument()", "declaration", d) + d.document = newDocument + d.SetConfig(d.document.config) + d.ResourceTypes = d.document.Types() + d.Attributes.SetResourceMapper(d.document.uris) +} + +func (d *Declaration) ResolveId(ctx context.Context) string { + defer func() { + if r := recover(); r != nil { + slog.Info("Declaration.ResolveId() - panic", "recover", r, "state", d.Attributes.StateMachine()) + if triggerErr := d.Attributes.StateMachine().Trigger("notexists"); triggerErr != nil { + panic(triggerErr) + } + } + }() + slog.Info("Declaration.ResolveId()") + id := d.Attributes.ResolveId(ctx) + return id +} + +func (d *Declaration) Clone() data.Declaration { + return &Declaration { + Type: d.Type, + Transition: d.Transition, + Attributes: d.Attributes.Clone(), + //runtime: luaruntime.New(), + Config: d.Config, + } +} + +func (d *Declaration) Load(docData []byte, f codec.Format) (err error) { + err = f.StringDecoder(string(docData)).Decode(d) + return +} + +func (d *Declaration) LoadReader(r io.ReadCloser, f codec.Format) (err error) { + err = f.Decoder(r).Decode(d) + return +} + +func (d *Declaration) LoadString(docData string, f codec.Format) (err error) { + err = f.StringDecoder(docData).Decode(d) + return +} + +func (d *Declaration) ResourceType() data.TypeName { + return data.TypeName(d.Type) +} + +func (d *Declaration) URI() string { + return d.Attributes.URI() +} + +func (d *Declaration) JSON() ([]byte, error) { + return json.Marshal(d) +} + +func (d *Declaration) Validate() (err error) { + var declarationJson []byte + if declarationJson, err = d.JSON(); err == nil { + s := schema.New(fmt.Sprintf("%s-declaration", d.Type), schemaFiles) + err = s.Validate(string(declarationJson)) + } + return err +} + +func (d *Declaration) NewResource(uri *string) (err error) { + if d.ResourceTypes == nil { + panic(fmt.Errorf("Undefined type registry: unable to create new resource %s", *uri)) + } + if uri == nil { + d.Attributes, err = d.ResourceTypes.New(fmt.Sprintf("%s://", d.Type)) + } else { + d.Attributes, err = d.ResourceTypes.New(*uri) + } + return +} + +func (d *Declaration) Resource() data.Resource { + return d.Attributes +} + +func (d *Declaration) Apply() (result error) { + defer func() { + if r := recover(); r != nil { + result = fmt.Errorf("%s", r) + } + }() + + stater := d.Attributes.StateMachine() + slog.Info("Declaration.Apply()", "machine", stater, "machine.state", stater.CurrentState(), "uri", d.Attributes.URI()) + switch d.Transition { + case "construct": + if doc, ok := DocumentRegistry.DeclarationMap[d]; ok { + d.SetDocument(doc) + } + case "read": + result = stater.Trigger("read") + case "delete", "absent": + if stater.CurrentState() == "present" { + result = stater.Trigger("delete") + } + case "update": + if result = stater.Trigger("update"); result != nil { + return result + } + result = stater.Trigger("read") + default: + fallthrough + case "create", "present": + if stater.CurrentState() == "absent" || stater.CurrentState() == "unknown" { + if result = stater.Trigger("create"); result != nil { + return result + } + } + result = stater.Trigger("read") + } + return result +} + +func (d *Declaration) SetConfig(configDoc data.Document) { + if configDoc != nil { + if configDoc.Has(string(d.Config)) { + v, _ := configDoc.Get(string(d.Config)) + d.configBlock = v.(data.Block) + d.Attributes.UseConfig(d.configBlock) + } + } +} + +func (d *Declaration) SetURI(uri string) (err error) { + slog.Info("Declaration.SetURI()", "uri", uri, "declaration", d) + if d.Attributes == nil { + if err = d.NewResource(&uri); err != nil { + return + } + } + if d.Attributes == nil { + return fmt.Errorf("%w: %s", ErrUnknownResourceType, uri) + } + d.Type = TypeName(d.Attributes.Type()) + _, err = d.Attributes.Read(context.Background()) // fix context + slog.Info("Declaration.SetURI() - read", "error", err) + return +} + + +func (d *Declaration) UnmarshalValue(value *DeclarationType) error { + slog.Info("Declaration.UnmarshalValue", "declaration", d, "value", value, "addr", d) + if d.ResourceTypes == nil { + panic(fmt.Errorf("Undefined type registry: unable to create new resource %s", value.Type)) + } + d.Type = value.Type + d.Transition = value.Transition + d.Config = value.Config + newResource, resourceErr := d.ResourceTypes.New(fmt.Sprintf("%s://", value.Type)) + if resourceErr != nil { + slog.Info("Declaration.UnmarshalValue", "value", value, "error", resourceErr) + return resourceErr + } + d.Attributes = newResource + return nil +} + +func (d *Declaration) UnmarshalYAML(value *yaml.Node) error { + if d.ResourceTypes == nil { + d.ResourceTypes = DocumentRegistry.ResourceTypes + } + t := &DeclarationType{} + if unmarshalResourceTypeErr := value.Decode(t); unmarshalResourceTypeErr != nil { + return unmarshalResourceTypeErr + } + + if err := d.UnmarshalValue(t); err != nil { + return err + } + + resourceAttrs := struct { + Attributes yaml.Node `json:"attributes"` + }{} + if unmarshalAttributesErr := value.Decode(&resourceAttrs); unmarshalAttributesErr != nil { + return unmarshalAttributesErr + } + if unmarshalResourceErr := resourceAttrs.Attributes.Decode(d.Attributes); unmarshalResourceErr != nil { + return unmarshalResourceErr + } + return nil +} + +func (d *Declaration) UnmarshalJSON(jsonData []byte) error { + if d.ResourceTypes == nil { + d.ResourceTypes = DocumentRegistry.ResourceTypes + } + t := &DeclarationType{} + if unmarshalResourceTypeErr := json.Unmarshal(jsonData, t); unmarshalResourceTypeErr != nil { + return unmarshalResourceTypeErr + } + + if err := d.UnmarshalValue(t); err != nil { + return err + } + + resourceAttrs := struct { + Attributes data.Resource `json:"attributes"` + }{Attributes: d.Attributes} + if unmarshalAttributesErr := json.Unmarshal(jsonData, &resourceAttrs); unmarshalAttributesErr != nil { + return unmarshalAttributesErr + } + + return nil +} + +/* +func (d *Declaration) MarshalJSON() ([]byte, error) { + buf := new(bytes.Buffer) + buf.WriteByte('"') + buf.WriteString("value")) + buf.WriteByte('"') + return buf.Bytes(), nil +} +*/ + +/* +func (d *Declaration) MarshalYAML() (any, error) { + return d, nil +} +*/ + +/* +func (l *LuaWorker) Receive(m message.Envelope) { + s := m.Sender() + switch b := m.Body().(type) { + case *message.Error: + // case *worker.Terminated: + case *CodeExecute: + stackSize := l.runtime.Api().GetTop() + if e := l.runtime.LoadScriptFromString(b.Code); e != nil { + s.Send(message.New(&message.Error{ E: e }, l)) + } + returnsCount := l.runtime.Api().GetTop() - stackSize + if len(b.Entrypoint) == 0 { + if ! l.runtime.Api().IsNil(-1) { + if returnsCount == 0 { + s.Send(message.New(&CodeResult{ Result: []interface{}{ 0 } }, l)) + } else { + lr,le := l.runtime.CopyReturnValuesFromCall(int(returnsCount)) + if le != nil { + s.Send(message.New(&message.Error{ E: le }, l)) + } else { + s.Send(message.New(&CodeResult{ Result: lr }, l)) + } + } + } + } else { + r,ce := l.runtime.CallFunction(b.Entrypoint, b.Args) + if ce != nil { + s.Send(message.New(&message.Error{ E: ce }, l)) + } + s.Send(message.New(&CodeResult{ Result: r }, l)) + } + } +} +*/ diff --git a/internal/folio/declaration_test.go b/internal/folio/declaration_test.go new file mode 100644 index 0000000..437e9ac --- /dev/null +++ b/internal/folio/declaration_test.go @@ -0,0 +1,135 @@ +// Copyright 2024 Matthew Rich . All rights reserved. + + +package folio + +import ( +_ "encoding/json" + "fmt" + "github.com/stretchr/testify/assert" +_ "log" +_ "os" + "path/filepath" +_ "decl/internal/types" + "decl/internal/codec" + "testing" +) + +/* +func TestYamlLoadDecl(t *testing.T) { + + file := filepath.Join(TempDir, "fooread.txt") + + resourceAttributes := make(map[string]any) + decl := fmt.Sprintf(` + path: "%s" + owner: "nobody" + group: "nobody" + mode: "0600" + content: |- + test line 1 + test line 2 +`, file) + + e := YamlLoadDecl(decl, &resourceAttributes) + assert.Equal(t, nil, e) + + assert.Equal(t, "nobody", resourceAttributes["group"]) +} +*/ + +func TestNewResourceDeclaration(t *testing.T) { + resourceDeclaration := NewDeclaration() + assert.NotEqual(t, nil, resourceDeclaration) +} + +func TestNewResourceDeclarationType(t *testing.T) { + file := filepath.Join(TempDir, "fooread.txt") + + decl := fmt.Sprintf(` + type: foo + attributes: + name: "%s" +`, file) + + resourceDeclaration := NewDeclaration() + resourceDeclaration.ResourceTypes = TestResourceTypes + assert.NotNil(t, resourceDeclaration) + + e := resourceDeclaration.LoadString(decl, codec.FormatYaml) + assert.Nil(t, e) + assert.Equal(t, TypeName("foo"), resourceDeclaration.Type) + assert.NotNil(t, resourceDeclaration.Attributes) +} +/* +func TestDeclarationNewResource(t *testing.T) { + resourceDeclaration := NewDeclaration() + resourceDeclaration.ResourceTypes = TestResourceTypes + assert.NotNil(t, resourceDeclaration) + + errNewUnknownResource := resourceDeclaration.NewResource(nil) + assert.ErrorIs(t, errNewUnknownResource, types.ErrUnknownType) + + resourceDeclaration.Type = "foo" + errNewFileResource := resourceDeclaration.NewResource(nil) + assert.Nil(t, errNewFileResource) + + assert.NotNil(t, resourceDeclaration.Attributes) +} + +func TestDeclarationJson(t *testing.T) { + fileDeclJson := ` +{ + "type": "file", + "attributes": { + "path": "foo" + } +} +` + resourceDeclaration := NewDeclaration() + e := json.Unmarshal([]byte(fileDeclJson), resourceDeclaration) + assert.Nil(t, e) + assert.Equal(t, TypeName("file"), resourceDeclaration.Type) + + //assert.Equal(t, "foo", resourceDeclaration.Attributes.(*File).Path) + + userDeclJson := ` +{ + "type": "user", + "attributes": { + "name": "testuser", + "uid": "10012" + } +} +` + userResourceDeclaration := NewDeclaration() + ue := json.Unmarshal([]byte(userDeclJson), userResourceDeclaration) + assert.Nil(t, ue) + assert.Equal(t, TypeName("user"), userResourceDeclaration.Type) + //assert.Equal(t, "testuser", userResourceDeclaration.Attributes.(*User).Name) + //assert.Equal(t, "10012", userResourceDeclaration.Attributes.(*User).UID) + +} + +func TestDeclarationTransition(t *testing.T) { + fileName := filepath.Join(TempDir, "testdecl.txt") + fileDeclJson := fmt.Sprintf(` +{ + "type": "file", + "transition": "present", + "attributes": { + "path": "%s" + } +} +`, fileName) + + resourceDeclaration := NewDeclaration() + e := json.Unmarshal([]byte(fileDeclJson), resourceDeclaration) + assert.Nil(t, e) + assert.Equal(t, TypeName("file"), resourceDeclaration.Type) + //assert.Equal(t, fileName, resourceDeclaration.Attributes.(*File).Path) + err := resourceDeclaration.Apply() + assert.Nil(t, err) + assert.FileExists(t, fileName) +} +*/ diff --git a/internal/folio/document.go b/internal/folio/document.go new file mode 100644 index 0000000..48105de --- /dev/null +++ b/internal/folio/document.go @@ -0,0 +1,424 @@ +// Copyright 2024 Matthew Rich . 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 +} + diff --git a/internal/folio/document_test.go b/internal/folio/document_test.go new file mode 100644 index 0000000..593e9c3 --- /dev/null +++ b/internal/folio/document_test.go @@ -0,0 +1,208 @@ +// Copyright 2024 Matthew Rich . All rights reserved. + +package folio + +import ( + "fmt" + "github.com/stretchr/testify/assert" + "path/filepath" + "strings" + "testing" + "decl/internal/data" + "decl/internal/codec" + "decl/internal/types" + "io" + "log/slog" +) + +var ( + TestResourceTypes *types.Types[data.Resource] = types.New[data.Resource]() +) + +func TestNewDocument(t *testing.T) { + DocumentRegistry.ResourceTypes = TestResourceTypes + d := NewDocument(nil) + assert.NotNil(t, d) +} + +func TestDocumentInterface(t *testing.T) { + DocumentRegistry.ResourceTypes = TestResourceTypes + var doc data.Document = NewDocument(nil) + assert.NotNil(t, doc) +} + +func TestDocumentLoader(t *testing.T) { + DocumentRegistry.ResourceTypes = TestResourceTypes + slog.Info("TestDocumentLoader", "rt", TestResourceTypes) + + file, _ := filepath.Abs(filepath.Join(TempDir, "foo.txt")) + + document := fmt.Sprintf(` +--- +resources: +- type: foo + attributes: + name: "%s" + size: %d +- type: bar + attributes: + name: "testbar" + size: %d +`, file, 55, 11) + d := NewDocument(nil) + assert.NotNil(t, d) + assert.Equal(t, TestResourceTypes, d.Types()) + + docReader := io.NopCloser(strings.NewReader(document)) + + e := d.LoadReader(docReader, codec.FormatYaml) + assert.Nil(t, e) + + resources := d.Resources() + assert.Equal(t, 2, len(resources)) +} + +func TestDocumentGenerator(t *testing.T) { + DocumentRegistry.ResourceTypes = TestResourceTypes + f := NewFooResource() + assert.NotNil(t, f) + + expected := fmt.Sprintf(` +format: yaml +resources: +- type: foo + attributes: + name: %s + size: %d +`, "mytestresource", 3) + + var documentYaml strings.Builder + d := NewDocument(nil) + assert.NotNil(t, d) + + f.Name = "mytestresource" + f.Size = 3 + d.AddResourceDeclaration("foo", f) + + ey := d.Generate(&documentYaml) + assert.Nil(t, ey) + + assert.Greater(t, documentYaml.Len(), 0) + assert.YAMLEq(t, expected, documentYaml.String()) +} + +func TestDocumentAddResource(t *testing.T) { + DocumentRegistry.ResourceTypes = TestResourceTypes + + d := NewDocument(nil) + assert.NotNil(t, d) + e := d.AddResource("foo://bar") + assert.Nil(t, e) +} + +func TestDocumentJSON(t *testing.T) { + DocumentRegistry.ResourceTypes = TestResourceTypes + document := ` +--- +resources: +- type: foo + attributes: + name: "testfoo" + size: 10022 +` + d := NewDocument(nil) + assert.NotNil(t, d) + docReader := io.NopCloser(strings.NewReader(document)) + + e := d.LoadReader(docReader, codec.FormatYaml) + assert.Nil(t, e) + + marshalledJSON, jsonErr := d.JSON() + assert.Nil(t, jsonErr) + assert.Greater(t, len(marshalledJSON), 0) +} + +func TestDocumentJSONSchema(t *testing.T) { + DocumentRegistry.ResourceTypes = TestResourceTypes + document := NewDocument(nil) + document.ResourceDeclarations = []*Declaration{} + e := document.Validate() + assert.Nil(t, e) + + f := NewFooResource() + assert.NotNil(t, f) + f.Name = "mytestresource" + f.Size = 3 + document.AddResourceDeclaration("foo", f) + + resourceErr := document.Validate() + assert.ErrorContains(t, resourceErr, "Must be greater than or equal to 5") + + f.Size = 7 + + b := NewBarResource() + assert.NotNil(t, b) + b.Name = "testbarresource" + b.Size = 3 + b.Owner = "a" + document.AddResourceDeclaration("bar", b) + + assert.ErrorContains(t, document.Validate(), "String length must be greater than or equal to 3") +} + +func TestDocumentYAML(t *testing.T) { + DocumentRegistry.ResourceTypes = TestResourceTypes + document := ` +--- +format: yaml +resources: +- type: testuser + attributes: + name: "foo" + uid: "10022" + group: "10022" + home: "/home/foo" +` + d := NewDocument(nil) + assert.NotNil(t, d) + docReader := io.NopCloser(strings.NewReader(document)) + + e := d.LoadReader(docReader, codec.FormatYaml) + assert.Nil(t, e) + + marshalledYAML, yamlErr := d.YAML() + assert.Nil(t, yamlErr) + assert.YAMLEq(t, string(document), string(marshalledYAML)) + +} + +func TestDocumentResourceFilter(t *testing.T) { + DocumentRegistry.ResourceTypes = TestResourceTypes + document := ` +--- +resources: +- type: testuser + attributes: + name: "testuser" + uid: "10022" + home: "/home/testuser" +- type: foo + attributes: + name: "foo.txt" +- type: bar + attributes: + name: "bar.txt" +` + + d := NewDocument(nil) + assert.NotNil(t, d) + docReader := io.NopCloser(strings.NewReader(document)) + + e := d.LoadReader(docReader, codec.FormatYaml) + assert.Nil(t, e) + + resources := d.Filter(func(d data.Declaration) bool { + return d.ResourceType() == "foo" + }) + assert.Equal(t, 1, len(resources)) +} diff --git a/internal/folio/folio_test.go b/internal/folio/folio_test.go new file mode 100644 index 0000000..a26463c --- /dev/null +++ b/internal/folio/folio_test.go @@ -0,0 +1,60 @@ +// Copyright 2024 Matthew Rich . All rights reserved. + +package folio + +import ( +_ "fmt" +_ "github.com/stretchr/testify/assert" + "log" + "os" + "os/user" + "os/exec" +_ "path/filepath" + "testing" +) + +var TempDir string + +var ProcessTestUserName string +var ProcessTestGroupName string + +func TestMain(m *testing.M) { + var err error + TempDir, err = os.MkdirTemp("", "testfolio") + if err != nil || TempDir == "" { + log.Fatal(err) + } + + ProcessTestUserName, ProcessTestGroupName = ProcessUserName() + + RegisterMocks() + + rc := m.Run() + + os.RemoveAll(TempDir) + os.Exit(rc) +} + +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 "" +} diff --git a/internal/folio/mock_foo_resource_test.go b/internal/folio/mock_foo_resource_test.go new file mode 100644 index 0000000..a3ad24f --- /dev/null +++ b/internal/folio/mock_foo_resource_test.go @@ -0,0 +1,101 @@ +// Copyright 2024 Matthew Rich . All rights reserved. + +package folio + +import ( + "context" +_ "gopkg.in/yaml.v3" + "gitea.rosskeen.house/rosskeen.house/machine" + "decl/internal/codec" + "decl/internal/data" + "io" + "net/url" + "path/filepath" + "fmt" +) + +func RegisterMocks() { + TestResourceTypes.Register([]string{"foo"}, func(u *url.URL) data.Resource { + f := NewFooResource() + f.Name = filepath.Join(u.Hostname(), u.Path) + return f + }) + TestResourceTypes.Register([]string{"bar"}, func(u *url.URL) data.Resource { + f := NewBarResource() + f.Name = filepath.Join(u.Hostname(), u.Path) + return f + }) + TestResourceTypes.Register([]string{"testuser"}, func(u *url.URL) data.Resource { + f := NewTestuserResource() + f.Name = filepath.Join(u.Hostname(), u.Path) + return f + }) +} + + +type MockFoo struct { + stater machine.Stater `json:"-" yaml:"-"` + *MockResource `json:"-" yaml:"-"` + Name string `json:"name" yaml:"name"` + Size int `json:"size" yaml:"size"` +} + +type MockBar struct { + stater machine.Stater `json:"-" yaml:"-"` + *MockResource `json:"-" yaml:"-"` + Name string `json:"name" yaml:"name"` + Size int `json:"size,omitempty" yaml:"size,omitempty"` + Owner string `json:"owner,omitempty" yaml:"owner,omitempty"` +} + +type MockTestuser struct { + stater machine.Stater `json:"-" yaml:"-"` + *MockResource `json:"-" yaml:"-"` + Name string `json:"name" yaml:"name"` + Uid string `json:"uid" yaml:"uid"` + Group string `json:"group" yaml:"group"` + Home string `json:"home" yaml:"home"` +} + +func NewMockResource(typename string, stater machine.Stater) *MockResource { + return &MockResource { + InjectType: func() string { return typename }, + InjectResolveId: func(ctx context.Context) string { return "bar" }, + InjectLoadDecl: func(string) error { return nil }, + InjectLoadString: func(string, codec.Format) (error) { return nil }, + InjectLoad: func([]byte, codec.Format) (error) { return nil }, + InjectLoadReader: func(io.ReadCloser, codec.Format) (error) { return nil }, + InjectApply: func() error { return nil }, + InjectStateMachine: func() machine.Stater { return stater }, + InjectValidate: func() error { return nil }, + InjectCreate: func(context.Context) error { return nil }, + InjectRead: func(context.Context) ([]byte, error) { return nil, nil }, + InjectUpdate: func(context.Context) error { return nil }, + InjectDelete: func(context.Context) error { return nil }, + InjectUseConfig: func(data.ConfigurationValueGetter) {}, + InjectSetResourceMapper: func(data.ResourceMapper) {}, + InjectURI: func() string { return fmt.Sprintf("%s://bar", typename) }, + InjectNotify: func(*machine.EventMessage) {}, + } +} + +func NewFooResource() *MockFoo { + f := &MockFoo {} + f.stater = data.StorageMachine(f) + f.MockResource = NewMockResource("foo", f.stater) + return f +} + +func NewBarResource() *MockBar { + b := &MockBar {} + b.stater = data.StorageMachine(b) + b.MockResource = NewMockResource("bar", b.stater) + return b +} + +func NewTestuserResource() *MockTestuser { + u := &MockTestuser {} + u.stater = data.StorageMachine(u) + u.MockResource = NewMockResource("testuser", u.stater) + return u +} diff --git a/internal/folio/mock_resource_test.go b/internal/folio/mock_resource_test.go new file mode 100644 index 0000000..ee3c748 --- /dev/null +++ b/internal/folio/mock_resource_test.go @@ -0,0 +1,137 @@ +// Copyright 2024 Matthew Rich . All rights reserved. + +package folio + +import ( + "context" +_ "gopkg.in/yaml.v3" + "encoding/json" +_ "fmt" + "gitea.rosskeen.house/rosskeen.house/machine" + "decl/internal/data" + "decl/internal/codec" + "io" +) + +type MockResource struct { + InjectURI func() string `json:"-" yaml:"-"` + InjectType func() string `json:"-" yaml:"-"` + InjectResolveId func(ctx context.Context) string `json:"-" yaml:"-"` + InjectLoadDecl func(string) error `json:"-" yaml:"-"` + InjectValidate func() error `json:"-" yaml:"-"` + InjectApply func() error `json:"-" yaml:"-"` + InjectJSON func() ([]byte, error) `json:"-" yaml:"-"` + InjectYAML func() ([]byte, error) `json:"-" yaml:"-"` + InjectPB func() ([]byte, error) `json:"-" yaml:"-"` + InjectGenerate func(w io.Writer) (error) `json:"-" yaml:"-"` + InjectLoadString func(string, codec.Format) (error) `json:"-" yaml:"-"` + InjectLoad func([]byte, codec.Format) (error) `json:"-" yaml:"-"` + InjectLoadReader func(io.ReadCloser, codec.Format) (error) `json:"-" yaml:"-"` + InjectCreate func(context.Context) error `json:"-" yaml:"-"` + InjectRead func(context.Context) ([]byte, error) `json:"-" yaml:"-"` + InjectUpdate func(context.Context) error `json:"-" yaml:"-"` + InjectDelete func(context.Context) error `json:"-" yaml:"-"` + InjectStateMachine func() machine.Stater `json:"-" yaml:"-"` + InjectSetResourceMapper func(data.ResourceMapper) `json:"-" yaml:"-"` + InjectUseConfig func(data.ConfigurationValueGetter) `json:"-" yaml:"-"` + InjectNotify func(*machine.EventMessage) `json:"-" yaml:"-"` +} + +func (m *MockResource) Clone() data.Resource { + return nil +} + +func (m *MockResource) StateMachine() machine.Stater { + return nil +} + +func (m *MockResource) UseConfig(config data.ConfigurationValueGetter) { + m.InjectUseConfig(config) +} + +func (m *MockResource) SetURI(uri string) error { + return nil +} + +func (m *MockResource) URI() string { + return m.InjectURI() +} + +func (m *MockResource) ResolveId(ctx context.Context) string { + return m.InjectResolveId(ctx) +} + +func (m *MockResource) SetResourceMapper(rm data.ResourceMapper) { + m.InjectSetResourceMapper(rm) +} + +func (m *MockResource) LoadDecl(yamlResourceDeclaration string) error { + return m.InjectLoadDecl(yamlResourceDeclaration) +} + +func (m *MockResource) JSON() ([]byte, error) { + return m.InjectJSON() +} + +func (m *MockResource) YAML() ([]byte, error) { + return m.InjectYAML() +} + +func (m *MockResource) PB() ([]byte, error) { + return m.InjectPB() +} + +func (m *MockResource) Generate(w io.Writer) (error) { + return m.InjectGenerate(w) +} + +func (m *MockResource) LoadString(docData string, format codec.Format) (error) { + return m.InjectLoadString(docData, format) +} + +func (m *MockResource) Load(docData []byte, format codec.Format) (error) { + return m.InjectLoad(docData, format) +} + +func (m *MockResource) LoadReader(r io.ReadCloser, format codec.Format) (error) { + return m.InjectLoadReader(r, format) +} + +func (m *MockResource) Create(ctx context.Context) error { + return m.InjectCreate(ctx) +} + +func (m *MockResource) Read(ctx context.Context) ([]byte, error) { + return m.InjectRead(ctx) +} + +func (m *MockResource) Update(ctx context.Context) error { + return m.InjectUpdate(ctx) +} + +func (m *MockResource) Delete(ctx context.Context) error { + return m.InjectDelete(ctx) +} + +func (m *MockResource) Validate() error { + return m.InjectValidate() +} + +func (m *MockResource) Apply() error { + return m.InjectApply() +} + +func (m *MockResource) Type() string { + return m.InjectType() +} + +func (m *MockResource) Notify(em *machine.EventMessage) { + m.InjectNotify(em) +} + +func (m *MockResource) UnmarshalJSON(data []byte) error { + if err := json.Unmarshal(data, m); err != nil { + return err + } + return nil +} diff --git a/internal/folio/registry.go b/internal/folio/registry.go new file mode 100644 index 0000000..d038d34 --- /dev/null +++ b/internal/folio/registry.go @@ -0,0 +1,75 @@ +// Copyright 2024 Matthew Rich . All rights reserved. + +package folio + +import ( +_ "errors" +_ "fmt" +_ "net/url" +_ "strings" + "decl/internal/types" + "decl/internal/data" + "decl/internal/mapper" + "io/fs" +) + +var ( +) + +type Registry struct { + Schemas mapper.Store[URI, fs.FS] + ConfigurationTypes *types.Types[data.Configuration] // Config Factory + ResourceTypes *types.Types[data.Resource] // Resource Factory + ConverterTypes *types.Types[data.Converter] // Converter Factory + Documents []*Document + UriMap mapper.Store[URI, *Document] + DeclarationMap mapper.Store[*Declaration, *Document] + ConfigurationMap mapper.Store[*Block, *Document] + DefaultSchema URI +} + +func NewRegistry() *Registry { + r := &Registry{ + ConfigurationTypes: types.New[data.Configuration](), + ResourceTypes: types.New[data.Resource](), + ConverterTypes: types.New[data.Converter](), + Documents: make([]*Document, 0, 10), + UriMap: mapper.New[URI, *Document](), + DeclarationMap: mapper.New[*Declaration, *Document](), + ConfigurationMap: mapper.New[*Block, *Document](), + Schemas: mapper.New[URI, fs.FS](), + DefaultSchema: schemaFilesUri, + } + r.Schemas[schemaFilesUri] = schemaFiles + return r +} + +func (r *Registry) Get(key *Declaration) (*Document, bool) { + return r.DeclarationMap.Get(key) +} + +func (r *Registry) Has(key *Declaration) (bool) { + return r.DeclarationMap.Has(key) +} + +func (r *Registry) NewDocument(uri URI) (doc *Document) { + doc = NewDocument(r) + r.Documents = append(r.Documents, doc) + if uri != "" { + r.UriMap[uri] = doc + } + return +} + +func (r *Registry) Load(uri URI) (documents []data.Document, err error) { + var extractor data.Converter + var sourceResource data.Resource + if extractor, err = r.ConverterTypes.New(string(uri)); err == nil { + if sourceResource, err = uri.NewResource(nil); err == nil { + documents, err = extractor.(data.ManyExtractor).ExtractMany(sourceResource, nil) + } + } + return +} + + diff --git a/internal/folio/registry_test.go b/internal/folio/registry_test.go new file mode 100644 index 0000000..6adb0fd --- /dev/null +++ b/internal/folio/registry_test.go @@ -0,0 +1,54 @@ +// Copyright 2024 Matthew Rich . All rights reserved. + +package folio + +import ( + "github.com/stretchr/testify/assert" + "testing" + "decl/internal/mapper" + "log/slog" +) + +// Registry maps documents +// lookup document by uri +// lookup document by declaration +// generate declaration for document/config +func TestNewRegistry(t *testing.T) { + r := NewRegistry() + assert.NotNil(t, r) + + r.ResourceTypes = TestResourceTypes + var docs mapper.Getter[*Declaration, *Document] = r + + decl := NewDeclaration() + _, notexists := docs.Get(decl) + assert.False(t, notexists) + + doc := NewDocument(r) + res, e := doc.NewResource("foo://") + assert.Nil(t, e) + slog.Info("TestNewRegistry", "doc", doc) + + assert.Equal(t, doc.ResourceDeclarations[0].Attributes, res) + r.DeclarationMap[doc.ResourceDeclarations[0]] = doc + _, exists := docs.Get(doc.ResourceDeclarations[0]) + assert.True(t, exists) + +} + +func TestRegistryResourceTypes(t *testing.T) { + r := NewRegistry() + assert.NotNil(t, r) + r.ResourceTypes = TestResourceTypes + doc := r.NewDocument("") + assert.NotNil(t, doc) + res, e := doc.NewResource("foo://") + assert.Nil(t, e) + assert.NotNil(t, res) + decl := doc.ResourceDeclarations[0] + assert.Equal(t, decl.Attributes, res) + assert.Equal(t, TypeName("foo"), decl.Type) + mappedDoc, ok := r.DeclarationMap[decl] + assert.True(t, ok) + assert.Equal(t, doc, mappedDoc) +} diff --git a/internal/folio/resourcereference.go b/internal/folio/resourcereference.go new file mode 100644 index 0000000..f98b558 --- /dev/null +++ b/internal/folio/resourcereference.go @@ -0,0 +1,69 @@ +// Copyright 2024 Matthew Rich . All rights reserved. + +package folio + +import ( + "decl/internal/transport" + "decl/internal/data" + "errors" + "log/slog" + "net/url" +) + + +var ( + ErrInvalidResourceURI error = errors.New("Invalid resource URI") +) + +type ContentReader interface { + ContentReaderStream() (*transport.Reader, error) +} + +type ContentWriter interface { + ContentWriterStream() (*transport.Writer, error) +} + +type ContentReadWriter interface { + ContentReader + ContentWriter +} + +type ResourceReference URI + +// Return a Content ReadWriter for the resource referred to. +func (r ResourceReference) Lookup(look data.ResourceMapper) ContentReadWriter { + slog.Info("ResourceReference.Lookup()", "resourcereference", r, "resourcemapper", look) + if look != nil { + if v,ok := look.Get(string(r)); ok { + return v.(ContentReadWriter) + } + } + return r +} + +func (r ResourceReference) Dereference(look data.ResourceMapper) data.Resource { + slog.Info("ResourceReference.Dereference()", "resourcereference", r, "resourcemapper", look) + if look != nil { + if v,ok := look.Get(string(r)); ok { + slog.Info("ResourceReference.Dereference()", "resourcereference", r, "result", v) + return v.(*Declaration).Attributes + } + } + return nil +} + +func (r ResourceReference) Parse() *url.URL { + return URI(r).Parse() +} + +func (r ResourceReference) Exists() bool { + return URI(r).Exists() +} + +func (r ResourceReference) ContentReaderStream() (*transport.Reader, error) { + return URI(r).ContentReaderStream() +} + +func (r ResourceReference) ContentWriterStream() (*transport.Writer, error) { + return URI(r).ContentWriterStream() +} diff --git a/internal/folio/resourcereference_test.go b/internal/folio/resourcereference_test.go new file mode 100644 index 0000000..b38b8a5 --- /dev/null +++ b/internal/folio/resourcereference_test.go @@ -0,0 +1,30 @@ +// Copyright 2024 Matthew Rich . All rights reserved. + +package folio + +import ( + "github.com/stretchr/testify/assert" + "testing" + "fmt" + "decl/internal/data" + "decl/internal/mapper" +) + +func TestResourceReference(t *testing.T) { + f := NewFooResource() + resourceMapper := mapper.New[string, data.Declaration]() + f.Name = TempDir + f.Size = 10 + d := NewDeclaration() + d.Type = "foo" + d.Attributes = f + resourceMapper[d.URI()] = d + + var foo ResourceReference = ResourceReference(fmt.Sprintf("foo://%s", TempDir)) + u := foo.Parse() + assert.Equal(t, "foo", u.Scheme) + assert.True(t, foo.Exists()) + + fromRef := foo.Lookup(resourceMapper) + assert.NotNil(t, fromRef) +} diff --git a/internal/folio/schema.go b/internal/folio/schema.go new file mode 100644 index 0000000..c16fd66 --- /dev/null +++ b/internal/folio/schema.go @@ -0,0 +1,12 @@ +// Copyright 2024 Matthew Rich . All rights reserved. + +package folio + +import ( + "embed" +) + +var schemaFilesUri URI = "file://folio/schemas/*.schema.json" + +//go:embed schemas/*.schema.json +var schemaFiles embed.FS diff --git a/internal/folio/schemas/bar-declaration.schema.json b/internal/folio/schemas/bar-declaration.schema.json new file mode 100644 index 0000000..9e2977f --- /dev/null +++ b/internal/folio/schemas/bar-declaration.schema.json @@ -0,0 +1,23 @@ +{ + "$id": "bar-declaration.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "bar-declaration", + "type": "object", + "required": [ "type", "attributes" ], + "properties": { + "type": { + "type": "string", + "description": "Resource type name.", + "enum": [ "bar" ] + }, + "config": { + "type": "string", + "description": "Config name" + }, + "attributes": { + "oneOf": [ + { "$ref": "bar.schema.json" } + ] + } + } +} diff --git a/internal/folio/schemas/bar.schema.json b/internal/folio/schemas/bar.schema.json new file mode 100644 index 0000000..2664d34 --- /dev/null +++ b/internal/folio/schemas/bar.schema.json @@ -0,0 +1,22 @@ +{ + "$id": "bar.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "bar", + "type": "object", + "required": [ "name" ], + "properties": { + "name": { + "type": "string", + "description": "bar name", + "minLength": 1 + }, + "size": { + "type": "integer", + "minimum": 1 + }, + "owner": { + "type": "string", + "minLength": 3 + } + } +} diff --git a/internal/folio/schemas/declaration.schema.json b/internal/folio/schemas/declaration.schema.json new file mode 100644 index 0000000..84147d1 --- /dev/null +++ b/internal/folio/schemas/declaration.schema.json @@ -0,0 +1,23 @@ +{ + "$id": "foo-declaration.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "foo-declaration", + "type": "object", + "required": [ "type", "attributes" ], + "properties": { + "type": { + "type": "string", + "description": "Resource type name.", + "enum": [ "foo" ] + }, + "config": { + "type": "string", + "description": "Config name" + }, + "attributes": { + "oneOf": [ + { "$ref": "bar.schema.json" } + ] + } + } +} diff --git a/internal/folio/schemas/document.schema.json b/internal/folio/schemas/document.schema.json new file mode 100644 index 0000000..07c1354 --- /dev/null +++ b/internal/folio/schemas/document.schema.json @@ -0,0 +1,20 @@ +{ + "$id": "document.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "document", + "type": "object", + "required": [ "resources" ], + "properties": { + "resources": { + "type": "array", + "description": "Resources list", + "items": { + "oneOf": [ + { "$ref": "foo-declaration.schema.json" }, + { "$ref": "bar-declaration.schema.json" } + ] + } + } + } +} + diff --git a/internal/folio/schemas/foo-declaration.schema.json b/internal/folio/schemas/foo-declaration.schema.json new file mode 100644 index 0000000..0a45d41 --- /dev/null +++ b/internal/folio/schemas/foo-declaration.schema.json @@ -0,0 +1,23 @@ +{ + "$id": "foo-declaration.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "foo-declaration", + "type": "object", + "required": [ "type", "attributes" ], + "properties": { + "type": { + "type": "string", + "description": "Resource type name.", + "enum": [ "foo" ] + }, + "config": { + "type": "string", + "description": "Config name" + }, + "attributes": { + "oneOf": [ + { "$ref": "foo.schema.json" } + ] + } + } +} diff --git a/internal/folio/schemas/foo.schema.json b/internal/folio/schemas/foo.schema.json new file mode 100644 index 0000000..f91eee0 --- /dev/null +++ b/internal/folio/schemas/foo.schema.json @@ -0,0 +1,18 @@ +{ + "$id": "foo.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "foo", + "type": "object", + "required": [ "name", "size" ], + "properties": { + "name": { + "type": "string", + "description": "foo name", + "minLength": 1 + }, + "size": { + "type": "integer", + "minimum": 5 + } + } +} diff --git a/internal/folio/types.go b/internal/folio/types.go new file mode 100644 index 0000000..f216773 --- /dev/null +++ b/internal/folio/types.go @@ -0,0 +1,29 @@ +// Copyright 2024 Matthew Rich . All rights reserved. + +package folio + +import ( + "errors" + "fmt" +_ "net/url" + "strings" + "decl/internal/types" + "decl/internal/data" +) + +var ( + ErrUnknownResourceType = errors.New("Unknown resource type") + ResourceTypes *types.Types[data.Resource] = types.New[data.Resource]() + DocumentRegistry *Registry = NewRegistry() +) + +type TypeName data.TypeName //`json:"type"` + +func (n *TypeName) UnmarshalJSON(b []byte) error { + ResourceTypeName := strings.Trim(string(b), "\"") + if DocumentRegistry.ResourceTypes.Has(ResourceTypeName) { + *n = TypeName(ResourceTypeName) + return nil + } + return fmt.Errorf("%w: %s", ErrUnknownResourceType, ResourceTypeName) +} diff --git a/internal/folio/uri.go b/internal/folio/uri.go index c2d5f51..099e645 100644 --- a/internal/folio/uri.go +++ b/internal/folio/uri.go @@ -21,7 +21,7 @@ func (u URI) NewResource(document data.Document) (newResource data.Resource, err if document == nil { declaration := NewDeclaration() if err = declaration.NewResource((*string)(&u)); err == nil { - return declaration.Attributes.(data.Resource), err + return declaration.Attributes, err } } else { newResource, err = document.NewResource(string(u)) diff --git a/internal/folio/uri_test.go b/internal/folio/uri_test.go index 80e3e7a..1c21f56 100644 --- a/internal/folio/uri_test.go +++ b/internal/folio/uri_test.go @@ -13,4 +13,27 @@ func TestURI(t *testing.T) { u := file.Parse() assert.Equal(t, "file", u.Scheme) assert.True(t, file.Exists()) + file = URI(fmt.Sprintf("0file:_/%s", TempDir)) + u = file.Parse() + assert.Nil(t, u) } + +func TestURISetURL(t *testing.T) { + var file URI = URI(fmt.Sprintf("file://%s", TempDir)) + u := file.Parse() + var fileFromURL URI + fileFromURL.SetURL(u) + assert.Equal(t, fileFromURL, file) + exttype, ext := file.Extension() + assert.Equal(t, "", exttype) + assert.Equal(t, "", ext) +} + +func TestURINewResource(t *testing.T) { + var file URI = URI(fmt.Sprintf("foo://%s", TempDir)) + resource, err := file.NewResource(nil) + assert.Nil(t, err) + assert.NotNil(t, resource) + +} +