Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Inject typed values. #142

Merged
merged 5 commits into from
Jan 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions cmd/cmd_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"errors"
"testing"

"github.com/onsi/gomega"
Expand Down Expand Up @@ -126,3 +127,83 @@ func TestIncidentSelector(t *testing.T) {
selector = scope.incidentSelector()
g.Expect("(!package||package=a||package=b) && !(package=C||package=D)").To(gomega.Equal(selector))
}

func TestInjectorDefaults(t *testing.T) {
g := gomega.NewGomegaWithT(t)
inj := ResourceInjector{dict: make(map[string]any)}
r := &Resource{
Fields: []Field{
{
Name: "Name",
Key: "person.name",
Default: "Elmer",
},
{
Name: "Age",
Key: "person.age",
},
},
}
err := inj.addDefaults(r)
g.Expect(err).To(gomega.BeNil())
g.Expect(inj.dict[r.Fields[0].Key]).To(gomega.Equal(r.Fields[0].Default))
g.Expect(inj.dict[r.Fields[1].Key]).To(gomega.BeNil())
}

func TestInjectorTypeCast(t *testing.T) {
g := gomega.NewGomegaWithT(t)
inj := ResourceInjector{dict: make(map[string]any)}
r := &Resource{
Fields: []Field{
{
Name: "Name",
Key: "person.name",
Default: "Elmer",
},
{
Name: "Age",
Key: "person.age",
Type: "integer",
Default: "18",
},
{
Name: "Resident",
Key: "person.resident",
Type: "boolean",
Default: "true",
},
{
Name: "Member",
Key: "person.member",
Type: "boolean",
Default: 1,
},
{
Name: "One",
Key: "person.one",
Type: "integer",
Default: true,
},
},
}
err := inj.addDefaults(r)
g.Expect(err).To(gomega.BeNil())
g.Expect(inj.dict[r.Fields[0].Key]).To(gomega.Equal(r.Fields[0].Default))
g.Expect(inj.dict[r.Fields[1].Key]).To(gomega.Equal(18))
g.Expect(inj.dict[r.Fields[2].Key]).To(gomega.BeTrue())
g.Expect(inj.dict[r.Fields[3].Key]).To(gomega.BeTrue())
g.Expect(inj.dict[r.Fields[4].Key]).To(gomega.Equal(1))

// cast error.
inj.dict = make(map[string]any)
r.Fields = append(
r.Fields,
Field{
Name: "Resident",
Key: "person.parent",
Type: "integer",
Default: "true",
})
err = inj.addDefaults(r)
g.Expect(errors.Is(err, &TypeError{})).To(gomega.BeTrue())
}
188 changes: 166 additions & 22 deletions cmd/injector.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ import (
"os"
pathlib "path"
"regexp"
"strconv"
"strings"

"github.com/konveyor/analyzer-lsp/provider"
"github.com/konveyor/tackle2-hub/api"
"github.com/konveyor/tackle2-hub/nas"
)

// KeyRegex $(variable)
var (
KeyRegex = regexp.MustCompile(`(\$\()([^)]+)(\))`)
)
Expand Down Expand Up @@ -49,11 +51,92 @@ func (e *FieldNotMatched) Is(err error) (matched bool) {
return
}

// TypeError used to report resource field cast error.
type TypeError struct {
Field *Field
Reason string
Object any
}

func (e *TypeError) Error() (s string) {
return fmt.Sprintf(
"Resource injector: cast failed. field=%s type=%s reason=%s, object:%v",
e.Field.Name,
e.Field.Type,
e.Reason,
e.Object)
}

func (e *TypeError) Is(err error) (matched bool) {
var inst *TypeError
matched = errors.As(err, &inst)
return
}

// Field injection specification.
type Field struct {
Name string `json:"name"`
Path string `json:"path"`
Key string `json:"key"`
Name string `json:"name"`
Path string `json:"path"`
Key string `json:"key"`
Type string `json:"type"`
Default any `json:"default"`
}

