Skip to content

Commit

Permalink
Merge pull request #223 from rsteube/generic-macro
Browse files Browse the repository at this point in the history
macro: extract generic macro
  • Loading branch information
rsteube authored Sep 21, 2023
2 parents 4a6b2ad + 5e89174 commit 3b975fe
Show file tree
Hide file tree
Showing 5 changed files with 144 additions and 93 deletions.
4 changes: 2 additions & 2 deletions action.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func (value) JSONSchema() *jsonschema.Schema {

examples := make([]interface{}, 0, len(macros))
for _, name := range sortedNames {
examples = append(examples, fmt.Sprintf("$%v(%v)", name, macros[name].Signature()))
examples = append(examples, fmt.Sprintf("$%v(%v)", name, macros[name].macro.Signature()))
}
return &jsonschema.Schema{
Type: "string",
Expand All @@ -59,7 +59,7 @@ func ActionMacro(s string) carapace.Action {
if m, ok := macros[matches["macro"]]; !ok {
return carapace.ActionMessage("unknown macro: %#v", s)
} else {
return m.f(matches["arg"])
return m.Parse(s)
}
})
}
Expand Down
107 changes: 26 additions & 81 deletions macro.go
Original file line number Diff line number Diff line change
@@ -1,31 +1,28 @@
package spec

import (
"fmt"
"reflect"
"regexp"
"strings"

"github.com/rsteube/carapace"
"gopkg.in/yaml.v3"
"github.com/rsteube/carapace-spec/pkg/macro"
)

type Macro struct {
f func(string) carapace.Action
s func() string
macro macro.Macro[carapace.Action]
disableFlagParsing bool
}

