Repository: ogen-go/ogen
Pull request #365 (2022-05-28 00:29:30)

feat(json): add json value comparator
Updates #132


Patch:
diff --git a/gen/_template/client.tmpl b/gen/_template/client.tmpl
index ead7dea5..2e3e0a58 100644
--- a/gen/_template/client.tmpl
+++ b/gen/_template/client.tmpl
@@ -45,7 +45,7 @@ func NewClient(serverURL string, {{- if $.Securities }}sec SecuritySource,{{- en
 // {{ $op.Name }} invokes {{ $op.Spec.OperationID }} operation.
 //
 {{- template "godoc_def" $op.GoDoc }}
-// {{ upper $op.Spec.HTTPMethod }} {{ $op.Spec.Path }}
+// {{ $op.Spec.HTTPMethod }} {{ $op.Spec.Path }}
 func (c *Client) {{ $op.Name }}(ctx context.Context {{ if $op.Request }}, request {{ $op.Request.Type.Go }}{{ end }}{{ if $op.Params }}, params {{ $op.Name }}Params {{ end }}) (res {{ $op.Responses.Type.Go }}, err error) {
 	{{- if $op.Request }}{{/* Request validation */}}
 		{{- if $op.Request.Type.IsInterface }}
@@ -153,7 +153,7 @@ func (c *Client) {{ $op.Name }}(ctx context.Context {{ if $op.Request }}, reques
 	{{- template "encode_query_parameters" $op }}
 	{{- end }}
 
-	r := ht.NewRequest(ctx, {{ $op.Spec.HTTPMethod | upper | quote }}, u, {{ if $op.Request }}reqBody{{ else }}nil{{ end }})
+	r := ht.NewRequest(ctx, {{ quote $op.Spec.HTTPMethod }}, u, {{ if $op.Request }}reqBody{{ else }}nil{{ end }})
 	defer ht.PutRequest(r)
 
 	{{ if $op.Request }}r.Header.Set("Content-Type", contentType){{ end }}
diff --git a/gen/_template/handlers.tmpl b/gen/_template/handlers.tmpl
index 57f4b2d3..e30e1368 100644
--- a/gen/_template/handlers.tmpl
+++ b/gen/_template/handlers.tmpl
@@ -7,7 +7,7 @@
 {{- range $op := $.Operations }}
 // Handle{{ $op.Name }}Request handles {{ $op.Spec.OperationID }} operation.
 //
-// {{ upper $op.Spec.HTTPMethod }} {{ $op.Spec.Path }}
+// {{ $op.Spec.HTTPMethod }} {{ $op.Spec.Path }}
 func (s *Server) handle{{ $op.Name }}Request(args [{{ $op.PathParamsCount }}]string, w http.ResponseWriter, r *http.Request) {
 	startTime := time.Now()
 	otelAttrs := []attribute.KeyValue{
diff --git a/gen/_template/server.tmpl b/gen/_template/server.tmpl
index d2b73a05..2bfc9ad0 100644
--- a/gen/_template/server.tmpl
+++ b/gen/_template/server.tmpl
@@ -10,7 +10,7 @@ type Handler interface {
 	// {{ $op.Name }} implements {{ $op.Spec.OperationID }} operation.
 	//
 	{{- template "godoc_def" $op.GoDoc }}
-	// {{ upper $op.Spec.HTTPMethod }} {{ $op.Spec.Path }}
+	// {{ $op.Spec.HTTPMethod }} {{ $op.Spec.Path }}
 	{{ $op.Name }}(ctx context.Context {{ if $op.Request }}, req {{ $op.Request.Type.Go }}{{ end }}{{ if $op.Params }}, params {{ $op.Name }}Params {{ end }}) ({{ $op.Responses.Type.Go }}, error)
 {{- end }}
 {{- if $.Error }}
diff --git a/gen/_template/unimplemented.tmpl b/gen/_template/unimplemented.tmpl
index 57ec2337..f2f6b618 100644
--- a/gen/_template/unimplemented.tmpl
+++ b/gen/_template/unimplemented.tmpl
@@ -13,7 +13,7 @@ type UnimplementedHandler struct {}
 // {{ $op.Name }} implements {{ $op.Spec.OperationID }} operation.
 //
 {{- template "godoc_def" $op.GoDoc }}
-// {{ upper $op.Spec.HTTPMethod }} {{ $op.Spec.Path }}
+// {{ $op.Spec.HTTPMethod }} {{ $op.Spec.Path }}
 func (UnimplementedHandler) {{ $op.Name }}(ctx context.Context {{ if $op.Request }}, req {{ $op.Request.Type.Go }}{{ end }}{{ if $op.Params }}, params {{ $op.Name }}Params {{ end }}) (r {{ $op.Responses.Type.Go }}, _ error) {
 	return r, ht.ErrNotImplemented
 }
diff --git a/gen/gen_contents.go b/gen/gen_contents.go
index 645daf16..ca3707ab 100644
--- a/gen/gen_contents.go
+++ b/gen/gen_contents.go
@@ -59,7 +59,7 @@ func (g *Generator) generateFormContent(
 		return nil, &ErrNotImplemented{"complex form schema"}
 	}
 
-	t, err := g.generateSchema(ctx.appendPath("schema"), typeName, media.Schema)
+	t, err := g.generateSchema(ctx, typeName, media.Schema)
 	if err != nil {
 		return nil, errors.Wrap(err, "generate schema")
 	}
@@ -142,7 +142,7 @@ func (g *Generator) generateContents(
 		if err := func() error {
 			switch parsedContentType {
 			case "application/json":
-				t, err := g.generateSchema(ctx.appendPath("schema"), typeName, media.Schema)
+				t, err := g.generateSchema(ctx, typeName, media.Schema)
 				if err != nil {
 					return errors.Wrap(err, "generate schema")
 				}
diff --git a/gen/gen_headers.go b/gen/gen_headers.go
index 2e72ba4e..ea4fd04b 100644
--- a/gen/gen_headers.go
+++ b/gen/gen_headers.go
@@ -6,7 +6,6 @@ import (
 	"github.com/go-faster/errors"
 	"github.com/ogen-go/ogen/gen/ir"
 	"github.com/ogen-go/ogen/openapi"
-	"go.uber.org/zap"
 )
 
 func (g *Generator) generateHeaders(ctx *genctx, name string, headers map[string]*openapi.Header) (_ map[string]*ir.Parameter, err error) {
@@ -18,7 +17,6 @@ func (g *Generator) generateHeaders(ctx *genctx, name string, headers map[string
 	for hname, header := range headers {
 		ctx := ctx.appendPath(hname)
 		if http.CanonicalHeaderKey(hname) == "Content-Type" {
-			g.log.Warn("Content-Type is described separately and will be ignored in this section.", zap.String("pointer", ctx.JSONPointer()))
 			continue
 		}
 
diff --git a/gen/gen_parameters.go b/gen/gen_parameters.go
index 9fe2b519..e8f0bd05 100644
--- a/gen/gen_parameters.go
+++ b/gen/gen_parameters.go
@@ -13,7 +13,7 @@ import (
 func (g *Generator) generateParameters(ctx *genctx, opName string, params []*openapi.Parameter) (_ []*ir.Parameter, err error) {
 	result := make([]*ir.Parameter, 0, len(params))
 	for i, p := range params {
-		ctx := ctx.appendPath(strconv.Itoa(i))
+		ctx := ctx.appendPath("[" + strconv.Itoa(i) + "]")
 		param, err := g.generateParameter(ctx, opName, p)
 		if err != nil {
 			if err := g.fail(err); err != nil {
diff --git a/gen/gen_request_body.go b/gen/gen_request_body.go
index 53140c4d..b166df41 100644
--- a/gen/gen_request_body.go
+++ b/gen/gen_request_body.go
@@ -10,7 +10,7 @@ import (
 func (g *Generator) generateRequest(ctx *genctx, opName string, body *openapi.RequestBody) (*ir.Request, error) {
 	name := opName + "Req"
 
-	contents, err := g.generateContents(ctx.appendPath("content"), name, !body.Required, body.Content)
+	contents, err := g.generateContents(ctx, name, !body.Required, body.Content)
 	if err != nil {
 		return nil, errors.Wrap(err, "contents")
 	}
diff --git a/gen/gen_responses.go b/gen/gen_responses.go
index 4c6ad6ee..3dd76fbf 100644
--- a/gen/gen_responses.go
+++ b/gen/gen_responses.go
@@ -48,7 +48,6 @@ func (g *Generator) generateResponses(ctx *genctx, opName string, responses map[
 			resp = responses[strconv.Itoa(code)]
 			doc  = fmt.Sprintf("%s is response for %s operation.", respName, opName)
 		)
-		ctx := ctx.appendPath(strconv.Itoa(code))
 		result.StatusCode[code], err = g.responseToIR(ctx, respName, doc, resp, false)
 		if err != nil {
 			return nil, errors.Wrapf(err, "%d", code)
@@ -60,7 +59,6 @@ func (g *Generator) generateResponses(ctx *genctx, opName string, responses map[
 			respName = opName + "Def"
 			doc      = fmt.Sprintf("%s is default response for %s operation.", respName, opName)
 		)
-		ctx := ctx.appendPath("default")
 		result.Default, err = g.responseToIR(ctx, respName, doc, def, true)
 		if err != nil {
 			return nil, errors.Wrap(err, "default")
@@ -178,7 +176,7 @@ func (g *Generator) responseToIR(
 		}, nil
 	}
 
-	contents, err := g.generateContents(ctx.appendPath("content"), name, false, resp.Content)
+	contents, err := g.generateContents(ctx, name, false, resp.Content)
 	if err != nil {
 		return nil, errors.Wrap(err, "contents")
 	}
diff --git a/gen/gen_schema.go b/gen/gen_schema.go
index 08fd7bb8..4fa96d07 100644
--- a/gen/gen_schema.go
+++ b/gen/gen_schema.go
@@ -94,9 +94,9 @@ func GenerateSchema(schema *jsonschema.Schema, fs FileSystem, opts GenerateSchem
 	opts.setDefaults()
 
 	ctx := &genctx{
-		jsonptr: newJSONPointer("#"),
-		global:  newTStorage(),
-		local:   newTStorage(),
+		path:   []string{"#"},
+		global: newTStorage(),
+		local:  newTStorage(),
 	}
 
 	gen := newSchemaGen(func(ref string) (*ir.Type, bool) {
diff --git a/gen/genctx.go b/gen/genctx.go
index 6848eebe..eb1ba297 100644
--- a/gen/genctx.go
+++ b/gen/genctx.go
@@ -1,22 +1,23 @@
 package gen
 
-import (
-	"github.com/ogen-go/ogen/gen/ir"
-)
+import "github.com/ogen-go/ogen/gen/ir"
 
 // genctx is a generation context.
 type genctx struct {
-	jsonptr jsonpointer
+	path []string
 
 	global *tstorage // readonly
 	local  *tstorage
 }
 
 func (g *genctx) appendPath(v ...string) *genctx {
+	var newPath []string
+	newPath = append(newPath, g.path...)
+	newPath = append(newPath, v...)
 	return &genctx{
-		jsonptr: g.jsonptr.Append(v...),
-		global:  g.global,
-		local:   g.local,
+		path:   newPath,
+		global: g.global,
+		local:  g.local,
 	}
 }
 
@@ -65,7 +66,3 @@ func (g *genctx) lookupWType(ref string) (*ir.Type, bool) {
 	}
 	return nil, false
 }
-
-func (g *genctx) JSONPointer() string {
-	return g.jsonptr.String()
-}
diff --git a/gen/generator.go b/gen/generator.go
index f5fbf1a1..680335cf 100644
--- a/gen/generator.go
+++ b/gen/generator.go
@@ -129,9 +129,9 @@ func (g *Generator) makeIR(ops []*openapi.Operation) error {
 		}
 
 		ctx := &genctx{
-			jsonptr: newJSONPointer("#", "paths", routePath, spec.HTTPMethod),
-			global:  g.tstorage,
-			local:   newTStorage(),
+			path:   []string{"#", "paths", routePath, spec.HTTPMethod},
+			global: g.tstorage,
+			local:  newTStorage(),
 		}
 
 		op, err := g.generateOperation(ctx, spec)
diff --git a/gen/jsonpointer.go b/gen/jsonpointer.go
deleted file mode 100644
index ed4b22fa..00000000
--- a/gen/jsonpointer.go
+++ /dev/null
@@ -1,31 +0,0 @@
-package gen
-
-import "strings"
-
-type jsonpointer struct {
-	parts []string
-}
-
-func newJSONPointer(parts ...string) jsonpointer {
-	return jsonpointer{parts: parts}
-}
-
-func (p jsonpointer) Append(parts ...string) jsonpointer {
-	newParts := make([]string, 0, len(p.parts)+len(parts))
-	newParts = append(newParts, p.parts...)
-	newParts = append(newParts, parts...)
-	return jsonpointer{parts: newParts}
-}
-
-var escapeReplacer = strings.NewReplacer(
-	"/", "~1",
-	"~", "~0",
-)
-
-func (p jsonpointer) String() string {
-	escaped := make([]string, len(p.parts))
-	for i, part := range p.parts {
-		escaped[i] = escapeReplacer.Replace(part)
-	}
-	return strings.Join(escaped, "/")
-}
diff --git a/gen/reduce.go b/gen/reduce.go
index cb333114..027461f9 100644
--- a/gen/reduce.go
+++ b/gen/reduce.go
@@ -33,9 +33,9 @@ func (g *Generator) reduceDefault(ops []*openapi.Operation) error {
 	}
 
 	ctx := &genctx{
-		jsonptr: newJSONPointer("x-ogen-reduce-default"),
-		global:  g.tstorage,
-		local:   g.tstorage,
+		path:   []string{"x-ogen-reduce-default"},
+		global: g.tstorage,
+		local:  g.tstorage,
 	}
 
 	resp, err := g.responseToIR(ctx, "ErrResp", "reduced default response", d, true)
diff --git a/gen/router.go b/gen/router.go
index ac3c7df1..5366033e 100644
--- a/gen/router.go
+++ b/gen/router.go
@@ -82,7 +82,7 @@ func (g *Generator) route() error {
 	var maxParametersCount int
 	for _, op := range g.operations {
 		if err := g.router.Add(Route{
-			Method:    strings.ToUpper(op.Spec.HTTPMethod),
+			Method:    op.Spec.HTTPMethod,
 			Path:      op.Spec.Path.String(),
 			Operation: op,
 		}); err != nil {
diff --git a/gen/templates.go b/gen/templates.go
index dc92cde2..3c916234 100644
--- a/gen/templates.go
+++ b/gen/templates.go
@@ -69,7 +69,6 @@ func templateFunctions() template.FuncMap {
 		"pascalSpecial": pascalSpecial,
 		"camelSpecial":  camelSpecial,
 		"capitalize":    capitalize.Capitalize,
-		"upper":         strings.ToUpper,
 
 		// Helpers for recursive encoding and decoding.
 		"elem": func(t *ir.Type, v string) Elem {
diff --git a/json/equal.go b/json/equal.go
new file mode 100644
index 00000000..d61a7b30
--- /dev/null
+++ b/json/equal.go
@@ -0,0 +1,222 @@
+package json
+
+import (
+	"bytes"
+	"math/big"
+
+	"github.com/go-faster/errors"
+	"github.com/go-faster/jx"
+)
+
+type compare struct {
+	left, right *jx.Decoder
+}
+
+func (c compare) equalBool() (bool, error) {
+	lval, err := c.left.Bool()
+	if err != nil {
+		return false, errors.Wrap(err, "left")
+	}
+	rval, err := c.right.Bool()
+	if err != nil {
+		return false, errors.Wrap(err, "right")
+	}
+	return lval == rval, nil
+}
+
+func (c compare) equalString() (bool, error) {
+	lval, err := c.left.StrBytes()
+	if err != nil {
+		return false, errors.Wrap(err, "left")
+	}
+	rval, err := c.right.StrBytes()
+	if err != nil {
+		return false, errors.Wrap(err, "right")
+	}
+	return bytes.Equal(lval, rval), nil
+}
+
+func (c compare) equalNumber() (bool, error) {
+	lval, err := c.left.Num()
+	if err != nil {
+		return false, errors.Wrap(err, "left")
+	}
+	rval, err := c.right.Num()
+	if err != nil {
+		return false, errors.Wrap(err, "right")
+	}
+
+	// Fast path comparing.
+	switch {
+	case lval.Zero() && rval.Zero():
+		return true, nil
+	case lval.Equal(rval): // Compare like byte slices.
+		return true, nil
+	case lval.IsInt() && rval.IsInt():
+		// If both values are integer, non-zero and are not equal as byte slice,
+		// so they are not equal as numbers.
+		return false, nil
+	default:
+		lnum, err := lval.Float64()
+		if err != nil {
+			break
+		}
+		rnum, err := rval.Float64()
+		if err != nil {
+			break
+		}
+		return lnum == rnum, nil
+	}
+
+	lnum, rnum := new(big.Rat), new(big.Rat)
+	if err := lnum.UnmarshalText(lval); err != nil {
+		return false, errors.Wrap(err, "left")
+	}
+	if err := rnum.UnmarshalText(rval); err != nil {
+		return false, errors.Wrap(err, "right")
+	}
+	return lnum.Cmp(rnum) == 0, nil
+}
+
+func (c compare) equalArray() (bool, error) {
+	liter, err := c.left.ArrIter()
+	if err != nil {
+		return false, errors.Wrap(err, "left")
+	}
+	riter, err := c.right.ArrIter()
+	if err != nil {
+		return false, errors.Wrap(err, "right")
+	}
+
+	i := 0
+	for liter.Next() {
+		// Left array is bigger than right.
+		if !riter.Next() {
+			return false, nil
+		}
+
+		ok, err := c.equal()
+		if err != nil {
+			return false, errors.Wrapf(err, "[%d]", i)
+		}
+		if !ok {
+			return false, nil
+		}
+		i++
+	}
+
+	if err := liter.Err(); err != nil {
+		return false, errors.Wrap(err, "left")
+	}
+	if err := riter.Err(); err != nil {
+		return false, errors.Wrap(err, "right")
+	}
+
+	// Right array is bigger than left.
+	return !riter.Next(), nil
+}
+
+func (c compare) equalObject() (bool, error) {
+	// TODO(tdakkota): is there a more efficient way?
+	collectObject := func(d *jx.Decoder) (m map[string]jx.Raw, err error) {
+		m = map[string]jx.Raw{}
+		err = d.ObjBytes(func(d *jx.Decoder, key []byte) error {
+			raw, err := d.Raw()
+			if err != nil {
+				return errors.Wrapf(err, "%q", key)
+			}
+			m[string(key)] = raw
+			return nil
+		})
+		return m, err
+	}
+
+	lmap, err := collectObject(c.left)
+	if err != nil {
+		return false, errors.Wrap(err, "left")
+	}
+
+	riter, err := c.right.ObjIter()
+	if err != nil {
+		return false, errors.Wrap(err, "right")
+	}
+	i := 0
+	for riter.Next() {
+		lval, ok := lmap[string(riter.Key())]
+		if !ok {
+			return false, nil
+		}
+		i++
+
+		rval, err := c.right.Raw()
+		if err != nil {
+			return false, errors.Wrap(err, "right")
+		}
+
+		ok, err = Equal(lval, rval)
+		if err != nil {
+			return false, errors.Wrapf(err, "%q", riter.Key())
+		}
+		if !ok {
+			return false, nil
+		}
+	}
+	if err := riter.Err(); err != nil {
+		return false, errors.Wrap(err, "right")
+	}
+
+	// Right object is smaller than left.
+	if len(lmap) != i {
+		return false, nil
+	}
+
+	return true, nil
+}
+
+func (c compare) equal() (bool, error) {
+	lt, rt := c.left.Next(), c.right.Next()
+	switch {
+	case lt == jx.Invalid:
+		return false, errors.Wrap(c.left.Validate(), "left")
+	case rt == jx.Invalid:
+		return false, errors.Wrap(c.right.Validate(), "right")
+	case lt != rt:
+		return false, nil
+	}
+
+	switch lt {
+	case jx.Null:
+		// lt is equal to rt, so values are equal.
+		return true, nil
+	case jx.Bool:
+		return c.equalBool()
+	case jx.String:
+		return c.equalString()
+	case jx.Number:
+		return c.equalNumber()
+	case jx.Array:
+		return c.equalArray()
+	case jx.Object:
+		return c.equalObject()
+	default:
+		panic("unreachable")
+	}
+}
+
+// Equal compares two JSON values.
+func Equal(a, b []byte) (bool, error) {
+	l, r := jx.GetDecoder(), jx.GetDecoder()
+	defer func() {
+		jx.PutDecoder(l)
+		jx.PutDecoder(r)
+	}()
+
+	l.ResetBytes(a)
+	r.ResetBytes(b)
+
+	c := compare{
+		left:  l,
+		right: r,
+	}
+	return c.equal()
+}
diff --git a/openapi/parser/parser.go b/openapi/parser/parser.go
index 8418f39a..b302ff0a 100644
--- a/openapi/parser/parser.go
+++ b/openapi/parser/parser.go
@@ -1,6 +1,8 @@
 package parser
 
 import (
+	"strings"
+
 	"github.com/go-faster/errors"
 
 	"github.com/ogen-go/ogen"
@@ -120,7 +122,7 @@ func (p *parser) parseOp(path, httpMethod string, spec ogen.Operation, itemParam
 	op := &openapi.Operation{
 		OperationID: spec.OperationID,
 		Description: spec.Description,
-		HTTPMethod:  httpMethod,
+		HTTPMethod:  strings.ToUpper(httpMethod),
 	}
 
 	opParams, err := p.parseParams(spec.Parameters)


Test patch:
diff --git a/json/equal_test.go b/json/equal_test.go
new file mode 100644
index 00000000..0580cba4
--- /dev/null
+++ b/json/equal_test.go
@@ -0,0 +1,167 @@
+package json
+
+import (
+	"fmt"
+	"sort"
+	"testing"
+
+	"github.com/go-faster/jx"
+	"github.com/stretchr/testify/require"
+)
+
+func TestEqual(t *testing.T) {
+	type testCase struct {
+		a, b    string
+		want    bool
+		wantErr bool
+	}
+	tests := map[string][]testCase{}
+
+	for _, c := range []struct {
+		a, b string
+		want bool
+	}{
+		// Null.
+		{`null`, `null`, true},
+		// Bool.
+		{`false`, `false`, true},
+		{`true`, `true`, true},
+		{`false`, `true`, false},
+		// String.
+		{`"foo"`, `"foo"`, true},
+		{`"foo"`, `"foo" `, true},
+		{`"foo\u000a"`, `"foo\n"`, true},
+		{`"foo"`, `"foo\n"`, false},
+		{`"foo"`, `"foo "`, false},
+		// Number.
+		{`0`, `0`, true},
+		{`-0`, `-0`, true},
+		{`-0`, `0`, true},
+		{`1`, `1`, true},
+		{`10`, `10`, true},
+		{`0.0`, `0.0`, true},
+		{`10`, `1e1`, true},
+		{` 1000000000000000000000000000000`, `1000000000000000000000000000000`, true},
+		{`1e1000`, `1.0e1000`, true},
+		{`10`, `1.0e1`, true},
+		{`0`, `1`, false},
+		{`-1`, `1`, false},
+		{`1e1`, `100`, false},
+		// Array.
+		{`[]`, `[]`, true},
+		{`[]`, `[ ]`, true},
+		{`[[]]`, `[[] ]`, true},
+		{`["a", "b"]`, `["a", "b"]`, true},
+		{`["a"]`, `[]`, false},
+		{`[1,2,3]`, `[1,2]`, false},
+		{`[[]]`, `[[1]]`, false},
+		{`["b","a"]`, `["a","b"]`, false},
+		// Object.
+		{`{}`, `{}`, true},
+		{`{}`, `{ }`, true},
+		{`{"a":"b"}`, `{"a":"b"}`, true},
+		{`{"a":"b","b":"a"}`, `{"b":"a", "a":"b"}`, true},
+		{`{}`, `{"a":"b"}`, false},
+		{`{"b":"a"}`, `{"a":"b"}`, false},
+		{`{"a":10}`, `{"a":"b"}`, false},
+		// Type comparison.
+		{`{}`, `[]`, false},
+		{`{}`, `0`, false},
+		{`{}`, `null`, false},
+		{`{}`, `false`, false},
+		{`{}`, `""`, false},
+	} {
+		typ := jx.DecodeStr(c.a).Next().String()
+		typb := jx.DecodeStr(c.b).Next().String()
+		require.NotContains(t, []string{typ, typb}, jx.Invalid.String())
+		if typb != typ {
+			typ = "type"
+		}
+		tests[typ] = append(tests[typ], testCase{
+			a:       c.a,
+			b:       c.b,
+			want:    c.want,
+			wantErr: false,
+		})
+	}
+
+	sortedIter := func(cb func(k string, tts []testCase)) {
+		var keys []string
+		for k := range tests {
+			keys = append(keys, k)
+		}
+		sort.Strings(keys)
+		for _, k := range keys {
+			cb(k, tests[k])
+		}
+	}
+
+	sortedIter(func(typ string, tts []testCase) {
+		t.Run(typ, func(t *testing.T) {
+			for i, tt := range tts {
+				tt := tt
+				t.Run(fmt.Sprintf("Test%d", i+1), func(t *testing.T) {
+					a := require.New(t)
+					check := func(x, y []byte) {
+						got, err := Equal(x, y)
+						if tt.wantErr {
+							a.Error(err)
+							a.False(got)
+							return
+						}
+						a.NoError(err)
+						a.Equal(tt.want, got, "%q == %q must be %v", x, y, tt.want)
+					}
+
+					x, y := []byte(tt.a), []byte(tt.b)
+					check(x, y)
+					check(y, x)
+				})
+			}
+		})
+	})
+}
+
+func BenchmarkEqual(b *testing.B) {
+	benches := []struct {
+		name string
+		a, b string
+		sink bool
+	}{
+		{`Zero`, `0`, `0`, true},
+		{`SmallInt`, `10`, `21`, false},
+		{`BigInt`, `1000000000000000000000000`, `21000000000000000000000000`, false},
+		{`SmallFloat`, `15.20`, `69.12`, false},
+		{`BigFloat`, `1e100`, `1e101`, false},
+		{`PlainString`, `"foo10bar"`, `"foobar"`, false},
+		{`EscapedString`, `"foo\nbar"`, `"foobar"`, false},
+		{`Bool`, `true`, `true`, true},
+		{`Array`, `[1,2,3,5,6,732,4312]`, `[1,2,3,5,6,732,4312]`, true},
+		{`Object`, `{"b":"a","a":"b","foo":"bar"}'`, `{"foo":"bar","a":"b","b":"a"}`, true},
+	}
+
+	for _, bb := range benches {
+		bb := bb
+		b.Run(bb.name, func(b *testing.B) {
+			x, y := []byte(bb.a), []byte(bb.b)
+			var (
+				sink bool
+				err  error
+			)
+
+			b.ReportAllocs()
+			b.ResetTimer()
+
+			for i := 0; i < b.N; i++ {
+				sink, err = Equal(x, y)
+			}
+
+			if err != nil {
+				b.Fatal(err)
+			}
+			if bb.sink != sink {
+				b.Fatalf("%q == %q must be %v", x, y, bb.sink)
+			}
+		})
+	}
+}