Skip to content

Commit

Permalink
Add regexp module (#103)
Browse files Browse the repository at this point in the history
  • Loading branch information
myzie authored Sep 18, 2023
1 parent 6ebe6f4 commit 0719c97
Show file tree
Hide file tree
Showing 6 changed files with 292 additions and 1 deletion.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@ func main() {

Modules are included that generally wrap the equivalent Go package. For example,
there is direct correspondence between `base64`, `bytes`, `json`, `math`, `os`,
`rand`, `strconv`, `strings`, and `time` Risor modules and the Go standard library.
`rand`, `regexp`, `strconv`, `strings`, and `time` Risor modules and the Go
standard library.

Risor modules that are beyond the Go standard library currently include
`aws`, `pgx`, and `uuid`.
Expand Down
2 changes: 2 additions & 0 deletions internal/cfg/cfg.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
modMath "github.com/risor-io/risor/modules/math"
modOs "github.com/risor-io/risor/modules/os"
modRand "github.com/risor-io/risor/modules/rand"
modRegexp "github.com/risor-io/risor/modules/regexp"
modStrconv "github.com/risor-io/risor/modules/strconv"
modStrings "github.com/risor-io/risor/modules/strings"
modTime "github.com/risor-io/risor/modules/time"
Expand Down Expand Up @@ -99,6 +100,7 @@ func (cfg *RisorConfig) addDefaultGlobals() {
"math": modMath.Module(),
"os": modOs.Module(),
"rand": modRand.Module(),
"regexp": modRegexp.Module(),
"strconv": modStrconv.Module(),
"strings": modStrings.Module(),
"time": modTime.Module(),
Expand Down
2 changes: 2 additions & 0 deletions modules/all/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
modMath "github.com/risor-io/risor/modules/math"
modOs "github.com/risor-io/risor/modules/os"
modRand "github.com/risor-io/risor/modules/rand"
modRegexp "github.com/risor-io/risor/modules/regexp"
modStrconv "github.com/risor-io/risor/modules/strconv"
modStrings "github.com/risor-io/risor/modules/strings"
modTime "github.com/risor-io/risor/modules/time"
Expand All @@ -28,6 +29,7 @@ func Builtins() map[string]object.Object {
"math": modMath.Module(),
"os": modOs.Module(),
"rand": modRand.Module(),
"regexp": modRegexp.Module(),
"strconv": modStrconv.Module(),
"strings": modStrings.Module(),
"time": modTime.Module(),
Expand Down
50 changes: 50 additions & 0 deletions modules/regexp/regexp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package rand

import (
"context"
"regexp"

"github.com/risor-io/risor/internal/arg"
"github.com/risor-io/risor/object"
)

func Compile(ctx context.Context, args ...object.Object) object.Object {
if err := arg.Require("regexp.compile", 1, args); err != nil {
return err
}
pattern, err := object.AsString(args[0])
if err != nil {
return err
}
r, rErr := regexp.Compile(pattern)
if rErr != nil {
return object.NewError(rErr)
}
return object.NewRegexp(r)
}

func Match(ctx context.Context, args ...object.Object) object.Object {
if err := arg.Require("regexp.match", 2, args); err != nil {
return err
}
pattern, err := object.AsString(args[0])
if err != nil {
return err
}
str, err := object.AsString(args[1])
if err != nil {
return err
}
matched, rErr := regexp.MatchString(pattern, str)
if rErr != nil {
return object.NewError(rErr)
}
return object.NewBool(matched)
}

func Module() *object.Module {
return object.NewBuiltinsModule("regexp", map[string]object.Object{
"compile": object.NewBuiltin("compile", Compile),
"match": object.NewBuiltin("match", Match),
}, Compile)
}
194 changes: 194 additions & 0 deletions object/regexp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
package object

import (
"context"
"fmt"
"regexp"

"github.com/risor-io/risor/op"
)

type Regexp struct {
*base
value *regexp.Regexp
}

func (r *Regexp) Type() Type {
return REGEXP
}

func (r *Regexp) Inspect() string {
return fmt.Sprintf("regexp(%q)", r.value.String())
}

func (r *Regexp) String() string {
return r.Inspect()
}

func (r *Regexp) Interface() interface{} {
return r.value
}

func (r *Regexp) HashKey() HashKey {
return HashKey{Type: r.Type(), StrValue: r.value.String()}
}

func (r *Regexp) Compare(other Object) (int, error) {
typeComp := CompareTypes(r, other)
if typeComp != 0 {
return typeComp, nil
}
otherRegex := other.(*Regexp)
if r.value == otherRegex.value {
return 0, nil
}
if r.value.String() > otherRegex.value.String() {
return 1, nil
}
return -1, nil
}

func (r *Regexp) Equals(other Object) Object {
switch other := other.(type) {
case *Regexp:
if r.value == other.value {
return True
}
}
return False
}

func (r *Regexp) MarshalJSON() ([]byte, error) {
return []byte(r.value.String()), nil
}

func (r *Regexp) RunOperation(opType op.BinaryOpType, right Object) Object {
return NewError(fmt.Errorf("eval error: unsupported operation for regexp: %v", opType))
}

func (r *Regexp) GetAttr(name string) (Object, bool) {
switch name {
case "match":
return &Builtin{
name: "regexp.match",
fn: func(ctx context.Context, args ...Object) Object {
if len(args) != 1 {
return NewArgsError("regexp.match", 1, len(args))
}
strValue, err := AsString(args[0])
if err != nil {
return err
}
return NewBool(r.value.MatchString(strValue))
},
}, true
case "find":
return &Builtin{
name: "regexp.find",
fn: func(ctx context.Context, args ...Object) Object {
if len(args) != 1 {
return NewArgsError("regexp.find", 1, len(args))
}
strValue, err := AsString(args[0])
if err != nil {
return err
}
return NewString(r.value.FindString(strValue))
},
}, true
case "find_all":
return &Builtin{
name: "regexp.find_all",
fn: func(ctx context.Context, args ...Object) Object {
if len(args) < 1 || len(args) > 2 {
return NewArgsRangeError("regexp.find_all", 1, 2, len(args))
}
strValue, err := AsString(args[0])
if err != nil {
return err
}
n := -1
if len(args) == 2 {
i64, err := AsInt(args[1])
if err != nil {
return err
}
n = int(i64)
}
var matches []Object
for _, match := range r.value.FindAllString(strValue, n) {
matches = append(matches, NewString(match))
}
return NewList(matches)
},
}, true
case "find_submatch":
return &Builtin{
name: "regexp.find_submatch",
fn: func(ctx context.Context, args ...Object) Object {
if len(args) != 1 {
return NewArgsError("regexp.find_submatch", 1, len(args))
}
strValue, err := AsString(args[0])
if err != nil {
return err
}
var matches []Object
for _, match := range r.value.FindStringSubmatch(strValue) {
matches = append(matches, NewString(match))
}
return NewList(matches)
},
}, true
case "replace_all":
return &Builtin{
name: "regexp.replace_all",
fn: func(ctx context.Context, args ...Object) Object {
if len(args) != 2 {
return NewArgsError("regexp.replace_all", 2, len(args))
}
strValue, err := AsString(args[0])
if err != nil {
return err
}
replaceValue, err := AsString(args[1])
if err != nil {
return err
}
return NewString(r.value.ReplaceAllString(strValue, replaceValue))
},
}, true
case "split":
return &Builtin{
name: "regexp.split",
fn: func(ctx context.Context, args ...Object) Object {
if len(args) < 1 || len(args) > 2 {
return NewArgsRangeError("regexp.split", 1, 2, len(args))
}
strValue, err := AsString(args[0])
if err != nil {
return err
}
n := -1
if len(args) == 2 {
i64, err := AsInt(args[1])
if err != nil {
return err
}
n = int(i64)
}
matches := r.value.Split(strValue, n)
matchObjects := make([]Object, 0, len(matches))
for _, match := range matches {
matchObjects = append(matchObjects, NewString(match))
}
return NewList(matchObjects)
},
}, true
}
return nil, false
}

func NewRegexp(value *regexp.Regexp) *Regexp {
return &Regexp{value: value}
}
42 changes: 42 additions & 0 deletions object/regexp_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package object

import (
"context"
"regexp"
"testing"

"github.com/stretchr/testify/require"
)

func TestRegexpMatch(t *testing.T) {
// From example: https://pkg.go.dev/regexp#MatchString
obj := NewRegexp(regexp.MustCompile(`foo.*`))
match, ok := obj.GetAttr("match")
require.True(t, ok)
result := match.(*Builtin).Call(context.Background(), NewString("seafood"))
require.Equal(t, True, result)

obj = NewRegexp(regexp.MustCompile(`bar.*`))
match, ok = obj.GetAttr("match")
require.True(t, ok)
result = match.(*Builtin).Call(context.Background(), NewString("seafood"))
require.Equal(t, False, result)
}

func TestRegexpFind(t *testing.T) {
// From example: https://pkg.go.dev/regexp#Regexp.Find
obj := NewRegexp(regexp.MustCompile(`foo.?`))
find, ok := obj.GetAttr("find")
require.True(t, ok)
result := find.(*Builtin).Call(context.Background(), NewString("seafood fool"))
require.Equal(t, NewString("food"), result)
}

func TestRegexpFindAll(t *testing.T) {
// From example: https://pkg.go.dev/regexp#Regexp.FindAll
obj := NewRegexp(regexp.MustCompile(`foo.?`))
findAll, ok := obj.GetAttr("find_all")
require.True(t, ok)
result := findAll.(*Builtin).Call(context.Background(), NewString("seafood fool"))
require.Equal(t, NewList([]Object{NewString("food"), NewString("fool")}), result)
}

0 comments on commit 0719c97

Please sign in to comment.