Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add duplicate json key validator #650

Merged
merged 3 commits into from
Sep 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
<!-- markdownlint-disable single-title -->
# v2.0.0 (Unreleased)

# v2.0.0-beta.36 (2023-09-21)

BREAKING CHANGES

* The `ValidateRegion` function has been moved to the `validation` package and renamed to `SupportedRegion` ([#650](https://github.com/hashicorp/aws-sdk-go-base/pull/650))

ENHANCEMENTS

* Adds `JSONNoDuplicateKeys` function to the `validation` package ([#650](https://github.com/hashicorp/aws-sdk-go-base/pull/650))

# v2.0.0-beta.35 (2023-09-05)

ENHANCEMENTS
Expand Down
105 changes: 105 additions & 0 deletions validation/json.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package validation

import (
"encoding/json"
"errors"
"fmt"
"strconv"
"strings"
)

// DuplicateKeyError is returned when duplicate key names are detected
// inside a JSON object
type DuplicateKeyError struct {
path []string
key string
}

func (e *DuplicateKeyError) Error() string {
return fmt.Sprintf(`duplicate key "%s"`, strings.Join(append(e.path, e.key), "."))
}

// JSONNoDuplicateKeys verifies the provided JSON object contains
// no duplicated keys
//
// The function expects a single JSON object, and will error prior to
// checking for duplicate keys should an invalid input be provided.
func JSONNoDuplicateKeys(s string) error {
var out map[string]any
if err := json.Unmarshal([]byte(s), &out); err != nil {
return fmt.Errorf("unmarshaling input: %w", err)
}

dec := json.NewDecoder(strings.NewReader(s))
return checkToken(dec, nil)
}

// checkToken walks a JSON object checking for duplicated keys
//
// The function is called recursively on the value of each key
// inside and object, or item inside an array.
//
// Adapted from: https://stackoverflow.com/a/50109335
func checkToken(dec *json.Decoder, path []string) error {
t, err := dec.Token()
if err != nil {
return err
}

delim, ok := t.(json.Delim)
if !ok {
// non-delimiter, nothing to do
return nil
}

var dupErrs []error
switch delim {
case '{':
keys := make(map[string]bool)
for dec.More() {
// Get the field key
t, err := dec.Token()
if err != nil {
return err
}
key := t.(string)

if keys[key] {
// Duplicate found
dupErrs = append(dupErrs, &DuplicateKeyError{path: path, key: key})
}
keys[key] = true

// Check the keys value
if err := checkToken(dec, append(path, key)); err != nil {
dupErrs = append(dupErrs, err)
}
}

// consume trailing "}"
_, err := dec.Token()
if err != nil {
return err
}
case '[':
i := 0
for dec.More() {
// Check each items value
if err := checkToken(dec, append(path, strconv.Itoa(i))); err != nil {
dupErrs = append(dupErrs, err)
}
i++
}

// consume trailing "]"
_, err := dec.Token()
if err != nil {
return err
}
}

return errors.Join(dupErrs...)
}
133 changes: 133 additions & 0 deletions validation/json_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package validation

import (
"testing"
)

func TestJSONNoDuplicateKeys(t *testing.T) {
tests := []struct {
name string
s string
wantErr bool
}{
{
name: "invalid",
s: "{{{",
wantErr: true,
},
{
name: "valid",
s: `{
"a": "foo",
"b": {
"c": "bar",
"d": [
{
"e": "baz"
},
{
"f": "qux",
"g": "foo"
}
]
}
}`,
wantErr: false,
},
{
name: "root",
s: `{
"a": "foo",
"a": "bar"
}`,
wantErr: true,
},
{
name: "nested object",
s: `{
"a": "foo",
"b": {
"c": "bar",
"c": "baz"
}
}`,
wantErr: true,
},
{
name: "nested array",
s: `{
"a": "foo",
"b": {
"c": "bar",
"d": [
{
"e": "foo",
"e": "bar"
},
{
"f": "baz",
"g": "qux"
}
]
}
}`,
wantErr: true,
},
{
name: "multiple",
s: `{
"a": "foo",
"a": "bar",
"b": {
"c": "baz",
"c": "qux",
"d": [
{
"e": "foo"
},
{
"f": "bar",
"f": "baz",
"g": "qux"
}
]
}
}`,
wantErr: true,
},
{
name: "aws iam condition keys",
s: `{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "iam:PassRole",
"Resource": "*",
"Condition": {
"StringEquals": {
"iam:PassedToService": "cloudwatch.amazonaws.com"
},
"StringEquals": {
"iam:PassedToService": "ec2.amazonaws.com"
}
}
}
]
}`,

wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := JSONNoDuplicateKeys(tt.s); (err != nil) != tt.wantErr {
t.Errorf("JSONNoDuplicateKeys() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
6 changes: 3 additions & 3 deletions validation.go → validation/region.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package awsbase
package validation

import (
"fmt"
Expand All @@ -17,8 +17,8 @@ func (e *InvalidRegionError) Error() string {
return fmt.Sprintf("Invalid AWS Region: %s", e.region)
}

// ValidateRegion checks if the given region is a valid AWS region.
func ValidateRegion(region string) error {
// SupportedRegion checks if the given region is a valid AWS region.
func SupportedRegion(region string) error {
for _, partition := range endpoints.Partitions() {
for _, partitionRegion := range partition.Regions() {
if region == partitionRegion {
Expand Down
6 changes: 3 additions & 3 deletions validation_test.go → validation/region_test.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package awsbase
package validation

import (
"testing"
)

func TestValidateRegion(t *testing.T) {
func TestSupportedRegion(t *testing.T) {
var testCases = []struct {
Region string
ExpectError bool
Expand Down Expand Up @@ -38,7 +38,7 @@ func TestValidateRegion(t *testing.T) {
testCase := testCase

t.Run(testCase.Region, func(t *testing.T) {
err := ValidateRegion(testCase.Region)
err := SupportedRegion(testCase.Region)
if err != nil && !testCase.ExpectError {
t.Fatalf("Expected no error, received error: %s", err)
}
Expand Down