Skip to content

Commit

Permalink
feat(latest_version): regex templating
Browse files Browse the repository at this point in the history
  • Loading branch information
JosephKav committed Jan 6, 2024
1 parent e016e3f commit 80f22be
Show file tree
Hide file tree
Showing 23 changed files with 371 additions and 75 deletions.
10 changes: 10 additions & 0 deletions service/latest_version/filter/help_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,16 @@ func testURLCommandRegex() URLCommand {
Regex: &regex,
Index: index}
}
func testURLCommandRegexTemplate() URLCommand {
regex := "-([0-9.]+)-"
index := 0
template := "_$1_"
return URLCommand{
Type: "regex",
Regex: &regex,
Index: index,
Template: &template}
}

func testURLCommandReplace() URLCommand {
old := "foo"
Expand Down
45 changes: 37 additions & 8 deletions service/latest_version/filter/urlcommand.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,13 @@ func (s *URLCommandSlice) String() (str string) {

// URLCommand is a command to be ran to filter version from the URL body.
type URLCommand struct {
Type string `yaml:"type" json:"type"` // regex/replace/split
Regex *string `yaml:"regex,omitempty" json:"regex,omitempty"` // regex: regexp.MustCompile(Regex)
Index int `yaml:"index,omitempty" json:"index,omitempty"` // regex/split: re.FindAllString(URL_content, -1)[Index] / strings.Split("text")[Index]
Text *string `yaml:"text,omitempty" json:"text,omitempty"` // split: strings.Split(tgtString, "Text")
New *string `yaml:"new,omitempty" json:"new,omitempty"` // replace: strings.ReplaceAll(tgtString, "Old", "New")
Old *string `yaml:"old,omitempty" json:"old,omitempty"` // replace: strings.ReplaceAll(tgtString, "Old", "New")
Type string `yaml:"type" json:"type"` // regex/replace/split
Regex *string `yaml:"regex,omitempty" json:"regex,omitempty"` // regex: regexp.MustCompile(Regex)
Index int `yaml:"index,omitempty" json:"index,omitempty"` // regex/split: re.FindAllString(URL_content, -1)[Index] / strings.Split("text")[Index]
Template *string `yaml:"template,omitempty" json:"template,omitempty"` // regex: template
Text *string `yaml:"text,omitempty" json:"text,omitempty"` // split: strings.Split(tgtString, "Text")
New *string `yaml:"new,omitempty" json:"new,omitempty"` // replace: strings.ReplaceAll(tgtString, "Old", "New")
Old *string `yaml:"old,omitempty" json:"old,omitempty"` // replace: strings.ReplaceAll(tgtString, "Old", "New")
}

// String returns a string representation of the URLCommand.
Expand Down Expand Up @@ -113,6 +114,9 @@ func (c *URLCommand) run(text string, logFrom *util.LogFrom) (string, error) {
text = strings.ReplaceAll(text, *c.Old, *c.New)
case "regex":
msg = fmt.Sprintf("Regexing %q", *c.Regex)
if c.Template != nil {
msg = fmt.Sprintf("%s with template %q", msg, *c.Template)
}
text, err = c.regex(text, logFrom)
}
if err != nil {
Expand All @@ -137,6 +141,7 @@ func (c *URLCommand) regex(text string, logFrom *util.LogFrom) (string, error) {
index = len(texts) + c.Index
}

// No matches.
if len(texts) == 0 {
err := fmt.Errorf("%s %q didn't return any matches",
c.Type, *c.Regex)
Expand All @@ -148,7 +153,7 @@ func (c *URLCommand) regex(text string, logFrom *util.LogFrom) (string, error) {

return text, err
}

// Index out of range.
if (len(texts) - index) < 1 {
err := fmt.Errorf("%s (%s) returned %d elements on %q, but the index wants element number %d",
c.Type, *c.Regex, len(texts), text, (index + 1))
Expand All @@ -157,7 +162,27 @@ func (c *URLCommand) regex(text string, logFrom *util.LogFrom) (string, error) {
return text, err
}

return texts[index][len(texts[index])-1], nil
return c.regexTemplate(texts, index, logFrom), nil
}

// regexTemplate `text` with the URLCommand's regex template.
func (c *URLCommand) regexTemplate(texts [][]string, index int, logFrom *util.LogFrom) (result string) {
// No template, return the text at the index.
if c.Template == nil {
return texts[index][len(texts[index])-1]
}

text := texts[index]

// Replace placeholders in the template with matched groups in reverse order
// (so that '$10' isn't replace by '$1')
result = *c.Template
for i := len(text) - 1; i > 0; i-- {
placeholder := fmt.Sprintf("$%d", i)
result = strings.ReplaceAll(result, placeholder, text[i])
}

return result
}

// split `text` with the URLCommand's text amd return the index specified.
Expand Down Expand Up @@ -225,6 +250,10 @@ func (c *URLCommand) CheckValues(prefix string) (errs error) {
util.ErrorToString(errs), prefix, *c.Regex)
}
}
// Remove the template if it's empty
if util.DefaultIfNil(c.Template) == "" {
c.Template = nil
}
case "replace":
if c.New == nil {
errs = fmt.Errorf("%s%snew: <required> (text you want to replace with)\\",
Expand Down
110 changes: 108 additions & 2 deletions service/latest_version/filter/urlcommand_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ func TestURLCommandSlice_String(t *testing.T) {
want: `
- type: regex
regex: -([0-9.]+)-
`,
},
"regex (templated)": {
slice: &URLCommandSlice{
testURLCommandRegexTemplate()},
want: `
- type: regex
regex: -([0-9.]+)-
template: _$1_
`,
},
"replace": {
Expand Down Expand Up @@ -240,6 +249,12 @@ func TestURLCommandSlice_Run(t *testing.T) {
errRegex: `regex .* returned \d elements on "[^']+", but the index wants element number \d`,
want: testText,
},
"regex with template": {
slice: &URLCommandSlice{
{Type: "regex", Regex: stringPtr("([a-z]+)[0-9]+"), Index: 1, Template: stringPtr("$1_$2")}},
errRegex: "^$",
want: "abc_123",
},
"replace": {
slice: &URLCommandSlice{
{Type: "replace", Old: stringPtr("-"), New: stringPtr(" ")}},
Expand Down Expand Up @@ -306,6 +321,75 @@ func TestURLCommandSlice_Run(t *testing.T) {
}
}

func TestURLCommand_regexTemplate(t *testing.T) {
// GIVEN a URLCommand and text to run it on
tests := map[string]struct {
text string
regex string
index int
template *string
want string
}{
"datetime template": {
text: "2024-01-01T16-36-33Z",
regex: `([\d-]+)T(\d+)-(\d+)-(\d+)Z`,
template: stringPtr("$1T$2:$3:$4Z"),
want: "2024-01-01T16:36:33Z",
},
"template with 10+ matches": {
text: "abcdefghijklmnopqrstuvwxyz",
regex: `(\d)(\d)(\d)(\d)(\d{2})(\d)(\d)(\d)(\d)(\d)(\d)`,
template: stringPtr("$1_$2_$3_$4_$5_$6_$7_$8_$9_$10_$11"),
want: "a_b_c_d_ef_g_h_i_j_k_l",
},
"template using non-zero index": {
text: "abc123-def456-ghi789",
regex: `(\s+)(\d+)`,
index: 1,
template: stringPtr("$2$1"),
want: "456def",
},
"template with placeholder out of range": {
text: "abc123-def456-ghi789",
regex: `(\s+)(\d+)`,
template: stringPtr("$1$4-$10"),
want: "abc-abc0",
},
"template with all placeholders out of range": {
text: "abc123-def456-ghi789",
regex: `(\s+)(\d+)`,
template: stringPtr("$4$5"),
want: "",
},
"no template": {
text: "abc123-def456-ghi789",
regex: `(\s+)(\d+)`,
index: 1,
want: "456",
},
}

for name, tc := range tests {
t.Run(name, func(t *testing.T) {
re := regexp.MustCompile(tc.regex)
texts := re.FindAllStringSubmatch(tc.text, -1)
urlCommand := URLCommand{
Regex: stringPtr(tc.regex),
Index: tc.index,
Template: tc.template}

// WHEN regexTemplate is called on the regex matches
got := urlCommand.regexTemplate(texts, 0, &util.LogFrom{})

// THEN the expected string is returned
if got != tc.want {
t.Fatalf("want: %q\n got: %q",
tc.want, got)
}
})
}
}

func TestURLCommand_String(t *testing.T) {
// GIVEN a URLCommand
regex := testURLCommandRegex()
Expand Down Expand Up @@ -363,8 +447,9 @@ text: this
func TestURLCommandSlice_CheckValues(t *testing.T) {
// GIVEN a URLCommandSlice
tests := map[string]struct {
slice *URLCommandSlice
errRegex []string
slice *URLCommandSlice
wantSlice *URLCommandSlice
errRegex []string
}{
"nil slice": {
slice: nil,
Expand All @@ -384,6 +469,17 @@ func TestURLCommandSlice_CheckValues(t *testing.T) {
{Type: "regex", Regex: stringPtr("[0-")}},
errRegex: []string{`^ regex: .* <invalid>`},
},
"valid regex with template": {
slice: &URLCommandSlice{testURLCommandRegexTemplate()},
errRegex: []string{`^$`},
},
"valid regex with empty template": {
slice: &URLCommandSlice{
{Type: "regex", Regex: stringPtr("[0-"), Template: stringPtr("")}},
wantSlice: &URLCommandSlice{
{Type: "regex", Regex: stringPtr("[0-")}},
errRegex: []string{`^$`},
},
"valid replace": {
slice: &URLCommandSlice{
testURLCommandReplace()},
Expand Down Expand Up @@ -456,6 +552,16 @@ func TestURLCommandSlice_CheckValues(t *testing.T) {
tc.errRegex[i], strings.ReplaceAll(e, `\`, "\n"))
}
}

// AND the slice is as expected
if tc.wantSlice != nil {
strHave := tc.slice.String()
strWant := tc.wantSlice.String()
if strHave != strWant {
t.Errorf("want slice:\n%q\ngot: %q",
strWant, strHave)
}
}
})
}
}
Expand Down
13 changes: 7 additions & 6 deletions web/api/types/argus.go
Original file line number Diff line number Diff line change
Expand Up @@ -532,12 +532,13 @@ func (slice *URLCommandSlice) String() (str string) {

// URLCommand is a command to be ran to filter version from the URL body.
type URLCommand struct {
Type string `json:"type,omitempty" yaml:"type,omitempty"` // regex/replace/split
Regex *string `json:"regex,omitempty" yaml:"regex,omitempty"` // regex: regexp.MustCompile(Regex)
Index int `json:"index,omitempty" yaml:"index,omitempty"` // regex/split: re.FindAllString(URL_content, -1)[Index] / strings.Split("text")[Index]
Text *string `json:"text,omitempty" yaml:"text,omitempty"` // split: strings.Split(tgtString, "Text")
New *string `json:"new,omitempty" yaml:"new,omitempty"` // replace: strings.ReplaceAll(tgtString, "Old", "New")
Old *string `json:"old,omitempty" yaml:"old,omitempty"` // replace: strings.ReplaceAll(tgtString, "Old", "New")
Type string `json:"type,omitempty" yaml:"type,omitempty"` // regex/replace/split
Regex *string `json:"regex,omitempty" yaml:"regex,omitempty"` // regex: regexp.MustCompile(Regex)
Index int `json:"index,omitempty" yaml:"index,omitempty"` // regex/split: re.FindAllString(URL_content, -1)[Index] / strings.Split("text")[Index]
Template *string `yaml:"template,omitempty" json:"template,omitempty"` // regex: template
Text *string `json:"text,omitempty" yaml:"text,omitempty"` // split: strings.Split(tgtString, "Text")
New *string `json:"new,omitempty" yaml:"new,omitempty"` // replace: strings.ReplaceAll(tgtString, "Old", "New")
Old *string `json:"old,omitempty" yaml:"old,omitempty"` // replace: strings.ReplaceAll(tgtString, "Old", "New")
}

type Command []string
Expand Down
13 changes: 7 additions & 6 deletions web/api/v1/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,12 +169,13 @@ func convertURLCommandSlice(commands *filter.URLCommandSlice) *api_type.URLComma
slice := make(api_type.URLCommandSlice, len(*commands))
for index := range *commands {
slice[index] = api_type.URLCommand{
Type: (*commands)[index].Type,
Regex: (*commands)[index].Regex,
Index: (*commands)[index].Index,
Text: (*commands)[index].Text,
Old: (*commands)[index].Old,
New: (*commands)[index].New}
Type: (*commands)[index].Type,
Regex: (*commands)[index].Regex,
Index: (*commands)[index].Index,
Template: (*commands)[index].Template,
Text: (*commands)[index].Text,
Old: (*commands)[index].Old,
New: (*commands)[index].New}
}
return &slice
}
Expand Down
6 changes: 3 additions & 3 deletions web/ui/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

73 changes: 73 additions & 0 deletions web/ui/react-app/src/components/generic/form-check.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { Col, FormCheck as FormCheckRB, FormGroup } from "react-bootstrap";
import { FC, JSX, useMemo } from "react";

import { FormCheckType } from "react-bootstrap/esm/FormCheck";
import FormLabel from "./form-label";
import { useFormContext } from "react-hook-form";

interface FormCheckProps {
name?: string;

col_xs?: number;
col_sm?: number;
size?: "sm" | "lg";
label?: string;
smallLabel?: boolean;
tooltip?: string | JSX.Element;
type?: FormCheckType;

onRight?: boolean;
onMiddle?: boolean;
}

const FormCheck: FC<FormCheckProps> = ({
name,

col_xs = 12,
col_sm = 6,
size = "sm",
label,
smallLabel,
tooltip,
type = "checkbox",

onRight,
onMiddle,
}) => {
const { register } = useFormContext();

const padding = useMemo(() => {
return [
col_sm !== 12
? onRight
? "ps-sm-2"
: onMiddle
? "ps-sm-1 pe-sm-1"
: "pe-sm-2"
: "",
col_xs !== 12 ? (onRight ? "ps-2" : onMiddle ? "ps-1 pe-1" : "pe-2") : "",
].join(" ");
}, [col_xs, col_sm, onRight, onMiddle]);

const registrationProps = useMemo(() => {
return name ? { ...register(name) } : {};
}, [name]);

return (
<Col xs={col_xs} sm={col_sm} className={`${padding} pt-1 pb-1 col-form`}>
<FormGroup>
{label && (
<FormLabel text={label} tooltip={tooltip} small={!!smallLabel} />
)}
<FormCheckRB
className={`form-check${size === "lg" ? "-large" : ""}`}
type={type}
autoFocus={false}
{...registrationProps}
/>
</FormGroup>
</Col>
);
};

export default FormCheck;
2 changes: 2 additions & 0 deletions web/ui/react-app/src/components/generic/form.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import FormCheck from "./form-check";
import FormItem from "./form-item";
import FormItemColour from "./form-item-colour";
import FormItemWithPreview from "./form-item-with-preview";
Expand All @@ -8,6 +9,7 @@ import FormSelect from "./form-select";
import FormTextArea from "./form-textarea";

export {
FormCheck,
FormItem,
FormItemColour,
FormItemWithPreview,
Expand Down
Loading

0 comments on commit 80f22be

Please sign in to comment.