Skip to content
This repository was archived by the owner on Aug 17, 2025. It is now read-only.
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
95 changes: 1 addition & 94 deletions cmd/ftl/cmd_call.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@ import (
"encoding/json"
"fmt"
"net/url"
"strings"
"time"
"unicode/utf8"

"connectrpc.com/connect"
"github.com/alecthomas/errors"
Expand All @@ -20,7 +18,6 @@ import (
"github.com/block/ftl/common/reflection"
"github.com/block/ftl/internal/rpc"
"github.com/block/ftl/internal/rpc/headers"
"github.com/block/ftl/internal/schema/schemaeventsource"
status "github.com/block/ftl/internal/terminal"
)

Expand All @@ -35,7 +32,6 @@ type callCmd struct {
func (c *callCmd) Run(
ctx context.Context,
verbClient ftlv1connect.VerbServiceClient,
schemaClient *schemaeventsource.EventSource,
) error {
if err := rpc.Wait(ctx, backoff.Backoff{Max: time.Second * 2}, c.Wait, verbClient); err != nil {
return errors.WithStack(err)
Expand All @@ -54,13 +50,12 @@ func (c *callCmd) Run(

logger.Debugf("Calling %s", c.Verb)

return errors.WithStack(callVerb(ctx, verbClient, schemaClient, c.Verb, requestJSON, c.Verbose, c))
return errors.WithStack(callVerb(ctx, verbClient, c.Verb, requestJSON, c.Verbose, c))
}

func callVerb(
ctx context.Context,
verbClient ftlv1connect.VerbServiceClient,
schemaClient *schemaeventsource.EventSource,
verb reflection.Ref,
requestJSON []byte,
verbose bool,
Expand All @@ -73,14 +68,6 @@ func callVerb(
Body: requestJSON,
}))

if cerr := new(connect.Error); errors.As(err, &cerr) && cerr.Code() == connect.CodeNotFound {
suggestions, err := findSuggestions(ctx, schemaClient, verb)

// If we have suggestions, return a helpful error message, otherwise continue to the original error.
if err == nil {
return errors.Errorf("verb not found: %s\n\nDid you mean one of these?\n%s", verb, strings.Join(suggestions, "\n"))
}
}
if err != nil {
return errors.WithStack(err)
}
Expand Down Expand Up @@ -114,83 +101,3 @@ func callVerb(
}
return nil
}

// findSuggestions looks up the schema and finds verbs that are similar to the one that was not found
// it uses the levenshtein distance to determine similarity - if the distance is less than 40% of the length of the verb,
// it returns an error if no closely matching suggestions are found
func findSuggestions(
ctx context.Context,
schemaClient *schemaeventsource.EventSource,
verb reflection.Ref,
) ([]string, error) {
logger := log.FromContext(ctx)

// lookup the verbs
schemaClient.WaitForInitialSync(ctx)
res := schemaClient.CanonicalView()
verbs := []string{}

// build a list of all the verbs
for _, module := range res.InternalModules() {
for _, v := range module.Verbs() {
verbName := fmt.Sprintf("%s.%s", module.Name, v.Name)
if verbName == fmt.Sprintf("%s.%s", verb.Module, verb.Name) {
break
}

verbs = append(verbs, module.Name+"."+v.Name)
}
}

suggestions := []string{}

logger.Debugf("Found %d verbs", len(verbs))
needle := fmt.Sprintf("%s.%s", verb.Module, verb.Name)

// only consider suggesting verbs that are within 40% of the length of the needle
distanceThreshold := int(float64(len(needle))*0.4) + 1
for _, verb := range verbs {
d := levenshtein(verb, needle)
logger.Debugf("Verb %s distance %d", verb, d)

if d <= distanceThreshold {
suggestions = append(suggestions, verb)
}
}

if len(suggestions) > 0 {
return suggestions, nil
}

return nil, errors.Errorf("no suggestions found")
}

// Levenshtein computes the Levenshtein distance between two strings.
//
// credit goes to https://en.wikibooks.org/wiki/Algorithm_Implementation/Strings/Levenshtein_distance#Go
func levenshtein(a, b string) int {
f := make([]int, utf8.RuneCountInString(b)+1)

for j := range f {
f[j] = j
}

for _, ca := range a {
j := 1
fj1 := f[0] // fj1 is the value of f[j - 1] in last iteration
f[0]++
for _, cb := range b {
mn := min(f[j]+1, f[j-1]+1) // delete & insert
if cb != ca {
mn = min(mn, fj1+1) // change
} else {
mn = min(mn, fj1) // matched
}

fj1, f[j] = f[j], mn // save f[j] to fj1(j is about to increase), update f[j] to mn
j++
}
}

return f[len(f)-1]
}
8 changes: 1 addition & 7 deletions cmd/ftl/cmd_replay.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package main
import (
"context"
"net/url"
"strings"
"time"

"connectrpc.com/connect"
Expand Down Expand Up @@ -62,11 +61,6 @@ func (c *replayCmd) Run(
}
}
if !found {
suggestions, err := findSuggestions(ctx, eventSource, c.Verb)
// if we have suggestions, return a helpful error message. otherwise continue to the original error
if err == nil {
return errors.Errorf("verb not found: %s\n\nDid you mean one of these?\n%s", c.Verb, strings.Join(suggestions, "\n"))
}
return errors.Errorf("verb not found: %s", c.Verb)
}

Expand Down Expand Up @@ -109,5 +103,5 @@ func (c *replayCmd) Run(
ConsoleEndpoint: c.ConsoleEndpoint,
Verbose: c.Verbose,
}
return errors.WithStack(callVerb(ctx, verbClient, eventSource, c.Verb, []byte(requestJSON), c.Verbose, cmd))
return errors.WithStack(callVerb(ctx, verbClient, c.Verb, []byte(requestJSON), c.Verbose, cmd))
}
Loading