// cast returns object cast as defined by Field.Type.
func (f *Field) cast(object any) (cast any, err error) {
cast = object
if f.Type == "" {
return
}
defer func() {
if err != nil {
err = &TypeError{
Field: f,
Reason: err.Error(),
Object: object,
}
}
}()
switch strings.ToLower(f.Type) {
case "string":
cast = fmt.Sprintf("%v", object)
case "integer":
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider supporting case "integer" | "int" and case "boolean" | "bool"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking longer form of integer|boolean|string would be more language (Go) agnostic. Also, supporting the shorter forms would be more to document. Seemed like less/simpler choices would be best.

switch x := object.(type) {
case int,
int8,
int16,
int32,
int64:
cast = x
case bool:
cast = 0
if x {
cast = 1
}
case string:
cast, err = strconv.Atoi(x)
default:
err = errors.New("expected: integer|boolean|string")
}
case "boolean":
switch x := object.(type) {
case bool:
cast = x
case int,
int8,
int16,
int32,
int64:
cast = x != 0
case string:
cast, err = strconv.ParseBool(x)
default:
err = errors.New("expected: integer|boolean|string")
}
default:
err = errors.New("expected: integer|boolean|string")
}
return
}

// Resource injection specification.
Expand Down Expand Up @@ -97,8 +180,27 @@ func (p *ParsedSelector) With(s string) {
}

// ResourceInjector inject resources into extension metadata.
// Example:
// metadata:
// provider:
// address: localhost:$(PORT)
// initConfig:
// - providerSpecificConfig:
// mavenInsecure: $(maven.insecure)
// mavenSettingsFile: $(maven.settings.path)
// name: java
// resources:
// - selector: identity:kind=maven
// fields:
// - key: maven.settings.path
// name: settings
// path: /shared/creds/maven/settings.xml
// - selector: setting:key=mvn.insecure.enabled
// fields:
// - key: maven.insecure
// name: value
type ResourceInjector struct {
dict map[string]string
dict map[string]any
}

// Inject resources into extension metadata.
Expand Down Expand Up @@ -126,11 +228,14 @@ func (r *ResourceInjector) Inject(extension *api.Extension) (p *provider.Config,

// build builds resource dictionary.
func (r *ResourceInjector) build(md *Metadata) (err error) {
r.dict = make(map[string]string)
r.dict = make(map[string]any)
application, err := addon.Task.Application()
if err != nil {
return
}
for _, resource := range md.Resources {
err = r.addDefaults(&resource)
}
for _, resource := range md.Resources {
parsed := ParsedSelector{}
parsed.With(resource.Selector)
Expand Down Expand Up @@ -165,6 +270,20 @@ func (r *ResourceInjector) build(md *Metadata) (err error) {
return
}

// addDefaults adds defaults when specified.
func (r *ResourceInjector) addDefaults(resource *Resource) (err error) {
for _, f := range resource.Fields {
if f.Default == nil {
continue
}
err = r.addField(&f, f.Default)
if err != nil {
return
}
}
return
}

// add the resource fields specified in the injector.
func (r *ResourceInjector) add(resource *Resource, object any) (err error) {
mp := r.asMap(object)
Expand All @@ -177,30 +296,49 @@ func (r *ResourceInjector) add(resource *Resource, object any) (err error) {
}
return
}
fv := r.string(v)
if f.Path != "" {
err = r.write(f.Path, fv)
if err != nil {
return
}
fv = f.Path
err = r.addField(&f, v)
if err != nil {
return
}
r.dict[f.Key] = fv
}
return
}

// addField adds field to the dict.
// When field has a path defined, the values is written to the
// file and the dict[key] = path.
func (r *ResourceInjector) addField(f *Field, v any) (err error) {
if f.Path != "" {
err = r.write(f.Path, v)
if err != nil {
return
}
v = f.Path
} else {
v, err = f.cast(v)
if err != nil {
return
}
}
r.dict[f.Key] = v
return
}

// write a resource field value to a file.
func (r *ResourceInjector) write(path string, s string) (err error) {
func (r *ResourceInjector) write(path string, object any) (err error) {
err = nas.MkDir(pathlib.Dir(path), 0755)
if err != nil {
return
}
f, err := os.Create(path)
if err == nil {
_, err = f.Write([]byte(s))
_ = f.Close()
if err != nil {
return
}
defer func() {
_ = f.Close()
}()
s := r.string(object)
_, err = f.Write([]byte(s))
return
}

Expand Down Expand Up @@ -249,11 +387,17 @@ func (r *ResourceInjector) inject(in any) (out any) {
if len(match) < 3 {
break
}
node = strings.Replace(
node,
match[0],
r.dict[match[2]],
-1)
v := r.dict[match[2]]
if len(node) > len(match[0]) {
node = strings.Replace(
node,
match[0],
r.string(v),
-1)
} else {
out = v
return
}
}
out = node
default:
Expand Down
Loading