Repository: ogen-go/ogen
Pull request #619 (2022-10-12 15:19:26)

feat(xslices): add generic helper package


Patch:
diff --git a/cmd/ogen/main.go b/cmd/ogen/main.go
index f8bd0f13..7a877b2f 100644
--- a/cmd/ogen/main.go
+++ b/cmd/ogen/main.go
@@ -279,6 +279,11 @@ func run() error {
 	}
 	if *debugNoerr {
 		opts.IgnoreNotImplemented = []string{"all"}
+	} else {
+		// Normalize ignore rules.
+		for i := range opts.IgnoreNotImplemented {
+			opts.IgnoreNotImplemented[i] = strings.TrimSpace(opts.IgnoreNotImplemented[i])
+		}
 	}
 
 	data, err := os.ReadFile(specPath)
diff --git a/gen/errors.go b/gen/errors.go
index be86b3df..8a20b058 100644
--- a/gen/errors.go
+++ b/gen/errors.go
@@ -6,6 +6,7 @@ import (
 
 	"github.com/go-faster/errors"
 	"go.uber.org/zap"
+	"golang.org/x/exp/slices"
 )
 
 type unimplementedError interface {
@@ -80,30 +81,22 @@ func (g *Generator) fail(err error) error {
 	if err == nil {
 		return nil
 	}
-
-	hook := g.opt.NotImplementedHook
-	if hook == nil {
-		hook = func(string, error) {}
+	hasAll := slices.Contains(g.opt.IgnoreNotImplemented, "all")
+	handle := func(name string, err error) error {
+		if hook := g.opt.NotImplementedHook; hook != nil {
+			hook(name, err)
+		}
+		if hasAll || slices.Contains(g.opt.IgnoreNotImplemented, name) {
+			return nil
+		}
+		return err
 	}
-
 	if notImplementedErr, ok := errors.Into[*ErrNotImplemented](err); ok {
-		hook(notImplementedErr.Name, err)
-		for _, s := range g.opt.IgnoreNotImplemented {
-			s = strings.TrimSpace(s)
-			if s == "all" || s == notImplementedErr.Name {
-				return nil
-			}
-		}
+		return handle(notImplementedErr.Name, err)
 	}
-
 	if _, ok := errors.Into[*ErrUnsupportedContentTypes](err); ok {
-		hook("unsupported content types", err)
-		for _, s := range g.opt.IgnoreNotImplemented {
-			s = strings.TrimSpace(s)
-			if s == "all" || s == "unsupported content types" {
-				return nil
-			}
-		}
+		const name = "unsupported content types"
+		return handle(name, err)
 	}
 	return err
 }
diff --git a/gen/gen_operation.go b/gen/gen_operation.go
index deeba6eb..056efb6c 100644
--- a/gen/gen_operation.go
+++ b/gen/gen_operation.go
@@ -6,6 +6,7 @@ import (
 	"github.com/go-faster/errors"
 
 	"github.com/ogen-go/ogen/gen/ir"
+	"github.com/ogen-go/ogen/internal/xslices"
 	"github.com/ogen-go/ogen/openapi"
 )
 
@@ -59,12 +60,9 @@ func (g *Generator) generateOperation(ctx *genctx, spec *openapi.Operation) (_ *
 
 func convertPathParts(parts openapi.Path, params []*ir.Parameter) []*ir.PathPart {
 	find := func(pname string) (*ir.Parameter, bool) {
-		for _, p := range params {
-			if p.Spec.Name == pname && p.Spec.In == openapi.LocationPath {
-				return p, true
-			}
-		}
-		return nil, false
+		return xslices.FindFunc(params, func(p *ir.Parameter) bool {
+			return p.Spec.Name == pname && p.Spec.In == openapi.LocationPath
+		})
 	}
 
 	result := make([]*ir.PathPart, 0, len(parts))
diff --git a/gen/ir/field.go b/gen/ir/field.go
index 6349b1ad..f6ed2b3a 100644
--- a/gen/ir/field.go
+++ b/gen/ir/field.go
@@ -4,6 +4,7 @@ import (
 	"fmt"
 	"strconv"
 
+	"github.com/ogen-go/ogen/internal/xslices"
 	"github.com/ogen-go/ogen/jsonschema"
 	"github.com/ogen-go/ogen/openapi"
 )
@@ -98,12 +99,9 @@ func (t Type) DefaultFields() (r []*Field) {
 
 // HasDefaultFields whether type has fields with default values.
 func (t Type) HasDefaultFields() bool {
-	for _, f := range t.Fields {
-		if val := f.Default(); val.Set {
-			return true
-		}
-	}
-	return false
+	return xslices.ContainsFunc(t.Fields, func(f *Field) bool {
+		return f.Default().Set
+	})
 }
 
 func (t Type) parameters(keep func(t *Type) bool) (params []Parameter) {
diff --git a/gen/ir/json.go b/gen/ir/json.go
index 0c7be25e..8a5150b0 100644
--- a/gen/ir/json.go
+++ b/gen/ir/json.go
@@ -6,6 +6,7 @@ import (
 	"golang.org/x/exp/slices"
 
 	"github.com/ogen-go/ogen/internal/naming"
+	"github.com/ogen-go/ogen/internal/xslices"
 	"github.com/ogen-go/ogen/jsonschema"
 )
 
@@ -53,12 +54,9 @@ func (j JSONFields) FirstRequiredIndex() int {
 
 // HasRequired whether object has required fields
 func (j JSONFields) HasRequired() bool {
-	for _, f := range j {
-		if f.Spec != nil && f.Spec.Required {
-			return true
-		}
-	}
-	return false
+	return xslices.ContainsFunc(j, func(f *Field) bool {
+		return f.Spec != nil && f.Spec.Required
+	})
 }
 
 // RequiredMask returns array of 64-bit bitmasks for required fields.
@@ -94,13 +92,11 @@ func (j JSON) Fields() (fields JSONFields) {
 }
 
 // AdditionalProps return field of Type that should be encoded as inlined map.
-func (j JSON) AdditionalProps() (field *Field) {
-	for _, f := range j.t.Fields {
-		if f.Inline == InlineAdditional {
-			return f
-		}
-	}
-	return nil
+func (j JSON) AdditionalProps() *Field {
+	f, _ := xslices.FindFunc(j.t.Fields, func(f *Field) bool {
+		return f.Inline == InlineAdditional
+	})
+	return f
 }
 
 // PatternProps return field of Type that should be encoded as inlined map with pattern.
diff --git a/gen/ir/params.go b/gen/ir/params.go
index bc21824c..5a477da7 100644
--- a/gen/ir/params.go
+++ b/gen/ir/params.go
@@ -1,6 +1,7 @@
 package ir
 
 import (
+	"github.com/ogen-go/ogen/internal/xslices"
 	"github.com/ogen-go/ogen/openapi"
 )
 
@@ -10,21 +11,15 @@ func (op *Operation) CookieParams() []*Parameter { return op.getParams(openapi.L
 func (op *Operation) HeaderParams() []*Parameter { return op.getParams(openapi.LocationHeader) }
 
 func (op Operation) HasQueryParams() bool {
-	for _, p := range op.Params {
-		if p.Spec != nil && p.Spec.In.Query() {
-			return true
-		}
-	}
-	return false
+	return xslices.ContainsFunc(op.Params, func(p *Parameter) bool {
+		return p.Spec != nil && p.Spec.In.Query()
+	})
 }
 
 func (op Operation) HasHeaderParams() bool {
-	for _, p := range op.Params {
-		if p.Spec != nil && p.Spec.In.Header() {
-			return true
-		}
-	}
-	return false
+	return xslices.ContainsFunc(op.Params, func(p *Parameter) bool {
+		return p.Spec != nil && p.Spec.In.Header()
+	})
 }
 
 func (op Operation) PathParamsCount() (r int) {
diff --git a/gen/ir/recursion.go b/gen/ir/recursion.go
index 62b72040..ef23bcca 100644
--- a/gen/ir/recursion.go
+++ b/gen/ir/recursion.go
@@ -3,6 +3,10 @@ package ir
 import (
 	"fmt"
 	"reflect"
+
+	"golang.org/x/exp/slices"
+
+	"github.com/ogen-go/ogen/internal/xslices"
 )
 
 func (t *Type) RecursiveTo(target *Type) bool {
@@ -40,22 +44,17 @@ func (t *Type) recursive(target *Type, path *walkpath) bool {
 	case KindGeneric:
 		return t.recursive(target.GenericOf, path)
 	case KindStruct:
-		for _, f := range target.Fields {
+		return xslices.ContainsFunc(target.Fields, func(f *Field) bool {
+			// Ignore optional fields: we are using pointers for them.
 			if f.Spec != nil && !f.Spec.Required {
-				continue
-			}
-			if t.recursive(f.Type, path) {
-				return true
+				return false
 			}
-		}
-		return false
+			return t.recursive(f.Type, path)
+		})
 	case KindSum:
-		for _, of := range target.SumOf {
-			if t.recursive(of, path) {
-				return true
-			}
-		}
-		return false
+		return xslices.ContainsFunc(target.SumOf, func(of *Type) bool {
+			return t.recursive(of, path)
+		})
 	default:
 		panic(fmt.Sprintf("unexpected kind: %s", t.Kind))
 	}
@@ -66,12 +65,7 @@ type walkpath struct {
 }
 
 func (wp *walkpath) has(t *Type) bool {
-	for _, n := range wp.nodes {
-		if n == t {
-			return true
-		}
-	}
-	return false
+	return slices.Contains(wp.nodes, t)
 }
 
 func (wp *walkpath) append(t *Type) *walkpath {
diff --git a/gen/ir/type_features.go b/gen/ir/type_features.go
index 0f82a9f3..95ee90a5 100644
--- a/gen/ir/type_features.go
+++ b/gen/ir/type_features.go
@@ -1,13 +1,9 @@
 package ir
 
-func (t *Type) HasFeature(feature string) bool {
-	for _, f := range t.Features {
-		if feature == f {
-			return true
-		}
-	}
+import "golang.org/x/exp/slices"
 
-	return false
+func (t *Type) HasFeature(feature string) bool {
+	return slices.Contains(t.Features, feature)
 }
 
 func (t *Type) AddFeature(feature string) {
@@ -45,8 +41,5 @@ func (t *Type) CloneFeatures() []string {
 	if t == nil {
 		return nil
 	}
-
-	out := make([]string, len(t.Features))
-	_ = copy(out, t.Features)
-	return out
+	return slices.Clone(t.Features)
 }
diff --git a/gen/ir/validation.go b/gen/ir/validation.go
index b816083c..dc9701f6 100644
--- a/gen/ir/validation.go
+++ b/gen/ir/validation.go
@@ -8,6 +8,7 @@ import (
 	"github.com/go-faster/errors"
 	"github.com/go-faster/jx"
 
+	"github.com/ogen-go/ogen/internal/xslices"
 	"github.com/ogen-go/ogen/jsonschema"
 	"github.com/ogen-go/ogen/validate"
 )
@@ -151,12 +152,9 @@ func (t *Type) needValidation(path *walkpath) (result bool) {
 	case KindEnum:
 		return true
 	case KindSum:
-		for _, s := range t.SumOf {
-			if s.needValidation(path) {
-				return true
-			}
-		}
-		return false
+		return xslices.ContainsFunc(t.SumOf, func(s *Type) bool {
+			return s.needValidation(path)
+		})
 	case KindAlias:
 		return t.AliasTo.needValidation(path)
 	case KindPointer:
@@ -175,12 +173,9 @@ func (t *Type) needValidation(path *walkpath) (result bool) {
 		}
 		return t.Item.needValidation(path)
 	case KindStruct:
-		for _, f := range t.Fields {
-			if f.Type.needValidation(path) {
-				return true
-			}
-		}
-		return false
+		return xslices.ContainsFunc(t.Fields, func(f *Field) bool {
+			return f.Type.needValidation(path)
+		})
 	case KindMap:
 		if t.Validators.Object.Set() {
 			return true
diff --git a/gen/options.go b/gen/options.go
index fec58e73..8493be82 100644
--- a/gen/options.go
+++ b/gen/options.go
@@ -8,6 +8,7 @@ import (
 	"go.uber.org/zap"
 
 	"github.com/ogen-go/ogen/gen/ir"
+	"github.com/ogen-go/ogen/internal/xslices"
 	"github.com/ogen-go/ogen/openapi"
 )
 
@@ -105,12 +106,7 @@ func (f Filters) accept(op *openapi.Operation) bool {
 	}
 
 	if len(f.Methods) > 0 {
-		for _, m := range f.Methods {
-			if strings.EqualFold(m, op.HTTPMethod) {
-				return true
-			}
-		}
-		return false
+		return xslices.ContainsFunc(f.Methods, func(m string) bool { return strings.EqualFold(m, op.HTTPMethod) })
 	}
 
 	return true
diff --git a/gen/router.go b/gen/router.go
index 587fe396..25bc6f4b 100644
--- a/gen/router.go
+++ b/gen/router.go
@@ -7,6 +7,7 @@ import (
 	"github.com/go-faster/errors"
 
 	"github.com/ogen-go/ogen/gen/ir"
+	"github.com/ogen-go/ogen/internal/xslices"
 	"github.com/ogen-go/ogen/openapi"
 )
 
@@ -37,10 +38,8 @@ func (n Routes) Swap(i, j int) {
 
 // AddRoute adds new route. If the route is already added, it returns error.
 func (n *Routes) AddRoute(nr Route) error {
-	for _, r := range *n {
-		if strings.EqualFold(r.Method, nr.Method) {
-			return errors.Errorf("duplicate method %q", nr.Method)
-		}
+	if xslices.ContainsFunc(*n, func(r Route) bool { return strings.EqualFold(r.Method, nr.Method) }) {
+		return errors.Errorf("duplicate method %q", nr.Method)
 	}
 	*n = append(*n, nr)
 	// Keep routes sorted by method.
diff --git a/gen/schema_gen_sum.go b/gen/schema_gen_sum.go
index f6de3722..d8d376ca 100644
--- a/gen/schema_gen_sum.go
+++ b/gen/schema_gen_sum.go
@@ -12,6 +12,7 @@ import (
 
 	"github.com/ogen-go/ogen/gen/ir"
 	"github.com/ogen-go/ogen/internal/xmaps"
+	"github.com/ogen-go/ogen/internal/xslices"
 	"github.com/ogen-go/ogen/jsonschema"
 )
 
@@ -234,14 +235,9 @@ func (g *schemaGen) oneOf(name string, schema *jsonschema.Schema) (*ir.Type, err
 					})
 
 					// Filter discriminator field in-place.
-					n := 0
-					for _, f := range s.Fields {
-						if f.Tag.JSON != propName {
-							s.Fields[n] = f
-							n++
-						}
-					}
-					s.Fields = s.Fields[:n]
+					s.Fields = xslices.Filter(s.Fields, func(f *ir.Field) bool {
+						return f.Tag.JSON != propName
+					})
 				}
 			}
 			if !found {
@@ -364,14 +360,7 @@ func (g *schemaGen) oneOf(name string, schema *jsonschema.Schema) (*ir.Type, err
 				continue
 			}
 			for _, f := range s.JSON().Fields() {
-				var skip bool
-				for _, n := range v.Unique {
-					if n == f.Name {
-						skip = true // not unique
-						break
-					}
-				}
-				if !skip {
+				if !slices.Contains(v.Unique, f.Name) {
 					continue
 				}
 				s.SumSpec.Unique = append(s.SumSpec.Unique, f)
diff --git a/internal/location/lines.go b/internal/location/lines.go
index 3b0dd7d6..d99e5238 100644
--- a/internal/location/lines.go
+++ b/internal/location/lines.go
@@ -2,7 +2,6 @@ package location
 
 import (
 	"bytes"
-	"sort"
 )
 
 // Lines is a sorted slice of newline offsets.
@@ -14,18 +13,6 @@ type Lines struct {
 	lines []int
 }
 
-// Search returns the nearest line number to the given offset.
-//
-// NOTE: may return index bigger than lines length.
-func (l Lines) Search(offset int) int {
-	// The index is the line number.
-	lines := l.lines
-	idx := sort.Search(len(lines), func(i int) bool {
-		return lines[i] >= offset
-	})
-	return idx
-}
-
 // Line returns offset range of the line.
 //
 // NOTE: the line number is 1-based. Returns (-1, -1) if the line is invalid.
diff --git a/internal/xmaps/sorted_keys.go b/internal/xmaps/xmaps.go
similarity index 79%
rename from internal/xmaps/sorted_keys.go
rename to internal/xmaps/xmaps.go
index 79bdb067..9797f5bf 100644
--- a/internal/xmaps/sorted_keys.go
+++ b/internal/xmaps/xmaps.go
@@ -1,3 +1,4 @@
+// Package xmaps provides some generic utilities missed from x/exp/maps.
 package xmaps
 
 import (
diff --git a/internal/xslices/xslices.go b/internal/xslices/xslices.go
new file mode 100644
index 00000000..23ca0f3c
--- /dev/null
+++ b/internal/xslices/xslices.go
@@ -0,0 +1,30 @@
+// Package xslices provides some generic utilities missed from x/exp/slices.
+package xslices
+
+import "golang.org/x/exp/slices"
+
+// Filter performs in-place filtering of a slice.
+func Filter[S ~[]E, E any](s S, keep func(E) bool) S {
+	var n int
+	for _, v := range s {
+		if keep(v) {
+			s[n] = v
+			n++
+		}
+	}
+	return s[:n]
+}
+
+// ContainsFunc returns true if the slice contains an element satisfying the predicate.
+func ContainsFunc[S ~[]E, E any](s S, equal func(E) bool) bool {
+	return slices.IndexFunc(s, equal) >= 0
+}
+
+// FindFunc returns the first element satisfying the predicate.
+func FindFunc[S ~[]E, E any](s S, equal func(E) bool) (r E, _ bool) {
+	idx := slices.IndexFunc(s, equal)
+	if idx < 0 {
+		return r, false
+	}
+	return s[idx], true
+}
diff --git a/jsonschema/parser.go b/jsonschema/parser.go
index 1e8b3e4e..3fe1fc1a 100644
--- a/jsonschema/parser.go
+++ b/jsonschema/parser.go
@@ -7,6 +7,7 @@ import (
 	"regexp"
 
 	"github.com/go-faster/errors"
+	"golang.org/x/exp/slices"
 
 	"github.com/ogen-go/ogen/internal/jsonpointer"
 	"github.com/ogen-go/ogen/internal/location"
@@ -269,12 +270,7 @@ func (p *Parser) parseSchema(schema *RawSchema, ctx *jsonpointer.ResolveCtx, hoo
 		}
 
 		required := func(name string) bool {
-			for _, p := range schema.Required {
-				if p == name {
-					return true
-				}
-			}
-			return false
+			return slices.Contains(schema.Required, name)
 		}
 
 		s := hook(&Schema{
diff --git a/jsonschema/parser_enum.go b/jsonschema/parser_enum.go
index d4d9da34..719b5d47 100644
--- a/jsonschema/parser_enum.go
+++ b/jsonschema/parser_enum.go
@@ -5,6 +5,8 @@ import (
 
 	"github.com/go-faster/errors"
 	"github.com/go-faster/jx"
+
+	"github.com/ogen-go/ogen/internal/xslices"
 )
 
 func inferJSONType(v json.RawMessage) (string, error) {
@@ -110,21 +112,13 @@ func handleNullableEnum(s *Schema) {
 	// Notice that nullable enum requires `null` in value list.
 	//
 	// Check that enum contains `null` value.
-	for _, v := range s.Enum {
-		if v == nil {
-			s.Nullable = true
-			break
-		}
-	}
+	s.Nullable = s.Nullable || xslices.ContainsFunc(s.Enum, func(v any) bool {
+		return v == nil
+	})
 	// Filter all `null`s.
 	if s.Nullable {
-		n := 0
-		for _, v := range s.Enum {
-			if v != nil {
-				s.Enum[n] = v
-				n++
-			}
-		}
-		s.Enum = s.Enum[:n]
+		s.Enum = xslices.Filter(s.Enum, func(v any) bool {
+			return v != nil
+		})
 	}
 }
diff --git a/openapi/parser/path_parser.go b/openapi/parser/path_parser.go
index 60700f78..ffbe7daf 100644
--- a/openapi/parser/path_parser.go
+++ b/openapi/parser/path_parser.go
@@ -7,6 +7,7 @@ import (
 
 	"github.com/go-faster/errors"
 
+	"github.com/ogen-go/ogen/internal/xslices"
 	"github.com/ogen-go/ogen/openapi"
 )
 
@@ -67,12 +68,9 @@ func parsePath(path string, params []*openapi.Parameter) (openapi.Path, error) {
 	return (&pathParser[*openapi.Parameter]{
 		path: path,
 		lookup: func(name string) (*openapi.Parameter, bool) {
-			for _, p := range params {
-				if p.Name == name && p.In == openapi.LocationPath {
-					return p, true
-				}
-			}
-			return nil, false
+			return xslices.FindFunc(params, func(p *openapi.Parameter) bool {
+				return p.Name == name && p.In == openapi.LocationPath
+			})
 		},
 	}).Parse()
 }


Test patch:
diff --git a/internal/xmaps/xmaps_test.go b/internal/xmaps/xmaps_test.go
new file mode 100644
index 00000000..8e025695
--- /dev/null
+++ b/internal/xmaps/xmaps_test.go
@@ -0,0 +1,25 @@
+package xmaps
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestSortedKeys(t *testing.T) {
+	tests := []struct {
+		name string
+		m    map[string]struct{}
+		want []string
+	}{
+		{"Nil", nil, []string{}},
+		{"Empty", map[string]struct{}{}, []string{}},
+		{"One", map[string]struct{}{"a": {}}, []string{"a"}},
+		{"Two", map[string]struct{}{"a": {}, "b": {}}, []string{"a", "b"}},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			require.Equal(t, tt.want, SortedKeys(tt.m))
+		})
+	}
+}
diff --git a/internal/xslices/xslices_test.go b/internal/xslices/xslices_test.go
new file mode 100644
index 00000000..036af426
--- /dev/null
+++ b/internal/xslices/xslices_test.go
@@ -0,0 +1,59 @@
+package xslices
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestFilter(t *testing.T) {
+	a := require.New(t)
+
+	odd := func(x int) bool {
+		return x%2 == 1
+	}
+	a.Empty(Filter([]int(nil), odd))
+	a.Empty(Filter([]int{}, odd))
+	a.Equal(Filter([]int{1, 2, 3, 4, 5}, odd), []int{1, 3, 5})
+}
+
+func TestContainsFunc(t *testing.T) {
+	a := require.New(t)
+	foldEqual := func(a string) func(string) bool {
+		return func(b string) bool {
+			return strings.EqualFold(a, b)
+		}
+	}
+	a.False(ContainsFunc([]string(nil), foldEqual("a")))
+	a.False(ContainsFunc([]string{}, foldEqual("a")))
+	a.True(ContainsFunc([]string{"a"}, foldEqual("a")))
+	a.True(ContainsFunc([]string{"A"}, foldEqual("a")))
+	a.True(ContainsFunc([]string{"foo"}, foldEqual("FoO")))
+	a.True(ContainsFunc([]string{"FoO"}, foldEqual("foo")))
+}
+
+func TestFindFunc(t *testing.T) {
+	findA := func(s string) bool {
+		return s == "a"
+	}
+	tests := []struct {
+		r      []string
+		f      func(string) bool
+		wantR  string
+		wantOk bool
+	}{
+		{nil, findA, "", false},
+		{[]string{}, findA, "", false},
+		{[]string{"b"}, findA, "", false},
+		{[]string{"a", "b"}, findA, "a", true},
+		{[]string{"b", "a"}, findA, "a", true},
+		{[]string{"a", "a"}, findA, "a", true},
+	}
+	a := require.New(t)
+	for _, tt := range tests {
+		gotR, gotOk := FindFunc(tt.r, tt.f)
+		a.Equal(tt.wantR, gotR)
+		a.Equal(tt.wantOk, gotOk)
+	}
+}
diff --git a/jsonschema/_testdata/negative/enum/duplicate_string_value.json b/jsonschema/_testdata/negative/enum/duplicate_string_value.json
index 1ab76f20..cbdb02f9 100644
--- a/jsonschema/_testdata/negative/enum/duplicate_string_value.json
+++ b/jsonschema/_testdata/negative/enum/duplicate_string_value.json
@@ -2,7 +2,8 @@
   "$schema": "http://json-schema.org/draft-04/schema#",
   "type": "string",
   "enum": [
-    "\n", "\u000a",
-    ""
+    "\n",
+    "",
+    "\u000a"
   ]
 }