[feature] support nested configuration files, and setting ALL configuration variables by CLI and env (#4109)

This updates our configuration code generator to now also include map marshal and unmarshalers. So we now have much more control over how things get read from pflags, and stored / read from viper configuration. This allows us to set ALL configuration variables by CLI and environment now, AND support nested configuration files. e.g.

```yaml
advanced:
    scraper-deterrence = true

http-client:
    allow-ips = ["127.0.0.1"]
```

is the same as

```yaml
advanced-scraper-deterrence = true

http-client-allow-ips = ["127.0.0.1"]
```

This also starts cleaning up of our jumbled Configuration{} type by moving the advanced configuration options into their own nested structs, also as a way to show what it's capable of. It's worth noting however that nesting only works if the Go types are nested too (as this is how we hint to our code generator to generate the necessary flattening code :p).

closes #3195

Reviewed-on: https://codeberg.org/superseriousbusiness/gotosocial/pulls/4109
Co-authored-by: kim <grufwub@gmail.com>
Co-committed-by: kim <grufwub@gmail.com>
This commit is contained in:
kim
2025-05-06 15:51:45 +00:00
committed by kim
parent 7d74548a91
commit 6acf56cde9
30 changed files with 4764 additions and 1184 deletions

View File

@ -424,6 +424,7 @@ The following open source libraries, frameworks, and tools are used by GoToSocia
- [gruf/go-mutexes](https://codeberg.org/gruf/go-mutexes); safemutex & mutex map. [MIT License](https://spdx.org/licenses/MIT.html).
- [gruf/go-runners](https://codeberg.org/gruf/go-runners); synchronization utilities. [MIT License](https://spdx.org/licenses/MIT.html).
- [gruf/go-sched](https://codeberg.org/gruf/go-sched); task scheduler. [MIT License](https://spdx.org/licenses/MIT.html).
- [gruf/go-split](https://codeberg.org/gruf/go-split); configuration string handling. [MIT License](https://spdx.org/licenses/MIT.html).
- [gruf/go-storage](https://codeberg.org/gruf/go-storage); file storage backend (local & s3). [MIT License](https://spdx.org/licenses/MIT.html).
- [gruf/go-structr](https://codeberg.org/gruf/go-structr); struct caching + queueing with automated indexing by field. [MIT License](https://spdx.org/licenses/MIT.html).
- jackc:

View File

@ -32,11 +32,8 @@ var Config action.GTSAction = func(ctx context.Context) (err error) {
// Marshal configuration to a raw JSON map
config.Config(func(cfg *config.Configuration) {
raw, err = cfg.MarshalMap()
raw = cfg.MarshalMap()
})
if err != nil {
return err
}
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")

View File

@ -43,16 +43,16 @@ type preRunArgs struct {
// env vars or cli flag.
func preRun(a preRunArgs) error {
if err := config.BindFlags(a.cmd); err != nil {
return fmt.Errorf("error binding flags: %s", err)
return fmt.Errorf("error binding flags: %w", err)
}
if err := config.Reload(); err != nil {
return fmt.Errorf("error reloading config: %s", err)
if err := config.LoadConfigFile(); err != nil {
return fmt.Errorf("error loading config file: %w", err)
}
if !a.skipValidation {
if err := config.Validate(); err != nil {
return fmt.Errorf("invalid config: %s", err)
return fmt.Errorf("invalid config: %w", err)
}
}

View File

@ -19,7 +19,6 @@ package main
import (
configaction "code.superseriousbusiness.org/gotosocial/cmd/gotosocial/action/debug/config"
"code.superseriousbusiness.org/gotosocial/internal/config"
"github.com/spf13/cobra"
)
@ -39,7 +38,6 @@ func debugCommands() *cobra.Command {
return run(cmd.Context(), configaction.Config)
},
}
config.AddServerFlags(debugConfigCmd)
debugCmd.AddCommand(debugConfigCmd)
return debugCmd
}

View File

@ -23,10 +23,9 @@ import (
godebug "runtime/debug"
"strings"
"github.com/spf13/cobra"
_ "code.superseriousbusiness.org/gotosocial/docs"
"code.superseriousbusiness.org/gotosocial/internal/config"
"github.com/spf13/cobra"
)
// Version is the version of GoToSocial being used.
@ -41,24 +40,18 @@ func main() {
// override version in config store
config.SetSoftwareVersion(version)
// instantiate the root command
rootCmd := &cobra.Command{
Use: "gotosocial",
Short: "GoToSocial - a fediverse social media server",
Long: "GoToSocial - a fediverse social media server\n\nFor help, see: https://docs.gotosocial.org.\n\nCode: https://codeberg.org/superseriousbusiness/gotosocial",
Version: version,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
// before running any other cmd funcs, we must load config-path
return config.LoadEarlyFlags(cmd)
},
SilenceErrors: true,
SilenceUsage: true,
}
rootCmd := new(cobra.Command)
rootCmd.Use = "gotosocial"
rootCmd.Short = "GoToSocial - a fediverse social media server"
rootCmd.Long = "GoToSocial - a fediverse social media server\n\nFor help, see: https://docs.gotosocial.org.\n\nCode: https://codeberg.org/superseriousbusiness/gotosocial"
rootCmd.Version = version
rootCmd.SilenceErrors = true
rootCmd.SilenceUsage = true
// attach global flags to the root command so that they can be accessed from any subcommand
config.AddGlobalFlags(rootCmd)
// Register global flags with root.
config.RegisterGlobalFlags(rootCmd)
// add subcommands
// Add subcommands with their flags.
rootCmd.AddCommand(serverCommands())
rootCmd.AddCommand(debugCommands())
rootCmd.AddCommand(adminCommands())
@ -70,7 +63,7 @@ func main() {
log.Fatal("gotosocial must be built and run with the DEBUG enviroment variable set to enable and access testrig")
}
// run
// Run the prepared root command.
if err := rootCmd.Execute(); err != nil {
log.Fatalf("error executing command: %s", err)
}

View File

@ -19,7 +19,6 @@ package main
import (
"code.superseriousbusiness.org/gotosocial/cmd/gotosocial/action/server"
"code.superseriousbusiness.org/gotosocial/internal/config"
"github.com/spf13/cobra"
)
@ -39,7 +38,6 @@ func serverCommands() *cobra.Command {
return run(cmd.Context(), server.Start)
},
}
config.AddServerFlags(serverStartCmd)
serverCmd.AddCommand(serverStartCmd)
serverMaintenanceCmd := &cobra.Command{
@ -52,7 +50,6 @@ func serverCommands() *cobra.Command {
return run(cmd.Context(), server.Maintenance)
},
}
config.AddServerFlags(serverMaintenanceCmd)
serverCmd.AddCommand(serverMaintenanceCmd)
return serverCmd

7
go.mod
View File

@ -29,6 +29,7 @@ require (
codeberg.org/gruf/go-mutexes v1.5.2
codeberg.org/gruf/go-runners v1.6.3
codeberg.org/gruf/go-sched v1.2.4
codeberg.org/gruf/go-split v1.2.0
codeberg.org/gruf/go-storage v0.2.0
codeberg.org/gruf/go-structr v0.9.7
github.com/DmitriyVTitov/size v1.5.0
@ -42,7 +43,6 @@ require (
github.com/gin-gonic/gin v1.10.0
github.com/go-playground/form/v4 v4.2.1
github.com/go-swagger/go-swagger v0.31.0
github.com/go-viper/mapstructure/v2 v2.2.1
github.com/google/go-cmp v0.7.0
github.com/google/uuid v1.6.0
github.com/gorilla/feeds v1.2.0
@ -57,7 +57,9 @@ require (
github.com/oklog/ulid v1.3.1
github.com/pquerna/otp v1.4.0
github.com/rivo/uniseg v0.4.7
github.com/spf13/cast v1.7.1
github.com/spf13/cobra v1.9.1
github.com/spf13/pflag v1.0.6
github.com/spf13/viper v1.20.1
github.com/stretchr/testify v1.10.0
github.com/tdewolff/minify/v2 v2.23.1
@ -140,6 +142,7 @@ require (
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.26.0 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
@ -194,8 +197,6 @@ require (
github.com/shopspring/decimal v1.3.1 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.12.0 // indirect
github.com/spf13/cast v1.7.1 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tdewolff/parse/v2 v2.7.23 // indirect
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect

2
go.sum generated
View File

@ -46,6 +46,8 @@ codeberg.org/gruf/go-runners v1.6.3 h1:To/AX7eTrWuXrTkA3RA01YTP5zha1VZ68LQ+0D4RY
codeberg.org/gruf/go-runners v1.6.3/go.mod h1:oXAaUmG2VxoKttpCqZGv5nQBeSvZSR2BzIk7h1yTRlU=
codeberg.org/gruf/go-sched v1.2.4 h1:ddBB9o0D/2oU8NbQ0ldN5aWxogpXPRBATWi58+p++Hw=
codeberg.org/gruf/go-sched v1.2.4/go.mod h1:wad6l+OcYGWMA2TzNLMmLObsrbBDxdJfEy5WvTgBjNk=
codeberg.org/gruf/go-split v1.2.0 h1:PmzL23nVEVHm8VxjsJmv4m4wGQz2bGgQw52dgSSj65c=
codeberg.org/gruf/go-split v1.2.0/go.mod h1:0rejWJpqvOoFAd7nwm5tIXYKaAqjtFGOXmTqQV+VO38=
codeberg.org/gruf/go-storage v0.2.0 h1:mKj3Lx6AavEkuXXtxqPhdq+akW9YwrnP16yQBF7K5ZI=
codeberg.org/gruf/go-storage v0.2.0/go.mod h1:o3GzMDE5QNUaRnm/daUzFqvuAaC4utlgXDXYO79sWKU=
codeberg.org/gruf/go-structr v0.9.7 h1:yQeIxTjYb6reNdgESk915twyjolydYBqat/mlZrP7bg=

View File

@ -19,11 +19,11 @@ package config
import (
"reflect"
"strings"
"time"
"code.superseriousbusiness.org/gotosocial/internal/language"
"codeberg.org/gruf/go-bytesize"
"github.com/mitchellh/mapstructure"
)
// cfgtype is the reflected type information of Configuration{}.
@ -32,9 +32,15 @@ var cfgtype = reflect.TypeOf(Configuration{})
// fieldtag will fetch the string value for the given tag name
// on the given field name in the Configuration{} struct.
func fieldtag(field, tag string) string {
sfield, ok := cfgtype.FieldByName(field)
if !ok {
panic("unknown struct field")
nextType := cfgtype
var sfield reflect.StructField
for _, field := range strings.Split(field, ".") {
var ok bool
sfield, ok = nextType.FieldByName(field)
if !ok {
panic("unknown struct field")
}
nextType = sfield.Type
}
return sfield.Tag.Get(tag)
}
@ -45,21 +51,22 @@ func fieldtag(field, tag string) string {
// will need to regenerate the global Getter/Setter helpers by running:
// `go run ./internal/config/gen/ -out ./internal/config/helpers.gen.go`
type Configuration struct {
LogLevel string `name:"log-level" usage:"Log level to run at: [trace, debug, info, warn, fatal]"`
LogTimestampFormat string `name:"log-timestamp-format" usage:"Format to use for the log timestamp, as supported by Go's time.Layout"`
LogDbQueries bool `name:"log-db-queries" usage:"Log database queries verbosely when log-level is trace or debug"`
LogClientIP bool `name:"log-client-ip" usage:"Include the client IP in logs"`
ApplicationName string `name:"application-name" usage:"Name of the application, used in various places internally"`
LandingPageUser string `name:"landing-page-user" usage:"the user that should be shown on the instance's landing page"`
ConfigPath string `name:"config-path" usage:"Path to a file containing gotosocial configuration. Values set in this file will be overwritten by values set as env vars or arguments"`
Host string `name:"host" usage:"Hostname to use for the server (eg., example.org, gotosocial.whatever.com). DO NOT change this on a server that's already run!"`
AccountDomain string `name:"account-domain" usage:"Domain to use in account names (eg., example.org, whatever.com). If not set, will default to the setting for host. DO NOT change this on a server that's already run!"`
Protocol string `name:"protocol" usage:"Protocol to use for the REST api of the server (only use http if you are debugging or behind a reverse proxy!)"`
BindAddress string `name:"bind-address" usage:"Bind address to use for the GoToSocial server (eg., 0.0.0.0, 172.138.0.9, [::], localhost). For ipv6, enclose the address in square brackets, eg [2001:db8::fed1]. Default binds to all interfaces."`
Port int `name:"port" usage:"Port to use for GoToSocial. Change this to 443 if you're running the binary directly on the host machine."`
TrustedProxies []string `name:"trusted-proxies" usage:"Proxies to trust when parsing x-forwarded headers into real IPs."`
SoftwareVersion string `name:"software-version" usage:""`
LogLevel string `name:"log-level" usage:"Log level to run at: [trace, debug, info, warn, fatal]"`
LogTimestampFormat string `name:"log-timestamp-format" usage:"Format to use for the log timestamp, as supported by Go's time.Layout"`
LogDbQueries bool `name:"log-db-queries" usage:"Log database queries verbosely when log-level is trace or debug"`
LogClientIP bool `name:"log-client-ip" usage:"Include the client IP in logs"`
RequestIDHeader string `name:"request-id-header" usage:"Header to extract the Request ID from. Eg.,'X-Request-Id'."`
ConfigPath string `name:"config-path" usage:"Path to a file containing gotosocial configuration. Values set in this file will be overwritten by values set as env vars or arguments"`
ApplicationName string `name:"application-name" usage:"Name of the application, used in various places internally"`
LandingPageUser string `name:"landing-page-user" usage:"the user that should be shown on the instance's landing page"`
Host string `name:"host" usage:"Hostname to use for the server (eg., example.org, gotosocial.whatever.com). DO NOT change this on a server that's already run!"`
AccountDomain string `name:"account-domain" usage:"Domain to use in account names (eg., example.org, whatever.com). If not set, will default to the setting for host. DO NOT change this on a server that's already run!"`
Protocol string `name:"protocol" usage:"Protocol to use for the REST api of the server (only use http if you are debugging or behind a reverse proxy!)"`
BindAddress string `name:"bind-address" usage:"Bind address to use for the GoToSocial server (eg., 0.0.0.0, 172.138.0.9, [::], localhost). For ipv6, enclose the address in square brackets, eg [2001:db8::fed1]. Default binds to all interfaces."`
Port int `name:"port" usage:"Port to use for GoToSocial. Change this to 443 if you're running the binary directly on the host machine."`
TrustedProxies []string `name:"trusted-proxies" usage:"Proxies to trust when parsing x-forwarded headers into real IPs."`
SoftwareVersion string `name:"software-version" usage:""`
DbType string `name:"db-type" usage:"Database type: eg., postgres"`
DbAddress string `name:"db-address" usage:"Database ipv4 address, hostname, or filename"`
DbPort int `name:"db-port" usage:"Database port"`
@ -160,15 +167,8 @@ type Configuration struct {
SyslogProtocol string `name:"syslog-protocol" usage:"Protocol to use when directing logs to syslog. Leave empty to connect to local syslog."`
SyslogAddress string `name:"syslog-address" usage:"Address:port to send syslog logs to. Leave empty to connect to local syslog."`
AdvancedCookiesSamesite string `name:"advanced-cookies-samesite" usage:"'strict' or 'lax', see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite"`
AdvancedRateLimitRequests int `name:"advanced-rate-limit-requests" usage:"Amount of HTTP requests to permit within a 5 minute window. 0 or less turns rate limiting off."`
AdvancedRateLimitExceptions IPPrefixes `name:"advanced-rate-limit-exceptions" usage:"Slice of CIDRs to exclude from rate limit restrictions."`
AdvancedThrottlingMultiplier int `name:"advanced-throttling-multiplier" usage:"Multiplier to use per cpu for http request throttling. 0 or less turns throttling off."`
AdvancedThrottlingRetryAfter time.Duration `name:"advanced-throttling-retry-after" usage:"Retry-After duration response to send for throttled requests."`
AdvancedSenderMultiplier int `name:"advanced-sender-multiplier" usage:"Multiplier to use per cpu for batching outgoing fedi messages. 0 or less turns batching off (not recommended)."`
AdvancedCSPExtraURIs []string `name:"advanced-csp-extra-uris" usage:"Additional URIs to allow when building content-security-policy for media + images."`
AdvancedHeaderFilterMode string `name:"advanced-header-filter-mode" usage:"Set incoming request header filtering mode."`
AdvancedScraperDeterrence bool `name:"advanced-scraper-deterrence" usage:"Enable proof-of-work based scraper deterrence on profile / status pages"`
// Advanced flags.
Advanced AdvancedConfig `name:"advanced"`
// HTTPClient configuration vars.
HTTPClient HTTPClientConfiguration `name:"http-client"`
@ -177,15 +177,13 @@ type Configuration struct {
Cache CacheConfiguration `name:"cache"`
// TODO: move these elsewhere, these are more ephemeral vs long-running flags like above
AdminAccountUsername string `name:"username" usage:"the username to create/delete/etc"`
AdminAccountEmail string `name:"email" usage:"the email address of this account"`
AdminAccountPassword string `name:"password" usage:"the password to set for this account"`
AdminTransPath string `name:"path" usage:"the path of the file to import from/export to"`
AdminMediaPruneDryRun bool `name:"dry-run" usage:"perform a dry run and only log number of items eligible for pruning"`
AdminMediaListLocalOnly bool `name:"local-only" usage:"list only local attachments/emojis; if specified then remote-only cannot also be true"`
AdminMediaListRemoteOnly bool `name:"remote-only" usage:"list only remote attachments/emojis; if specified then local-only cannot also be true"`
RequestIDHeader string `name:"request-id-header" usage:"Header to extract the Request ID from. Eg.,'X-Request-Id'."`
AdminAccountUsername string `name:"username" usage:"the username to create/delete/etc" ephemeral:"yes"`
AdminAccountEmail string `name:"email" usage:"the email address of this account" ephemeral:"yes"`
AdminAccountPassword string `name:"password" usage:"the password to set for this account" ephemeral:"yes"`
AdminTransPath string `name:"path" usage:"the path of the file to import from/export to" ephemeral:"yes"`
AdminMediaPruneDryRun bool `name:"dry-run" usage:"perform a dry run and only log number of items eligible for pruning" ephemeral:"yes"`
AdminMediaListLocalOnly bool `name:"local-only" usage:"list only local attachments/emojis; if specified then remote-only cannot also be true" ephemeral:"yes"`
AdminMediaListRemoteOnly bool `name:"remote-only" usage:"list only remote attachments/emojis; if specified then local-only cannot also be true" ephemeral:"yes"`
}
type HTTPClientConfiguration struct {
@ -255,15 +253,27 @@ type CacheConfiguration struct {
VisibilityMemRatio float64 `name:"visibility-mem-ratio"`
}
// MarshalMap will marshal current Configuration into a map structure (useful for JSON/TOML/YAML).
func (cfg *Configuration) MarshalMap() (map[string]interface{}, error) {
var dst map[string]interface{}
dec, _ := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
TagName: "name",
Result: &dst,
})
if err := dec.Decode(cfg); err != nil {
return nil, err
}
return dst, nil
type AdvancedConfig struct {
CookiesSamesite string `name:"cookies-samesite" usage:"'strict' or 'lax', see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite"`
SenderMultiplier int `name:"sender-multiplier" usage:"Multiplier to use per cpu for batching outgoing fedi messages. 0 or less turns batching off (not recommended)."`
CSPExtraURIs []string `name:"csp-extra-uris" usage:"Additional URIs to allow when building content-security-policy for media + images."`
HeaderFilterMode string `name:"header-filter-mode" usage:"Set incoming request header filtering mode."`
ScraperDeterrence bool `name:"scraper-deterrence" usage:"Enable proof-of-work based scraper deterrence on profile / status pages"`
RateLimit RateLimitConfig `name:"rate-limit"`
Throttling ThrottlingConfig `name:"throttling"`
}
type RateLimitConfig struct {
Requests int `name:"requests" usage:"Amount of HTTP requests to permit within a 5 minute window. 0 or less turns rate limiting off."`
Exceptions IPPrefixes `name:"exceptions" usage:"Slice of CIDRs to exclude from rate limit restrictions."`
}
type ThrottlingConfig struct {
Multiplier int `name:"multiplier" usage:"Multiplier to use per cpu for http request throttling. 0 or less turns throttling off."`
RetryAfter time.Duration `name:"retry-after" usage:"Retry-After duration response to send for throttled requests."`
}
// type ScraperDeterrenceConfig struct {
// Enabled bool `name:"enabled" usage:"Enable proof-of-work based scraper deterrence on profile / status pages"`
// Difficulty uint8 `name:"difficulty" usage:"The proof-of-work difficulty, which determines how many leading zeros to try solve in hash solutions."`
// }

View File

@ -24,19 +24,18 @@ import (
"testing"
"code.superseriousbusiness.org/gotosocial/internal/config"
"codeberg.org/gruf/go-kv"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
"gopkg.in/yaml.v3"
)
func expectedKV(kvpairs ...string) map[string]interface{} {
ret := make(map[string]interface{}, len(kvpairs)/2)
for i := 0; i < len(kvpairs)-1; i += 2 {
ret[kvpairs[i]] = kvpairs[i+1]
func expectedKV(kvs ...kv.Field) map[string]interface{} {
ret := make(map[string]interface{}, len(kvs))
for _, kv := range kvs {
ret[kv.K] = kv.V
}
return ret
}
@ -61,7 +60,7 @@ func TestCLIParsing(t *testing.T) {
expected map[string]interface{}
}
defaults, _ := config.Defaults.MarshalMap()
defaults := config.Defaults.MarshalMap()
testcases := map[string]testcase{
"Make sure defaults are set correctly": {
@ -73,7 +72,7 @@ func TestCLIParsing(t *testing.T) {
"--db-address", "some.db.address",
},
expected: expectedKV(
"db-address", "some.db.address",
kv.Field{"db-address", "some.db.address"},
),
},
@ -82,7 +81,7 @@ func TestCLIParsing(t *testing.T) {
"GTS_DB_ADDRESS=some.db.address",
},
expected: expectedKV(
"db-address", "some.db.address",
kv.Field{"db-address", "some.db.address"},
),
},
@ -94,7 +93,7 @@ func TestCLIParsing(t *testing.T) {
"GTS_DB_ADDRESS=some.other.db.address",
},
expected: expectedKV(
"db-address", "some.db.address",
kv.Field{"db-address", "some.db.address"},
),
},
@ -119,8 +118,8 @@ func TestCLIParsing(t *testing.T) {
},
// only checking our overridden one and one non-default from the config file here instead of including all of test.yaml
expected: expectedKV(
"account-domain", "my.test.domain",
"host", "gts.example.org",
kv.Field{"account-domain", "my.test.domain"},
kv.Field{"host", "gts.example.org"},
),
},
@ -133,8 +132,8 @@ func TestCLIParsing(t *testing.T) {
},
// only checking our overridden one and one non-default from the config file here instead of including all of test.yaml
expected: expectedKV(
"account-domain", "my.test.domain",
"host", "gts.example.org",
kv.Field{"account-domain", "my.test.domain"},
kv.Field{"host", "gts.example.org"},
),
},
@ -148,8 +147,8 @@ func TestCLIParsing(t *testing.T) {
},
// only checking our overridden one and one non-default from the config file here instead of including all of test.yaml
expected: expectedKV(
"account-domain", "my.test.domain",
"host", "gts.example.org",
kv.Field{"account-domain", "my.test.domain"},
kv.Field{"host", "gts.example.org"},
),
},
@ -165,9 +164,19 @@ func TestCLIParsing(t *testing.T) {
"--config-path", "testdata/test2.yaml",
},
expected: expectedKV(
"log-level", "trace",
"account-domain", "peepee.poopoo",
"application-name", "gotosocial",
kv.Field{"log-level", "trace"},
kv.Field{"account-domain", "peepee.poopoo"},
kv.Field{"application-name", "gotosocial"},
),
},
"Loading nested config file. This should also work the same": {
cli: []string{
"--config-path", "testdata/test3.yaml",
},
expected: expectedKV(
kv.Field{"advanced-scraper-deterrence", true},
kv.Field{"advanced-rate-limit-requests", 5000},
),
},
}
@ -185,8 +194,7 @@ func TestCLIParsing(t *testing.T) {
state := config.NewState()
cmd := cobra.Command{}
state.AddGlobalFlags(&cmd)
state.AddServerFlags(&cmd)
config.RegisterGlobalFlags(&cmd)
if data.cli != nil {
cmd.ParseFlags(data.cli)
@ -194,7 +202,7 @@ func TestCLIParsing(t *testing.T) {
state.BindFlags(&cmd)
state.Reload()
state.LoadConfigFile()
state.Viper(func(v *viper.Viper) {
for k, ev := range data.expected {

View File

@ -130,15 +130,23 @@ var Defaults = Configuration{
SyslogProtocol: "udp",
SyslogAddress: "localhost:514",
AdvancedCookiesSamesite: "lax",
AdvancedRateLimitRequests: 300, // 1 per second per 5 minutes
AdvancedRateLimitExceptions: IPPrefixes{},
AdvancedThrottlingMultiplier: 8, // 8 open requests per CPU
AdvancedThrottlingRetryAfter: time.Second * 30,
AdvancedSenderMultiplier: 2, // 2 senders per CPU
AdvancedCSPExtraURIs: []string{},
AdvancedHeaderFilterMode: RequestHeaderFilterModeDisabled,
AdvancedScraperDeterrence: false,
Advanced: AdvancedConfig{
SenderMultiplier: 2, // 2 senders per CPU
CSPExtraURIs: []string{},
HeaderFilterMode: RequestHeaderFilterModeDisabled,
CookiesSamesite: "lax",
ScraperDeterrence: false,
RateLimit: RateLimitConfig{
Requests: 300, // 1 per second per 5 minutes
Exceptions: IPPrefixes{},
},
Throttling: ThrottlingConfig{
Multiplier: 8, // 8 open requests per CPU
RetryAfter: 30 * time.Second,
},
},
Cache: CacheConfiguration{
// Rough memory target that the total

View File

@ -23,150 +23,6 @@ import (
// TODO: consolidate these methods into the Configuration{} or ConfigState{} structs.
// AddGlobalFlags will attach global configuration flags to given cobra command, loading defaults from global config.
func AddGlobalFlags(cmd *cobra.Command) {
global.AddGlobalFlags(cmd)
}
// AddGlobalFlags will attach global configuration flags to given cobra command, loading defaults from State.
func (s *ConfigState) AddGlobalFlags(cmd *cobra.Command) {
s.Config(func(cfg *Configuration) {
// General
cmd.PersistentFlags().String(ApplicationNameFlag(), cfg.ApplicationName, fieldtag("ApplicationName", "usage"))
cmd.PersistentFlags().String(LandingPageUserFlag(), cfg.LandingPageUser, fieldtag("LandingPageUser", "usage"))
cmd.PersistentFlags().String(HostFlag(), cfg.Host, fieldtag("Host", "usage"))
cmd.PersistentFlags().String(AccountDomainFlag(), cfg.AccountDomain, fieldtag("AccountDomain", "usage"))
cmd.PersistentFlags().String(ProtocolFlag(), cfg.Protocol, fieldtag("Protocol", "usage"))
cmd.PersistentFlags().String(LogLevelFlag(), cfg.LogLevel, fieldtag("LogLevel", "usage"))
cmd.PersistentFlags().String(LogTimestampFormatFlag(), cfg.LogTimestampFormat, fieldtag("LogTimestampFormat", "usage"))
cmd.PersistentFlags().Bool(LogDbQueriesFlag(), cfg.LogDbQueries, fieldtag("LogDbQueries", "usage"))
cmd.PersistentFlags().String(ConfigPathFlag(), cfg.ConfigPath, fieldtag("ConfigPath", "usage"))
// Database
cmd.PersistentFlags().String(DbTypeFlag(), cfg.DbType, fieldtag("DbType", "usage"))
cmd.PersistentFlags().String(DbAddressFlag(), cfg.DbAddress, fieldtag("DbAddress", "usage"))
cmd.PersistentFlags().Int(DbPortFlag(), cfg.DbPort, fieldtag("DbPort", "usage"))
cmd.PersistentFlags().String(DbUserFlag(), cfg.DbUser, fieldtag("DbUser", "usage"))
cmd.PersistentFlags().String(DbPasswordFlag(), cfg.DbPassword, fieldtag("DbPassword", "usage"))
cmd.PersistentFlags().String(DbDatabaseFlag(), cfg.DbDatabase, fieldtag("DbDatabase", "usage"))
cmd.PersistentFlags().String(DbTLSModeFlag(), cfg.DbTLSMode, fieldtag("DbTLSMode", "usage"))
cmd.PersistentFlags().String(DbTLSCACertFlag(), cfg.DbTLSCACert, fieldtag("DbTLSCACert", "usage"))
cmd.PersistentFlags().Int(DbMaxOpenConnsMultiplierFlag(), cfg.DbMaxOpenConnsMultiplier, fieldtag("DbMaxOpenConnsMultiplier", "usage"))
cmd.PersistentFlags().String(DbSqliteJournalModeFlag(), cfg.DbSqliteJournalMode, fieldtag("DbSqliteJournalMode", "usage"))
cmd.PersistentFlags().String(DbSqliteSynchronousFlag(), cfg.DbSqliteSynchronous, fieldtag("DbSqliteSynchronous", "usage"))
cmd.PersistentFlags().Uint64(DbSqliteCacheSizeFlag(), uint64(cfg.DbSqliteCacheSize), fieldtag("DbSqliteCacheSize", "usage"))
cmd.PersistentFlags().Duration(DbSqliteBusyTimeoutFlag(), cfg.DbSqliteBusyTimeout, fieldtag("DbSqliteBusyTimeout", "usage"))
// HTTPClient
cmd.PersistentFlags().StringSlice(HTTPClientAllowIPsFlag(), cfg.HTTPClient.AllowIPs, "no usage string")
cmd.PersistentFlags().StringSlice(HTTPClientBlockIPsFlag(), cfg.HTTPClient.BlockIPs, "no usage string")
cmd.PersistentFlags().Duration(HTTPClientTimeoutFlag(), cfg.HTTPClient.Timeout, "no usage string")
cmd.PersistentFlags().Bool(HTTPClientTLSInsecureSkipVerifyFlag(), cfg.HTTPClient.TLSInsecureSkipVerify, "no usage string")
})
}
// AddServerFlags will attach server configuration flags to given cobra command, loading defaults from global config.
func AddServerFlags(cmd *cobra.Command) {
global.AddServerFlags(cmd)
}
// AddServerFlags will attach server configuration flags to given cobra command, loading defaults from State.
func (s *ConfigState) AddServerFlags(cmd *cobra.Command) {
s.Config(func(cfg *Configuration) {
// Router
cmd.PersistentFlags().String(BindAddressFlag(), cfg.BindAddress, fieldtag("BindAddress", "usage"))
cmd.PersistentFlags().Int(PortFlag(), cfg.Port, fieldtag("Port", "usage"))
cmd.PersistentFlags().StringSlice(TrustedProxiesFlag(), cfg.TrustedProxies, fieldtag("TrustedProxies", "usage"))
// Template
cmd.Flags().String(WebTemplateBaseDirFlag(), cfg.WebTemplateBaseDir, fieldtag("WebTemplateBaseDir", "usage"))
cmd.Flags().String(WebAssetBaseDirFlag(), cfg.WebAssetBaseDir, fieldtag("WebAssetBaseDir", "usage"))
// Instance
cmd.Flags().String(InstanceFederationModeFlag(), cfg.InstanceFederationMode, fieldtag("InstanceFederationMode", "usage"))
cmd.Flags().Bool(InstanceFederationSpamFilterFlag(), cfg.InstanceFederationSpamFilter, fieldtag("InstanceFederationSpamFilter", "usage"))
cmd.Flags().Bool(InstanceExposePeersFlag(), cfg.InstanceExposePeers, fieldtag("InstanceExposePeers", "usage"))
cmd.Flags().Bool(InstanceExposeSuspendedFlag(), cfg.InstanceExposeSuspended, fieldtag("InstanceExposeSuspended", "usage"))
cmd.Flags().Bool(InstanceExposeSuspendedWebFlag(), cfg.InstanceExposeSuspendedWeb, fieldtag("InstanceExposeSuspendedWeb", "usage"))
cmd.Flags().Bool(InstanceDeliverToSharedInboxesFlag(), cfg.InstanceDeliverToSharedInboxes, fieldtag("InstanceDeliverToSharedInboxes", "usage"))
cmd.Flags().StringSlice(InstanceLanguagesFlag(), cfg.InstanceLanguages.TagStrs(), fieldtag("InstanceLanguages", "usage"))
cmd.Flags().String(InstanceSubscriptionsProcessFromFlag(), cfg.InstanceSubscriptionsProcessFrom, fieldtag("InstanceSubscriptionsProcessFrom", "usage"))
cmd.Flags().Duration(InstanceSubscriptionsProcessEveryFlag(), cfg.InstanceSubscriptionsProcessEvery, fieldtag("InstanceSubscriptionsProcessEvery", "usage"))
cmd.Flags().String(InstanceStatsModeFlag(), cfg.InstanceStatsMode, fieldtag("InstanceStatsMode", "usage"))
cmd.Flags().Bool(InstanceAllowBackdatingStatusesFlag(), cfg.InstanceAllowBackdatingStatuses, fieldtag("InstanceAllowBackdatingStatuses", "usage"))
// Accounts
cmd.Flags().Bool(AccountsRegistrationOpenFlag(), cfg.AccountsRegistrationOpen, fieldtag("AccountsRegistrationOpen", "usage"))
cmd.Flags().Bool(AccountsReasonRequiredFlag(), cfg.AccountsReasonRequired, fieldtag("AccountsReasonRequired", "usage"))
cmd.Flags().Bool(AccountsAllowCustomCSSFlag(), cfg.AccountsAllowCustomCSS, fieldtag("AccountsAllowCustomCSS", "usage"))
// Media
cmd.Flags().Int(MediaDescriptionMinCharsFlag(), cfg.MediaDescriptionMinChars, fieldtag("MediaDescriptionMinChars", "usage"))
cmd.Flags().Int(MediaDescriptionMaxCharsFlag(), cfg.MediaDescriptionMaxChars, fieldtag("MediaDescriptionMaxChars", "usage"))
cmd.Flags().Int(MediaRemoteCacheDaysFlag(), cfg.MediaRemoteCacheDays, fieldtag("MediaRemoteCacheDays", "usage"))
cmd.Flags().Uint64(MediaLocalMaxSizeFlag(), uint64(cfg.MediaLocalMaxSize), fieldtag("MediaLocalMaxSize", "usage"))
cmd.Flags().Uint64(MediaRemoteMaxSizeFlag(), uint64(cfg.MediaRemoteMaxSize), fieldtag("MediaRemoteMaxSize", "usage"))
cmd.Flags().Uint64(MediaEmojiLocalMaxSizeFlag(), uint64(cfg.MediaEmojiLocalMaxSize), fieldtag("MediaEmojiLocalMaxSize", "usage"))
cmd.Flags().Uint64(MediaEmojiRemoteMaxSizeFlag(), uint64(cfg.MediaEmojiRemoteMaxSize), fieldtag("MediaEmojiRemoteMaxSize", "usage"))
cmd.Flags().String(MediaCleanupFromFlag(), cfg.MediaCleanupFrom, fieldtag("MediaCleanupFrom", "usage"))
cmd.Flags().Duration(MediaCleanupEveryFlag(), cfg.MediaCleanupEvery, fieldtag("MediaCleanupEvery", "usage"))
// Storage
cmd.Flags().String(StorageBackendFlag(), cfg.StorageBackend, fieldtag("StorageBackend", "usage"))
cmd.Flags().String(StorageLocalBasePathFlag(), cfg.StorageLocalBasePath, fieldtag("StorageLocalBasePath", "usage"))
// Statuses
cmd.Flags().Int(StatusesMaxCharsFlag(), cfg.StatusesMaxChars, fieldtag("StatusesMaxChars", "usage"))
cmd.Flags().Int(StatusesPollMaxOptionsFlag(), cfg.StatusesPollMaxOptions, fieldtag("StatusesPollMaxOptions", "usage"))
cmd.Flags().Int(StatusesPollOptionMaxCharsFlag(), cfg.StatusesPollOptionMaxChars, fieldtag("StatusesPollOptionMaxChars", "usage"))
cmd.Flags().Int(StatusesMediaMaxFilesFlag(), cfg.StatusesMediaMaxFiles, fieldtag("StatusesMediaMaxFiles", "usage"))
// LetsEncrypt
cmd.Flags().Bool(LetsEncryptEnabledFlag(), cfg.LetsEncryptEnabled, fieldtag("LetsEncryptEnabled", "usage"))
cmd.Flags().Int(LetsEncryptPortFlag(), cfg.LetsEncryptPort, fieldtag("LetsEncryptPort", "usage"))
cmd.Flags().String(LetsEncryptCertDirFlag(), cfg.LetsEncryptCertDir, fieldtag("LetsEncryptCertDir", "usage"))
cmd.Flags().String(LetsEncryptEmailAddressFlag(), cfg.LetsEncryptEmailAddress, fieldtag("LetsEncryptEmailAddress", "usage"))
// Manual TLS
cmd.Flags().String(TLSCertificateChainFlag(), cfg.TLSCertificateChain, fieldtag("TLSCertificateChain", "usage"))
cmd.Flags().String(TLSCertificateKeyFlag(), cfg.TLSCertificateKey, fieldtag("TLSCertificateKey", "usage"))
// OIDC
cmd.Flags().Bool(OIDCEnabledFlag(), cfg.OIDCEnabled, fieldtag("OIDCEnabled", "usage"))
cmd.Flags().String(OIDCIdpNameFlag(), cfg.OIDCIdpName, fieldtag("OIDCIdpName", "usage"))
cmd.Flags().Bool(OIDCSkipVerificationFlag(), cfg.OIDCSkipVerification, fieldtag("OIDCSkipVerification", "usage"))
cmd.Flags().String(OIDCIssuerFlag(), cfg.OIDCIssuer, fieldtag("OIDCIssuer", "usage"))
cmd.Flags().String(OIDCClientIDFlag(), cfg.OIDCClientID, fieldtag("OIDCClientID", "usage"))
cmd.Flags().String(OIDCClientSecretFlag(), cfg.OIDCClientSecret, fieldtag("OIDCClientSecret", "usage"))
cmd.Flags().StringSlice(OIDCScopesFlag(), cfg.OIDCScopes, fieldtag("OIDCScopes", "usage"))
// SMTP
cmd.Flags().String(SMTPHostFlag(), cfg.SMTPHost, fieldtag("SMTPHost", "usage"))
cmd.Flags().Int(SMTPPortFlag(), cfg.SMTPPort, fieldtag("SMTPPort", "usage"))
cmd.Flags().String(SMTPUsernameFlag(), cfg.SMTPUsername, fieldtag("SMTPUsername", "usage"))
cmd.Flags().String(SMTPPasswordFlag(), cfg.SMTPPassword, fieldtag("SMTPPassword", "usage"))
cmd.Flags().String(SMTPFromFlag(), cfg.SMTPFrom, fieldtag("SMTPFrom", "usage"))
cmd.Flags().Bool(SMTPDiscloseRecipientsFlag(), cfg.SMTPDiscloseRecipients, fieldtag("SMTPDiscloseRecipients", "usage"))
// Syslog
cmd.Flags().Bool(SyslogEnabledFlag(), cfg.SyslogEnabled, fieldtag("SyslogEnabled", "usage"))
cmd.Flags().String(SyslogProtocolFlag(), cfg.SyslogProtocol, fieldtag("SyslogProtocol", "usage"))
cmd.Flags().String(SyslogAddressFlag(), cfg.SyslogAddress, fieldtag("SyslogAddress", "usage"))
// Advanced flags
cmd.Flags().String(AdvancedCookiesSamesiteFlag(), cfg.AdvancedCookiesSamesite, fieldtag("AdvancedCookiesSamesite", "usage"))
cmd.Flags().Int(AdvancedRateLimitRequestsFlag(), cfg.AdvancedRateLimitRequests, fieldtag("AdvancedRateLimitRequests", "usage"))
cmd.Flags().StringSlice(AdvancedRateLimitExceptionsFlag(), cfg.AdvancedRateLimitExceptions.Strings(), fieldtag("AdvancedRateLimitExceptions", "usage"))
cmd.Flags().Int(AdvancedThrottlingMultiplierFlag(), cfg.AdvancedThrottlingMultiplier, fieldtag("AdvancedThrottlingMultiplier", "usage"))
cmd.Flags().Duration(AdvancedThrottlingRetryAfterFlag(), cfg.AdvancedThrottlingRetryAfter, fieldtag("AdvancedThrottlingRetryAfter", "usage"))
cmd.Flags().Int(AdvancedSenderMultiplierFlag(), cfg.AdvancedSenderMultiplier, fieldtag("AdvancedSenderMultiplier", "usage"))
cmd.Flags().StringSlice(AdvancedCSPExtraURIsFlag(), cfg.AdvancedCSPExtraURIs, fieldtag("AdvancedCSPExtraURIs", "usage"))
cmd.Flags().String(AdvancedHeaderFilterModeFlag(), cfg.AdvancedHeaderFilterMode, fieldtag("AdvancedHeaderFilterMode", "usage"))
cmd.Flags().String(RequestIDHeaderFlag(), cfg.RequestIDHeader, fieldtag("RequestIDHeader", "usage"))
})
}
// AddAdminAccount attaches flags pertaining to admin account actions.
func AddAdminAccount(cmd *cobra.Command) {
name := AdminAccountUsernameFlag()

View File

@ -25,6 +25,7 @@ import (
"os/exec"
"reflect"
"strings"
"time"
"code.superseriousbusiness.org/gotosocial/internal/config"
)
@ -48,6 +49,11 @@ const license = `// GoToSocial
`
var durationType = reflect.TypeOf(time.Duration(0))
var stringerType = reflect.TypeOf((*interface{ String() string })(nil)).Elem()
var stringersType = reflect.TypeOf((*interface{ Strings() []string })(nil)).Elem()
var flagSetType = reflect.TypeOf((*interface{ Set(string) error })(nil)).Elem()
func main() {
var out string
@ -61,41 +67,392 @@ func main() {
panic(err)
}
fmt.Fprint(output, "// THIS IS A GENERATED FILE, DO NOT EDIT BY HAND\n")
fmt.Fprint(output, license)
fmt.Fprint(output, "package config\n\n")
fmt.Fprint(output, "import (\n")
fmt.Fprint(output, "\t\"time\"\n\n")
fmt.Fprint(output, "\t\"codeberg.org/gruf/go-bytesize\"\n")
fmt.Fprint(output, "\t\"code.superseriousbusiness.org/gotosocial/internal/language\"\n")
fmt.Fprint(output, ")\n\n")
generateFields(output, nil, reflect.TypeOf(config.Configuration{}))
_ = output.Close()
_ = exec.Command("gofumpt", "-w", out).Run()
configType := reflect.TypeOf(config.Configuration{})
// The plan here is that eventually we might be able
// to generate an example configuration from struct tags
// Parse our config type for usable fields.
fields := loadConfigFields(nil, nil, configType)
fprintf(output, "// THIS IS A GENERATED FILE, DO NOT EDIT BY HAND\n")
fprintf(output, license)
fprintf(output, "package config\n\n")
fprintf(output, "import (\n")
fprintf(output, "\t\"fmt\"\n")
fprintf(output, "\t\"time\"\n\n")
fprintf(output, "\t\"codeberg.org/gruf/go-bytesize\"\n")
fprintf(output, "\t\"code.superseriousbusiness.org/gotosocial/internal/language\"\n")
fprintf(output, "\t\"github.com/spf13/pflag\"\n")
fprintf(output, "\t\"github.com/spf13/cast\"\n")
fprintf(output, ")\n")
fprintf(output, "\n")
generateFlagRegistering(output, fields)
generateMapMarshaler(output, fields)
generateMapUnmarshaler(output, fields)
generateGetSetters(output, fields)
generateMapFlattener(output, fields)
must(output.Close())
must(exec.Command("gofumpt", "-w", out).Run())
}
func generateFields(output io.Writer, prefixes []string, t reflect.Type) {
type ConfigField struct {
// Any CLI flag prefixes,
// i.e. with nested fields.
Prefixes []string
// The base CLI flag
// name of the field.
Name string
// Path to struct field
// in dot-separated form.
Path string
// Usage string.
Usage string
// The underlying Go type
// of the config field.
Type reflect.Type
// i.e. is this found in the configuration file?
// or just used in specific CLI commands? in the
// future we'll remove these from config struct.
Ephemeral bool
}
// Flag returns the combined "prefixes-name" CLI flag for config field.
func (f ConfigField) Flag() string {
flag := strings.Join(append(f.Prefixes, f.Name), "-")
flag = strings.ToLower(flag)
return flag
}
// PossibleKeys returns a list of possible map key combinations
// that this config field may be found under. The combined "prefixes-name"
// will always be in the list, but also separates them out to account for
// possible nesting. This allows us to support both nested and un-nested
// configuration files, always prioritizing "prefixes-name" as its the CLI flag.
func (f ConfigField) PossibleKeys() [][]string {
if len(f.Prefixes) == 0 {
return [][]string{{f.Name}}
}
var keys [][]string
combined := f.Flag()
keys = append(keys, []string{combined})
basePrefix := strings.TrimSuffix(combined, "-"+f.Name)
keys = append(keys, []string{basePrefix, f.Name})
for i := len(f.Prefixes) - 1; i >= 0; i-- {
prefix := f.Prefixes[i]
basePrefix = strings.TrimSuffix(basePrefix, prefix)
basePrefix = strings.TrimSuffix(basePrefix, "-")
if len(basePrefix) == 0 {
break
}
var key []string
key = append(key, basePrefix)
key = append(key, f.Prefixes[i:]...)
key = append(key, f.Name)
keys = append(keys, key)
}
return keys
}
func loadConfigFields(pathPrefixes, flagPrefixes []string, t reflect.Type) []ConfigField {
var out []ConfigField
for i := 0; i < t.NumField(); i++ {
// Struct field at index.
field := t.Field(i)
if ft := field.Type; ft.Kind() == reflect.Struct {
// This is a struct field containing further nested config vars.
generateFields(output, append(prefixes, field.Name), ft)
// Get field's tagged name.
name := field.Tag.Get("name")
if name == "" || name == "-" {
continue
}
// Get prefixed config variable name
name := strings.Join(prefixes, "") + field.Name
if ft := field.Type; ft.Kind() == reflect.Struct {
// This is a nested struct, load nested fields.
pathPrefixes := append(pathPrefixes, field.Name)
flagPrefixes := append(flagPrefixes, name)
out = append(out, loadConfigFields(pathPrefixes, flagPrefixes, ft)...)
continue
}
// Get period-separated (if nested) config variable "path"
fieldPath := strings.Join(append(prefixes, field.Name), ".")
// Get prefixed, period-separated, config variable struct "path".
fieldPath := strings.Join(append(pathPrefixes, field.Name), ".")
// Get dash-separated config variable CLI flag "path"
flagPath := strings.Join(append(prefixes, field.Tag.Get("name")), "-")
flagPath = strings.ToLower(flagPath)
// Append prepared ConfigField.
out = append(out, ConfigField{
Prefixes: flagPrefixes,
Name: name,
Path: fieldPath,
Usage: field.Tag.Get("usage"),
Ephemeral: field.Tag.Get("ephemeral") == "yes",
Type: field.Type,
})
}
return out
}
// func generateFlagConsts(out io.Writer, fields []ConfigField) {
// fprintf(out, "const (\n")
// for _, field := range fields {
// name := strings.ReplaceAll(field.Path, ".", "")
// fprintf(out, "\t%sFlag = \"%s\"\n", name, field.Flag())
// }
// fprintf(out, ")\n\n")
// }
func generateFlagRegistering(out io.Writer, fields []ConfigField) {
fprintf(out, "func (cfg *Configuration) RegisterFlags(flags *pflag.FlagSet) {\n")
for _, field := range fields {
if field.Ephemeral {
// Skip registering
// ephemeral flags.
continue
}
// Check for easy cases of just regular primitive types.
if field.Type.Kind().String() == field.Type.String() {
typeName := field.Type.String()
typeName = strings.ToUpper(typeName[:1]) + typeName[1:]
fprintf(out, "\tflags.%s(\"%s\", cfg.%s, \"%s\")\n", typeName, field.Flag(), field.Path, field.Usage)
continue
}
// Check for easy cases of just
// regular primitive slice types.
if field.Type.Kind() == reflect.Slice {
elem := field.Type.Elem()
if elem.Kind().String() == elem.String() {
typeName := elem.String()
typeName = strings.ToUpper(typeName[:1]) + typeName[1:]
fprintf(out, "\tflags.%sSlice(\"%s\", cfg.%s, \"%s\")\n", typeName, field.Flag(), field.Path, field.Usage)
continue
}
}
// Durations should get set directly
// as their types as viper knows how
// to deal with this type directly.
if field.Type == durationType {
fprintf(out, "\tflags.Duration(\"%s\", cfg.%s, \"%s\")\n", field.Flag(), field.Path, field.Usage)
continue
}
if field.Type.Kind() == reflect.Slice {
// Check if the field supports Stringers{}.
if field.Type.Implements(stringersType) {
fprintf(out, "\tflags.StringSlice(\"%s\", cfg.%s.Strings(), \"%s\")\n", field.Flag(), field.Path, field.Usage)
continue
}
// Or the pointer type of the field value supports Stringers{}.
if ptr := reflect.PointerTo(field.Type); ptr.Implements(stringersType) {
fprintf(out, "\tflags.StringSlice(\"%s\", cfg.%s.Strings(), \"%s\")\n", field.Flag(), field.Path, field.Usage)
continue
}
fprintf(os.Stderr, "field %s doesn't implement %s!\n", field.Path, stringersType)
} else {
// Check if the field supports Stringer{}.
if field.Type.Implements(stringerType) {
fprintf(out, "\tflags.String(\"%s\", cfg.%s.String(), \"%s\")\n", field.Flag(), field.Path, field.Usage)
continue
}
// Or the pointer type of the field value supports Stringer{}.
if ptr := reflect.PointerTo(field.Type); ptr.Implements(stringerType) {
fprintf(out, "\tflags.String(\"%s\", cfg.%s.String(), \"%s\")\n", field.Flag(), field.Path, field.Usage)
continue
}
fprintf(os.Stderr, "field %s doesn't implement %s!\n", field.Path, stringerType)
}
}
fprintf(out, "}\n\n")
}
func generateMapMarshaler(out io.Writer, fields []ConfigField) {
fprintf(out, "func (cfg *Configuration) MarshalMap() map[string]any {\n")
fprintf(out, "\tcfgmap := make(map[string]any, %d)\n", len(fields))
for _, field := range fields {
// Check for easy cases of just regular primitive types.
if field.Type.Kind().String() == field.Type.String() {
fprintf(out, "\tcfgmap[\"%s\"] = cfg.%s\n", field.Flag(), field.Path)
continue
}
// Check for easy cases of just
// regular primitive slice types.
if field.Type.Kind() == reflect.Slice {
elem := field.Type.Elem()
if elem.Kind().String() == elem.String() {
fprintf(out, "\tcfgmap[\"%s\"] = cfg.%s\n", field.Flag(), field.Path)
continue
}
}
// Durations should get set directly
// as their types as viper knows how
// to deal with this type directly.
if field.Type == durationType {
fprintf(out, "\tcfgmap[\"%s\"] = cfg.%s\n", field.Flag(), field.Path)
continue
}
if field.Type.Kind() == reflect.Slice {
// Either the field must support Stringers{}.
if field.Type.Implements(stringersType) {
fprintf(out, "\tcfgmap[\"%s\"] = cfg.%s.Strings()\n", field.Flag(), field.Path)
continue
}
// Or the pointer type of the field value must support Stringers{}.
if ptr := reflect.PointerTo(field.Type); ptr.Implements(stringersType) {
fprintf(out, "\tcfgmap[\"%s\"] = cfg.%s.Strings()\n", field.Flag(), field.Path)
continue
}
fprintf(os.Stderr, "field %s doesn't implement %s!\n", field.Path, stringersType)
} else {
// Either the field must support Stringer{}.
if field.Type.Implements(stringerType) {
fprintf(out, "\tcfgmap[\"%s\"] = cfg.%s.String()\n", field.Flag(), field.Path)
continue
}
// Or the pointer type of the field value must support Stringer{}.
if ptr := reflect.PointerTo(field.Type); ptr.Implements(stringerType) {
fprintf(out, "\tcfgmap[\"%s\"] = cfg.%s.String()\n", field.Flag(), field.Path)
continue
}
fprintf(os.Stderr, "field %s doesn't implement %s!\n", field.Path, stringerType)
}
}
fprintf(out, "\treturn cfgmap")
fprintf(out, "}\n\n")
}
func generateMapUnmarshaler(out io.Writer, fields []ConfigField) {
fprintf(out, "func (cfg *Configuration) UnmarshalMap(cfgmap map[string]any) error {\n")
fprintf(out, "// VERY IMPORTANT FIRST STEP!\n")
fprintf(out, "// flatten to normalize map to\n")
fprintf(out, "// entirely un-nested key values\n")
fprintf(out, "flattenConfigMap(cfgmap)\n")
fprintf(out, "\n")
for _, field := range fields {
// Check for easy cases of just regular primitive types.
if field.Type.Kind().String() == field.Type.String() {
generateUnmarshalerPrimitive(out, field)
continue
}
// Check for easy cases of just
// regular primitive slice types.
if field.Type.Kind() == reflect.Slice {
elem := field.Type.Elem()
if elem.Kind().String() == elem.String() {
generateUnmarshalerPrimitive(out, field)
continue
}
}
// Durations should get set directly
// as their types as viper knows how
// to deal with this type directly.
if field.Type == durationType {
generateUnmarshalerPrimitive(out, field)
continue
}
// Either the field must support flag.Value{}.
if field.Type.Implements(flagSetType) {
generateUnmarshalerFlagType(out, field)
continue
}
// Or the pointer type of the field value must support flag.Value{}.
if ptr := reflect.PointerTo(field.Type); ptr.Implements(flagSetType) {
generateUnmarshalerFlagType(out, field)
continue
}
fprintf(os.Stderr, "field %s doesn't implement %s!\n", field.Path, flagSetType)
}
fprintf(out, "\treturn nil\n")
fprintf(out, "}\n\n")
}
func generateUnmarshalerPrimitive(out io.Writer, field ConfigField) {
fprintf(out, "\t\tif ival, ok := cfgmap[\"%s\"]; ok {\n", field.Flag())
if field.Type.Kind() == reflect.Slice {
elem := field.Type.Elem()
typeName := elem.String()
if i := strings.IndexRune(typeName, '.'); i >= 0 {
typeName = typeName[i+1:]
}
typeName = strings.ToUpper(typeName[:1]) + typeName[1:]
fprintf(out, "\t\t\tvar err error\n")
// note we specifically handle slice types ourselves to split by comma
fprintf(out, "\t\t\tcfg.%s, err = to%sSlice(ival)\n", field.Path, typeName)
fprintf(out, "\t\t\tif err != nil {\n")
fprintf(out, "\t\t\t\treturn fmt.Errorf(\"error casting %%#v -> []%s for '%s': %%w\", ival, err)\n", elem.String(), field.Flag())
fprintf(out, "\t\t\t}\n")
} else {
typeName := field.Type.String()
if i := strings.IndexRune(typeName, '.'); i >= 0 {
typeName = typeName[i+1:]
}
typeName = strings.ToUpper(typeName[:1]) + typeName[1:]
fprintf(out, "\t\t\tvar err error\n")
fprintf(out, "\t\t\tcfg.%s, err = cast.To%sE(ival)\n", field.Path, typeName)
fprintf(out, "\t\t\tif err != nil {\n")
fprintf(out, "\t\t\t\treturn fmt.Errorf(\"error casting %%#v -> %s for '%s': %%w\", ival, err)\n", field.Type.String(), field.Flag())
fprintf(out, "\t\t\t}\n")
}
fprintf(out, "\t}\n")
fprintf(out, "\n")
}
func generateUnmarshalerFlagType(out io.Writer, field ConfigField) {
fprintf(out, "\t\tif ival, ok := cfgmap[\"%s\"]; ok {\n", field.Flag())
if field.Type.Kind() == reflect.Slice {
// same as above re: slice types and splitting on comma
fprintf(out, "\t\tt, err := toStringSlice(ival)\n")
fprintf(out, "\t\tif err != nil {\n")
fprintf(out, "\t\t\treturn fmt.Errorf(\"error casting %%#v -> []string for '%s': %%w\", ival, err)\n", field.Flag())
fprintf(out, "\t\t}\n")
fprintf(out, "\t\tcfg.%s = %s{}\n", field.Path, strings.TrimPrefix(field.Type.String(), "config."))
fprintf(out, "\t\tfor _, in := range t {\n")
fprintf(out, "\t\t\tif err := cfg.%s.Set(in); err != nil {\n", field.Path)
fprintf(out, "\t\t\t\treturn fmt.Errorf(\"error parsing %%#v for '%s': %%w\", ival, err)\n", field.Flag())
fprintf(out, "\t\t\t}\n")
fprintf(out, "\t\t}\n")
} else {
fprintf(out, "\t\tt, err := cast.ToStringE(ival)\n")
fprintf(out, "\t\tif err != nil {\n")
fprintf(out, "\t\t\treturn fmt.Errorf(\"error casting %%#v -> string for '%s': %%w\", ival, err)\n", field.Flag())
fprintf(out, "\t\t}\n")
fprintf(out, "\t\tcfg.%s = %#v\n", field.Path, reflect.New(field.Type).Elem().Interface())
fprintf(out, "\t\tif err := cfg.%s.Set(t); err != nil {\n", field.Path)
fprintf(out, "\t\t\treturn fmt.Errorf(\"error parsing %%#v for '%s': %%w\", ival, err)\n", field.Flag())
fprintf(out, "\t\t}\n")
}
fprintf(out, "\t}\n")
fprintf(out, "\n")
}
func generateGetSetters(out io.Writer, fields []ConfigField) {
for _, field := range fields {
// Get name from struct path, without periods.
name := strings.ReplaceAll(field.Path, ".", "")
// Get type without "config." prefix.
fieldType := strings.ReplaceAll(
@ -103,29 +460,67 @@ func generateFields(output io.Writer, prefixes []string, t reflect.Type) {
"config.", "",
)
fprintf(out, "// %sFlag returns the flag name for the '%s' field\n", name, field.Path)
fprintf(out, "func %sFlag() string { return \"%s\" }\n\n", name, field.Flag())
// ConfigState structure helper methods
fmt.Fprintf(output, "// Get%s safely fetches the Configuration value for state's '%s' field\n", name, fieldPath)
fmt.Fprintf(output, "func (st *ConfigState) Get%s() (v %s) {\n", name, fieldType)
fmt.Fprintf(output, "\tst.mutex.RLock()\n")
fmt.Fprintf(output, "\tv = st.config.%s\n", fieldPath)
fmt.Fprintf(output, "\tst.mutex.RUnlock()\n")
fmt.Fprintf(output, "\treturn\n")
fmt.Fprintf(output, "}\n\n")
fmt.Fprintf(output, "// Set%s safely sets the Configuration value for state's '%s' field\n", name, fieldPath)
fmt.Fprintf(output, "func (st *ConfigState) Set%s(v %s) {\n", name, fieldType)
fmt.Fprintf(output, "\tst.mutex.Lock()\n")
fmt.Fprintf(output, "\tdefer st.mutex.Unlock()\n")
fmt.Fprintf(output, "\tst.config.%s = v\n", fieldPath)
fmt.Fprintf(output, "\tst.reloadToViper()\n")
fmt.Fprintf(output, "}\n\n")
fprintf(out, "// Get%s safely fetches the Configuration value for state's '%s' field\n", name, field.Path)
fprintf(out, "func (st *ConfigState) Get%s() (v %s) {\n", name, fieldType)
fprintf(out, "\tst.mutex.RLock()\n")
fprintf(out, "\tv = st.config.%s\n", field.Path)
fprintf(out, "\tst.mutex.RUnlock()\n")
fprintf(out, "\treturn\n")
fprintf(out, "}\n\n")
fprintf(out, "// Set%s safely sets the Configuration value for state's '%s' field\n", name, field.Path)
fprintf(out, "func (st *ConfigState) Set%s(v %s) {\n", name, fieldType)
fprintf(out, "\tst.mutex.Lock()\n")
fprintf(out, "\tdefer st.mutex.Unlock()\n")
fprintf(out, "\tst.config.%s = v\n", field.Path)
fprintf(out, "\tst.reloadToViper()\n")
fprintf(out, "}\n\n")
// Global ConfigState helper methods
// TODO: remove when we pass around a ConfigState{}
fmt.Fprintf(output, "// %sFlag returns the flag name for the '%s' field\n", name, fieldPath)
fmt.Fprintf(output, "func %sFlag() string { return \"%s\" }\n\n", name, flagPath)
fmt.Fprintf(output, "// Get%s safely fetches the value for global configuration '%s' field\n", name, fieldPath)
fmt.Fprintf(output, "func Get%[1]s() %[2]s { return global.Get%[1]s() }\n\n", name, fieldType)
fmt.Fprintf(output, "// Set%s safely sets the value for global configuration '%s' field\n", name, fieldPath)
fmt.Fprintf(output, "func Set%[1]s(v %[2]s) { global.Set%[1]s(v) }\n\n", name, fieldType)
fprintf(out, "// Get%s safely fetches the value for global configuration '%s' field\n", name, field.Path)
fprintf(out, "func Get%[1]s() %[2]s { return global.Get%[1]s() }\n\n", name, fieldType)
fprintf(out, "// Set%s safely sets the value for global configuration '%s' field\n", name, field.Path)
fprintf(out, "func Set%[1]s(v %[2]s) { global.Set%[1]s(v) }\n\n", name, fieldType)
}
}
func generateMapFlattener(out io.Writer, fields []ConfigField) {
fprintf(out, "func flattenConfigMap(cfgmap map[string]any) {\n")
fprintf(out, "\tnestedKeys := make(map[string]struct{})\n")
for _, field := range fields {
keys := field.PossibleKeys()
if len(keys) <= 1 {
continue
}
fprintf(out, "\tfor _, key := range [][]string{\n")
for _, key := range keys[1:] {
fprintf(out, "\t\t{\"%s\"},\n", strings.Join(key, "\", \""))
}
fprintf(out, "\t} {\n")
fprintf(out, "\t\tival, ok := mapGet(cfgmap, key...)\n")
fprintf(out, "\t\tif ok {\n")
fprintf(out, "\t\t\tcfgmap[\"%s\"] = ival\n", field.Flag())
fprintf(out, "\t\t\tnestedKeys[key[0]] = struct{}{}\n")
fprintf(out, "\t\t\tbreak\n")
fprintf(out, "\t\t}\n")
fprintf(out, "\t}\n\n")
}
fprintf(out, "\tfor key := range nestedKeys {\n")
fprintf(out, "\t\tdelete(cfgmap, key)\n")
fprintf(out, "\t}\n")
fprintf(out, "}\n\n")
}
func fprintf(out io.Writer, format string, args ...any) {
_, err := fmt.Fprintf(out, format, args...)
must(err)
}
func must(err error) {
if err != nil {
panic(err)
}
}

View File

@ -32,29 +32,17 @@ func init() {
// package, and instead pass the ConfigState round in a global gts state.
// Config provides you safe access to the global configuration.
func Config(fn func(cfg *Configuration)) {
global.Config(fn)
}
func Config(fn func(cfg *Configuration)) { global.Config(fn) }
// Reload will reload the current configuration values from file.
func Reload() error {
return global.Reload()
}
// LoadEarlyFlags will bind specific flags from given Cobra command to global viper
// instance, and load the current configuration values. This is useful for flags like
// .ConfigPath which have to parsed first in order to perform early configuration load.
func LoadEarlyFlags(cmd *cobra.Command) error {
return global.LoadEarlyFlags(cmd)
}
// RegisterGlobalFlags ...
func RegisterGlobalFlags(root *cobra.Command) { global.RegisterGlobalFlags(root) }
// BindFlags binds given command's pflags to the global viper instance.
func BindFlags(cmd *cobra.Command) error {
return global.BindFlags(cmd)
}
func BindFlags(cmd *cobra.Command) error { return global.BindFlags(cmd) }
// LoadConfigFile loads the currently set configuration file into the global viper instance.
func LoadConfigFile() error { return global.LoadConfigFile() }
// Reset will totally clear global
// ConfigState{}, loading defaults.
func Reset() {
global.Reset()
}
func Reset() { global.Reset() }

File diff suppressed because it is too large Load Diff

View File

@ -18,10 +18,11 @@
package config
import (
"os"
"path"
"strings"
"sync"
"github.com/go-viper/mapstructure/v2"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
@ -46,34 +47,25 @@ func NewState() *ConfigState {
// and will reload the current Configuration back into viper settings.
func (st *ConfigState) Config(fn func(*Configuration)) {
st.mutex.Lock()
defer func() {
st.reloadToViper()
st.mutex.Unlock()
}()
defer st.mutex.Unlock()
fn(&st.config)
st.reloadToViper()
}
// Viper provides safe access to the ConfigState's contained viper instance,
// and will reload the current viper setting state back into Configuration.
func (st *ConfigState) Viper(fn func(*viper.Viper)) {
st.mutex.Lock()
defer func() {
st.reloadFromViper()
st.mutex.Unlock()
}()
defer st.mutex.Unlock()
fn(st.viper)
st.reloadFromViper()
}
// LoadEarlyFlags will bind specific flags from given Cobra command to ConfigState's viper
// instance, and load the current configuration values. This is useful for flags like
// .ConfigPath which have to parsed first in order to perform early configuration load.
func (st *ConfigState) LoadEarlyFlags(cmd *cobra.Command) (err error) {
name := ConfigPathFlag()
flag := cmd.Flags().Lookup(name)
st.Viper(func(v *viper.Viper) {
err = v.BindPFlag(name, flag)
})
return
// RegisterGlobalFlags ...
func (st *ConfigState) RegisterGlobalFlags(root *cobra.Command) {
st.mutex.RLock()
st.config.RegisterFlags(root.PersistentFlags())
st.mutex.RUnlock()
}
// BindFlags will bind given Cobra command's pflags to this ConfigState's viper instance.
@ -84,15 +76,21 @@ func (st *ConfigState) BindFlags(cmd *cobra.Command) (err error) {
return
}
// Reload will reload the Configuration values from ConfigState's viper instance, and from file if set.
func (st *ConfigState) Reload() (err error) {
// LoadConfigFile loads the currently set configuration file into this ConfigState's viper instance.
func (st *ConfigState) LoadConfigFile() (err error) {
st.Viper(func(v *viper.Viper) {
if st.config.ConfigPath != "" {
// Ensure configuration path is set
v.SetConfigFile(st.config.ConfigPath)
if path := st.config.ConfigPath; path != "" {
var cfgmap map[string]any
// Read in configuration from file
if err = v.ReadInConfig(); err != nil {
// Read config map into memory.
cfgmap, err := readConfigMap(path)
if err != nil {
return
}
// Merge the parsed config into viper.
err = st.viper.MergeConfigMap(cfgmap)
if err != nil {
return
}
}
@ -108,18 +106,17 @@ func (st *ConfigState) Reset() {
defer st.mutex.Unlock()
// Create new viper.
viper := viper.New()
st.viper = viper.New()
// Flag 'some-flag-name' becomes env var 'GTS_SOME_FLAG_NAME'
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
viper.SetEnvPrefix("gts")
st.viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
st.viper.SetEnvPrefix("gts")
// Load appropriate
// named vals from env.
viper.AutomaticEnv()
st.viper.AutomaticEnv()
// Reset variables.
st.viper = viper
// Set default config.
st.config = Defaults
// Load into viper.
@ -128,31 +125,45 @@ func (st *ConfigState) Reset() {
// reloadToViper will reload Configuration{} values into viper.
func (st *ConfigState) reloadToViper() {
raw, err := st.config.MarshalMap()
if err != nil {
panic(err)
}
if err := st.viper.MergeConfigMap(raw); err != nil {
if err := st.viper.MergeConfigMap(st.config.MarshalMap()); err != nil {
panic(err)
}
}
// reloadFromViper will reload Configuration{} values from viper.
func (st *ConfigState) reloadFromViper() {
if err := st.viper.Unmarshal(&st.config, func(c *mapstructure.DecoderConfig) {
c.TagName = "name"
// empty config before marshaling
c.ZeroFields = true
oldhook := c.DecodeHook
// Use the TextUnmarshaler interface when decoding.
c.DecodeHook = mapstructure.ComposeDecodeHookFunc(
mapstructure.TextUnmarshallerHookFunc(),
oldhook,
)
}); err != nil {
if err := st.config.UnmarshalMap(st.viper.AllSettings()); err != nil {
panic(err)
}
}
// readConfigMap reads given configuration file into memory,
// using viper's codec registry to handle decoding into a map,
// flattening the result for standardization, returning this.
// this ensures the stored config map in viper always has the
// same level of nesting, given we support varying levels.
func readConfigMap(file string) (map[string]any, error) {
ext := path.Ext(file)
ext = strings.TrimPrefix(ext, ".")
registry := viper.NewCodecRegistry()
dec, err := registry.Decoder(ext)
if err != nil {
return nil, err
}
data, err := os.ReadFile(file)
if err != nil {
return nil, err
}
cfgmap := make(map[string]any)
if err := dec.Decode(data, cfgmap); err != nil {
return nil, err
}
flattenConfigMap(cfgmap)
return cfgmap, nil
}

4
internal/config/testdata/test3.yaml vendored Normal file
View File

@ -0,0 +1,4 @@
advanced:
scraper-deterrence: true
rate-limit:
requests: 5000

View File

@ -18,9 +18,8 @@
package config
import (
"errors"
"net/netip"
"codeberg.org/gruf/go-byteutil"
)
// IPPrefixes is a type-alias for []netip.Prefix
@ -28,6 +27,9 @@ import (
type IPPrefixes []netip.Prefix
func (p *IPPrefixes) Set(in string) error {
if p == nil {
return errors.New("nil receiver")
}
prefix, err := netip.ParsePrefix(in)
if err != nil {
return err
@ -36,20 +38,6 @@ func (p *IPPrefixes) Set(in string) error {
return nil
}
func (p *IPPrefixes) String() string {
if p == nil || len(*p) == 0 {
return ""
}
var buf byteutil.Buffer
for _, prefix := range *p {
str := prefix.String()
buf.B = append(buf.B, str...)
buf.B = append(buf.B, ',')
}
buf.Truncate(1)
return buf.String()
}
func (p *IPPrefixes) Strings() []string {
if p == nil || len(*p) == 0 {
return nil

74
internal/config/util.go Normal file
View File

@ -0,0 +1,74 @@
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package config
import (
"fmt"
"codeberg.org/gruf/go-split"
"github.com/spf13/cast"
)
func toStringSlice(a any) ([]string, error) {
switch a := a.(type) {
case []string:
return a, nil
case string:
return split.SplitStrings[string](a)
case []any:
ss := make([]string, len(a))
for i, a := range a {
var err error
ss[i], err = cast.ToStringE(a)
if err != nil {
return nil, err
}
}
return ss, nil
default:
return nil, fmt.Errorf("cannot cast %T to []string", a)
}
}
func mapGet(m map[string]any, keys ...string) (any, bool) {
for len(keys) > 0 {
key := keys[0]
keys = keys[1:]
// Check for key.
v, ok := m[key]
if !ok {
return nil, false
}
if len(keys) == 0 {
// Has to be value.
return v, true
}
// Else, it needs to have
// nesting to keep searching.
switch t := v.(type) {
case map[string]any:
m = t
default:
return nil, false
}
}
return nil, false
}

View File

@ -18,6 +18,8 @@
package language
import (
"errors"
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
"golang.org/x/text/language"
"golang.org/x/text/language/display"
@ -95,13 +97,35 @@ func (l *Language) UnmarshalText(text []byte) error {
if err != nil {
return err
}
*l = *lang
return nil
}
type Languages []*Language
func (l *Languages) Set(in string) error {
if l == nil {
return errors.New("nil receiver")
}
prefix, err := Parse(in)
if err != nil {
return err
}
(*l) = append((*l), prefix)
return nil
}
func (l *Languages) Strings() []string {
if l == nil || len(*l) == 0 {
return nil
}
strs := make([]string, len(*l))
for i, lang := range *l {
strs[i] = lang.TagStr
}
return strs
}
func (l Languages) Tags() []language.Tag {
tags := make([]language.Tag, len(l))
for i, lang := range l {

View File

@ -85,7 +85,11 @@ func (p *Processor) Edit(
}
// Process incoming content type.
contentType := processContentType(form.ContentType, status, requester.Settings.StatusContentType)
contentType := processContentType(
form.ContentType,
status,
requester.Settings.StatusContentType,
)
// Process incoming status edit content fields.
content, errWithCode := p.processContent(ctx,

View File

@ -25,65 +25,63 @@ EXPECT=$(cat << "EOF"
"advanced-throttling-retry-after": 10000000000,
"application-name": "gts",
"bind-address": "127.0.0.1",
"cache": {
"account-mem-ratio": 5,
"account-note-mem-ratio": 1,
"account-settings-mem-ratio": 0.1,
"account-stats-mem-ratio": 2,
"application-mem-ratio": 0.1,
"block-ids-mem-ratio": 3,
"block-mem-ratio": 2,
"boost-of-ids-mem-ratio": 3,
"client-mem-ratio": 0.1,
"conversation-last-status-ids-mem-ratio": 2,
"conversation-mem-ratio": 1,
"domain-permission-draft-mem-ratio": 0.5,
"domain-permission-subscription-mem-ratio": 0.5,
"emoji-category-mem-ratio": 0.1,
"emoji-mem-ratio": 3,
"filter-keyword-mem-ratio": 0.5,
"filter-mem-ratio": 0.5,
"filter-status-mem-ratio": 0.5,
"follow-ids-mem-ratio": 4,
"follow-mem-ratio": 2,
"follow-request-ids-mem-ratio": 2,
"follow-request-mem-ratio": 2,
"following-tag-ids-mem-ratio": 2,
"in-reply-to-ids-mem-ratio": 3,
"instance-mem-ratio": 1,
"interaction-request-mem-ratio": 1,
"list-ids-mem-ratio": 2,
"list-mem-ratio": 1,
"listed-ids-mem-ratio": 2,
"marker-mem-ratio": 0.5,
"media-mem-ratio": 4,
"memory-target": 104857600,
"mention-mem-ratio": 2,
"move-mem-ratio": 0.1,
"notification-mem-ratio": 2,
"poll-mem-ratio": 1,
"poll-vote-ids-mem-ratio": 2,
"poll-vote-mem-ratio": 2,
"report-mem-ratio": 1,
"sin-bin-status-mem-ratio": 0.5,
"status-bookmark-ids-mem-ratio": 2,
"status-bookmark-mem-ratio": 0.5,
"status-edit-mem-ratio": 2,
"status-fave-ids-mem-ratio": 3,
"status-fave-mem-ratio": 2,
"status-mem-ratio": 5,
"tag-mem-ratio": 2,
"thread-mute-mem-ratio": 0.2,
"token-mem-ratio": 0.75,
"tombstone-mem-ratio": 0.5,
"user-mem-ratio": 0.25,
"user-mute-ids-mem-ratio": 3,
"user-mute-mem-ratio": 2,
"visibility-mem-ratio": 2,
"web-push-subscription-ids-mem-ratio": 1,
"web-push-subscription-mem-ratio": 1,
"webfinger-mem-ratio": 0.1
},
"cache-account-mem-ratio": 5,
"cache-account-note-mem-ratio": 1,
"cache-account-settings-mem-ratio": 0.1,
"cache-account-stats-mem-ratio": 2,
"cache-application-mem-ratio": 0.1,
"cache-block-ids-mem-ratio": 3,
"cache-block-mem-ratio": 2,
"cache-boost-of-ids-mem-ratio": 3,
"cache-client-mem-ratio": 0.1,
"cache-conversation-last-status-ids-mem-ratio": 2,
"cache-conversation-mem-ratio": 1,
"cache-domain-permission-draft-mem-ratio": 0.5,
"cache-domain-permission-subscription-mem-ratio": 0.5,
"cache-emoji-category-mem-ratio": 0.1,
"cache-emoji-mem-ratio": 3,
"cache-filter-keyword-mem-ratio": 0.5,
"cache-filter-mem-ratio": 0.5,
"cache-filter-status-mem-ratio": 0.5,
"cache-follow-ids-mem-ratio": 4,
"cache-follow-mem-ratio": 2,
"cache-follow-request-ids-mem-ratio": 2,
"cache-follow-request-mem-ratio": 2,
"cache-following-tag-ids-mem-ratio": 2,
"cache-in-reply-to-ids-mem-ratio": 3,
"cache-instance-mem-ratio": 1,
"cache-interaction-request-mem-ratio": 1,
"cache-list-ids-mem-ratio": 2,
"cache-list-mem-ratio": 1,
"cache-listed-ids-mem-ratio": 2,
"cache-marker-mem-ratio": 0.5,
"cache-media-mem-ratio": 4,
"cache-memory-target": "100MiB",
"cache-mention-mem-ratio": 2,
"cache-move-mem-ratio": 0.1,
"cache-notification-mem-ratio": 2,
"cache-poll-mem-ratio": 1,
"cache-poll-vote-ids-mem-ratio": 2,
"cache-poll-vote-mem-ratio": 2,
"cache-report-mem-ratio": 1,
"cache-sin-bin-status-mem-ratio": 0.5,
"cache-status-bookmark-ids-mem-ratio": 2,
"cache-status-bookmark-mem-ratio": 0.5,
"cache-status-edit-mem-ratio": 2,
"cache-status-fave-ids-mem-ratio": 3,
"cache-status-fave-mem-ratio": 2,
"cache-status-mem-ratio": 5,
"cache-tag-mem-ratio": 2,
"cache-thread-mute-mem-ratio": 0.2,
"cache-token-mem-ratio": 0.75,
"cache-tombstone-mem-ratio": 0.5,
"cache-user-mem-ratio": 0.25,
"cache-user-mute-ids-mem-ratio": 3,
"cache-user-mute-mem-ratio": 2,
"cache-visibility-mem-ratio": 2,
"cache-web-push-subscription-ids-mem-ratio": 1,
"cache-web-push-subscription-mem-ratio": 1,
"cache-webfinger-mem-ratio": 0.1,
"config-path": "internal/config/testdata/test.yaml",
"db-address": ":memory:",
"db-database": "gotosocial_prod",
@ -92,7 +90,7 @@ EXPECT=$(cat << "EOF"
"db-port": 6969,
"db-postgres-connection-string": "",
"db-sqlite-busy-timeout": 1000000000,
"db-sqlite-cache-size": 0,
"db-sqlite-cache-size": "0B",
"db-sqlite-journal-mode": "DELETE",
"db-sqlite-synchronous": "FULL",
"db-tls-ca-cert": "",
@ -102,12 +100,10 @@ EXPECT=$(cat << "EOF"
"dry-run": true,
"email": "",
"host": "example.com",
"http-client": {
"allow-ips": [],
"block-ips": [],
"timeout": 30000000000,
"tls-insecure-skip-verify": false
},
"http-client-allow-ips": [],
"http-client-block-ips": [],
"http-client-timeout": 30000000000,
"http-client-tls-insecure-skip-verify": false,
"instance-allow-backdating-statuses": true,
"instance-deliver-to-shared-inboxes": false,
"instance-expose-peers": true,
@ -138,14 +134,14 @@ EXPECT=$(cat << "EOF"
"media-cleanup-from": "00:00",
"media-description-max-chars": 5000,
"media-description-min-chars": 69,
"media-emoji-local-max-size": 420,
"media-emoji-remote-max-size": 420,
"media-emoji-local-max-size": "420B",
"media-emoji-remote-max-size": "420B",
"media-ffmpeg-pool-size": 8,
"media-image-size-hint": 5242880,
"media-local-max-size": 420,
"media-image-size-hint": "5.00MiB",
"media-local-max-size": "420B",
"media-remote-cache-days": 30,
"media-remote-max-size": 420,
"media-video-size-hint": 41943040,
"media-remote-max-size": "420B",
"media-video-size-hint": "40.0MiB",
"metrics-enabled": false,
"oidc-admin-groups": [
"steamy"

View File

@ -161,11 +161,19 @@ func testDefaults() config.Configuration {
SyslogProtocol: "udp",
SyslogAddress: "localhost:514",
AdvancedCookiesSamesite: "lax",
AdvancedRateLimitRequests: 0, // disabled
AdvancedThrottlingMultiplier: 0, // disabled
AdvancedSenderMultiplier: 0, // 1 sender only, regardless of CPU
AdvancedScraperDeterrence: envBool("GTS_ADVANCED_SCRAPER_DETERRENCE", false),
Advanced: config.AdvancedConfig{
CookiesSamesite: "lax",
SenderMultiplier: 0, // 1 sender only, regardless of CPU
ScraperDeterrence: envBool("GTS_ADVANCED_SCRAPER_DETERRENCE", false),
RateLimit: config.RateLimitConfig{
Requests: 0, // disabled
},
Throttling: config.ThrottlingConfig{
Multiplier: 0, // disabled
},
},
SoftwareVersion: "0.0.0-testrig",

9
vendor/codeberg.org/gruf/go-split/LICENSE generated vendored Normal file
View File

@ -0,0 +1,9 @@
MIT License
Copyright (c) gruf
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

3
vendor/codeberg.org/gruf/go-split/README.md generated vendored Normal file
View File

@ -0,0 +1,3 @@
# go-split
Performant string splitting and joining by comma, taking quotes into account. Useful for user supplied input e.g. CLI args

112
vendor/codeberg.org/gruf/go-split/join.go generated vendored Normal file
View File

@ -0,0 +1,112 @@
package split
import (
"strconv"
"time"
"unsafe"
"codeberg.org/gruf/go-bytesize"
)
// joinFunc will join given slice of elements, using the passed function to append each element at index
// from the slice, forming a combined comma-space separated string. Passed size is for buffer preallocation.
func JoinFunc[T any](slice []T, each func(buf []byte, value T) []byte, size int) string {
// Move nil check
// outside main loop.
if each == nil {
panic("nil func")
}
// Catch easiest case
if len(slice) == 0 {
return ""
}
// Preallocate string buffer (size + commas)
buf := make([]byte, 0, size+len(slice)-1)
for _, value := range slice {
// Append each item
buf = each(buf, value)
buf = append(buf, ',', ' ')
}
// Drop final comma-space
buf = buf[:len(buf)-2]
// Directly cast buf to string
data := unsafe.SliceData(buf)
return unsafe.String(data, len(buf))
}
// JoinStrings will pass string slice to JoinFunc(), quoting where
// necessary and combining into a single comma-space separated string.
func JoinStrings[String ~string](slice []String) string {
var size int
for _, str := range slice {
size += len(str)
}
return JoinFunc(slice, func(buf []byte, value String) []byte {
return appendQuote(buf, string(value))
}, size)
}
// JoinBools will pass bool slice to JoinFunc(), formatting
// and combining into a single comma-space separated string.
func JoinBools[Bool ~bool](slice []Bool) string {
return JoinFunc(slice, func(buf []byte, value Bool) []byte {
return strconv.AppendBool(buf, bool(value))
}, len(slice)*5 /* len("false") */)
}
// JoinInts will pass signed integer slice to JoinFunc(), formatting
// and combining into a single comma-space separated string.
func JoinInts[Int Signed](slice []Int) string {
return JoinFunc(slice, func(buf []byte, value Int) []byte {
return strconv.AppendInt(buf, int64(value), 10)
}, len(slice)*20) // max signed int str len
}
// JoinUints will pass unsigned integer slice to JoinFunc(),
// formatting and combining into a single comma-space separated string.
func JoinUints[Uint Unsigned](slice []Uint) string {
return JoinFunc(slice, func(buf []byte, value Uint) []byte {
return strconv.AppendUint(buf, uint64(value), 10)
}, len(slice)*20) // max unsigned int str len
}
// JoinFloats will pass float slice to JoinFunc(), formatting
// and combining into a single comma-space separated string.
func JoinFloats[Float_ Float](slice []Float_) string {
bits := int(unsafe.Sizeof(Float_(0)) * 8) // param type bits
return JoinFunc(slice, func(buf []byte, value Float_) []byte {
return strconv.AppendFloat(buf, float64(value), 'g', -1, bits)
}, len(slice)*20) // max signed int str len (it's a good guesstimate)
}
// JoinSizes will pass byte size slice to JoinFunc(), formatting
// and combining into a single comma-space separated string.
func JoinSizes(slice []bytesize.Size) string {
const iecLen = 7 // max IEC string length
return JoinFunc(slice, func(buf []byte, value bytesize.Size) []byte {
return value.AppendFormatIEC(buf)
}, len(slice)*iecLen)
}
// JoinDurations will pass duration slice to JoinFunc(), formatting
// and combining into a single comma-space separated string.
func JoinDurations(slice []time.Duration) string {
const durLen = 10 // max duration string length
return JoinFunc(slice, func(buf []byte, value time.Duration) []byte {
return append(buf, value.String()...)
}, len(slice)*durLen)
}
// JoinTimes will pass time slice to JoinFunc(), formatting
// and combining into a single comma-space separated string.
func JoinTimes(slice []time.Time, format string) string {
return JoinFunc(slice, func(buf []byte, value time.Time) []byte {
return value.AppendFormat(buf, format)
}, len(slice)*len(format))
}

71
vendor/codeberg.org/gruf/go-split/join_util.go generated vendored Normal file
View File

@ -0,0 +1,71 @@
package split
import (
"strconv"
"strings"
)
// singleTermLine: beyond a certain length of string, all of the
// extra checks to handle quoting/not-quoting add a significant
// amount of extra processing time. Quoting in this manner only really
// effects readability on a single line, so a max string length that
// encompasses the maximum number of columns on *most* terminals was
// selected. This was chosen using the metric that 1080p is one of the
// most common display resolutions, and that a relatively small font size
// of 7 requires ~ 223 columns. So 256 should be >= $COLUMNS (fullscreen)
// in 99% of usecases (these figures all pulled out of my ass).
const singleTermLine = 256
// appendQuote will append 'str' to 'buf', double quoting and escaping if needed.
func appendQuote(buf []byte, str string) []byte {
switch {
case len(str) > singleTermLine || !strconv.CanBackquote(str):
// Append quoted and escaped string
return strconv.AppendQuote(buf, str)
case (strings.IndexByte(str, '"') != -1):
// Double quote and escape string
buf = append(buf, '"')
buf = appendEscape(buf, str)
buf = append(buf, '"')
return buf
case (strings.IndexByte(str, ',') != -1):
// Double quote this string as-is
buf = append(buf, '"')
buf = append(buf, str...)
buf = append(buf, '"')
return buf
default:
// Append string as-is
return append(buf, str...)
}
}
// appendEscape will append 'str' to 'buf' and escape any double quotes.
func appendEscape(buf []byte, str string) []byte {
var delim bool
for i := range str {
switch {
case str[i] == '\\' && !delim:
// Set delim flag
delim = true
case str[i] == '"' && !delim:
// Append escaped double quote
buf = append(buf, `\"`...)
case delim:
// Append skipped slash
buf = append(buf, `\`...)
delim = false
fallthrough
default:
// Append char as-is
buf = append(buf, str[i])
}
}
return buf
}

187
vendor/codeberg.org/gruf/go-split/split.go generated vendored Normal file
View File

@ -0,0 +1,187 @@
package split
import (
"strconv"
"time"
"unsafe"
"codeberg.org/gruf/go-bytesize"
)
// Signed defines a signed
// integer generic type parameter.
type Signed interface {
~int | ~int8 | ~int16 | ~int32 | ~int64
}
// Unsigned defines an unsigned
// integer generic type paramter.
type Unsigned interface {
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
}
// Float defines a float-type generic parameter.
type Float interface{ ~float32 | ~float64 }
// SplitFunc will split input string on commas, taking into account string quoting
// and stripping extra whitespace, passing each split to the given function hook.
func SplitFunc(str string, fn func(string) error) error {
return (&Splitter{}).SplitFunc(str, fn)
}
// SplitStrings will pass string input to SplitFunc(), compiling a slice of strings.
func SplitStrings[String ~string](str string) ([]String, error) {
var slice []String
// Simply append each split string to slice
if err := SplitFunc(str, func(s string) error {
slice = append(slice, String(s))
return nil
}); err != nil {
return nil, err
}
return slice, nil
}
// SplitBools will pass string input to SplitFunc(), parsing and compiling a slice of bools.
func SplitBools[Bool ~bool](str string) ([]Bool, error) {
var slice []Bool
// Parse each bool split from input string
if err := SplitFunc(str, func(s string) error {
b, err := strconv.ParseBool(s)
if err != nil {
return err
}
slice = append(slice, Bool(b))
return nil
}); err != nil {
return nil, err
}
return slice, nil
}
// SplitInts will pass string input to SplitFunc(), parsing and compiling a slice of signed integers.
func SplitInts[Int Signed](str string) ([]Int, error) {
// Determine bits from param type size
bits := int(unsafe.Sizeof(Int(0)) * 8)
var slice []Int
// Parse each int split from input string
if err := SplitFunc(str, func(s string) error {
i, err := strconv.ParseInt(s, 10, bits)
if err != nil {
return err
}
slice = append(slice, Int(i))
return nil
}); err != nil {
return nil, err
}
return slice, nil
}
// SplitUints will pass string input to SplitFunc(), parsing and compiling a slice of unsigned integers.
func SplitUints[Uint Unsigned](str string) ([]Uint, error) {
// Determine bits from param type size
bits := int(unsafe.Sizeof(Uint(0)) * 8)
var slice []Uint
// Parse each uint split from input string
if err := SplitFunc(str, func(s string) error {
u, err := strconv.ParseUint(s, 10, bits)
if err != nil {
return err
}
slice = append(slice, Uint(u))
return nil
}); err != nil {
return nil, err
}
return slice, nil
}
// SplitFloats will pass string input to SplitFunc(), parsing and compiling a slice of floats.
func SplitFloats[Float_ Float](str string) ([]Float_, error) {
// Determine bits from param type size
bits := int(unsafe.Sizeof(Float_(0)) * 8)
var slice []Float_
// Parse each float split from input string
if err := SplitFunc(str, func(s string) error {
f, err := strconv.ParseFloat(s, bits)
if err != nil {
return err
}
slice = append(slice, Float_(f))
return nil
}); err != nil {
return nil, err
}
return slice, nil
}
// SplitSizes will pass string input to SplitFunc(), parsing and compiling a slice of byte sizes.
func SplitSizes(str string) ([]bytesize.Size, error) {
var slice []bytesize.Size
// Parse each size split from input string
if err := SplitFunc(str, func(s string) error {
sz, err := bytesize.ParseSize(s)
if err != nil {
return err
}
slice = append(slice, sz)
return nil
}); err != nil {
return nil, err
}
return slice, nil
}
// SplitDurations will pass string input to SplitFunc(), parsing and compiling a slice of durations.
func SplitDurations(str string) ([]time.Duration, error) {
var slice []time.Duration
// Parse each duration split from input string
if err := SplitFunc(str, func(s string) error {
d, err := time.ParseDuration(s)
if err != nil {
return err
}
slice = append(slice, d)
return nil
}); err != nil {
return nil, err
}
return slice, nil
}
// SplitTimes will pass string input to SplitFunc(), parsing and compiling a slice of times.
func SplitTimes(str string, format string) ([]time.Time, error) {
var slice []time.Time
// Parse each time split from input string
if err := SplitFunc(str, func(s string) error {
t, err := time.Parse(s, format)
if err != nil {
return err
}
slice = append(slice, t)
return nil
}); err != nil {
return nil, err
}
return slice, nil
}

204
vendor/codeberg.org/gruf/go-split/splitter.go generated vendored Normal file
View File

@ -0,0 +1,204 @@
package split
import (
"errors"
"strings"
"unicode"
"unicode/utf8"
)
// Splitter holds onto a byte buffer for use in minimising allocations during SplitFunc().
type Splitter struct{ B []byte }
// SplitFunc will split input string on commas, taking into account string quoting and
// stripping extra whitespace, passing each split to the given function hook.
func (s *Splitter) SplitFunc(str string, fn func(string) error) error {
for {
// Reset buffer
s.B = s.B[0:0]
// Trim leading space
str = trimLeadingSpace(str)
if len(str) < 1 {
// Reached end
return nil
}
switch {
// Single / double quoted
case str[0] == '\'', str[0] == '"':
// Calculate next string elem
i := 1 + s.next(str[1:], str[0])
if i == 0 /* i.e. if .next() returned -1 */ {
return errors.New("missing end quote")
}
// Pass next element to callback func
if err := fn(string(s.B)); err != nil {
return err
}
// Reslice + trim leading space
str = trimLeadingSpace(str[i+1:])
if len(str) < 1 {
// reached end
return nil
}
if str[0] != ',' {
// malformed element without comma after quote
return errors.New("missing comma separator")
}
// Skip comma
str = str[1:]
// Empty segment
case str[0] == ',':
str = str[1:]
// No quoting
default:
// Calculate next string elem
i := s.next(str, ',')
switch i {
// Reached end
case -1:
// we know len > 0
// Pass to callback
return fn(string(s.B))
// Empty elem
case 0:
str = str[1:]
// Non-zero elem
default:
// Pass next element to callback
if err := fn(string(s.B)); err != nil {
return err
}
// Skip past eleme
str = str[i+1:]
}
}
}
}
// next will build the next string element in s.B up to non-delimited instance of c,
// returning number of characters iterated, or -1 if the end of the string was reached.
func (s *Splitter) next(str string, c byte) int {
var delims int
// Guarantee buf large enough
if len(str) > cap(s.B)-len(s.B) {
nb := make([]byte, 2*cap(s.B)+len(str))
_ = copy(nb, s.B)
s.B = nb[:len(s.B)]
}
for i := 0; i < len(str); i++ {
// Increment delims
if str[i] == '\\' {
delims++
continue
}
if str[i] == c {
var count int
if count = delims / 2; count > 0 {
// Add backslashes to buffer
slashes := backslashes(count)
s.B = append(s.B, slashes...)
}
// Reached delim'd char
if delims-count == 0 {
return i
}
} else if delims > 0 {
// Add backslashes to buffer
slashes := backslashes(delims)
s.B = append(s.B, slashes...)
}
// Write byte to buffer
s.B = append(s.B, str[i])
// Reset count
delims = 0
}
return -1
}
// asciiSpace is a lookup table of ascii space chars (see: strings.asciiSet).
var asciiSpace = func() (as [8]uint32) {
as['\t'/32] |= 1 << ('\t' % 32)
as['\n'/32] |= 1 << ('\n' % 32)
as['\v'/32] |= 1 << ('\v' % 32)
as['\f'/32] |= 1 << ('\f' % 32)
as['\r'/32] |= 1 << ('\r' % 32)
as[' '/32] |= 1 << (' ' % 32)
return
}()
// trimLeadingSpace trims the leading space from a string.
func trimLeadingSpace(str string) string {
var start int
for ; start < len(str); start++ {
// If beyond ascii range, trim using slower rune check.
if str[start] >= utf8.RuneSelf {
return trimLeadingSpaceSlow(str[start:])
}
// Ascii character
char := str[start]
// This is first non-space ASCII, trim up to here
if (asciiSpace[char/32] & (1 << (char % 32))) == 0 {
break
}
}
return str[start:]
}
// trimLeadingSpaceSlow trims leading space using the slower unicode.IsSpace check.
func trimLeadingSpaceSlow(str string) string {
for i, r := range str {
if !unicode.IsSpace(r) {
return str[i:]
}
}
return str
}
// backslashes will return a string of backslashes of given length.
func backslashes(count int) string {
const backslashes = `\\\\\\\\\\\\\\\\\\\\`
// Fast-path, use string const
if count < len(backslashes) {
return backslashes[:count]
}
// Slow-path, build custom string
return backslashSlow(count)
}
// backslashSlow will build a string of backslashes of custom length.
func backslashSlow(count int) string {
var buf strings.Builder
for i := 0; i < count; i++ {
buf.WriteByte('\\')
}
return buf.String()
}

3
vendor/modules.txt vendored
View File

@ -270,6 +270,9 @@ codeberg.org/gruf/go-runners
# codeberg.org/gruf/go-sched v1.2.4
## explicit; go 1.19
codeberg.org/gruf/go-sched
# codeberg.org/gruf/go-split v1.2.0
## explicit; go 1.20
codeberg.org/gruf/go-split
# codeberg.org/gruf/go-storage v0.2.0
## explicit; go 1.22
codeberg.org/gruf/go-storage