From 06f3247b0880a8fe795aff299555cc67fe3594e9 Mon Sep 17 00:00:00 2001 From: Matthew Rich Date: Thu, 15 Aug 2024 08:12:42 -0700 Subject: [PATCH] add interfaces --- internal/codec/decoder.go | 23 +++++++-- internal/codec/decoder_test.go | 41 +++++++++++++-- internal/codec/types.go | 4 ++ internal/data/config.go | 10 ++++ internal/data/converter.go | 26 ++++++++++ internal/data/data.go | 2 +- internal/data/document.go | 51 +++++++++++++++++++ internal/data/identifier.go | 22 ++++++++ internal/data/resource.go | 91 ++++++++++++++++++++++++++++++++++ internal/data/stater.go | 50 +++++++++++++++++++ internal/data/types.go | 23 +++++++++ internal/folio/uri.go | 64 ++++++++++++++++++++++++ internal/folio/uri_test.go | 16 ++++++ internal/identifier/id.go | 42 ++++++++++++++++ internal/identifier/id_test.go | 44 ++++++++++++++++ 15 files changed, 500 insertions(+), 9 deletions(-) create mode 100644 internal/data/config.go create mode 100644 internal/data/converter.go create mode 100644 internal/data/document.go create mode 100644 internal/data/identifier.go create mode 100644 internal/data/resource.go create mode 100644 internal/data/stater.go create mode 100644 internal/data/types.go create mode 100644 internal/folio/uri.go create mode 100644 internal/folio/uri_test.go create mode 100644 internal/identifier/id.go create mode 100644 internal/identifier/id_test.go diff --git a/internal/codec/decoder.go b/internal/codec/decoder.go index 9f89d54..9a03a03 100644 --- a/internal/codec/decoder.go +++ b/internal/codec/decoder.go @@ -8,8 +8,9 @@ _ "fmt" _ "github.com/xeipuuv/gojsonschema" "gopkg.in/yaml.v3" "io" -_ "log" + "log/slog" "strings" + "google.golang.org/protobuf/proto" ) //type JSONDecoder json.Decoder @@ -30,6 +31,10 @@ func NewDecoder(r io.Reader, format Format) Decoder { return nil } +func NewStringDecoder(s string, format Format) Decoder { + return NewDecoder(strings.NewReader(s), format) +} + func NewJSONDecoder(r io.Reader) Decoder { return json.NewDecoder(r) } @@ -39,6 +44,7 @@ func NewJSONStringDecoder(s string) Decoder { } func NewYAMLDecoder(r io.Reader) Decoder { + slog.Info("NewYAMLDecoder()", "reader", r) return yaml.NewDecoder(r) } @@ -46,6 +52,17 @@ func NewYAMLStringDecoder(s string) Decoder { return yaml.NewDecoder(strings.NewReader(s)) } -func NewProtoBufDecoder(r io.Reader) Decoder { - return nil +type ProtoDecoder struct { + reader io.Reader +} + +func (p *ProtoDecoder) Decode(v any) (err error) { + var protoData []byte + protoData, err = io.ReadAll(p.reader) + err = proto.Unmarshal(protoData, v.(proto.Message)) + return +} + +func NewProtoBufDecoder(r io.Reader) Decoder { + return &ProtoDecoder{ reader: r } } diff --git a/internal/codec/decoder_test.go b/internal/codec/decoder_test.go index 7533659..d4c9192 100644 --- a/internal/codec/decoder_test.go +++ b/internal/codec/decoder_test.go @@ -9,14 +9,17 @@ _ "log" "strings" "testing" "github.com/xeipuuv/gojsonschema" + "io" + "bytes" + "google.golang.org/protobuf/proto" ) type TestUser struct { - Name string `json:"name" yaml:"name"` - Uid string `json:"uid" yaml:"uid"` - Group string `json:"group" yaml:"group"` - Home string `json:"home" yaml:"home"` - State string `json:"state" yaml:"state"` + Name string `json:"name" yaml:"name" protobuf:"bytes,1,opt,name=name"` + Uid string `json:"uid" yaml:"uid" protobuf:"bytes,2,opt,name=uid"` + Group string `json:"group" yaml:"group" protobuf:"bytes,3,opt,name=group"` + Home string `json:"home" yaml:"home" protobuf:"bytes,4,opt,name=home"` + State string `json:"state" yaml:"state" protobuf:"bytes,5,opt,name=state"` } func TestNewYAMLDecoder(t *testing.T) { @@ -78,3 +81,31 @@ func TestNewJSONStringDecoder(t *testing.T) { docErr := e.Decode(&TestUser{}) assert.Nil(t, docErr) } + +func TestNewDecoder(t *testing.T) { + pbData, err := proto.Marshal(&TestUser{ Name: "pb", Uid: "15001", Group: "15005", Home: "/home/pb", State: "present" }) + assert.Nil(t, err) + for _, v := range []struct{ reader io.Reader; format Format; expectedhome string } { + { reader: strings.NewReader(`{ + "name": "testuser", + "uid": "12001", + "group": "12001", + "home": "/home/testuser", + "state": "present" }`), format: FormatJson, expectedhome: "/home/testuser" }, + { reader: strings.NewReader(` + name: "testuser" + uid: "12001" + group: "12001" + home: "/home/test" + state: "present" +`), format: FormatYaml, expectedhome: "/home/test" }, + { reader: bytes.NewReader(pbData), format: FormatProtoBuf, expectedhome: "/home/pb" }, + } { + + decoder := NewDecoder(v.reader, v.format) + assert.NotNil(t, decoder) + u := &TestUser{} + assert.Nil(t, decoder.Decode(u)) + assert.Equal(t, v.expectedhome, u.Home ) + } +} diff --git a/internal/codec/types.go b/internal/codec/types.go index 49fe0fb..232beb3 100644 --- a/internal/codec/types.go +++ b/internal/codec/types.go @@ -70,6 +70,10 @@ func (f Format) Decoder(r io.Reader) Decoder { return NewDecoder(r, f) } +func (f Format) StringDecoder(s string) Decoder { + return NewStringDecoder(s, f) +} + func (f Format) Serialize(object any, w io.Writer) error { return f.Encoder(w).Encode(object) } diff --git a/internal/data/config.go b/internal/data/config.go new file mode 100644 index 0000000..215a204 --- /dev/null +++ b/internal/data/config.go @@ -0,0 +1,10 @@ +// Copyright 2024 Matthew Rich . All rights reserved. + +package data + +import ( +) + +type ConfigurationValueGetter interface { + GetValue(key string) (any, error) +} diff --git a/internal/data/converter.go b/internal/data/converter.go new file mode 100644 index 0000000..91ad83d --- /dev/null +++ b/internal/data/converter.go @@ -0,0 +1,26 @@ +// Copyright 2024 Matthew Rich . All rights reserved. + +package data + +import ( +) + +// Convert a resource to a document and a document to a resource + +type Emitter interface { + Emit(document Document, filter ResourceSelector) (Resource, error) +} + +type Extracter interface { + Extract(resource Resource, filter ResourceSelector) (Document, error) +} + +type Converter interface { + Typer + Emitter + Extracter +} + +type ManyExtractor interface { + ExtractMany(resource Resource, filter ResourceSelector) ([]Document, error) +} diff --git a/internal/data/data.go b/internal/data/data.go index b7989aa..0e4de62 100644 --- a/internal/data/data.go +++ b/internal/data/data.go @@ -26,7 +26,7 @@ type Deleter interface { Delete(context.Context) error } -type Crudder struct { +type Crudder interface { Creator Reader Updater diff --git a/internal/data/document.go b/internal/data/document.go new file mode 100644 index 0000000..6899086 --- /dev/null +++ b/internal/data/document.go @@ -0,0 +1,51 @@ +// Copyright 2024 Matthew Rich . All rights reserved. + +package data + +import ( + "context" + "errors" + "decl/internal/codec" + "io" + "decl/internal/mapper" +) + +var ( + ErrEmptyDocument error = errors.New("Document contains no resources") +) + +type Serializer interface { + JSON() ([]byte, error) + YAML() ([]byte, error) + PB() ([]byte, error) + Generate(w io.Writer) (error) +} + +type Loader interface { + LoadString(string, codec.Format) (error) + Load([]byte, codec.Format) (error) + LoadReader(io.ReadCloser, codec.Format) (error) +} + +type DocumentGetter interface { + GetDocument() Document +} + +type Document interface { + Serializer + Loader + Validator + mapper.Mapper + + NewResource(uri string) (Resource, error) + Types() (TypesRegistry[Resource]) +// Resources() []Declaration + + SetConfig(config Document) + ConfigDoc() Document + Len() int + ResolveIds(ctx context.Context) + Filter(filter DeclarationSelector) []Declaration + + //Diff(with *Document, output io.Writer) (returnOutput string, diffErr error) +} diff --git a/internal/data/identifier.go b/internal/data/identifier.go new file mode 100644 index 0000000..f9fd589 --- /dev/null +++ b/internal/data/identifier.go @@ -0,0 +1,22 @@ +// Copyright 2024 Matthew Rich . All rights reserved. + +package data + +import ( + "errors" +) + +var ( + ErrInvalidURI error = errors.New("Invalid URI") +) + +type Identifier interface { + URI() string + SetURI(string) error +} + +type Selector[Item comparable] func(r Item) bool + +type ResourceSelector Selector[Resource] + +type DeclarationSelector Selector[Declaration] diff --git a/internal/data/resource.go b/internal/data/resource.go new file mode 100644 index 0000000..94c79b8 --- /dev/null +++ b/internal/data/resource.go @@ -0,0 +1,91 @@ +// Copyright 2024 Matthew Rich . All rights reserved. + +package data + +import ( + "context" + "errors" + "decl/internal/mapper" + "decl/internal/transport" + "gitea.rosskeen.house/rosskeen.house/machine" + "io" + "io/fs" +) + +var ( + ErrInvalidResource error = errors.New("Invalid resource") +) + +type ResourceMapper mapper.Map[string, Declaration] + +type StateTransformer interface { + Apply() error +} + +type Resource interface { + Identifier + Type() string + StateMachine() machine.Stater + UseConfig(config ConfigurationValueGetter) + ResolveId(context.Context) string + Loader + StateTransformer + Crudder + Validator + Clone() Resource + SetResourceMapper(ResourceMapper) +} + +type Declaration interface { + Identifier + ResourceType() TypeName + ResolveId(context.Context) string + Loader + Validator + StateTransformer + Resource() Resource + Clone() Declaration +} + +func NewResourceMapper() ResourceMapper { + return mapper.New[string, Declaration]() +} + +type ContentIdentifier interface { + ContentType() string +} + +type ContentReader interface { + ContentReaderStream() (*transport.Reader, error) +} + +type ContentWriter interface { + ContentWriterStream() (*transport.Writer, error) +} + +type ContentReadWriter interface { + ContentReader + ContentWriter +} + +type ContentGetter interface { + GetContent(w io.Writer) (contentReader io.ReadCloser, err error) +} + +type ContentSetter interface { + SetContent(r io.Reader) error +} + +type ContentGetSetter interface { + ContentGetter + ContentSetter +} + +type FileResource interface { + FilePath() string + SetFileInfo(fs.FileInfo) error + FileInfo() fs.FileInfo + ContentGetSetter + SetContentSourceRef(uri string) +} + diff --git a/internal/data/stater.go b/internal/data/stater.go new file mode 100644 index 0000000..b3f5c8c --- /dev/null +++ b/internal/data/stater.go @@ -0,0 +1,50 @@ +// Copyright 2024 Matthew Rich . All rights reserved. + +package data + +import ( + "gitea.rosskeen.house/rosskeen.house/machine" +) + +func StorageMachine(sub machine.Subscriber) machine.Stater { +// start_destroy -> absent -> start_create -> present -> start_destroy + stater := machine.New("unknown") + stater.AddStates("initialized", "unkonwn", "absent", "start_create", "present", "start_delete", "start_read", "start_update") + stater.AddTransition("construct", machine.States("unknown"), "initialized") + stater.AddTransition("create", machine.States("unknown", "initialized", "absent"), "start_create") + if e := stater.AddSubscription("create", sub); e != nil { + return nil + } + stater.AddTransition("created", machine.States("start_create"), "present") + if e := stater.AddSubscription("created", sub); e != nil { + return nil + } + stater.AddTransition("exists", machine.States("unknown", "initialized", "absent"), "present") + if e := stater.AddSubscription("exists", sub); e != nil { + return nil + } + stater.AddTransition("notexists", machine.States("*"), "absent") + if e := stater.AddSubscription("notexists", sub); e != nil { + return nil + } + stater.AddTransition("read", machine.States("*"), "start_read") + if e := stater.AddSubscription("read", sub); e != nil { + return nil + } + stater.AddTransition("state_read", machine.States("start_read"), "present") + stater.AddTransition("update", machine.States("*"), "start_update") + if e := stater.AddSubscription("update", sub); e != nil { + return nil + } + stater.AddTransition("updated", machine.States("start_update"), "present") + stater.AddTransition("delete", machine.States("*"), "start_delete") + if e := stater.AddSubscription("delete", sub); e != nil { + return nil + } + stater.AddTransition("deleted", machine.States("start_delete"), "absent") + if e := stater.AddSubscription("deleted", sub); e != nil { + return nil + } + return stater +} + diff --git a/internal/data/types.go b/internal/data/types.go new file mode 100644 index 0000000..dc5da75 --- /dev/null +++ b/internal/data/types.go @@ -0,0 +1,23 @@ +// Copyright 2024 Matthew Rich . All rights reserved. + +package data + +import ( + "net/url" +) + +type Factory[Product comparable] func(*url.URL) Product + +type TypesRegistry[Product comparable] interface { + New(uri string) (result Product, err error) + Has(typename string) bool + //Get(string) Factory[Product] +} + +type TypeName string //`json:"type"` + +func (t TypeName) String() string { return string(t) } + +type Typer interface { + Type() TypeName +} diff --git a/internal/folio/uri.go b/internal/folio/uri.go new file mode 100644 index 0000000..c2d5f51 --- /dev/null +++ b/internal/folio/uri.go @@ -0,0 +1,64 @@ +// Copyright 2024 Matthew Rich . All rights reserved. + +package folio + +import ( + "net/url" + "decl/internal/transport" + "decl/internal/data" + "decl/internal/identifier" + "errors" +) + + +var ( + ErrInvalidURI error = errors.New("Invalid URI") +) + +type URI identifier.ID + +func (u URI) NewResource(document data.Document) (newResource data.Resource, err error) { + if document == nil { + declaration := NewDeclaration() + if err = declaration.NewResource((*string)(&u)); err == nil { + return declaration.Attributes.(data.Resource), err + } + } else { + newResource, err = document.NewResource(string(u)) + return + } + return +} + +func (u URI) Parse() *url.URL { + url, e := url.Parse(string(u)) + if e == nil { + return url + } + return nil +} + +func (u URI) Exists() bool { + return transport.ExistsURI(string(u)) +} + +func (u URI) ContentReaderStream() (*transport.Reader, error) { + return transport.NewReaderURI(string(u)) +} + +func (u URI) ContentWriterStream() (*transport.Writer, error) { + return transport.NewWriterURI(string(u)) +} + +func (u URI) String() string { + return string(u) +} + +func (u *URI) SetURL(url *url.URL) { + (*identifier.ID)(u).SetURL(url) +} + +func (u URI) Extension() (string, string) { + return (identifier.ID)(u).Extension() +} + diff --git a/internal/folio/uri_test.go b/internal/folio/uri_test.go new file mode 100644 index 0000000..80e3e7a --- /dev/null +++ b/internal/folio/uri_test.go @@ -0,0 +1,16 @@ +// Copyright 2024 Matthew Rich . All rights reserved. + +package folio + +import ( + "github.com/stretchr/testify/assert" + "testing" + "fmt" +) + +func TestURI(t *testing.T) { + var file URI = URI(fmt.Sprintf("file://%s", TempDir)) + u := file.Parse() + assert.Equal(t, "file", u.Scheme) + assert.True(t, file.Exists()) +} diff --git a/internal/identifier/id.go b/internal/identifier/id.go new file mode 100644 index 0000000..0cc8c34 --- /dev/null +++ b/internal/identifier/id.go @@ -0,0 +1,42 @@ +// Copyright 2024 Matthew Rich . All rights reserved. + +package identifier + +import ( + "net/url" + "errors" + "strings" +) + + +var ( + ErrInvalidURI error = errors.New("Invalid URI") +) + +type ID string + +func (i *ID) SetURL(u *url.URL) { + *i = ID(u.String()) +} + +func (i ID) Parse() *url.URL { + url, e := url.Parse(string(i)) + if e == nil { + return url + } + return nil +} + +func (i ID) Extension() (exttype string, fileext string) { + elements := strings.Split(string(i), ".") + numberOfElements := len(elements) + if numberOfElements > 1 { + if numberOfElements > 2 { + exttype = elements[numberOfElements - 2] + fileext = elements[numberOfElements - 1] + } + exttype = elements[numberOfElements - 1] + } + return +} + diff --git a/internal/identifier/id_test.go b/internal/identifier/id_test.go new file mode 100644 index 0000000..8377ffc --- /dev/null +++ b/internal/identifier/id_test.go @@ -0,0 +1,44 @@ +// Copyright 2024 Matthew Rich . All rights reserved. + +package identifier + +import ( + "github.com/stretchr/testify/assert" + "testing" + "fmt" + "os" + "log" +) + +var TempDir string + +func TestMain(m *testing.M) { + var err error + TempDir, err = os.MkdirTemp("", "testidentifier") + if err != nil || TempDir == "" { + log.Fatal(err) + } + + rc := m.Run() + + os.RemoveAll(TempDir) + os.Exit(rc) +} + +func TestID(t *testing.T) { + var file ID = ID(fmt.Sprintf("file://%s", TempDir)) + u := file.Parse() + assert.Equal(t, "file", u.Scheme) + filetype, fileext := file.Extension() + assert.Equal(t, "", filetype) + assert.Equal(t, "", fileext) +} + +func TestSetID(t *testing.T) { + var file ID = ID(fmt.Sprintf("file://%s", TempDir)) + u := file.Parse() + + var setFile ID + setFile.SetURL(u) + assert.Equal(t, file, setFile) +}