Skip to content

Commit

Permalink
Merge pull request #35 from semperos/rate-limit
Browse files Browse the repository at this point in the history
Rate limit
  • Loading branch information
semperos authored Nov 5, 2024
2 parents 0a7024e + 02c2240 commit 93d50d8
Show file tree
Hide file tree
Showing 11 changed files with 113 additions and 116 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,10 @@ Flags:
- `help` based on Goal's, but allows adding help strings when used dyadically (e.g.,`"sql.q"help"Run SQL query"`)
- New Goal functions:
- `http.` functions for HTTP requests using [Resty]
- `ratelimit.new` and `ratelimit.take` for rate limiting (leaky bucket algorithm) using [uber-go/ratelimit]
- `sql.` functions for SQL queries and commands
- Table-related `csv.tbl` and `json.tbl` to make Goal tables from the output of `csv` and `json` respectively
- `tt.` test framework
- `csv.tbl` and `json.tbl` to make Goal tables from the output of `csv` and `json` respectively
- `time.` functions for more extensive date/time handling
- `tui.` functions for basic terminal UI styling (colors, padding/margin, borders)
- Dedicated SQL mode
Expand Down Expand Up @@ -194,3 +195,4 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

[Goal]: https://codeberg.org/anaseto/goal
[Resty]: https://github.com/go-resty/resty
[uber-go/ratelimit]: https://github.com/uber-go/ratelimit
6 changes: 3 additions & 3 deletions cmd/ari/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -353,9 +353,9 @@ func ariMain(cmd *cobra.Command, args []string) int {

func registerCliGoalBindings(ariContext *ari.Context) {
goalContext := ariContext.GoalContext
goalContext.RegisterMonad("tui.color", VFTuiColor)
goalContext.RegisterMonad("tui.style", VFTuiStyle)
goalContext.RegisterDyad("tui.render", VFTuiRender)
goalContext.RegisterMonad("tui.color", vfTuiColor)
goalContext.RegisterMonad("tui.style", vfTuiStyle)
goalContext.RegisterDyad("tui.render", vfTuiRender)
}

func rawREPL(cliSystem *CliSystem) {
Expand Down
6 changes: 3 additions & 3 deletions cmd/ari/tui.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ const (
triadic = 3
)

func VFTuiColor(_ *goal.Context, args []goal.V) goal.V {
func vfTuiColor(_ *goal.Context, args []goal.V) goal.V {
x := args[len(args)-1]
colorS, ok := x.BV().(goal.S)
switch len(args) {
Expand All @@ -94,7 +94,7 @@ const quadrilateral = 4
// Implements tui.style monad.
//
//nolint:cyclop,funlen,gocognit,gocyclo // These dictionary translators are best kept together
func VFTuiStyle(_ *goal.Context, args []goal.V) goal.V {
func vfTuiStyle(_ *goal.Context, args []goal.V) goal.V {
x := args[len(args)-1]
styleD, okD := x.BV().(*goal.D)
switch len(args) {
Expand Down Expand Up @@ -352,7 +352,7 @@ func VFTuiStyle(_ *goal.Context, args []goal.V) goal.V {
}
}

func VFTuiRender(_ *goal.Context, args []goal.V) goal.V {
func vfTuiRender(_ *goal.Context, args []goal.V) goal.V {
x := args[len(args)-1]
tuiStyle, ok := x.BV().(*TuiStyle)
switch len(args) {
Expand Down
23 changes: 10 additions & 13 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ type Context struct {
}

// Initialize a Goal language context with Ari's extensions.
func NewGoalContext(ariContext *Context, help Help, sqlDatabase *SQLDatabase) (*goal.Context, error) {
func newGoalContext(ariContext *Context, help Help, sqlDatabase *SQLDatabase) (*goal.Context, error) {
goalContext := goal.NewContext()
goalContext.Log = os.Stderr
goalRegisterVariadics(ariContext, goalContext, help, sqlDatabase)
Expand All @@ -92,7 +92,7 @@ func NewGoalContext(ariContext *Context, help Help, sqlDatabase *SQLDatabase) (*
}

// Initialize a Goal language context with Ari's extensions.
func NewUniversalGoalContext(ariContext *Context, help Help) (*goal.Context, error) {
func newUniversalGoalContext(ariContext *Context, help Help) (*goal.Context, error) {
goalContext := goal.NewContext()
goalContext.Log = os.Stderr
goalRegisterUniversalVariadics(ariContext, goalContext, help)
Expand All @@ -106,11 +106,11 @@ func NewUniversalGoalContext(ariContext *Context, help Help) (*goal.Context, err
// Initialize SQL struct, but don't open the DB yet.
//
// Call SQLDatabase.open to open the database.
func NewSQLDatabase(dataSourceName string) (*SQLDatabase, error) {
return &SQLDatabase{DataSource: dataSourceName, DB: nil, IsOpen: false}, nil
func newSQLDatabase(dataSourceName string) *SQLDatabase {
return &SQLDatabase{DataSource: dataSourceName, DB: nil, IsOpen: false}
}

func NewHelp() map[string]map[string]string {
func newHelp() map[string]map[string]string {
defaultSQLHelp := "A SQL keyword"
goalHelp := GoalKeywordsHelp()
sqlKeywords := SQLKeywords()
Expand All @@ -127,7 +127,7 @@ func NewHelp() map[string]map[string]string {
// Initialize a new Context without connecting to the database.
func NewContext(dataSourceName string) (*Context, error) {
ctx := Context{}
helpDictionary := NewHelp()
helpDictionary := newHelp()
ariHelpFunc := func(s string) string {
goalHelp, ok := helpDictionary["goal"]
if !ok {
Expand All @@ -141,11 +141,8 @@ func NewContext(dataSourceName string) (*Context, error) {
}
helpFunc := help.Wrap(ariHelpFunc, help.HelpFunc())
help := Help{Dictionary: helpDictionary, Func: helpFunc}
sqlDatabase, err := NewSQLDatabase(dataSourceName)
if err != nil {
return nil, err
}
goalContext, err := NewGoalContext(&ctx, help, sqlDatabase)
sqlDatabase := newSQLDatabase(dataSourceName)
goalContext, err := newGoalContext(&ctx, help, sqlDatabase)
if err != nil {
return nil, err
}
Expand All @@ -158,7 +155,7 @@ func NewContext(dataSourceName string) (*Context, error) {
// Initialize a new Context that can be used across platforms, including WASM.
func NewUniversalContext() (*Context, error) {
ctx := Context{}
helpDictionary := NewHelp()
helpDictionary := newHelp()
ariHelpFunc := func(s string) string {
goalHelp, ok := helpDictionary["goal"]
if !ok {
Expand All @@ -172,7 +169,7 @@ func NewUniversalContext() (*Context, error) {
}
helpFunc := help.Wrap(ariHelpFunc, help.HelpFunc())
help := Help{Dictionary: helpDictionary, Func: helpFunc}
goalContext, err := NewUniversalGoalContext(&ctx, help)
goalContext, err := newUniversalGoalContext(&ctx, help)
if err != nil {
return nil, err
}
Expand Down
93 changes: 47 additions & 46 deletions goal.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ var goalSourceAri string
// Goal functions implemented in Go

// Implements Goal's help monad + Ari's help dyad.
func VFGoalHelp(help Help) func(_ *goal.Context, args []goal.V) goal.V {
func vfGoalHelp(help Help) func(_ *goal.Context, args []goal.V) goal.V {
return func(_ *goal.Context, args []goal.V) goal.V {
switch len(args) {
case monadic:
Expand Down Expand Up @@ -168,60 +168,61 @@ func goalRegisterUniversalVariadics(ariContext *Context, goalContext *goal.Conte
goalContext.RegisterExtension("ari", AriVersion)
goalContext.AssignGlobal("ari.c", goal.NewI(0))
// Monads
goalContext.RegisterMonad("ratelimit.new", VFRateLimitNew)
goalContext.RegisterMonad("ratelimit.take", VFRateLimitTake)
goalContext.RegisterMonad("time.day", VFTimeDay)
goalContext.RegisterMonad("time.hour", VFTimeHour)
goalContext.RegisterMonad("time.loadlocation", VFTimeLoadLocation)
goalContext.RegisterMonad("time.location", VFTimeLocation)
goalContext.RegisterMonad("time.locationstring", VFTimeLocationString)
goalContext.RegisterMonad("time.microsecond", VFTimeMicrosecond)
goalContext.RegisterMonad("time.millisecond", VFTimeMillisecond)
goalContext.RegisterMonad("time.minute", VFTimeMinute)
goalContext.RegisterMonad("time.month", VFTimeMonth)
goalContext.RegisterMonad("time.now", VFTimeNow)
goalContext.RegisterMonad("time.nanosecond", VFTimeNanosecond)
goalContext.RegisterMonad("time.second", VFTimeSecond)
goalContext.RegisterMonad("time.unix", VFTimeUnix)
goalContext.RegisterMonad("time.unixmicro", VFTimeUnixMicro)
goalContext.RegisterMonad("time.unixmilli", VFTimeUnixMilli)
goalContext.RegisterMonad("time.unixnano", VFTimeUnixNano)
goalContext.RegisterMonad("time.utc", VFTimeUTC)
goalContext.RegisterMonad("time.weekday", VFTimeWeekDay)
goalContext.RegisterMonad("time.year", VFTimeYear)
goalContext.RegisterMonad("time.yearday", VFTimeYearDay)
goalContext.RegisterMonad("time.zonename", VFTimeZoneName)
goalContext.RegisterMonad("time.zoneoffset", VFTimeZoneOffset)
goalContext.RegisterMonad("url.encode", VFUrlEncode)
goalContext.RegisterMonad("ratelimit.new", vfRateLimitNew)
goalContext.RegisterMonad("ratelimit.take", vfRateLimitTake)
goalContext.RegisterMonad("time.day", vfTimeDay)
goalContext.RegisterMonad("time.hour", vfTimeHour)
goalContext.RegisterMonad("time.loadlocation", vfTimeLoadLocation)
goalContext.RegisterMonad("time.location", vfTimeLocation)
goalContext.RegisterMonad("time.locationstring", vfTimeLocationString)
goalContext.RegisterMonad("time.microsecond", vfTimeMicrosecond)
goalContext.RegisterMonad("time.millisecond", vfTimeMillisecond)
goalContext.RegisterMonad("time.minute", vfTimeMinute)
goalContext.RegisterMonad("time.month", vfTimeMonth)
goalContext.RegisterMonad("time.now", vfTimeNow)
goalContext.RegisterMonad("time.nanosecond", vfTimeNanosecond)
goalContext.RegisterMonad("time.second", vfTimeSecond)
goalContext.RegisterMonad("time.unix", vfTimeUnix)
goalContext.RegisterMonad("time.unixmicro", vfTimeUnixMicro)
goalContext.RegisterMonad("time.unixmilli", vfTimeUnixMilli)
goalContext.RegisterMonad("time.unixnano", vfTimeUnixNano)
goalContext.RegisterMonad("time.utc", vfTimeUTC)
goalContext.RegisterMonad("time.weekday", vfTimeWeekDay)
goalContext.RegisterMonad("time.year", vfTimeYear)
goalContext.RegisterMonad("time.yearday", vfTimeYearDay)
goalContext.RegisterMonad("time.zonename", vfTimeZoneName)
goalContext.RegisterMonad("time.zoneoffset", vfTimeZoneOffset)
goalContext.RegisterMonad("url.encode", vfURLEncode)
// Dyads
goalContext.RegisterDyad("help", VFGoalHelp(help))
goalContext.RegisterDyad("http.client", VFHTTPClientFn())
goalContext.RegisterDyad("http.delete", VFHTTPMaker(ariContext, "DELETE"))
goalContext.RegisterDyad("http.get", VFHTTPMaker(ariContext, "GET"))
goalContext.RegisterDyad("http.head", VFHTTPMaker(ariContext, "HEAD"))
goalContext.RegisterDyad("http.options", VFHTTPMaker(ariContext, "OPTIONS"))
goalContext.RegisterDyad("http.patch", VFHTTPMaker(ariContext, "PATCH"))
goalContext.RegisterDyad("http.post", VFHTTPMaker(ariContext, "POST"))
goalContext.RegisterDyad("http.put", VFHTTPMaker(ariContext, "PUT"))
goalContext.RegisterDyad("time.add", VFTimeAdd)
goalContext.RegisterDyad("time.date", VFTimeDate)
goalContext.RegisterDyad("time.fixedzone", VFTimeFixedZone)
goalContext.RegisterDyad("time.format", VFTimeFormat)
goalContext.RegisterDyad("time.parse", VFTimeParse)
goalContext.RegisterDyad("time.sub", VFTimeSub)
goalContext.RegisterDyad("help", vfGoalHelp(help))
goalContext.RegisterDyad("http.client", vfHTTPClientFn())
goalContext.RegisterDyad("http.delete", vfHTTPMaker(ariContext, "DELETE"))
goalContext.RegisterDyad("http.get", vfHTTPMaker(ariContext, "GET"))
goalContext.RegisterDyad("http.head", vfHTTPMaker(ariContext, "HEAD"))
goalContext.RegisterDyad("http.options", vfHTTPMaker(ariContext, "OPTIONS"))
goalContext.RegisterDyad("http.patch", vfHTTPMaker(ariContext, "PATCH"))
goalContext.RegisterDyad("http.post", vfHTTPMaker(ariContext, "POST"))
goalContext.RegisterDyad("http.put", vfHTTPMaker(ariContext, "PUT"))
goalContext.RegisterDyad("time.add", vfTimeAdd)
goalContext.RegisterDyad("time.addDate", vfTimeAddDate)
goalContext.RegisterDyad("time.date", vfTimeDate)
goalContext.RegisterDyad("time.fixedZone", vfTimeFixedZone)
goalContext.RegisterDyad("time.format", vfTimeFormat)
goalContext.RegisterDyad("time.parse", vfTimeParse)
goalContext.RegisterDyad("time.sub", vfTimeSub)
// Globals
registerTimeGlobals(goalContext)
}

func goalRegisterVariadics(ariContext *Context, goalContext *goal.Context, help Help, sqlDatabase *SQLDatabase) {
goalRegisterUniversalVariadics(ariContext, goalContext, help)
// Monads
goalContext.RegisterMonad("sql.close", VFSqlClose)
goalContext.RegisterMonad("sql.open", VFSqlOpen)
goalContext.RegisterMonad("sql.close", vfSQLClose)
goalContext.RegisterMonad("sql.open", vfSQLOpen)
// Dyads
goalContext.RegisterDyad("http.serve", VFServe)
goalContext.RegisterDyad("sql.q", VFSqlQFn(sqlDatabase))
goalContext.RegisterDyad("sql.exec", VFSqlExecFn(sqlDatabase))
goalContext.RegisterDyad("http.serve", vfServe)
goalContext.RegisterDyad("sql.q", vfSQLQFn(sqlDatabase))
goalContext.RegisterDyad("sql.exec", vfSQLExecFn(sqlDatabase))
}

//nolint:funlen
Expand Down
8 changes: 4 additions & 4 deletions http.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func (httpClient *HTTPClient) Append(_ *goal.Context, dst []byte, _ bool) []byte
return append(dst, fmt.Sprintf("<%v %#v>", httpClient.Type(), httpClient.Client)...)
}

//nolint:cyclop,funlen,gocognit,gocyclo // No code shared, ball of wax stays together.
//nolint:cyclop,funlen,gocognit,gocyclo // No code shared, ball of wax stays together. Public for tests.
func NewHTTPClient(optionsD *goal.D) (*HTTPClient, error) {
// Not currently implemented:
// Cookies []*http.Cookie // Medium-sized struct
Expand Down Expand Up @@ -327,7 +327,7 @@ func NewHTTPClient(optionsD *goal.D) (*HTTPClient, error) {
return &httpClient, nil
}

func VFHTTPClientFn() func(goalContext *goal.Context, args []goal.V) goal.V {
func vfHTTPClientFn() func(goalContext *goal.Context, args []goal.V) goal.V {
return func(_ *goal.Context, args []goal.V) goal.V {
x := args[len(args)-1]
clientOptions, ok := x.BV().(*goal.D)
Expand All @@ -347,7 +347,7 @@ func VFHTTPClientFn() func(goalContext *goal.Context, args []goal.V) goal.V {
}
}

func VFHTTPMaker(ariContext *Context, method string) func(goalContext *goal.Context, args []goal.V) goal.V {
func vfHTTPMaker(ariContext *Context, method string) func(goalContext *goal.Context, args []goal.V) goal.V {
methodLower := strings.ToLower(method) // Used for function name
methodUpper := strings.ToUpper(method) // Used by go-resty for HTTP method
return func(_ *goal.Context, args []goal.V) goal.V {
Expand Down Expand Up @@ -707,7 +707,7 @@ const (
)

// Implements http.serve dyad.
func VFServe(goalContext *goal.Context, args []goal.V) goal.V {
func vfServe(goalContext *goal.Context, args []goal.V) goal.V {
x := args[len(args)-1]
hostAndPort, ok := x.BV().(goal.S)
if !ok {
Expand Down
4 changes: 2 additions & 2 deletions ratelimit.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func (rateLimiter *RateLimiter) Type() string {
return "ari.RateLimiter"
}

func VFRateLimitNew(_ *goal.Context, args []goal.V) goal.V {
func vfRateLimitNew(_ *goal.Context, args []goal.V) goal.V {
if len(args) > 1 {
return goal.Panicf("ratelimit.new : too many arguments (%d), expects 1 argument", len(args))
}
Expand All @@ -43,7 +43,7 @@ func VFRateLimitNew(_ *goal.Context, args []goal.V) goal.V {
return panicType("ratelimit.new i", "i", x)
}

func VFRateLimitTake(_ *goal.Context, args []goal.V) goal.V {
func vfRateLimitTake(_ *goal.Context, args []goal.V) goal.V {
if len(args) > 1 {
return goal.Panicf("ratelimit.take : too many arguments (%d), expects 1 argument", len(args))
}
Expand Down
15 changes: 6 additions & 9 deletions sql.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ func SQLExec(sqlDatabase *SQLDatabase, sqlQuery string, args []any) (goal.V, err
}

// Implements sql.open to open a SQL database.
func VFSqlOpen(_ *goal.Context, args []goal.V) goal.V {
func vfSQLOpen(_ *goal.Context, args []goal.V) goal.V {
x := args[len(args)-1]
dataSourceName, ok := x.BV().(goal.S)
switch len(args) {
Expand All @@ -185,11 +185,8 @@ func VFSqlOpen(_ *goal.Context, args []goal.V) goal.V {
return panicType("sql.open s", "s", x)
}
dsn := string(dataSourceName)
sqlDatabase, err := NewSQLDatabase(dsn)
if err != nil {
return goal.NewPanicError(err)
}
err = sqlDatabase.Open()
sqlDatabase := newSQLDatabase(dsn)
err := sqlDatabase.Open()
if err != nil {
return goal.NewPanicError(err)
}
Expand All @@ -200,7 +197,7 @@ func VFSqlOpen(_ *goal.Context, args []goal.V) goal.V {
}

// Implements sql.close to close the SQL database.
func VFSqlClose(_ *goal.Context, args []goal.V) goal.V {
func vfSQLClose(_ *goal.Context, args []goal.V) goal.V {
x := args[len(args)-1]
sqlDatabase, ok := x.BV().(*SQLDatabase)
switch len(args) {
Expand All @@ -219,7 +216,7 @@ func VFSqlClose(_ *goal.Context, args []goal.V) goal.V {
}

// Implements sql.q for SQL querying.
func VFSqlQFn(sqlDatabase *SQLDatabase) func(goalContext *goal.Context, args []goal.V) goal.V {
func vfSQLQFn(sqlDatabase *SQLDatabase) func(goalContext *goal.Context, args []goal.V) goal.V {
return func(goalContext *goal.Context, args []goal.V) goal.V {
x := args[len(args)-1]
switch len(args) {
Expand Down Expand Up @@ -272,7 +269,7 @@ func sqlQDyadic(x goal.V, args []goal.V) goal.V {
}

// Implements sql.exec for executing SQL statements.
func VFSqlExecFn(sqlDatabase *SQLDatabase) func(goalContext *goal.Context, args []goal.V) goal.V {
func vfSQLExecFn(sqlDatabase *SQLDatabase) func(goalContext *goal.Context, args []goal.V) goal.V {
return func(goalContext *goal.Context, args []goal.V) goal.V {
x := args[len(args)-1]
switch len(args) {
Expand Down
2 changes: 1 addition & 1 deletion testing/time-test.goal
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ tt.t{pt["2019-02-03T04:00:00.000000000Z"]~time.date[2019;2;3;4]}
tt.t{pt["2019-02-03T04:05:00.000000000Z"]~time.date[2019;2;3;4;5]}
tt.t{pt["2019-02-03T04:05:06.000000000Z"]~time.date[2019;2;3;4;5;6]}
tt.t{pt["2019-02-03T04:05:06.789000000Z"]~time.date[2019;2;3;4;5;6;789000000]}
utc8:time.fixedzone["UTC-8";*/-8 60 60]
utc8:time.fixedZone["UTC-8";*/-8 60 60]
tt.t{time.parse[time.RFC822Z;"03 Feb 19 04:05 -0800"]~time.date[2019;2;3;4;5;0;0;utc8]}
tt.t{"UTC-8"~time.locationstring utc8}
tt.t{time.date[2020;1;1]~t2}
Expand Down
Loading

0 comments on commit 93d50d8

Please sign in to comment.