Compare commits
2 Commits
3086885655
...
04b27dc5df
Author | SHA1 | Date | |
---|---|---|---|
04b27dc5df | |||
dfd2a00541 |
192
internal/folio/block.go
Normal file
192
internal/folio/block.go
Normal file
@ -0,0 +1,192 @@
|
||||
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. 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
|
||||
}
|
49
internal/folio/block_test.go
Normal file
49
internal/folio/block_test.go
Normal file
@ -0,0 +1,49 @@
|
||||
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. 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)
|
||||
}
|
311
internal/folio/declaration.go
Normal file
311
internal/folio/declaration.go
Normal file
@ -0,0 +1,311 @@
|
||||
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
135
internal/folio/declaration_test.go
Normal file
135
internal/folio/declaration_test.go
Normal file
@ -0,0 +1,135 @@
|
||||
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. 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)
|
||||
}
|
||||
*/
|
424
internal/folio/document.go
Normal file
424
internal/folio/document.go
Normal file
@ -0,0 +1,424 @@
|
||||
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. 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
|
||||
}
|
||||
|
208
internal/folio/document_test.go
Normal file
208
internal/folio/document_test.go
Normal file
@ -0,0 +1,208 @@
|
||||
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. 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))
|
||||
}
|
60
internal/folio/folio_test.go
Normal file
60
internal/folio/folio_test.go
Normal file
@ -0,0 +1,60 @@
|
||||
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. 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 ""
|
||||
}
|
101
internal/folio/mock_foo_resource_test.go
Normal file
101
internal/folio/mock_foo_resource_test.go
Normal file
@ -0,0 +1,101 @@
|
||||
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. 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
|
||||
}
|
137
internal/folio/mock_resource_test.go
Normal file
137
internal/folio/mock_resource_test.go
Normal file
@ -0,0 +1,137 @@
|
||||
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. 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
|
||||
}
|
75
internal/folio/registry.go
Normal file
75
internal/folio/registry.go
Normal file
@ -0,0 +1,75 @@
|
||||
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. 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
|
||||
}
|
||||
|
||||
|
54
internal/folio/registry_test.go
Normal file
54
internal/folio/registry_test.go
Normal file
@ -0,0 +1,54 @@
|
||||
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. 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)
|
||||
}
|
69
internal/folio/resourcereference.go
Normal file
69
internal/folio/resourcereference.go
Normal file
@ -0,0 +1,69 @@
|
||||
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. 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()
|
||||
}
|
30
internal/folio/resourcereference_test.go
Normal file
30
internal/folio/resourcereference_test.go
Normal file
@ -0,0 +1,30 @@
|
||||
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. 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)
|
||||
}
|
12
internal/folio/schema.go
Normal file
12
internal/folio/schema.go
Normal file
@ -0,0 +1,12 @@
|
||||
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||
|
||||
package folio
|
||||
|
||||
import (
|
||||
"embed"
|
||||
)
|
||||
|
||||
var schemaFilesUri URI = "file://folio/schemas/*.schema.json"
|
||||
|
||||
//go:embed schemas/*.schema.json
|
||||
var schemaFiles embed.FS
|
23
internal/folio/schemas/bar-declaration.schema.json
Normal file
23
internal/folio/schemas/bar-declaration.schema.json
Normal file
@ -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" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
22
internal/folio/schemas/bar.schema.json
Normal file
22
internal/folio/schemas/bar.schema.json
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
}
|
23
internal/folio/schemas/declaration.schema.json
Normal file
23
internal/folio/schemas/declaration.schema.json
Normal file
@ -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" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
20
internal/folio/schemas/document.schema.json
Normal file
20
internal/folio/schemas/document.schema.json
Normal file
@ -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" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
23
internal/folio/schemas/foo-declaration.schema.json
Normal file
23
internal/folio/schemas/foo-declaration.schema.json
Normal file
@ -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" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
18
internal/folio/schemas/foo.schema.json
Normal file
18
internal/folio/schemas/foo.schema.json
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
}
|
29
internal/folio/types.go
Normal file
29
internal/folio/types.go
Normal file
@ -0,0 +1,29 @@
|
||||
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. 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)
|
||||
}
|
@ -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))
|
||||
|
@ -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)
|
||||
|
||||
}
|
||||
|
||||
|
@ -34,9 +34,10 @@ func (i ID) Extension() (exttype string, fileext string) {
|
||||
if numberOfElements > 2 {
|
||||
exttype = elements[numberOfElements - 2]
|
||||
fileext = elements[numberOfElements - 1]
|
||||
}
|
||||
} else {
|
||||
exttype = elements[numberOfElements - 1]
|
||||
}
|
||||
return
|
||||
}
|
||||
return exttype, fileext
|
||||
}
|
||||
|
||||
|
@ -34,6 +34,22 @@ func TestID(t *testing.T) {
|
||||
assert.Equal(t, "", fileext)
|
||||
}
|
||||
|
||||
func TestIDExt(t *testing.T) {
|
||||
for _, v := range []struct { File ID
|
||||
ExpectedExt string
|
||||
ExpectedType string }{
|
||||
{ File: "file:///tmp/foo/bar/baz.txt", ExpectedExt: "", ExpectedType: "txt" },
|
||||
{ File: "file:///tmp/foo/bar/baz.txt.gz", ExpectedExt: "gz", ExpectedType: "txt" },
|
||||
{ File: "file:///tmp/foo/bar/baz.quuz.txt.gz", ExpectedExt: "gz", ExpectedType: "txt" },
|
||||
} {
|
||||
u := v.File.Parse()
|
||||
assert.Equal(t, "file", u.Scheme)
|
||||
filetype, fileext := v.File.Extension()
|
||||
assert.Equal(t, v.ExpectedType, filetype)
|
||||
assert.Equal(t, v.ExpectedExt, fileext)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetID(t *testing.T) {
|
||||
var file ID = ID(fmt.Sprintf("file://%s", TempDir))
|
||||
u := file.Parse()
|
||||
|
Loading…
Reference in New Issue
Block a user