diff --git a/internal/folio/block.go b/internal/folio/block.go index b94febb..6adce36 100644 --- a/internal/folio/block.go +++ b/internal/folio/block.go @@ -7,6 +7,7 @@ import ( "encoding/json" "fmt" "io" + "net/url" "strings" "gopkg.in/yaml.v3" "decl/internal/codec" @@ -95,6 +96,15 @@ func (b *Block) NewConfiguration(uri *string) (err error) { return } +func (b *Block) NewConfigurationFromParsedURI(uri *url.URL) (err error) { + if uri == nil { + b.Values, err = b.ConfigurationTypes.New(fmt.Sprintf("%s://", b.Type)) + } else { + b.Values, err = b.ConfigurationTypes.NewFromParsedURI(uri) + } + return +} + func (b *Block) GetValue(key string) (any, error) { return b.Values.GetValue(key) } @@ -125,6 +135,20 @@ func (b *Block) SetURI(uri string) (err error) { return } +func (b *Block) SetParsedURI(uri *url.URL) (err error) { + if b.Values == nil { + if err = b.NewConfigurationFromParsedURI(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)) @@ -145,6 +169,9 @@ func (b *Block) UnmarshalValue(value *BlockType) error { } func (b *Block) UnmarshalYAML(value *yaml.Node) error { + if b.ConfigurationTypes == nil { + b.ConfigurationTypes = DocumentRegistry.ConfigurationTypes + } t := &BlockType{} if unmarshalConfigurationTypeErr := value.Decode(t); unmarshalConfigurationTypeErr != nil { return unmarshalConfigurationTypeErr @@ -168,6 +195,9 @@ func (b *Block) UnmarshalYAML(value *yaml.Node) error { } func (b *Block) UnmarshalJSON(jsonData []byte) error { + if b.ConfigurationTypes == nil { + b.ConfigurationTypes = DocumentRegistry.ConfigurationTypes + } t := &BlockType{} if unmarshalConfigurationTypeErr := json.Unmarshal(jsonData, t); unmarshalConfigurationTypeErr != nil { return unmarshalConfigurationTypeErr @@ -186,7 +216,3 @@ func (b *Block) UnmarshalJSON(jsonData []byte) error { _, 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 index 00e5ae6..9d7fe1c 100644 --- a/internal/folio/block_test.go +++ b/internal/folio/block_test.go @@ -5,27 +5,14 @@ package folio import ( _ "fmt" "github.com/stretchr/testify/assert" - "log" - "os" "testing" "strings" + "decl/internal/data" + "decl/internal/codec" + "io" + "log/slog" ) -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" @@ -36,14 +23,21 @@ values: docReader := strings.NewReader(configYaml) block := NewBlock() + block.ConfigurationTypes = TestConfigurationTypes + slog.Info("TestNewBlock()", "block", block, "types", block.ConfigurationTypes) + assert.NotNil(t, block) - assert.Nil(t, block.Load(docReader)) + assert.Nil(t, block.LoadReader(io.NopCloser(docReader), codec.FormatYaml)) + assert.Equal(t, "foo", block.Name) + + block.Values.(*MockQuuz).InjectGetValue = func(key string) (any, error) { return "test", nil } val, err := block.GetValue("http_user") assert.Nil(t, err) assert.Equal(t, "test", val) + block.Values.(*MockQuuz).InjectGetValue = func(key string) (any, error) { return nil, data.ErrUnknownConfigurationKey } missingVal, missingErr := block.GetValue("content") - assert.ErrorIs(t, missingErr, ErrUnknownConfigurationKey) + assert.ErrorIs(t, missingErr, data.ErrUnknownConfigurationKey) assert.Nil(t, missingVal) } diff --git a/internal/folio/constraint.go b/internal/folio/constraint.go new file mode 100644 index 0000000..ac173aa --- /dev/null +++ b/internal/folio/constraint.go @@ -0,0 +1,83 @@ +// Copyright 2024 Matthew Rich . All rights reserved. + +package folio + +import ( + "decl/internal/data" + "errors" + "log/slog" +) + +// Dependencies describe a requirement on a given system property + +// system properties: +// loaded from config +// +// deps assigned to decl +// match values in document configurations +// match values in all configurations? +// +// documents/facter.jx.yaml -> facts -> Get(key) +// +// ConfigMapper? -> DocumentRegistry + +// system: +// arch: amd64 +// foo: bar + +var ( + ErrConstraintFailure = errors.New("Constraint failure") +) + +type Constraint map[string]any + +//func Compare[aV, bV map[K]V, K, V comparable](a aV, b bV) (matched bool) { +func CompareMap(a, b any) (matched bool) { + for k,v := range a.(Constraint) { + if bv, exists := b.(map[string]any)[k]; exists && bv == v { + matched = true + } else { + matched = false + } + } + return +} + +func (c Constraint) CompareValues(a, b any) (matched bool) { + matched = true + slog.Info("Constraint.CompareValues()", "a", a, "b", b) + switch btype := b.(type) { + case map[string]string: + return CompareMap(a, b) + case map[string]any: + return CompareMap(a, b) + case []string: + acmp := a.([]string) + if len(acmp) > len(btype) { + return false + } + for i,v := range acmp { + if v != btype[i] { + return false + } + } + default: + return a == b + } + return +} + +func (c Constraint) Check(configs data.ConfigurationValueGetter) (matched bool) { + matched = true + for k,v := range c { + slog.Info("Constraint.Check()", "constraint", c, "config", configs) + + if configValue, err := configs.GetValue(k); err != nil || ! c.CompareValues(v, configValue) { + slog.Info("Constraint.Check() - FAILURE", "configvalue", configValue, "constraint", v, "error", err) + matched = false + } + } + return +} + + diff --git a/internal/folio/declaration.go b/internal/folio/declaration.go index 8162757..01eb9ab 100644 --- a/internal/folio/declaration.go +++ b/internal/folio/declaration.go @@ -15,6 +15,8 @@ _ "gitea.rosskeen.house/rosskeen.house/machine" "decl/internal/codec" "decl/internal/data" "decl/internal/schema" + "net/url" + "runtime/debug" ) type ConfigName string @@ -23,6 +25,9 @@ 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"` + OnError OnError `json:"onerror,omitempty" yaml:"onerror,omitempty"` + Error string `json:"error,omitempty" yaml:"error,omitempty"` + Requires Dependencies `json:"requires,omitempty" yaml:"requires,omitempty"` } type Declaration struct { @@ -30,6 +35,9 @@ type Declaration struct { Transition string `json:"transition,omitempty" yaml:"transition,omitempty"` Attributes data.Resource `json:"attributes" yaml:"attributes"` Config ConfigName `json:"config,omitempty" yaml:"config,omitempty"` + OnError OnError `json:"onerror,omitempty" yaml:"onerror,omitempty"` + Error string `json:"error,omitempty" yaml:"error,omitempty"` + Requires Dependencies `json:"requires,omitempty" yaml:"requires,omitempty"` // runtime luaruntime.LuaRunner document *Document configBlock data.Block @@ -69,10 +77,11 @@ func (d *Declaration) ResolveId(ctx context.Context) string { func (d *Declaration) Clone() data.Declaration { return &Declaration { Type: d.Type, - Transition: d.Transition, + Transition: d.Transition, Attributes: d.Attributes.Clone(), //runtime: luaruntime.New(), Config: d.Config, + Requires: d.Requires, } } @@ -112,6 +121,17 @@ func (d *Declaration) Validate() (err error) { return err } +func (d *Declaration) NewResourceFromParsedURI(u *url.URL) (err error) { + if u == nil { + d.Attributes, err = d.ResourceTypes.NewFromParsedURI(&url.URL{ Scheme: string(d.Type) }) + } else { + if d.Attributes, err = d.ResourceTypes.NewFromParsedURI(u); err == nil { + err = d.Attributes.SetParsedURI(u) + } + } + return +} + 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)) @@ -119,7 +139,9 @@ func (d *Declaration) NewResource(uri *string) (err error) { if uri == nil { d.Attributes, err = d.ResourceTypes.New(fmt.Sprintf("%s://", d.Type)) } else { - d.Attributes, err = d.ResourceTypes.New(*uri) + if d.Attributes, err = d.ResourceTypes.New(*uri); err == nil { + err = d.Attributes.SetURI(*uri) + } } return } @@ -131,8 +153,12 @@ func (d *Declaration) Resource() data.Resource { func (d *Declaration) Apply() (result error) { defer func() { if r := recover(); r != nil { + slog.Info("Declaration.Apply()", "error", r, "stacktrace", string(debug.Stack())) result = fmt.Errorf("%s", r) } + if result != nil { + d.Error = result.Error() + } }() stater := d.Attributes.StateMachine() @@ -158,10 +184,17 @@ func (d *Declaration) Apply() (result error) { case "create", "present": if stater.CurrentState() == "absent" || stater.CurrentState() == "unknown" { if result = stater.Trigger("create"); result != nil { + slog.Info("Declaration.Apply()", "trigger", "create", "state", stater.CurrentState(), "error", result, "declaration", d) return result } } result = stater.Trigger("read") + currentState := stater.CurrentState() + switch currentState { + case "create", "present": + default: + return fmt.Errorf("Failed to create resource: %s - state: %s, err: %s", d.URI(), currentState, result) + } } return result } @@ -179,9 +212,12 @@ func (d *Declaration) SetConfig(configDoc data.Document) { 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 - } + err = d.NewResource(&uri) + } else { + err = d.Attributes.SetURI(uri) + } + if err != nil { + return err } if d.Attributes == nil { return fmt.Errorf("%w: %s", ErrUnknownResourceType, uri) @@ -192,6 +228,24 @@ func (d *Declaration) SetURI(uri string) (err error) { return } +func (d *Declaration) SetParsedURI(uri *url.URL) (err error) { + slog.Info("Declaration.SetParsedURI()", "uri", uri, "declaration", d) + if d.Attributes == nil { + err = d.NewResourceFromParsedURI(uri) + } else { + err = d.Attributes.SetParsedURI(uri) + } + if err != nil { + return err + } + 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) @@ -201,6 +255,9 @@ func (d *Declaration) UnmarshalValue(value *DeclarationType) error { d.Type = value.Type d.Transition = value.Transition d.Config = value.Config + d.OnError = value.OnError + d.Error = value.Error + d.Requires = value.Requires newResource, resourceErr := d.ResourceTypes.New(fmt.Sprintf("%s://", value.Type)) if resourceErr != nil { slog.Info("Declaration.UnmarshalValue", "value", value, "error", resourceErr) diff --git a/internal/folio/dependencies.go b/internal/folio/dependencies.go new file mode 100644 index 0000000..58c499b --- /dev/null +++ b/internal/folio/dependencies.go @@ -0,0 +1,45 @@ +// Copyright 2024 Matthew Rich . All rights reserved. + +package folio + +import ( + "log/slog" +) + +// Dependencies describe a requirement on a given system property + +// system properties: +// loaded from config +// +// deps assigned to decl +// match values in document configurations +// match values in all configurations? +// +// documents/facter.jx.yaml -> facts -> Get(key) +// +// ConfigMapper? -> DocumentRegistry + +// requires: +// arch: amd64 +// foo: bar + +type Dependencies map[string]Constraint + +func (d Dependencies) Check() (matched bool) { + matched = true + for k,v := range d { + b, z := DocumentRegistry.ConfigNameMap.Get(k) + slog.Info("Dependencies.Check()", "key", k, "value", v, "block", b, "ok", z) + if configBlock, ok := DocumentRegistry.ConfigNameMap.Get(k); ok { + if ! v.Check(configBlock) { + matched = false + } + } else { + matched = false + return + } + } + return +} + + diff --git a/internal/folio/document.go b/internal/folio/document.go index 48105de..bfd5525 100644 --- a/internal/folio/document.go +++ b/internal/folio/document.go @@ -9,7 +9,7 @@ import ( "io" "io/fs" "log/slog" -_ "net/url" + "net/url" "github.com/sters/yaml-diff/yamldiff" "strings" "decl/internal/codec" @@ -24,18 +24,21 @@ 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"` + Requires Dependencies `json:"requires,omitempty" yaml:"requires,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"` + Requires Dependencies `json:"requires,omitempty" yaml:"requires,omitempty"` uris mapper.Store[string, data.Declaration] - ResourceDeclarations []*Declaration `json:"resources" yaml:"resources"` - configNames mapper.Store[string, data.Block] + ResourceDeclarations []*Declaration `json:"resources,omitempty" yaml:"resources,omitempty"` + configNames mapper.Store[string, data.Block] `json:"-" yaml:"-"` Configurations []*Block `json:"configurations,omitempty" yaml:"configurations,omitempty"` config data.Document Registry *Registry `json:"-" yaml:"-"` + failedResources int `json:"-" yaml:"-"` } func NewDocument(r *Registry) *Document { @@ -90,7 +93,11 @@ func (d *Document) GetResource(uri string) *Declaration { return nil } -func (d *Document) Clone() *Document { +func (d *Document) Failures() int { + return d.failedResources +} + +func (d *Document) Clone() data.Document { clone := NewDocument(d.Registry) clone.config = d.config @@ -105,6 +112,7 @@ func (d *Document) Clone() *Document { clone.ResourceDeclarations[i].SetDocument(clone) clone.ResourceDeclarations[i].SetConfig(d.config) } + clone.Requires = d.Requires return clone } @@ -122,6 +130,22 @@ func (d *Document) assignResourcesDocument() { } } +func (d *Document) assignConfigurationsDocument() { + slog.Info("Document.assignConfigurationsDocument()", "configurations", d.Configurations, "len", len(d.Configurations)) + for i := range d.Configurations { + if d.Configurations[i] == nil { + d.Configurations[i] = NewBlock() + } + slog.Info("Document.assignConfigurationsDocument()", "configuration", d.Configurations[i]) + //d.Configurations[i].SetDocument(d) + slog.Info("Document.assignConfigurationsDocument()", "configuration", d.Configurations[i]) + //d.MapConfigurationURI(d.Configurations[i].URI(), d.Configurations[i]) + d.configNames[d.Configurations[i].Name] = d.Configurations[i] + d.Registry.ConfigurationMap[d.Configurations[i]] = d + d.Registry.ConfigNameMap[d.Configurations[i].Name] = d.Configurations[i] + } +} + func (d *Document) LoadString(docData string, f codec.Format) (err error) { err = f.StringDecoder(docData).Decode(d) return @@ -148,7 +172,7 @@ func (d *Document) GetSchemaFiles() (schemaFs fs.FS) { func (d *Document) Validate() error { jsonDocument, jsonErr := d.JSON() - slog.Info("document.Validate() json", "json", jsonDocument, "err", jsonErr) + slog.Info("document.Validate() json", "err", jsonErr) if jsonErr == nil { s := schema.New("document", d.GetSchemaFiles()) err := s.Validate(string(jsonDocument)) @@ -189,6 +213,10 @@ func (d *Document) Len() int { return len(d.ResourceDeclarations) } +func (d *Document) CheckConstraints() bool { + return d.Requires.Check() +} + func (d *Document) ResolveIds(ctx context.Context) { for i := range d.ResourceDeclarations { d.ResourceDeclarations[i].ResolveId(ctx) @@ -213,9 +241,24 @@ func (d *Document) Apply(state string) error { 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 + + slog.Info("Document.Apply() applying resource", "index", idx, "uri", d.ResourceDeclarations[idx].Resource().URI(), "resource", d.ResourceDeclarations[idx].Resource()) + + if d.ResourceDeclarations[idx].Requires.Check() { + + if e := d.ResourceDeclarations[idx].Apply(); e != nil { + slog.Error("Document.Apply() error applying resource", "index", idx, "uri", d.ResourceDeclarations[idx].Resource().URI()) + + d.ResourceDeclarations[idx].Error = e.Error() + switch d.ResourceDeclarations[idx].OnError.GetStrategy() { + case OnErrorStop: + return e + case OnErrorFail: + d.failedResources++ + } + } + } else { + d.ResourceDeclarations[idx].Error = fmt.Sprintf("Constraint failure: %s", d.ResourceDeclarations[idx].Requires) } if i >= len(d.ResourceDeclarations) - 1 { break @@ -240,6 +283,12 @@ func (d *Document) Generate(w io.Writer) (err error) { return } +/* +func (d *Document) MapConfigurationURI(uri string, block data.Block) { + d.configUris[uri] = block +} +*/ + func (d *Document) MapResourceURI(uri string, declaration data.Declaration) { d.uris[uri] = declaration } @@ -247,39 +296,90 @@ func (d *Document) MapResourceURI(uri string, declaration data.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 + if decl.Requires.Check() { + + 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 + if decl.Requires.Check() { + 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 + + if decl.Requires.Check() { + slog.Info("Document.NewResource()", "type", decl.Type, "declaration", decl) + + d.ResourceDeclarations = append(d.ResourceDeclarations, decl) + d.MapResourceURI(decl.Attributes.URI(), decl) + decl.SetDocument(d) + d.Registry.DeclarationMap[decl] = d + newResource = decl.Attributes + } else { + err = fmt.Errorf("%w: %s", ErrConstraintFailure, uri) + } + return +} + +func (d *Document) NewResourceFromURI(uri URI) (newResource data.Resource, err error) { + return d.NewResourceFromParsedURI(uri.Parse()) +} + +func (d *Document) NewResourceFromParsedURI(uri *url.URL) (newResource data.Resource, err error) { + if uri == nil { + return nil, fmt.Errorf("%w: %s", ErrUnknownResourceType, uri) + } + + decl := NewDeclarationFromDocument(d) + if err = decl.NewResourceFromParsedURI(uri); err != nil { + return + } + + if decl.Attributes == nil { + err = fmt.Errorf("%w: %s", ErrUnknownResourceType, uri) + return + } + + decl.Type = TypeName(decl.Attributes.Type()) + + if decl.Requires.Check() { + d.ResourceDeclarations = append(d.ResourceDeclarations, decl) + + d.MapResourceURI(decl.Attributes.URI(), decl) + decl.SetDocument(d) + d.Registry.DeclarationMap[decl] = d + newResource = decl.Attributes + } else { + err = fmt.Errorf("%w: %s", ErrConstraintFailure, uri) + } return } @@ -288,10 +388,15 @@ func (d *Document) AddResource(uri string) error { 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 + + if decl.Requires.Check() { + d.ResourceDeclarations = append(d.ResourceDeclarations, decl) + d.MapResourceURI(decl.Attributes.URI(), decl) + decl.SetDocument(d) + d.Registry.DeclarationMap[decl] = d + } else { + return fmt.Errorf("%w: %s", ErrConstraintFailure, uri) + } return nil } @@ -340,21 +445,19 @@ func (d *Document) HasConfig(name string) bool { return ok } -func (d *Document) GetConfig(name string) *Block { - return d.configNames[name].(*Block) +func (d *Document) GetConfig(name string) data.Block { + return d.configNames[name] } 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) - } + 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) { +func (d *Document) Diff(with data.Document, output io.Writer) (returnOutput string, diffErr error) { defer func() { if r := recover(); r != nil { returnOutput = "" @@ -408,6 +511,7 @@ func (d *Document) UnmarshalYAML(value *yaml.Node) error { if unmarshalResourcesErr := value.Decode((*decodeDocument)(d)); unmarshalResourcesErr != nil { return unmarshalResourcesErr } + d.assignConfigurationsDocument() d.assignResourcesDocument() return nil } @@ -418,6 +522,7 @@ func (d *Document) UnmarshalJSON(data []byte) error { if unmarshalDocumentErr := json.Unmarshal(data, t); unmarshalDocumentErr != nil { return unmarshalDocumentErr } + d.assignConfigurationsDocument() d.assignResourcesDocument() return nil } diff --git a/internal/folio/document_test.go b/internal/folio/document_test.go index 593e9c3..ffc676d 100644 --- a/internal/folio/document_test.go +++ b/internal/folio/document_test.go @@ -17,6 +17,7 @@ import ( var ( TestResourceTypes *types.Types[data.Resource] = types.New[data.Resource]() + TestConfigurationTypes *types.Types[data.Configuration] = types.New[data.Configuration]() ) func TestNewDocument(t *testing.T) { diff --git a/internal/folio/folio_test.go b/internal/folio/folio_test.go index a26463c..ed7f7ff 100644 --- a/internal/folio/folio_test.go +++ b/internal/folio/folio_test.go @@ -28,6 +28,7 @@ func TestMain(m *testing.M) { ProcessTestUserName, ProcessTestGroupName = ProcessUserName() RegisterMocks() + RegisterConfigurationMocks() rc := m.Run() diff --git a/internal/folio/mock_configuration_test.go b/internal/folio/mock_configuration_test.go new file mode 100644 index 0000000..24ae471 --- /dev/null +++ b/internal/folio/mock_configuration_test.go @@ -0,0 +1,106 @@ +// Copyright 2024 Matthew Rich . All rights reserved. + +package folio + +import ( + "context" +_ "gopkg.in/yaml.v3" + "encoding/json" +_ "fmt" + "decl/internal/data" + "decl/internal/codec" + "io" + "net/url" +) + +type MockConfiguration struct { + InjectURI func() string `json:"-" yaml:"-"` + InjectType func() string `json:"-" yaml:"-"` + InjectResolveId func(ctx context.Context) string `json:"-" yaml:"-"` + InjectValidate 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:"-"` + InjectRead func(context.Context) ([]byte, error) `json:"-" yaml:"-"` + InjectGetValue func(key string) (any, error) `json:"-" yaml:"-"` + InjectHas func(key string) (bool) `json:"-" yaml:"-"` +} + +func (m *MockConfiguration) Clone() data.Configuration { + return nil +} + +func (m *MockConfiguration) SetURI(uri string) error { + return nil +} + +func (m *MockConfiguration) SetParsedURI(uri *url.URL) error { + return nil +} + +func (m *MockConfiguration) URI() string { + return m.InjectURI() +} + +func (m *MockConfiguration) ResolveId(ctx context.Context) string { + return m.InjectResolveId(ctx) +} + +func (m *MockConfiguration) JSON() ([]byte, error) { + return m.InjectJSON() +} + +func (m *MockConfiguration) YAML() ([]byte, error) { + return m.InjectYAML() +} + +func (m *MockConfiguration) PB() ([]byte, error) { + return m.InjectPB() +} + +func (m *MockConfiguration) Generate(w io.Writer) (error) { + return m.InjectGenerate(w) +} + +func (m *MockConfiguration) LoadString(docData string, format codec.Format) (error) { + return m.InjectLoadString(docData, format) +} + +func (m *MockConfiguration) Load(docData []byte, format codec.Format) (error) { + return m.InjectLoad(docData, format) +} + +func (m *MockConfiguration) LoadReader(r io.ReadCloser, format codec.Format) (error) { + return m.InjectLoadReader(r, format) +} + +func (m *MockConfiguration) Read(ctx context.Context) ([]byte, error) { + return m.InjectRead(ctx) +} + +func (m *MockConfiguration) Validate() error { + return m.InjectValidate() +} + +func (m *MockConfiguration) Type() string { + return m.InjectType() +} + +func (m *MockConfiguration) UnmarshalJSON(data []byte) error { + if err := json.Unmarshal(data, m); err != nil { + return err + } + return nil +} + +func (m *MockConfiguration) GetValue(key string) (any, error) { + return m.InjectGetValue(key) +} + +func (m *MockConfiguration) Has(key string) (bool) { + return m.InjectHas(key) +} diff --git a/internal/folio/mock_quuz_configuration_test.go b/internal/folio/mock_quuz_configuration_test.go new file mode 100644 index 0000000..668cb6f --- /dev/null +++ b/internal/folio/mock_quuz_configuration_test.go @@ -0,0 +1,48 @@ +// Copyright 2024 Matthew Rich . All rights reserved. + +package folio + +import ( + "context" +_ "gopkg.in/yaml.v3" + "decl/internal/codec" + "decl/internal/data" + "io" + "net/url" + "path/filepath" + "fmt" +) + +func RegisterConfigurationMocks() { + TestConfigurationTypes.Register([]string{"generic"}, func(u *url.URL) data.Configuration { + q := NewQuuzConfiguration() + q.Name = filepath.Join(u.Hostname(), u.Path) + return q + }) +} + +type MockQuuz struct { + *MockConfiguration `json:"-" yaml:"-"` + Name string `json:"name" yaml:"name"` + Value string `json:"value" yaml:"value"` +} + +func NewMockConfiguration(typename string) *MockConfiguration { + return &MockConfiguration { + InjectType: func() string { return typename }, + InjectResolveId: func(ctx context.Context) string { return "bar" }, + 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 }, + InjectValidate: func() error { return nil }, + InjectRead: func(context.Context) ([]byte, error) { return nil, nil }, + InjectGetValue: func(key string) (any, error) { return nil, nil }, + InjectURI: func() string { return fmt.Sprintf("%s://bar", typename) }, + } +} + +func NewQuuzConfiguration() *MockQuuz { + q := &MockQuuz {} + q.MockConfiguration = NewMockConfiguration("generic") + return q +} diff --git a/internal/folio/mock_resource_test.go b/internal/folio/mock_resource_test.go index ee3c748..4a056a0 100644 --- a/internal/folio/mock_resource_test.go +++ b/internal/folio/mock_resource_test.go @@ -11,6 +11,7 @@ _ "fmt" "decl/internal/data" "decl/internal/codec" "io" + "net/url" ) type MockResource struct { @@ -53,6 +54,10 @@ func (m *MockResource) SetURI(uri string) error { return nil } +func (m *MockResource) SetParsedURI(uri *url.URL) error { + return nil +} + func (m *MockResource) URI() string { return m.InjectURI() } diff --git a/internal/folio/onerror.go b/internal/folio/onerror.go new file mode 100644 index 0000000..2f49cbe --- /dev/null +++ b/internal/folio/onerror.go @@ -0,0 +1,76 @@ +// Copyright 2024 Matthew Rich . All rights reserved. + +package folio + +import ( + "errors" + "encoding/json" + "gopkg.in/yaml.v3" + "log/slog" +) + +var ( + ErrInvalidOnErrorStrategy = errors.New("Invalid OnError strategy") +) + +type OnError string + +const ( + OnErrorStop = "stop" + OnErrorFail = "fail" // set error value and return the error + OnErrorSkip = "skip" // set error value and continue processing resources +) + +func NewOnError() OnError { + return OnErrorFail +} + +func (o OnError) Strategy() string { + switch o { + case OnErrorStop, OnErrorFail, OnErrorSkip: + return string(o) + } + return "" +} + +func (o OnError) GetStrategy() OnError { + switch o { + case OnErrorStop, OnErrorFail, OnErrorSkip: + return o + default: + slog.Warn("OnError.GetStrategy() - invalid value, defaulting to OnErrorFail", "strategy", o) + return OnErrorFail + } +} + +func (o OnError) Validate() error { + switch o { + case OnErrorStop, OnErrorFail, OnErrorSkip: + return nil + default: + return ErrInvalidOnErrorStrategy + } +} + +func (o *OnError) UnmarshalValue(value string) (err error) { + if err = OnError(value).Validate(); err == nil { + *o = OnError(value) + } + return +} + +func (o *OnError) UnmarshalJSON(jsonData []byte) error { + var s string + if unmarshalErr := json.Unmarshal(jsonData, &s); unmarshalErr != nil { + return unmarshalErr + } + return o.UnmarshalValue(s) +} + +func (o *OnError) UnmarshalYAML(value *yaml.Node) error { + var s string + if err := value.Decode(&s); err != nil { + return err + } + return o.UnmarshalValue(s) +} diff --git a/internal/folio/onerror_test.go b/internal/folio/onerror_test.go new file mode 100644 index 0000000..004856d --- /dev/null +++ b/internal/folio/onerror_test.go @@ -0,0 +1,21 @@ +// Copyright 2024 Matthew Rich . All rights reserved. + +package folio + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestOnErrorStrategies(t *testing.T) { + for _, v := range []struct{ strategy OnError; expected OnError; validate error }{ + { strategy: OnErrorFail, expected: "fail", validate: nil }, + { strategy: OnErrorSkip, expected: "skip", validate: nil }, + { strategy: OnError("unknown"), expected: "", validate: ErrInvalidOnErrorStrategy }, + }{ + o := v.strategy + assert.Equal(t, v.expected, OnError(o.Strategy())) + assert.ErrorIs(t, o.Validate(), v.validate) + } + +} diff --git a/internal/folio/registry.go b/internal/folio/registry.go index d038d34..f94400e 100644 --- a/internal/folio/registry.go +++ b/internal/folio/registry.go @@ -5,12 +5,13 @@ package folio import ( _ "errors" _ "fmt" -_ "net/url" + "net/url" _ "strings" "decl/internal/types" "decl/internal/data" "decl/internal/mapper" "io/fs" + "log/slog" ) var ( @@ -24,6 +25,7 @@ type Registry struct { Documents []*Document UriMap mapper.Store[URI, *Document] DeclarationMap mapper.Store[*Declaration, *Document] + ConfigNameMap mapper.Store[string, *Block] ConfigurationMap mapper.Store[*Block, *Document] DefaultSchema URI } @@ -36,6 +38,7 @@ func NewRegistry() *Registry { Documents: make([]*Document, 0, 10), UriMap: mapper.New[URI, *Document](), DeclarationMap: mapper.New[*Declaration, *Document](), + ConfigNameMap: mapper.New[string, *Block](), ConfigurationMap: mapper.New[*Block, *Document](), Schemas: mapper.New[URI, fs.FS](), DefaultSchema: schemaFilesUri, @@ -52,8 +55,21 @@ func (r *Registry) Has(key *Declaration) (bool) { return r.DeclarationMap.Has(key) } +func (r *Registry) HasDocument(key URI) bool { + return r.UriMap.Has(key) +} + +func (r *Registry) GetDocument(key URI) (*Document, bool) { + return r.UriMap.Get(key) +} + +func (r *Registry) SetDocument(key URI, value *Document) { + r.UriMap.Set(key, value) +} + func (r *Registry) NewDocument(uri URI) (doc *Document) { doc = NewDocument(r) + doc.URI = uri r.Documents = append(r.Documents, doc) if uri != "" { r.UriMap[uri] = doc @@ -61,15 +77,64 @@ func (r *Registry) NewDocument(uri URI) (doc *Document) { return } -func (r *Registry) Load(uri URI) (documents []data.Document, err error) { - var extractor data.Converter +func (r *Registry) AppendParsedURI(uri *url.URL, documents []data.Document) (addedDocuments []data.Document, err error) { + var convertUri 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) + + slog.Info("folio.Registry.AppendParsedURI()", "uri", uri, "converter", r.ConverterTypes) + if convertUri, err = r.ConverterTypes.NewFromParsedURI(uri); err == nil { + if sourceResource, err = NewResourceFromParsedURI(uri, nil); err == nil { + switch extractor := convertUri.(type) { + case data.ManyExtractor: + var docs []data.Document + docs, err = extractor.ExtractMany(sourceResource, nil) + slog.Info("folio.Registry.Append() - ExtractMany", "uri", uri, "source", sourceResource, "docs", docs, "error", err) + documents = append(documents, docs...) + case data.Extractor: + var singleDocument data.Document + singleDocument, err = extractor.Extract(sourceResource, nil) + slog.Info("folio.Registry.Append() - Extract", "uri", uri, "source", sourceResource, "doc", singleDocument, "error", err) + documents = append(documents, singleDocument) + } } } + slog.Info("folio.Registry.Append()", "uri", uri, "converter", r.ConverterTypes, "error", err) + addedDocuments = documents return } +func (r *Registry) Append(uri URI, documents []data.Document) (addedDocuments []data.Document, err error) { + var convertUri data.Converter + var sourceResource data.Resource + slog.Info("folio.Registry.Append()", "uri", uri, "converter", r.ConverterTypes) + if convertUri, err = r.ConverterTypes.New(string(uri)); err == nil { + if sourceResource, err = uri.NewResource(nil); err == nil { + switch extractor := convertUri.(type) { + case data.ManyExtractor: + var docs []data.Document + docs, err = extractor.ExtractMany(sourceResource, nil) + slog.Info("folio.Registry.Append() - ExtractMany", "uri", uri, "source", sourceResource, "docs", docs, "error", err) + documents = append(documents, docs...) + case data.Extractor: + var singleDocument data.Document + singleDocument, err = extractor.Extract(sourceResource, nil) + slog.Info("folio.Registry.Append() - Extract", "uri", uri, "source", sourceResource, "doc", singleDocument, "error", err) + documents = append(documents, singleDocument) + } + } + } + slog.Info("folio.Registry.Append()", "uri", uri, "converter", r.ConverterTypes, "error", err) + addedDocuments = documents + return +} + +func (r *Registry) Load(uri URI) (documents []data.Document, err error) { + documents = make([]data.Document, 0, 10) + return r.Append(uri, documents) +} + +func (r *Registry) LoadFromURL(uri *url.URL) (documents []data.Document, err error) { + documents = make([]data.Document, 0, 10) + return r.AppendParsedURI(uri, documents) +} diff --git a/internal/folio/resource.go b/internal/folio/resource.go new file mode 100644 index 0000000..9e3c889 --- /dev/null +++ b/internal/folio/resource.go @@ -0,0 +1,26 @@ +// Copyright 2024 Matthew Rich . All rights reserved. + +package folio + +import ( + "net/url" + "decl/internal/data" +) + + +var ( +) + +/* Create a new resource using a parsed URI */ +func NewResourceFromParsedURI(u *url.URL, document data.Document) (newResource data.Resource, err error) { + if document == nil { + declaration := NewDeclaration() + if err = declaration.NewResourceFromParsedURI(u); err == nil { + return declaration.Attributes, err + } + } else { + newResource, err = document.NewResourceFromParsedURI(u) + return + } + return +} diff --git a/internal/folio/resourcereference.go b/internal/folio/resourcereference.go index f98b558..52be7e7 100644 --- a/internal/folio/resourcereference.go +++ b/internal/folio/resourcereference.go @@ -32,6 +32,9 @@ type ResourceReference URI // Return a Content ReadWriter for the resource referred to. func (r ResourceReference) Lookup(look data.ResourceMapper) ContentReadWriter { + if string(r) == "" { + return nil + } slog.Info("ResourceReference.Lookup()", "resourcereference", r, "resourcemapper", look) if look != nil { if v,ok := look.Get(string(r)); ok { diff --git a/internal/folio/schema.go b/internal/folio/schema.go index c16fd66..675ec0d 100644 --- a/internal/folio/schema.go +++ b/internal/folio/schema.go @@ -8,5 +8,6 @@ import ( var schemaFilesUri URI = "file://folio/schemas/*.schema.json" +//go:embed schemas/config/*.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 index 9e2977f..7b71ecc 100644 --- a/internal/folio/schemas/bar-declaration.schema.json +++ b/internal/folio/schemas/bar-declaration.schema.json @@ -14,6 +14,15 @@ "type": "string", "description": "Config name" }, + "onerror": { + "type": "string", + "description": "error handling strategy", + "enum": [ + "stop", + "fail", + "skip" + ] + }, "attributes": { "oneOf": [ { "$ref": "bar.schema.json" } diff --git a/internal/folio/schemas/codec.schema.json b/internal/folio/schemas/codec.schema.json new file mode 100644 index 0000000..e91bb12 --- /dev/null +++ b/internal/folio/schemas/codec.schema.json @@ -0,0 +1,8 @@ +{ + "$id": "codec.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "codec", + "type": "string", + "description": "Supported serialization encode/decode formats", + "enum": [ "yaml", "json", "protobuf" ] +} diff --git a/internal/folio/schemas/config/block.schema.json b/internal/folio/schemas/config/block.schema.json new file mode 100644 index 0000000..287f3eb --- /dev/null +++ b/internal/folio/schemas/config/block.schema.json @@ -0,0 +1,25 @@ +{ + "$id": "block.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "block", + "type": "object", + "required": [ "name", "values" ], + "properties": { + "name": { + "type": "string", + "description": "Config block name", + "minLength": 2 + }, + "type": { + "type": "string", + "description": "Config type name.", + "enum": [ "system", "generic", "exec", "certificate" ] + }, + "values": { + "oneOf": [ + { "type": "object" }, + { "$ref": "certificate.schema.json" } + ] + } + } +} diff --git a/internal/folio/schemas/config/certificate.schema.json b/internal/folio/schemas/config/certificate.schema.json new file mode 100644 index 0000000..27bc4d6 --- /dev/null +++ b/internal/folio/schemas/config/certificate.schema.json @@ -0,0 +1,62 @@ +{ + "$id": "certificate.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "certificate", + "type": "object", + "required": [ "path", "filetype" ], + "properties": { + "SerialNumber": { + "type": "integer", + "description": "Serial number", + "minLength": 1 + }, + "Issuer": { + "$ref": "pkixname.schema.json" + }, + "Subject": { + "$ref": "pkixname.schema.json" + }, + "NotBefore": { + "type": "string", + "format": "date-time", + "description": "Cert is not valid before time in YYYY-MM-DDTHH:MM:SS.sssssssssZ format." + }, + "NotAfter": { + "type": "string", + "format": "date-time", + "description": "Cert is not valid after time in YYYY-MM-DDTHH:MM:SS.sssssssssZ format." + }, + "KeyUsage": { + "type": "integer", + "enum": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ], + "description": "Actions valid for a key. E.g. 1 = KeyUsageDigitalSignature" + }, + "ExtKeyUsage": { + "type": "array", + "items": { + "type": "integer", + "minimum": 0, + "maximum": 13 + }, + "description": "Extended set of actions valid for a key" + }, + "BasicConstraintsValid": { + "type": "boolean", + "description": "BasicConstraintsValid indicates whether IsCA, MaxPathLen, and MaxPathLenZero are valid" + }, + "IsCA": { + "type": "boolean", + "description": "" + } + } +} diff --git a/internal/folio/schemas/config/pkixname.schema.json b/internal/folio/schemas/config/pkixname.schema.json new file mode 100644 index 0000000..4a8dcdf --- /dev/null +++ b/internal/folio/schemas/config/pkixname.schema.json @@ -0,0 +1,65 @@ +{ + "$id": "pkixname.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "pkixname", + "type": "object", + "properties": { + "Country": { + "type": "array", + "description": "Country name", + "items": { + "type": "string" + } + }, + "Organization": { + "type": "array", + "description": "Organization name", + "items": { + "type": "string" + } + }, + "OrganizationalUnit": { + "type": "array", + "description": "Organizational Unit name", + "items": { + "type": "string" + } + }, + "Locality": { + "type": "array", + "description": "Locality name", + "items": { + "type": "string" + } + }, + "Province": { + "type": "array", + "description": "Province name", + "items": { + "type": "string" + } + }, + "StreetAddress": { + "type": "array", + "description": "Street address", + "items": { + "type": "string" + } + }, + "PostalCode": { + "type": "array", + "description": "Postal Code", + "items": { + "type": "string" + } + }, + "SerialNumber": { + "type": "string", + "description": "" + }, + "CommonName": { + "type": "string", + "description": "Name" + } + } +} diff --git a/internal/folio/schemas/constraints.schema.json b/internal/folio/schemas/constraints.schema.json new file mode 100644 index 0000000..0f9d988 --- /dev/null +++ b/internal/folio/schemas/constraints.schema.json @@ -0,0 +1,9 @@ +{ + "$id": "constraints.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "constraints", + "type": "object", + "additionalProperties": { + "type": ["string", "number", "object", "array", "boolean", "null"] + } +} diff --git a/internal/folio/schemas/declaration.schema.json b/internal/folio/schemas/declaration.schema.json index 84147d1..f38f3e4 100644 --- a/internal/folio/schemas/declaration.schema.json +++ b/internal/folio/schemas/declaration.schema.json @@ -14,6 +14,15 @@ "type": "string", "description": "Config name" }, + "onerror": { + "type": "string", + "description": "error handling strategy", + "enum": [ + "stop", + "fail", + "skip" + ] + }, "attributes": { "oneOf": [ { "$ref": "bar.schema.json" } diff --git a/internal/folio/schemas/dependencies.schema.json b/internal/folio/schemas/dependencies.schema.json new file mode 100644 index 0000000..db1bc3f --- /dev/null +++ b/internal/folio/schemas/dependencies.schema.json @@ -0,0 +1,9 @@ +{ + "$id": "dependencies.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "dependencies", + "type": "object", + "additionalProperties": { + "$ref": "constraints.schema.json" + } +} diff --git a/internal/folio/schemas/document.schema.json b/internal/folio/schemas/document.schema.json index 07c1354..75578c3 100644 --- a/internal/folio/schemas/document.schema.json +++ b/internal/folio/schemas/document.schema.json @@ -3,8 +3,27 @@ "$schema": "http://json-schema.org/draft-07/schema#", "title": "document", "type": "object", - "required": [ "resources" ], + "required": [], "properties": { + "source": { + "type": "string", + "description": "Document URI" + }, + "format": { + "$ref": "codec.schema.json" + }, + "requires": { + "$ref": "dependencies.schema.json" + }, + "configurations": { + "type": "array", + "description": "Configurations list", + "items": { + "oneOf": [ + { "$ref": "config/block.schema.json" } + ] + } + }, "resources": { "type": "array", "description": "Resources list", @@ -17,4 +36,3 @@ } } } - diff --git a/internal/folio/signature.go b/internal/folio/signature.go new file mode 100644 index 0000000..aad82f6 --- /dev/null +++ b/internal/folio/signature.go @@ -0,0 +1,35 @@ +// Copyright 2024 Matthew Rich . All rights reserved. + +package folio + +import ( + "decl/internal/data" + "decl/internal/signature" + "errors" + "encoding/hex" +) + + +var ( + ErrInvalidSignature error = errors.New("Invalid signature") +) + +type Signature []byte + +func (s Signature) Verify(res data.ContentHasher) error { + i := &signature.Ident{} + return i.VerifySum(res.Hash(), s) +} + +func (s *Signature) SetHexString(sig string) error { + if v, e := hex.DecodeString(sig); e == nil { + *s = v + } else { + return e + } + return nil +} + +func (s *Signature) String() string { + return hex.EncodeToString(*s) +} diff --git a/internal/folio/uri.go b/internal/folio/uri.go index 099e645..3d9ce60 100644 --- a/internal/folio/uri.go +++ b/internal/folio/uri.go @@ -8,6 +8,7 @@ import ( "decl/internal/data" "decl/internal/identifier" "errors" + "strings" ) @@ -17,6 +18,10 @@ var ( type URI identifier.ID +func NewURI(u *url.URL) URI { + return URI(u.String()) +} + func (u URI) NewResource(document data.Document) (newResource data.Resource, err error) { if document == nil { declaration := NewDeclaration() @@ -62,3 +67,18 @@ func (u URI) Extension() (string, string) { return (identifier.ID)(u).Extension() } +func (u URI) ContentType() string { + var ext strings.Builder + exttype, fileext := u.Extension() + if fileext == "" { + return exttype + } + ext.WriteString(exttype) + ext.WriteRune('.') + ext.WriteString(fileext) + return ext.String() +} + +func (u URI) IsEmpty() bool { + return (identifier.ID)(u).IsEmpty() +}