Skip to content

fix: support multiple values for StringArray flags via env vars#679

Open
emilic wants to merge 1 commit intomainfrom
fix/string-array-env-var-678
Open

fix: support multiple values for StringArray flags via env vars#679
emilic wants to merge 1 commit intomainfrom
fix/string-array-env-var-678

Conversation

@emilic
Copy link
Copy Markdown

@emilic emilic commented Apr 17, 2026

Env vars for StringArray flags (e.g. FGA_CUSTOM_HEADERS, FGA_API_SCOPES) now support multiple values using JSON array syntax, e.g. FGA_CUSTOM_HEADERS='["X-Foo: bar","X-Baz: qux"]'. Also handles YAML list values from config files by iterating slice values individually.

Closes #678

Description

What problem is being solved?

How is it being solved?

What changes are made to solve it?

References

Review Checklist

  • I have clicked on "allow edits by maintainers".
  • I have added documentation for new/changed functionality in this PR or in a PR to openfga.dev [Provide a link to any relevant PRs in the references section above]
  • The correct base branch is being used, if not main
  • I have added tests to validate that the change in functionality is working as expected

Summary by CodeRabbit

  • New Features

    • Improved configuration value handling with enhanced support for array and multi-element configurations. Configuration strings representing arrays are now automatically parsed and properly converted to command-line flag values, providing more flexible configuration management.
  • Tests

    • Added comprehensive test suite validating configuration value parsing and conversion behavior across various data types and format scenarios.

Env vars for StringArray flags (e.g. FGA_CUSTOM_HEADERS, FGA_API_SCOPES)
now support multiple values using JSON array syntax, e.g.
FGA_CUSTOM_HEADERS='["X-Foo: bar","X-Baz: qux"]'. Also handles YAML list
values from config files by iterating slice values individually.

Closes #678
@emilic emilic requested a review from a team as a code owner April 17, 2026 13:51
Copilot AI review requested due to automatic review settings April 17, 2026 13:51
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 17, 2026

Walkthrough

Updated Viper configuration binding to handle multiple values from environment variables and config files. A new helper function viperValueToStrings converts Viper values (strings, slices, JSON arrays) into string slices, enabling proper multi-value support for StringArray flags when sourced from environment variables or config files.

Changes