func (m Macro) parse(s string) carapace.Action {
r := regexp.MustCompile(`^\$(?P<macro>[^(]*)(\((?P<arg>.*)\))?$`)
matches := r.FindStringSubmatch(s)
if matches == nil {
return carapace.ActionMessage("malformed macro: '%v'", s)
func (m Macro) Parse(s string) carapace.Action {
a, err := m.macro.Parse(s)
if err != nil {
return carapace.ActionMessage(err.Error())
}
return m.f(matches[3])
return *a
}

func (m Macro) Signature() string {
return m.macro.Signature()
}
func (m Macro) Signature() string { return m.s() }
func (m Macro) NoFlag() Macro { m.disableFlagParsing = true; return m }

func (m Macro) NoFlag() Macro { m.disableFlagParsing = true; return m }

var macros = make(map[string]Macro)

Expand All @@ -38,81 +35,29 @@ func AddMacro(s string, m Macro) {
macros["_"+s] = m
}

// MacroN creates a macro without an argument
func MacroN(f func() carapace.Action) Macro {
return Macro{
f: func(s string) carapace.Action {
return f()
},
s: func() string { return "" },
macro: macro.MacroN[carapace.Action](func() (*carapace.Action, error) {
a := f()
return &a, nil
}),
}
}

// MacroI creates a macro with an argument
func MacroI[T any](f func(t T) carapace.Action) Macro {
return Macro{
f: func(s string) carapace.Action {
var t T
if reflect.TypeOf(t).Kind() == reflect.String {
reflect.ValueOf(&t).Elem().SetString(s)
} else {
if err := yaml.Unmarshal([]byte(s), &t); err != nil {
return carapace.ActionMessage(err.Error())
}

if s == "" {
if m := reflect.ValueOf(&t).MethodByName("Default"); m.IsValid() && m.Type().NumIn() == 0 {
values := m.Call([]reflect.Value{}) // TODO check if needs args
if len(values) > 0 && values[0].Type().AssignableTo(reflect.TypeOf(t)) {
reflect.ValueOf(&t).Elem().Set(values[0])
}
}

}
}
return f(t)
},
s: func() string { return signature(new(T)) },
macro: macro.MacroI[T, carapace.Action](func(t T) (*carapace.Action, error) {
a := f(t)
return &a, nil
}),
}
}

// MacroV creates a macro with a variable argument
func MacroV[T any](f func(s ...T) carapace.Action) Macro {
func MacroV[T any](f func(t ...T) carapace.Action) Macro {
return Macro{
f: func(s string) carapace.Action {
if s == "" {
return f()
}

var t []T
if err := yaml.Unmarshal([]byte(s), &t); err != nil {
return carapace.ActionMessage("malformed macro arg: '%v', expected '%v'", s, reflect.TypeOf(t))
}
return f(t...)
},
s: func() string { return fmt.Sprintf("[%v]", signature(new(T))) },
}
}

func signature(i interface{}) string {
elem := reflect.ValueOf(i).Elem()
switch elem.Kind() {
case reflect.Struct:
out, err := yaml.Marshal(i)
if err != nil {
return err.Error()
}
lines := strings.Split(string(out), "\n")
return fmt.Sprintf("{%v}", strings.Join(lines[:len(lines)-1], ", "))

case reflect.Slice:
ptr := reflect.New(elem.Type().Elem()).Interface()
return fmt.Sprintf("[%v]", signature(ptr))

case reflect.String:
return `""`

default:
return fmt.Sprintf("%v", reflect.ValueOf(i).Elem())
macro: macro.MacroV[T, carapace.Action](func(t ...T) (*carapace.Action, error) {
a := f(t...)
return &a, nil
}),
}
}
16 changes: 8 additions & 8 deletions macro_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,32 +17,32 @@ func (a Arg) Default() Arg {
}

func TestSignature(t *testing.T) {
signature := MacroI(func(a Arg) carapace.Action { return carapace.ActionValues() }).Signature()
signature := MacroI(func(a Arg) carapace.Action { return carapace.ActionValues() }).macro.Signature()
if expected := `{name: "", option: false}`; signature != expected {
t.Errorf("should be: %v", expected)
}

signature = MacroI(func(a []Arg) carapace.Action { return carapace.ActionValues() }).Signature()
signature = MacroI(func(a []Arg) carapace.Action { return carapace.ActionValues() }).macro.Signature()
if expected := `[{name: "", option: false}]`; signature != expected {
t.Errorf("should be: %v", expected)
}

signature = MacroI(func(b bool) carapace.Action { return carapace.ActionValues() }).Signature()
signature = MacroI(func(b bool) carapace.Action { return carapace.ActionValues() }).macro.Signature()
if expected := `false`; signature != expected {
t.Errorf("should be: %v", expected)
}

signature = MacroV(func(a ...Arg) carapace.Action { return carapace.ActionValues() }).Signature()
signature = MacroV(func(a ...Arg) carapace.Action { return carapace.ActionValues() }).macro.Signature()
if expected := `[{name: "", option: false}]`; signature != expected {
t.Errorf("should be: %v", expected)
}

signature = MacroV(func(b ...bool) carapace.Action { return carapace.ActionValues() }).Signature()
signature = MacroV(func(b ...bool) carapace.Action { return carapace.ActionValues() }).macro.Signature()
if expected := `[false]`; signature != expected {
t.Errorf("should be: %v", expected)
}

signature = MacroI(func(s string) carapace.Action { return carapace.ActionValues() }).Signature()
signature = MacroI(func(s string) carapace.Action { return carapace.ActionValues() }).macro.Signature()
if expected := `""`; signature != expected {
t.Errorf("should be: %v", expected)
}
Expand All @@ -52,11 +52,11 @@ func TestDefault(t *testing.T) {
var actual Arg
m := MacroI(func(a Arg) carapace.Action { actual = a; return carapace.ActionValues() })

if m.f(""); !actual.Option {
if m.Parse("$default"); !actual.Option {
t.Error("should be true (default)")
}

if m.f("{option: false}"); actual.Option {
if m.Parse("$default({option: false})"); actual.Option {
t.Error("should be false")
}
}
4 changes: 2 additions & 2 deletions modifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func (m modifier) Parse(s string) carapace.Action {
}

if modifier, ok := modifiers[strings.SplitN(s, "(", 2)[0]]; ok {
return modifier.parse(s)
return modifier.Parse(s)
}
return carapace.ActionMessage("unknown macro: %#v", s)
})
Expand All @@ -65,7 +65,7 @@ func (m modifier) chdir(s string) carapace.Action {
"$xdgconfighome": MacroN(func() carapace.Action { return m.Action.ChdirF(traverse.XdgConfigHome) }),
}
if modifier, ok := traverse[strings.SplitN(s, "(", 2)[0]]; ok {
return modifier.parse(s)
return modifier.Parse(s)
}
return carapace.ActionMessage("unknown macro: %#v", s)
}
Expand Down
106 changes: 106 additions & 0 deletions pkg/macro/macro.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package macro

import (
"fmt"
"reflect"
"regexp"
"strings"

"gopkg.in/yaml.v3"
)

type Macro[T any] struct {
f func(string) (*T, error)
s func() string
}

type Default[T any] interface {
Default() T
}

func (m Macro[T]) Parse(s string) (*T, error) {
r := regexp.MustCompile(`^\$(?P<macro>[^(]*)(\((?P<arg>.*)\))?$`)
matches := r.FindStringSubmatch(s)
if matches == nil {
return nil, fmt.Errorf("malformed macro: '%v'", s)
}
return m.f(matches[3])
}

func (m Macro[T]) Signature() string { return m.s() }

// MacroN creates a macro without an argument
func MacroN[T any](f func() (*T, error)) Macro[T] {
return Macro[T]{
f: func(s string) (*T, error) {
return f()
},
s: func() string { return "" },
}
}

// MacroI creates a macro with an argument
func MacroI[A, T any](f func(arg A) (*T, error)) Macro[T] {
return Macro[T]{
f: func(s string) (*T, error) {
var arg A
switch reflect.TypeOf(arg).Kind() {
case reflect.String:
reflect.ValueOf(&arg).Elem().SetString(s)

default:
if err := yaml.Unmarshal([]byte(s), &arg); err != nil {
return nil, err
}
if s == "" {
if v, ok := interface{}(arg).(Default[A]); ok {
arg = v.Default()
}
}
}
return f(arg)
},
s: func() string { return signature(new(A)) },
}
}

// MacroV creates a macro with a variable argument
func MacroV[A, T any](f func(args ...A) (*T, error)) Macro[T] {
return Macro[T]{
f: func(s string) (*T, error) {
if s == "" {
return f()
}

var args []A
if err := yaml.Unmarshal([]byte(s), &args); err != nil {
return nil, fmt.Errorf("malformed macro arg: '%v', expected '%v'", s, reflect.TypeOf(args))
}
return f(args...)
},
s: func() string { return fmt.Sprintf("[%v]", signature(new(A))) },
}
}

func signature(i interface{}) string {
elem := reflect.ValueOf(i).Elem()
switch elem.Kind() {
case reflect.Struct:
out, err := yaml.Marshal(i)
if err != nil {
return err.Error()
}
lines := strings.Split(string(out), "\n")
return fmt.Sprintf("{%v}", strings.Join(lines[:len(lines)-1], ", "))

case reflect.Slice:
ptr := reflect.New(elem.Type().Elem()).Interface()
return fmt.Sprintf("[%v]", signature(ptr))

case reflect.String:
return `""`

default:
return fmt.Sprintf("%v", reflect.ValueOf(i).Elem())
}
}

0 comments on commit 3b975fe

Please sign in to comment.