Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Showing
2 changed files
with
211 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,92 @@ | ||
package flags | ||
|
||
import ( | ||
"errors" | ||
"slices" | ||
"strings" | ||
"unicode/utf8" | ||
) | ||
|
||
type GenerateBindingsOptions struct { | ||
Silent bool `name:"silent" description:"Silent mode"` | ||
ModelsFilename string `name:"m" description:"The filename for the models file, excluding the extension" default:"models"` | ||
BuildFlagsString string `name:"f" description:"Provide a list of additional space-separated Go build flags. Flags can be wrapped (even partially) in single or double quotes to include spaces"` | ||
OutputDirectory string `name:"d" description:"The output directory" default:"assets/bindings"` | ||
ModelsFilename string `name:"m" description:"The filename for the models file, excluding the extension" default:"$models"` | ||
TS bool `name:"ts" description:"Generate Typescript bindings"` | ||
TSPrefix string `description:"The prefix for typescript names" default:""` | ||
TSSuffix string `description:"The postfix for typescript names" default:""` | ||
UseInterfaces bool `name:"i" description:"Use interfaces instead of classes"` | ||
UseInterfaces bool `name:"i" description:"Generate Typescript interfaces instead of classes"` | ||
UseBundledRuntime bool `name:"b" description:"Use the bundled runtime instead of importing the npm package"` | ||
ProjectDirectory string `name:"p" description:"The project directory" default:"."` | ||
UseIDs bool `name:"ids" description:"Use IDs instead of names in the binding calls"` | ||
OutputDirectory string `name:"d" description:"The output directory" default:"frontend/bindings"` | ||
UseNames bool `name:"names" description:"Use names instead of IDs for the binding calls"` | ||
Silent bool `name:"silent" description:"Silent mode"` | ||
Verbose bool `name:"v" description:"Enable debugging output from the Go package loader"` | ||
} | ||
|
||
var ErrUnmatchedQuote = errors.New("build flags contain an unmatched quote") | ||
|
||
func isWhitespace(r rune) bool { | ||
// We use Go's definition of whitespace instead of the Unicode ones | ||
return r == ' ' || r == '\t' || r == '\r' || r == '\n' | ||
} | ||
|
||
func isNonWhitespace(r rune) bool { | ||
return !isWhitespace(r) | ||
} | ||
|
||
func isQuote(r rune) bool { | ||
return r == '\'' || r == '"' | ||
} | ||
|
||
func isQuoteOrWhitespace(r rune) bool { | ||
return isQuote(r) || isWhitespace(r) | ||
} | ||
|
||
func (options *GenerateBindingsOptions) BuildFlags() (flags []string, err error) { | ||
str := options.BuildFlagsString | ||
|
||
// temporary buffer for flag assembly | ||
flag := make([]byte, 0, 32) | ||
|
||
for start := strings.IndexFunc(str, isNonWhitespace); start >= 0; start = strings.IndexFunc(str, isNonWhitespace) { | ||
// each iteration starts at the beginning of a flag | ||
// skip initial whitespace | ||
str = str[start:] | ||
|
||
// iterate over all quoted and unquoted parts of the flag and join them | ||
for { | ||
breakpoint := strings.IndexFunc(str, isQuoteOrWhitespace) | ||
if breakpoint < 0 { | ||
breakpoint = len(str) | ||
} | ||
|
||
// append everything up to the breakpoint | ||
flag = append(flag, str[:breakpoint]...) | ||
str = str[breakpoint:] | ||
|
||
quote, quoteSize := utf8.DecodeRuneInString(str) | ||
if !isQuote(quote) { | ||
// if the breakpoint is not a quote, we reached the end of the flag | ||
break | ||
} | ||
|
||
// otherwise, look for the closing quote | ||
str = str[quoteSize:] | ||
closingQuote := strings.IndexRune(str, quote) | ||
|
||
// closing quote not found, append everything to the last flag and raise an error | ||
if closingQuote < 0 { | ||
flag = append(flag, str...) | ||
str = "" | ||
err = ErrUnmatchedQuote | ||
break | ||
} | ||
|
||
// closing quote found, append quoted content to the flag and restart after the quote | ||
flag = append(flag, str[:closingQuote]...) | ||
str = str[closingQuote+quoteSize:] | ||
} | ||
|
||
// append a clone of the flag to the result, then reuse buffer space | ||
flags = append(flags, string(slices.Clone(flag))) | ||
flag = flag[:0] | ||
} | ||
|
||
return | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
package flags | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/google/go-cmp/cmp" | ||
"github.com/google/go-cmp/cmp/cmpopts" | ||
) | ||
|
||
func TestBuildFlags(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
input string | ||
wantFlags []string | ||
wantErr bool | ||
}{ | ||
{ | ||
name: "empty string", | ||
input: "", | ||
wantFlags: nil, | ||
}, | ||
{ | ||
name: "single flag, multiple spaces", | ||
input: " -v ", | ||
wantFlags: []string{"-v"}, | ||
}, | ||
{ | ||
name: "multiple flags, complex spaces", | ||
input: " \t-v\r\n-x", | ||
wantFlags: []string{"-v", "-x"}, | ||
}, | ||
{ | ||
name: "empty flag (single quotes)", | ||
input: `''`, | ||
wantFlags: []string{""}, | ||
}, | ||
{ | ||
name: "empty flag (double quotes)", | ||
input: `""`, | ||
wantFlags: []string{""}, | ||
}, | ||
{ | ||
name: "flag with spaces (single quotes)", | ||
input: `'a b'`, | ||
wantFlags: []string{"a \tb"}, | ||
}, | ||
{ | ||
name: "flag with spaces (double quotes)", | ||
input: `'a b'`, | ||
wantFlags: []string{"a \tb"}, | ||
}, | ||
{ | ||
name: "mixed quoted and non-quoted flags (single quotes)", | ||
input: `-v 'a b ' -x`, | ||
wantFlags: []string{"-v", "a b ", "-x"}, | ||
}, | ||
{ | ||
name: "mixed quoted and non-quoted flags (double quotes)", | ||
input: `-v "a b " -x`, | ||
wantFlags: []string{"-v", "a b ", "-x"}, | ||
}, | ||
{ | ||
name: "mixed quoted and non-quoted flags (mixed quotes)", | ||
input: `-v "a b " '-x'`, | ||
wantFlags: []string{"-v", "a b ", "-x"}, | ||
}, | ||
{ | ||
name: "double quote within single quotes", | ||
input: `' " '`, | ||
wantFlags: []string{" \" "}, | ||
}, | ||
{ | ||
name: "single quote within double quotes", | ||
input: `" ' "`, | ||
wantFlags: []string{" ' "}, | ||
}, | ||
{ | ||
name: "unmatched single quote", | ||
input: `-v "a b " '-x -y`, | ||
wantFlags: []string{"-v", "a b ", "-x -y"}, | ||
wantErr: true, | ||
}, | ||
{ | ||
name: "unmatched double quote", | ||
input: `-v "a b " "-x -y`, | ||
wantFlags: []string{"-v", "a b ", "-x -y"}, | ||
wantErr: true, | ||
}, | ||
{ | ||
name: "mismatched single quote", | ||
input: `-v "a b " '-x" -y`, | ||
wantFlags: []string{"-v", "a b ", "-x\" -y"}, | ||
wantErr: true, | ||
}, | ||
{ | ||
name: "mismatched double quote", | ||
input: `-v "a b " "-x' -y`, | ||
wantFlags: []string{"-v", "a b ", "-x' -y"}, | ||
wantErr: true, | ||
}, | ||
} | ||
|
||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
options := GenerateBindingsOptions{ | ||
BuildFlagsString: tt.input, | ||
} | ||
|
||
var wantErr error = nil | ||
if tt.wantErr { | ||
wantErr = ErrUnmatchedQuote | ||
} | ||
|
||
gotFlags, gotErr := options.BuildFlags() | ||
|
||
if diff := cmp.Diff(tt.wantFlags, gotFlags); diff != "" { | ||
t.Errorf("BuildFlags() unexpected result: %s\n", diff) | ||
} | ||
|
||
if diff := cmp.Diff(wantErr, gotErr, cmpopts.EquateErrors()); diff != "" { | ||
t.Errorf("BuildFlags() unexpected error: %s\n", diff) | ||
} | ||
}) | ||
} | ||
} |