Cohort / File(s) Summary
Viper-to-Flags Conversion
internal/cmdutils/bind-viper-to-flags.go
Refactored flag value setting to use new viperValueToStrings helper. Converts Viper values into string slices: expands slice/array kinds into individual elements, attempts JSON array parsing for strings wrapped with [ and ], and formats scalars as single-element slices. Added imports for encoding/json, reflect, and strings.
Test Coverage
internal/cmdutils/bind-viper-to-flags_test.go
New comprehensive test suite for viperValueToStrings with parallel subtests covering scalar strings, typed/untyped slices, JSON array parsing, malformed strings, and boolean/integer conversions.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: adding support for multiple values in StringArray flags via environment variables, which aligns with the PR objectives and linked issue #678.
Linked Issues check ✅ Passed The code changes implement the core requirement from issue #678: parsing environment variables as JSON arrays for StringArray flags, handling slice/array types from YAML configs, and converting values appropriately.
Out of Scope Changes check ✅ Passed All changes are directly related to the linked issue #678. The modifications to BindViperToFlags and new tests for viperValueToStrings conversion logic support the stated objective without introducing unrelated changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/string-array-env-var-678

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment on lines +54 to +75
func viperValueToStrings(value any) []string {
reflectValue := reflect.ValueOf(value)

if reflectValue.Kind() == reflect.Slice || reflectValue.Kind() == reflect.Array {
result := make([]string, 0, reflectValue.Len())
for i := range reflectValue.Len() {
result = append(result, fmt.Sprintf("%v", reflectValue.Index(i).Interface()))
}

return result
}

str := fmt.Sprintf("%v", value)
if strings.HasPrefix(str, "[") && strings.HasSuffix(str, "]") {
var parsed []string
if err := json.Unmarshal([]byte(str), &parsed); err == nil {
return parsed
}
}

return []string{str}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of using this function with viperInstance.Get(configName), why wouldn't we just use viperInstance.GetStringSlice(configName) and call it a day?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my comment here as well: #670 (comment)

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
internal/cmdutils/bind-viper-to-flags_test.go (1)

28-88: Consider a couple of additional edge-case cases.

Good coverage overall. A few worth adding:

  • Empty JSON-array string "[]" → expect []string{} (documents intended behavior and guards against regressions).
  • nil input (defensive — reflect.ValueOf(nil).Kind() is Invalid, falls through to fmt.Sprintf("%v", nil)"<nil>"; confirming/asserting this behavior prevents surprise).
  • A JSON array containing non-string elements (e.g. "[1,2]") to document that it falls back to the scalar path.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/cmdutils/bind-viper-to-flags_test.go` around lines 28 - 88, Add
three edge-case entries to the testcases slice in bind-viper-to-flags_test.go:
one where value is the JSON empty-array string "[]" with expected empty
[]string{}, one where value is nil with expected []string{"<nil>"} (to lock in
current fmt.Sprintf behavior), and one where value is a JSON array of
non-strings like "[1,2]" with expected []string{"[1,2]"} (confirming it falls
back to scalar). Locate and append these cases in the existing testcases
variable so they run with the other table-driven tests.
internal/cmdutils/bind-viper-to-flags.go (1)

66-72: Nit: consider trimming whitespace before the bracket check.

A user-provided env var value with surrounding whitespace (e.g. ' ["a","b"] ') would bypass the JSON-array path. Optional hardening:

♻️ Proposed refinement
-	str := fmt.Sprintf("%v", value)
-	if strings.HasPrefix(str, "[") && strings.HasSuffix(str, "]") {
+	str := fmt.Sprintf("%v", value)
+	trimmed := strings.TrimSpace(str)
+	if strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]") {
 		var parsed []string
-		if err := json.Unmarshal([]byte(str), &parsed); err == nil {
+		if err := json.Unmarshal([]byte(trimmed), &parsed); err == nil {
 			return parsed
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/cmdutils/bind-viper-to-flags.go` around lines 66 - 72, The code
currently converts the value to str and checks strings.HasPrefix/HasSuffix for
"["/"]" without trimming, so inputs with surrounding whitespace (e.g. '
["a","b"] ') are missed; update the logic in the block that creates str (the
local variable named str) to first run strings.TrimSpace(str) (and use the
trimmed value for the bracket checks and the json.Unmarshal call that populates
parsed) so the HasPrefix/HasSuffix checks and the JSON unmarshalling handle
padded input correctly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/cmdutils/bind-viper-to-flags.go`:
- Around line 54-75: viperValueToStrings currently treats any value that looks
like a JSON array as a string slice, which causes scalar flags to be split when
supplied as env vars; restrict JSON-array parsing to only flag types that are
actual slices by checking the pflag Value implementation or name before
splitting: in the code paths that call viperValueToStrings (and inside
viperValueToStrings itself), detect whether the target flag's pflag.Value
implements pflag.SliceValue or has a type/name like "stringArray"/"stringSlice"
and only then attempt the json.Unmarshal branch; for all other flag types (plain
String, Bool, Int, etc.) return the scalar string unmodified so Set is called
once as before (reference viperValueToStrings, pflag.SliceValue, and
pflag.Value/Set).

---

Nitpick comments:
In `@internal/cmdutils/bind-viper-to-flags_test.go`:
- Around line 28-88: Add three edge-case entries to the testcases slice in
bind-viper-to-flags_test.go: one where value is the JSON empty-array string "[]"
with expected empty []string{}, one where value is nil with expected
[]string{"<nil>"} (to lock in current fmt.Sprintf behavior), and one where value
is a JSON array of non-strings like "[1,2]" with expected []string{"[1,2]"}
(confirming it falls back to scalar). Locate and append these cases in the
existing testcases variable so they run with the other table-driven tests.

In `@internal/cmdutils/bind-viper-to-flags.go`:
- Around line 66-72: The code currently converts the value to str and checks
strings.HasPrefix/HasSuffix for "["/"]" without trimming, so inputs with
surrounding whitespace (e.g. ' ["a","b"] ') are missed; update the logic in the
block that creates str (the local variable named str) to first run
strings.TrimSpace(str) (and use the trimmed value for the bracket checks and the
json.Unmarshal call that populates parsed) so the HasPrefix/HasSuffix checks and
the JSON unmarshalling handle padded input correctly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 38fdc7c0-5c3c-4675-b847-b9c5ef7bdc71

📥 Commits

Reviewing files that changed from the base of the PR and between 892a37d and 5784aea.

📒 Files selected for processing (2)
  • internal/cmdutils/bind-viper-to-flags.go
  • internal/cmdutils/bind-viper-to-flags_test.go

Comment on lines +54 to +75
func viperValueToStrings(value any) []string {
reflectValue := reflect.ValueOf(value)

if reflectValue.Kind() == reflect.Slice || reflectValue.Kind() == reflect.Array {
result := make([]string, 0, reflectValue.Len())
for i := range reflectValue.Len() {
result = append(result, fmt.Sprintf("%v", reflectValue.Index(i).Interface()))
}

return result
}

str := fmt.Sprintf("%v", value)
if strings.HasPrefix(str, "[") && strings.HasSuffix(str, "]") {
var parsed []string
if err := json.Unmarshal([]byte(str), &parsed); err == nil {
return parsed
}
}

return []string{str}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

JSON-array parsing is applied to every flag type, not just StringArray.

viperValueToStrings is invoked for all flags (String, Bool, Int, StringArray, etc.). If a scalar String flag is supplied via env var with a value that happens to start with [ and end with ] and also parses as a JSON string array (e.g. ["foo"]), it will now be split and Set called once per element. For non-array flags, the last value wins, which silently changes the effective value compared to prior behavior.

Impact is likely low given typical inputs, but consider gating the JSON-array path on the flag type (e.g. only when the pflag Value implements SliceValue or has type stringArray/stringSlice) to avoid surprising scalar-flag consumers. At minimum, it may be worth calling this out in docs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/cmdutils/bind-viper-to-flags.go` around lines 54 - 75,
viperValueToStrings currently treats any value that looks like a JSON array as a
string slice, which causes scalar flags to be split when supplied as env vars;
restrict JSON-array parsing to only flag types that are actual slices by
checking the pflag Value implementation or name before splitting: in the code
paths that call viperValueToStrings (and inside viperValueToStrings itself),
detect whether the target flag's pflag.Value implements pflag.SliceValue or has
a type/name like "stringArray"/"stringSlice" and only then attempt the
json.Unmarshal branch; for all other flag types (plain String, Bool, Int, etc.)
return the scalar string unmodified so Set is called once as before (reference
viperValueToStrings, pflag.SliceValue, and pflag.Value/Set).

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves how Viper-provided config values are bound into Cobra/PFlag flags so StringArray flags can be populated with multiple entries from (a) YAML list config values and (b) environment variables using JSON array syntax (issue #678).

Changes:

  • Iterate slice/array config values and call flag.Set(...) once per element instead of stringifying the entire slice.
  • Add JSON-array parsing for scalar string env var values that look like [...] to support multiple entries.
  • Add unit tests for the new viperValueToStrings helper.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
internal/cmdutils/bind-viper-to-flags.go Introduces viperValueToStrings and uses it when binding Viper values into flags, enabling multi-value StringArray behavior.
internal/cmdutils/bind-viper-to-flags_test.go Adds unit tests validating conversion/parsing behavior for scalar, slice, and JSON-array-like values.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +54 to +55
func viperValueToStrings(value any) []string {
reflectValue := reflect.ValueOf(value)
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

viperValueToStrings will panic if value is nil because reflect.ValueOf(nil) returns an invalid reflect.Value and calling Kind() on it panics. Viper can return nil for explicitly-null config values; consider guarding value == nil / !reflectValue.IsValid() and returning an empty slice (so no Flags().Set calls) or otherwise handling it explicitly.

Suggested change
func viperValueToStrings(value any) []string {
reflectValue := reflect.ValueOf(value)
func viperValueToStrings(value any) []string {
if value == nil {
return []string{}
}
reflectValue := reflect.ValueOf(value)
if !reflectValue.IsValid() {
return []string{}
}

Copilot uses AI. Check for mistakes.
// pflag.Set calls. Slice values (e.g. from YAML lists) produce one string per
// element. Scalar strings that look like a JSON array (start with "[" and end
// with "]") are parsed as JSON to support multiple values via env vars, e.g.
// FGA_CUSTOM_HEADERS='["X-Foo: bar","X-Baz: qux"]'. Other scalars produce a
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The doc comment example references FGA_CUSTOM_HEADERS, but there doesn’t appear to be a custom-headers flag/env binding in this repo (only api-scopes, contextual-tuple, etc.). Using a non-existent flag/env var in the example is misleading; please update the example to a flag that actually exists (e.g. FGA_API_SCOPES).

Suggested change
// FGA_CUSTOM_HEADERS='["X-Foo: bar","X-Baz: qux"]'. Other scalars produce a
// FGA_API_SCOPES='["scope1","scope2"]'. Other scalars produce a

Copilot uses AI. Check for mistakes.
expected: []string{"42"},
},
}

Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests cover several value shapes, but there’s no case for a nil value. Since viperValueToStrings(nil) currently panics (due to reflect.ValueOf(nil).Kind()), adding an explicit nil testcase would help prevent regressions once nil handling is added.

Suggested change
t.Run("nil value panics", func(t *testing.T) {
t.Parallel()
assert.Panics(t, func() {
_ = viperValueToStrings(nil)
})
})

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

StringArray flags don't support multiple values via environment variables

3 participants