You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
147 lines
3.0 KiB
Go
147 lines
3.0 KiB
Go
package path
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/alecthomas/participle/v2"
|
|
"github.com/alecthomas/participle/v2/lexer"
|
|
)
|
|
|
|
// FullMatch represents the complete match in a match result context map
|
|
const FullMatch = "*"
|
|
|
|
type PathPart interface {
|
|
regex() string
|
|
}
|
|
|
|
var _ PathPart = PathLiteral{}
|
|
var _ PathPart = PathShortPattern{}
|
|
var _ PathPart = PathLongPattern{}
|
|
var _ PathPart = PathString{}
|
|
|
|
type PathLiteral struct {
|
|
LongAny bool `parser:" @'**'"`
|
|
ShortAny bool `parser:"| @'*' "`
|
|
Slash bool `parser:"| @'/' "`
|
|
}
|
|
|
|
func (p PathLiteral) regex() string {
|
|
switch {
|
|
case p.Slash:
|
|
return regexp.QuoteMeta("/")
|
|
case p.ShortAny:
|
|
return "([^/]+?)"
|
|
case p.LongAny:
|
|
return "(.+?)"
|
|
default:
|
|
panic("illegal enum state")
|
|
}
|
|
}
|
|
|
|
type PathShortPattern struct {
|
|
Name string `parser:"'{' @Ident '}'"`
|
|
}
|
|
|
|
func (p PathShortPattern) regex() string {
|
|
return fmt.Sprintf("(?P<%s>[^/]+?)", p.Name)
|
|
}
|
|
|
|
type PathLongPattern struct {
|
|
Name string `parser:"'{{' @Ident '}}'"`
|
|
}
|
|
|
|
func (p PathLongPattern) regex() string {
|
|
return fmt.Sprintf("(?P<%s>.+?)", p.Name)
|
|
}
|
|
|
|
type PathString struct {
|
|
Value string `parser:"@(Ident|Rune)+"`
|
|
}
|
|
|
|
func (p PathString) regex() string {
|
|
return regexp.QuoteMeta(p.Value)
|
|
}
|
|
|
|
type Pattern struct {
|
|
Parts []PathPart `parser:"@@+"`
|
|
}
|
|
|
|
var parser = participle.MustBuild[Pattern](
|
|
participle.Union[PathPart](
|
|
&PathLiteral{},
|
|
&PathLongPattern{},
|
|
&PathShortPattern{},
|
|
&PathString{},
|
|
),
|
|
participle.Lexer(
|
|
lexer.MustSimple([]lexer.SimpleRule{
|
|
{Name: "Slash", Pattern: `/`},
|
|
{Name: "LongAny", Pattern: `\*\*`},
|
|
{Name: "ShortAny", Pattern: `\*`},
|
|
{Name: "PatternLongOpen", Pattern: `{{`},
|
|
{Name: "PatternLongClose", Pattern: `}}`},
|
|
{Name: "PatternShortOpen", Pattern: `{`},
|
|
{Name: "PatternShortClose", Pattern: `}`},
|
|
{Name: "Ident", Pattern: `[a-zA-Z][a-zA-Z0-9\_]*`},
|
|
{Name: "Rune", Pattern: `[^/{}]+`},
|
|
}),
|
|
),
|
|
)
|
|
|
|
func ParsePattern(path string) (*Pattern, error) {
|
|
return parser.ParseString("", path)
|
|
}
|
|
|
|
func MustParsePattern(path string) *Pattern {
|
|
p, err := ParsePattern(path)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
return p
|
|
}
|
|
|
|
// Match takes a path "s" and matches it against this pattern and returns whether it matched and in that case a map containing all captures for this pattern. This will throw an error if it can't compile the internal regex (should never happen).
|
|
func (p Pattern) Match(s string) (bool, map[string]string, error) {
|
|
r, err := regexp.Compile("^" + p.regex() + "$")
|
|
if err != nil {
|
|
return false, nil, err
|
|
}
|
|
|
|
ms := r.FindStringSubmatch(s)
|
|
if ms == nil {
|
|
return false, nil, nil
|
|
}
|
|
|
|
ctx := map[string]string{}
|
|
for i, name := range r.SubexpNames() {
|
|
if name != "" {
|
|
ctx[name] = ms[i]
|
|
}
|
|
}
|
|
|
|
ctx[FullMatch] = ms[0]
|
|
|
|
return true, ctx, nil
|
|
}
|
|
|
|
func (p Pattern) regex() string {
|
|
sb := &strings.Builder{}
|
|
|
|
for _, part := range p.Parts {
|
|
sb.WriteString(part.regex())
|
|
}
|
|
|
|
return sb.String()
|
|
}
|
|
|
|
func RenderTemplate(tmpl string, ctx map[string]string) string {
|
|
s := tmpl
|
|
for k, v := range ctx {
|
|
s = strings.ReplaceAll(s, "{"+k+"}", v)
|
|
}
|
|
return s
|
|
}
|