From 20e1f43ed191c63967be89ebfaca1c79e416c908 Mon Sep 17 00:00:00 2001 From: Julin <142230457+c-julin@users.noreply.github.com> Date: Thu, 21 May 2026 16:42:35 +0100 Subject: [PATCH 1/3] feat(produce): schema-aware Produce Record backend (UX-1292) Adds schema-introspection + sample-generator infrastructure for the schema-aware Produce Record flow: - proto: new GenerateSchemaSample RPC; extended PublishMessagePayloadOptions with optional schema_id / index_path so the wire format can address nested Protobuf messages. - backend/pkg/schemasample: schema-type-aware sample JSON generator (Avro / Protobuf / JSON Schema), unit-tested. - backend/pkg/console: GetSchemaRegistrySubjectDetails now embeds per-version MessageTypes for Protobuf entries (descriptor walk via cachedSchemaClient), so the produce UI can render a typed message-type picker without a second RPC. /schema-registry/schemas endpoint + new GetAllSchemas service method. Per-version message-type resolution extracted to populateProtoMessageTypes helper to keep the parent function under the cyclop ceiling. - backend/pkg/serde: Protobuf+schemaId payloads now dispatch through the schema-registry serde path so registered subjects resolve correctly. --- .../pkg/api/connect/service/console/mapper.go | 15 +- .../api/connect/service/console/service.go | 27 + backend/pkg/api/handle_schema_registry.go | 71 +++ backend/pkg/api/routes.go | 1 + backend/pkg/console/proto_message_types.go | 55 ++ backend/pkg/console/schema_registry.go | 140 ++++- backend/pkg/console/schema_sample.go | 60 ++ backend/pkg/console/servicer.go | 2 + backend/pkg/proto/service.go | 7 + .../console/v1alpha1/console_service.pb.go | 74 ++- .../console/v1alpha1/console_service.pb.gw.go | 74 ++- .../v1alpha1/console_service_grpc.pb.go | 46 +- .../console_service.connect.go | 44 +- .../console_service.connect.gw.go | 10 +- .../console/v1alpha1/publish_messages.pb.go | 201 +++++-- backend/pkg/schemasample/schemasample.go | 537 ++++++++++++++++++ backend/pkg/schemasample/schemasample_test.go | 405 +++++++++++++ backend/pkg/serde/protobuf_schema.go | 32 +- ...ole_service-ConsoleService_connectquery.ts | 8 + .../console/v1alpha1/console_service_pb.ts | 15 +- .../console/v1alpha1/publish_messages_pb.ts | 56 +- .../console/v1alpha1/console_service.proto | 9 + .../console/v1alpha1/publish_messages.proto | 16 +- 23 files changed, 1800 insertions(+), 105 deletions(-) create mode 100644 backend/pkg/console/proto_message_types.go create mode 100644 backend/pkg/console/schema_sample.go create mode 100644 backend/pkg/schemasample/schemasample.go create mode 100644 backend/pkg/schemasample/schemasample_test.go diff --git a/backend/pkg/api/connect/service/console/mapper.go b/backend/pkg/api/connect/service/console/mapper.go index 218e131c13..6e1f9757b9 100644 --- a/backend/pkg/api/connect/service/console/mapper.go +++ b/backend/pkg/api/connect/service/console/mapper.go @@ -54,6 +54,13 @@ func rpcPublishMessagePayloadOptionsToSerializeInput(po *v1alpha.PublishMessageP encoding = serde.PayloadEncodingProtobufBSR } + // When the client picks Protobuf together with a schema ID, route through the schema-registry + // path (ProtobufSchemaSerde) instead of the static-config ProtobufSerde — the latter requires + // a configured proto.Service and panics when only schema-registry-backed schemas are in use. + if encoding == serde.PayloadEncodingProtobuf && po.GetSchemaId() > 0 { + encoding = serde.PayloadEncodingProtobufSchema + } + input := &serde.RecordPayloadInput{ Payload: po.GetData(), Encoding: encoding, @@ -63,7 +70,13 @@ func rpcPublishMessagePayloadOptionsToSerializeInput(po *v1alpha.PublishMessageP input.Options = []serde.SerdeOpt{serde.WithSchemaID(uint32(po.GetSchemaId()))} } - if po.GetIndex() > 0 { + if path := po.GetIndexPath(); len(path) > 0 { + ints := make([]int, len(path)) + for i, v := range path { + ints[i] = int(v) + } + input.Options = append(input.Options, serde.WithIndex(ints...)) + } else if po.GetIndex() > 0 { input.Options = append(input.Options, serde.WithIndex(int(po.GetIndex()))) } diff --git a/backend/pkg/api/connect/service/console/service.go b/backend/pkg/api/connect/service/console/service.go index 9ab9bcbfe7..e2344a4629 100644 --- a/backend/pkg/api/connect/service/console/service.go +++ b/backend/pkg/api/connect/service/console/service.go @@ -215,3 +215,30 @@ func (api *Service) PublishMessage( }, ), nil } + +// GenerateSchemaSample renders a zero-valued JSON skeleton for any Schema +// Registry-backed schema (Avro / Protobuf / JSON Schema). The backend +// dispatches on the registered schema type so the frontend can call a single +// RPC regardless of which encoding the user picked. +func (api *Service) GenerateSchemaSample( + ctx context.Context, + req *connect.Request[v1alpha.GenerateSchemaSampleRequest], +) (*connect.Response[v1alpha.GenerateSchemaSampleResponse], error) { + indexPath := make([]int, 0, len(req.Msg.GetIndexPath())) + for _, v := range req.Msg.GetIndexPath() { + indexPath = append(indexPath, int(v)) + } + + sample, err := api.consoleSvc.GenerateSchemaSampleJSON(ctx, int(req.Msg.GetSchemaId()), indexPath) + if err != nil { + return nil, apierrors.NewConnectError( + connect.CodeInvalidArgument, + fmt.Errorf("failed to generate schema sample JSON: %w", err), + apierrors.NewErrorInfo(commonv1alpha1.Reason_REASON_INVALID_INPUT.String()), + ) + } + + return connect.NewResponse(&v1alpha.GenerateSchemaSampleResponse{ + SampleJson: string(sample), + }), nil +} diff --git a/backend/pkg/api/handle_schema_registry.go b/backend/pkg/api/handle_schema_registry.go index 886ae9d673..7078fb3114 100644 --- a/backend/pkg/api/handle_schema_registry.go +++ b/backend/pkg/api/handle_schema_registry.go @@ -464,6 +464,77 @@ func (api *API) handleGetSchemaSubjects() http.HandlerFunc { } } +func (api *API) handleGetAllSchemas() http.HandlerFunc { + if !api.Cfg.SchemaRegistry.Enabled { + return api.handleSchemaRegistryNotConfigured() + } + + parseBool := func(r *http.Request, name string) (bool, error) { + raw := rest.GetQueryParam(r, name) + if raw == "" { + return false, nil + } + v, err := strconv.ParseBool(raw) + if err != nil { + return false, fmt.Errorf("invalid %q query param: %w", name, err) + } + return v, nil + } + parseInt := func(r *http.Request, name string) (int, error) { + raw := rest.GetQueryParam(r, name) + if raw == "" { + return 0, nil + } + v, err := strconv.Atoi(raw) + if err != nil { + return 0, fmt.Errorf("invalid %q query param: %w", name, err) + } + if v < 0 { + return 0, fmt.Errorf("invalid %q query param: must be non-negative", name) + } + return v, nil + } + + return func(w http.ResponseWriter, r *http.Request) { + opts := console.GetAllSchemasOptions{ + SubjectPrefix: rest.GetQueryParam(r, "subjectPrefix"), + } + var err error + if opts.LatestOnly, err = parseBool(r, "latestOnly"); err != nil { + rest.SendRESTError(w, r, api.Logger, &rest.Error{Err: err, Status: http.StatusBadRequest, Message: err.Error()}) + return + } + if opts.Deleted, err = parseBool(r, "deleted"); err != nil { + rest.SendRESTError(w, r, api.Logger, &rest.Error{Err: err, Status: http.StatusBadRequest, Message: err.Error()}) + return + } + if opts.DeletedOnly, err = parseBool(r, "deletedOnly"); err != nil { + rest.SendRESTError(w, r, api.Logger, &rest.Error{Err: err, Status: http.StatusBadRequest, Message: err.Error()}) + return + } + if opts.Offset, err = parseInt(r, "offset"); err != nil { + rest.SendRESTError(w, r, api.Logger, &rest.Error{Err: err, Status: http.StatusBadRequest, Message: err.Error()}) + return + } + if opts.Limit, err = parseInt(r, "limit"); err != nil { + rest.SendRESTError(w, r, api.Logger, &rest.Error{Err: err, Status: http.StatusBadRequest, Message: err.Error()}) + return + } + + res, err := api.ConsoleSvc.GetAllSchemas(r.Context(), opts) + if err != nil { + rest.SendRESTError(w, r, api.Logger, &rest.Error{ + Err: err, + Status: http.StatusBadGateway, + Message: fmt.Sprintf("Failed to retrieve schemas from the schema registry: %v", err.Error()), + IsSilent: false, + }) + return + } + rest.SendResponse(w, r, api.Logger, http.StatusOK, res) + } +} + func (api *API) handleGetSchemaSubjectDetails() http.HandlerFunc { if !api.Cfg.SchemaRegistry.Enabled { return api.handleSchemaRegistryNotConfigured() diff --git a/backend/pkg/api/routes.go b/backend/pkg/api/routes.go index fe9fde15f3..a16f6f650d 100644 --- a/backend/pkg/api/routes.go +++ b/backend/pkg/api/routes.go @@ -635,6 +635,7 @@ func (api *API) routes() *chi.Mux { r.Delete("/schema-registry/config/{subject}", api.handleDeleteSchemaRegistrySubjectConfig()) r.Get("/schema-registry/contexts", api.handleGetSchemaRegistryContexts()) r.Get("/schema-registry/subjects", api.handleGetSchemaSubjects()) + r.Get("/schema-registry/schemas", api.handleGetAllSchemas()) r.Get("/schema-registry/schemas/types", api.handleGetSchemaRegistrySchemaTypes()) r.Get("/schema-registry/schemas/ids/{id}/versions", api.handleGetSchemaUsagesByID()) r.Delete("/schema-registry/subjects/{subject}", api.handleDeleteSubject()) diff --git a/backend/pkg/console/proto_message_types.go b/backend/pkg/console/proto_message_types.go new file mode 100644 index 0000000000..f30fea02ac --- /dev/null +++ b/backend/pkg/console/proto_message_types.go @@ -0,0 +1,55 @@ +// Copyright 2026 Redpanda Data, Inc. +// +// Use of this software is governed by the Business Source License +// included in the file https://github.com/redpanda-data/redpanda/blob/dev/licenses/bsl.md +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0 + +package console + +import ( + "context" + "errors" + "fmt" + + "google.golang.org/protobuf/reflect/protoreflect" + + "github.com/redpanda-data/console/backend/pkg/proto" +) + +// protoMessageTypesByID walks the descriptor tree of the schema, returning each message paired with +// its Confluent wire-format index path. Goes through cachedSchemaClient so it works without the +// optional static Protobuf service. +func (s *Service) protoMessageTypesByID(ctx context.Context, schemaID int) ([]proto.MessageTypeInfo, error) { + if s.cachedSchemaClient == nil { + return nil, errors.New("schema registry is not configured") + } + + files, rootFilename, err := s.cachedSchemaClient.ProtoFilesByID(ctx, schemaID) + if err != nil { + return nil, fmt.Errorf("failed to load proto files for schema %d: %w", schemaID, err) + } + + rootFile := files.FindFileByPath(rootFilename) + if rootFile == nil { + return nil, fmt.Errorf("root proto file %q not found for schema %d", rootFilename, schemaID) + } + + var out []proto.MessageTypeInfo + walkMessageTypes(rootFile.Messages(), nil, &out) + return out, nil +} + +func walkMessageTypes(msgs protoreflect.MessageDescriptors, prefix []int32, out *[]proto.MessageTypeInfo) { + for i := 0; i < msgs.Len(); i++ { + md := msgs.Get(i) + path := append(append([]int32(nil), prefix...), int32(i)) + *out = append(*out, proto.MessageTypeInfo{ + FullyQualifiedName: string(md.FullName()), + IndexPath: path, + }) + walkMessageTypes(md.Messages(), path, out) + } +} diff --git a/backend/pkg/console/schema_registry.go b/backend/pkg/console/schema_registry.go index 6e6b43c073..c04a39ab14 100644 --- a/backend/pkg/console/schema_registry.go +++ b/backend/pkg/console/schema_registry.go @@ -25,6 +25,8 @@ import ( "github.com/twmb/franz-go/pkg/sr" "golang.org/x/exp/slices" "golang.org/x/sync/errgroup" + + "github.com/redpanda-data/console/backend/pkg/proto" ) // SchemaRegistryMode returns the schema registry mode. @@ -214,6 +216,101 @@ func (s *Service) GetSchemaRegistrySubjects(ctx context.Context, subjectPrefix s return result, nil } +// SchemaRegistrySchema is a single (subject, version) entry returned by the +// registry's GET /schemas endpoint. The shape mirrors sr.SubjectSchema so the +// payload is suitable for a future dataplane API surface. +type SchemaRegistrySchema struct { + Subject string `json:"subject"` + Version int `json:"version"` + ID int `json:"id"` + Type sr.SchemaType `json:"type"` + Schema string `json:"schema,omitempty"` + References []Reference `json:"references,omitempty"` + Metadata *SchemaMetadata `json:"metadata,omitempty"` +} + +// GetAllSchemasOptions controls the query parameters forwarded to the schema +// registry's GET /schemas endpoint. +type GetAllSchemasOptions struct { + SubjectPrefix string + LatestOnly bool + Deleted bool // include soft-deleted entries + DeletedOnly bool + Offset int + Limit int +} + +// GetAllSchemas lists schemas from the registry's GET /schemas endpoint and +// returns the full (subject, version, id, type, schema, references, metadata) +// for each entry. Callers control filtering and pagination via opts. +func (s *Service) GetAllSchemas(ctx context.Context, opts GetAllSchemasOptions) ([]SchemaRegistrySchema, error) { + srClient, err := s.schemaClientFactory.GetSchemaRegistryClient(ctx) + if err != nil { + return nil, err + } + + params := make([]sr.Param, 0, 6) + if opts.SubjectPrefix != "" { + params = append(params, sr.SubjectPrefix(opts.SubjectPrefix)) + } + if opts.LatestOnly { + params = append(params, sr.LatestOnly) + } + if opts.Deleted { + params = append(params, sr.ShowDeleted) + } + if opts.DeletedOnly { + params = append(params, sr.DeletedOnly) + } + if opts.Offset > 0 { + params = append(params, sr.Offset(opts.Offset)) + } + if opts.Limit > 0 { + params = append(params, sr.Limit(opts.Limit)) + } + + schemas, err := srClient.AllSchemas(sr.WithParams(ctx, params...)) + if err != nil { + return nil, err + } + + result := make([]SchemaRegistrySchema, 0, len(schemas)) + for _, schema := range schemas { + references := make([]Reference, len(schema.References)) + for i, ref := range schema.References { + references[i] = Reference{ + Name: ref.Name, + Subject: ref.Subject, + Version: ref.Version, + } + } + var metadata *SchemaMetadata + if schema.SchemaMetadata != nil { + metadata = &SchemaMetadata{ + Tags: schema.SchemaMetadata.Tags, + Properties: schema.SchemaMetadata.Properties, + Sensitive: schema.SchemaMetadata.Sensitive, + } + } + result = append(result, SchemaRegistrySchema{ + Subject: schema.Subject, + Version: schema.Version, + ID: schema.ID, + Type: schema.Type, + Schema: schema.Schema.Schema, + References: references, + Metadata: metadata, + }) + } + slices.SortFunc(result, func(a, b SchemaRegistrySchema) int { + if c := strings.Compare(a.Subject, b.Subject); c != 0 { + return c + } + return a.Version - b.Version + }) + return result, nil +} + // SchemaRegistrySubjectDetails represents a schema registry subject along // with other information such as the registered versions that belong to it, // or the full schema information that's part of the subject. @@ -344,6 +441,8 @@ func (s *Service) GetSchemaRegistrySubjectDetails(ctx context.Context, subjectNa return nil, err } + s.populateProtoMessageTypes(ctx, subjectName, schemas) + var schemaType sr.SchemaType if len(schemas) > 0 { schemaType = schemas[len(schemas)-1].Type @@ -360,6 +459,32 @@ func (s *Service) GetSchemaRegistrySubjectDetails(ctx context.Context, subjectNa }, nil } +// populateProtoMessageTypes resolves message types for every Protobuf version in schemas. Failures +// are non-fatal: a parse error leaves MessageTypes nil rather than failing the parent call. +func (s *Service) populateProtoMessageTypes(ctx context.Context, subjectName string, schemas []SchemaRegistryVersionedSchema) { + grp, grpCtx := errgroup.WithContext(ctx) + grp.SetLimit(10) + for i := range schemas { + if schemas[i].Type != sr.TypeProtobuf { + continue + } + i := i + grp.Go(func() error { + types, err := s.protoMessageTypesByID(grpCtx, schemas[i].ID) + if err != nil { + s.logger.WarnContext(grpCtx, "failed to resolve protobuf message types", + slog.String("subject", subjectName), + slog.Int("schemaId", schemas[i].ID), + slog.Any("err", err)) + return nil + } + schemas[i].MessageTypes = types + return nil + }) + } + _ = grp.Wait() +} + // SchemaRegistrySubjectDetailsVersion represents a schema version and if it's // soft-deleted or not. type SchemaRegistrySubjectDetailsVersion struct { @@ -486,13 +611,14 @@ func (s *Service) getSubjectMode(ctx context.Context, srClient *rpsr.Client, sub // SchemaRegistryVersionedSchema describes a retrieved schema. type SchemaRegistryVersionedSchema struct { - ID int `json:"id"` - Version int `json:"version"` - IsSoftDeleted bool `json:"isSoftDeleted"` - Type sr.SchemaType `json:"type"` - Schema string `json:"schema"` - References []Reference `json:"references"` - Metadata *SchemaMetadata `json:"metadata,omitempty"` + ID int `json:"id"` + Version int `json:"version"` + IsSoftDeleted bool `json:"isSoftDeleted"` + Type sr.SchemaType `json:"type"` + Schema string `json:"schema"` + References []Reference `json:"references"` + Metadata *SchemaMetadata `json:"metadata,omitempty"` + MessageTypes []proto.MessageTypeInfo `json:"messageTypes,omitempty"` // Protobuf only. } // Reference describes a reference to a different schema stored in the schema registry. diff --git a/backend/pkg/console/schema_sample.go b/backend/pkg/console/schema_sample.go new file mode 100644 index 0000000000..749f6a40ac --- /dev/null +++ b/backend/pkg/console/schema_sample.go @@ -0,0 +1,60 @@ +// Copyright 2026 Redpanda Data, Inc. +// +// Use of this software is governed by the Business Source License +// included in the file https://github.com/redpanda-data/redpanda/blob/dev/licenses/bsl.md +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0 + +package console + +import ( + "context" + "errors" + "fmt" + + "github.com/twmb/franz-go/pkg/sr" + + "github.com/redpanda-data/console/backend/pkg/schemasample" +) + +// GenerateSchemaSampleJSON returns a zero-valued JSON skeleton for the schema +// identified by schemaID. The shape depends on the schema's registered type: +// +// - AVRO: walks the schema JSON and emits valid Avro JSON encoding (unions +// including null serialize as null; non-null unions wrap the chosen branch). +// - JSON: walks the JSON Schema and emits zero values per type (string="", +// integer/number=0, boolean=false, array=[], object={...}). +// - PROTOBUF: resolves the descriptor via the schema-registry cache, then +// marshals an empty dynamicpb message with EmitDefaultValues. indexPath +// selects the message inside the schema; empty means first top-level. +func (s *Service) GenerateSchemaSampleJSON(ctx context.Context, schemaID int, indexPath []int) ([]byte, error) { + if s.cachedSchemaClient == nil { + return nil, errors.New("schema registry is not configured") + } + + sch, err := s.cachedSchemaClient.SchemaByID(ctx, schemaID) + if err != nil { + return nil, fmt.Errorf("failed to load schema %d: %w", schemaID, err) + } + + switch sch.Type { + case sr.TypeAvro: + return schemasample.Avro(sch.Schema) + case sr.TypeJSON: + return schemasample.JSONSchema(sch.Schema) + case sr.TypeProtobuf: + return s.generateProtobufSample(ctx, schemaID, indexPath) + default: + return nil, fmt.Errorf("unsupported schema type %q for sample generation", sch.Type.String()) + } +} + +func (s *Service) generateProtobufSample(ctx context.Context, schemaID int, indexPath []int) ([]byte, error) { + files, rootFilename, err := s.cachedSchemaClient.ProtoFilesByID(ctx, schemaID) + if err != nil { + return nil, fmt.Errorf("failed to load proto files for schema %d: %w", schemaID, err) + } + return schemasample.Protobuf(files, rootFilename, indexPath) +} diff --git a/backend/pkg/console/servicer.go b/backend/pkg/console/servicer.go index d9628670e7..63a3691185 100644 --- a/backend/pkg/console/servicer.go +++ b/backend/pkg/console/servicer.go @@ -41,6 +41,7 @@ type Servicer interface { AlterPartitionAssignments(ctx context.Context, topics []kmsg.AlterPartitionAssignmentsRequestTopic) ([]AlterPartitionReassignmentsResponse, error) ProducePlainRecords(ctx context.Context, records []*kgo.Record, useTransactions bool, compressionOpts []kgo.CompressionCodec) ProduceRecordsResponse ProduceRecord(context.Context, string, int32, []kgo.RecordHeader, *serde.RecordPayloadInput, *serde.RecordPayloadInput, bool, []kgo.CompressionCodec) (*ProduceRecordResponse, error) + GenerateSchemaSampleJSON(ctx context.Context, schemaID int, indexPath []int) ([]byte, error) Start(ctx context.Context) error Stop() GetTopicConfigs(ctx context.Context, topicName string, configNames []string) (*TopicConfig, *rest.Error) @@ -98,6 +99,7 @@ type SchemaRegistryServicer interface { PutSchemaRegistryConfig(ctx context.Context, subject string, compatibility sr.SetCompatibility) (*SchemaRegistryConfig, error) DeleteSchemaRegistrySubjectConfig(ctx context.Context, subject string) error GetSchemaRegistrySubjects(ctx context.Context, subjectPrefix string) ([]SchemaRegistrySubject, error) + GetAllSchemas(ctx context.Context, opts GetAllSchemasOptions) ([]SchemaRegistrySchema, error) GetSchemaRegistrySubjectDetails(ctx context.Context, subjectName string, version string) (*SchemaRegistrySubjectDetails, error) GetSchemaRegistrySchemaReferencedBy(ctx context.Context, subjectName string, version int) ([]SchemaReference, error) DeleteSchemaRegistrySubject(ctx context.Context, subjectName string, deletePermanently bool) (*SchemaRegistryDeleteSubjectResponse, error) diff --git a/backend/pkg/proto/service.go b/backend/pkg/proto/service.go index e016a4d8b3..3b53e6b1ca 100644 --- a/backend/pkg/proto/service.go +++ b/backend/pkg/proto/service.go @@ -225,6 +225,13 @@ func (s *Service) GetMessageDescriptorForSchema(schemaID int, index []int) (prot return messageDescriptor, nil } +// MessageTypeInfo pairs a Protobuf message's fully-qualified name with its Confluent wire-format +// index path. +type MessageTypeInfo struct { + FullyQualifiedName string + IndexPath []int32 +} + // SerializeJSONToConfluentProtobufMessage serialized the JSON message to confluent wrapped payload // using the schema ID and message index. func (s *Service) SerializeJSONToConfluentProtobufMessage(json []byte, schemaID int, index []int) ([]byte, error) { diff --git a/backend/pkg/protogen/redpanda/api/console/v1alpha1/console_service.pb.go b/backend/pkg/protogen/redpanda/api/console/v1alpha1/console_service.pb.go index 2035024b2f..ade5242360 100644 --- a/backend/pkg/protogen/redpanda/api/console/v1alpha1/console_service.pb.go +++ b/backend/pkg/protogen/redpanda/api/console/v1alpha1/console_service.pb.go @@ -39,7 +39,7 @@ var file_redpanda_api_console_v1alpha1_console_service_proto_rawDesc = []byte{ 0x6f, 0x1a, 0x34, 0x72, 0x65, 0x64, 0x70, 0x61, 0x6e, 0x64, 0x61, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x63, 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, - 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x32, 0xa0, 0x02, 0x0a, 0x0e, 0x43, 0x6f, 0x6e, 0x73, + 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x32, 0xbc, 0x03, 0x0a, 0x0e, 0x43, 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x83, 0x01, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x12, 0x32, 0x2e, 0x72, 0x65, 0x64, 0x70, 0x61, 0x6e, 0x64, 0x61, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x6f, @@ -57,42 +57,56 @@ var file_redpanda_api_console_v1alpha1_console_service_proto_rawDesc = []byte{ 0x61, 0x6e, 0x64, 0x61, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x22, 0x08, 0x8a, 0xa6, 0x1d, 0x04, 0x08, 0x02, 0x10, 0x01, 0x42, 0xb4, 0x02, 0x0a, 0x21, 0x63, - 0x6f, 0x6d, 0x2e, 0x72, 0x65, 0x64, 0x70, 0x61, 0x6e, 0x64, 0x61, 0x2e, 0x61, 0x70, 0x69, 0x2e, - 0x63, 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, - 0x42, 0x13, 0x43, 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x63, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, - 0x63, 0x6f, 0x6d, 0x2f, 0x72, 0x65, 0x64, 0x70, 0x61, 0x6e, 0x64, 0x61, 0x2d, 0x64, 0x61, 0x74, - 0x61, 0x2f, 0x63, 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, - 0x64, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x67, 0x65, 0x6e, 0x2f, 0x72, - 0x65, 0x64, 0x70, 0x61, 0x6e, 0x64, 0x61, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x63, 0x6f, 0x6e, 0x73, - 0x6f, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x3b, 0x63, 0x6f, 0x6e, - 0x73, 0x6f, 0x6c, 0x65, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0xa2, 0x02, 0x03, 0x52, - 0x41, 0x43, 0xaa, 0x02, 0x1d, 0x52, 0x65, 0x64, 0x70, 0x61, 0x6e, 0x64, 0x61, 0x2e, 0x41, 0x70, - 0x69, 0x2e, 0x43, 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65, 0x2e, 0x56, 0x31, 0x61, 0x6c, 0x70, 0x68, - 0x61, 0x31, 0xca, 0x02, 0x1d, 0x52, 0x65, 0x64, 0x70, 0x61, 0x6e, 0x64, 0x61, 0x5c, 0x41, 0x70, - 0x69, 0x5c, 0x43, 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65, 0x5c, 0x56, 0x31, 0x61, 0x6c, 0x70, 0x68, - 0x61, 0x31, 0xe2, 0x02, 0x29, 0x52, 0x65, 0x64, 0x70, 0x61, 0x6e, 0x64, 0x61, 0x5c, 0x41, 0x70, - 0x69, 0x5c, 0x43, 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65, 0x5c, 0x56, 0x31, 0x61, 0x6c, 0x70, 0x68, - 0x61, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, - 0x20, 0x52, 0x65, 0x64, 0x70, 0x61, 0x6e, 0x64, 0x61, 0x3a, 0x3a, 0x41, 0x70, 0x69, 0x3a, 0x3a, - 0x43, 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65, 0x3a, 0x3a, 0x56, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, - 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x22, 0x08, 0x8a, 0xa6, 0x1d, 0x04, 0x08, 0x02, 0x10, 0x01, 0x12, 0x99, 0x01, 0x0a, 0x14, 0x47, + 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x53, 0x61, 0x6d, + 0x70, 0x6c, 0x65, 0x12, 0x3a, 0x2e, 0x72, 0x65, 0x64, 0x70, 0x61, 0x6e, 0x64, 0x61, 0x2e, 0x61, + 0x70, 0x69, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, + 0x68, 0x61, 0x31, 0x2e, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x53, 0x63, 0x68, 0x65, + 0x6d, 0x61, 0x53, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x3b, 0x2e, 0x72, 0x65, 0x64, 0x70, 0x61, 0x6e, 0x64, 0x61, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x63, + 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, + 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x53, 0x61, + 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x08, 0x8a, 0xa6, + 0x1d, 0x04, 0x08, 0x01, 0x10, 0x01, 0x42, 0xb4, 0x02, 0x0a, 0x21, 0x63, 0x6f, 0x6d, 0x2e, 0x72, + 0x65, 0x64, 0x70, 0x61, 0x6e, 0x64, 0x61, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x63, 0x6f, 0x6e, 0x73, + 0x6f, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x42, 0x13, 0x43, 0x6f, + 0x6e, 0x73, 0x6f, 0x6c, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x50, 0x72, 0x6f, 0x74, + 0x6f, 0x50, 0x01, 0x5a, 0x63, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, + 0x72, 0x65, 0x64, 0x70, 0x61, 0x6e, 0x64, 0x61, 0x2d, 0x64, 0x61, 0x74, 0x61, 0x2f, 0x63, 0x6f, + 0x6e, 0x73, 0x6f, 0x6c, 0x65, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x2f, 0x70, 0x6b, + 0x67, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x67, 0x65, 0x6e, 0x2f, 0x72, 0x65, 0x64, 0x70, 0x61, + 0x6e, 0x64, 0x61, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x63, 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65, 0x2f, + 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x3b, 0x63, 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65, + 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0xa2, 0x02, 0x03, 0x52, 0x41, 0x43, 0xaa, 0x02, + 0x1d, 0x52, 0x65, 0x64, 0x70, 0x61, 0x6e, 0x64, 0x61, 0x2e, 0x41, 0x70, 0x69, 0x2e, 0x43, 0x6f, + 0x6e, 0x73, 0x6f, 0x6c, 0x65, 0x2e, 0x56, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0xca, 0x02, + 0x1d, 0x52, 0x65, 0x64, 0x70, 0x61, 0x6e, 0x64, 0x61, 0x5c, 0x41, 0x70, 0x69, 0x5c, 0x43, 0x6f, + 0x6e, 0x73, 0x6f, 0x6c, 0x65, 0x5c, 0x56, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0xe2, 0x02, + 0x29, 0x52, 0x65, 0x64, 0x70, 0x61, 0x6e, 0x64, 0x61, 0x5c, 0x41, 0x70, 0x69, 0x5c, 0x43, 0x6f, + 0x6e, 0x73, 0x6f, 0x6c, 0x65, 0x5c, 0x56, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x5c, 0x47, + 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x20, 0x52, 0x65, 0x64, + 0x70, 0x61, 0x6e, 0x64, 0x61, 0x3a, 0x3a, 0x41, 0x70, 0x69, 0x3a, 0x3a, 0x43, 0x6f, 0x6e, 0x73, + 0x6f, 0x6c, 0x65, 0x3a, 0x3a, 0x56, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x62, 0x06, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x33, } var file_redpanda_api_console_v1alpha1_console_service_proto_goTypes = []any{ - (*ListMessagesRequest)(nil), // 0: redpanda.api.console.v1alpha1.ListMessagesRequest - (*PublishMessageRequest)(nil), // 1: redpanda.api.console.v1alpha1.PublishMessageRequest - (*ListMessagesResponse)(nil), // 2: redpanda.api.console.v1alpha1.ListMessagesResponse - (*PublishMessageResponse)(nil), // 3: redpanda.api.console.v1alpha1.PublishMessageResponse + (*ListMessagesRequest)(nil), // 0: redpanda.api.console.v1alpha1.ListMessagesRequest + (*PublishMessageRequest)(nil), // 1: redpanda.api.console.v1alpha1.PublishMessageRequest + (*GenerateSchemaSampleRequest)(nil), // 2: redpanda.api.console.v1alpha1.GenerateSchemaSampleRequest + (*ListMessagesResponse)(nil), // 3: redpanda.api.console.v1alpha1.ListMessagesResponse + (*PublishMessageResponse)(nil), // 4: redpanda.api.console.v1alpha1.PublishMessageResponse + (*GenerateSchemaSampleResponse)(nil), // 5: redpanda.api.console.v1alpha1.GenerateSchemaSampleResponse } var file_redpanda_api_console_v1alpha1_console_service_proto_depIdxs = []int32{ 0, // 0: redpanda.api.console.v1alpha1.ConsoleService.ListMessages:input_type -> redpanda.api.console.v1alpha1.ListMessagesRequest 1, // 1: redpanda.api.console.v1alpha1.ConsoleService.PublishMessage:input_type -> redpanda.api.console.v1alpha1.PublishMessageRequest - 2, // 2: redpanda.api.console.v1alpha1.ConsoleService.ListMessages:output_type -> redpanda.api.console.v1alpha1.ListMessagesResponse - 3, // 3: redpanda.api.console.v1alpha1.ConsoleService.PublishMessage:output_type -> redpanda.api.console.v1alpha1.PublishMessageResponse - 2, // [2:4] is the sub-list for method output_type - 0, // [0:2] is the sub-list for method input_type + 2, // 2: redpanda.api.console.v1alpha1.ConsoleService.GenerateSchemaSample:input_type -> redpanda.api.console.v1alpha1.GenerateSchemaSampleRequest + 3, // 3: redpanda.api.console.v1alpha1.ConsoleService.ListMessages:output_type -> redpanda.api.console.v1alpha1.ListMessagesResponse + 4, // 4: redpanda.api.console.v1alpha1.ConsoleService.PublishMessage:output_type -> redpanda.api.console.v1alpha1.PublishMessageResponse + 5, // 5: redpanda.api.console.v1alpha1.ConsoleService.GenerateSchemaSample:output_type -> redpanda.api.console.v1alpha1.GenerateSchemaSampleResponse + 3, // [3:6] is the sub-list for method output_type + 0, // [0:3] is the sub-list for method input_type 0, // [0:0] is the sub-list for extension type_name 0, // [0:0] is the sub-list for extension extendee 0, // [0:0] is the sub-list for field type_name diff --git a/backend/pkg/protogen/redpanda/api/console/v1alpha1/console_service.pb.gw.go b/backend/pkg/protogen/redpanda/api/console/v1alpha1/console_service.pb.gw.go index efc0cf2777..f17fb3027d 100644 --- a/backend/pkg/protogen/redpanda/api/console/v1alpha1/console_service.pb.gw.go +++ b/backend/pkg/protogen/redpanda/api/console/v1alpha1/console_service.pb.gw.go @@ -85,6 +85,33 @@ func local_request_ConsoleService_PublishMessage_0(ctx context.Context, marshale return msg, metadata, err } +func request_ConsoleService_GenerateSchemaSample_0(ctx context.Context, marshaler runtime.Marshaler, client ConsoleServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GenerateSchemaSampleRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + msg, err := client.GenerateSchemaSample(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_ConsoleService_GenerateSchemaSample_0(ctx context.Context, marshaler runtime.Marshaler, server ConsoleServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GenerateSchemaSampleRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.GenerateSchemaSample(ctx, &protoReq) + return msg, metadata, err +} + // RegisterConsoleServiceHandlerServer registers the http handlers for service ConsoleService to "mux". // UnaryRPC :call ConsoleServiceServer directly. // StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. @@ -117,6 +144,26 @@ func RegisterConsoleServiceHandlerServer(ctx context.Context, mux *runtime.Serve } forward_ConsoleService_PublishMessage_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) + mux.Handle(http.MethodPost, pattern_ConsoleService_GenerateSchemaSample_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/redpanda.api.console.v1alpha1.ConsoleService/GenerateSchemaSample", runtime.WithHTTPPathPattern("/redpanda.api.console.v1alpha1.ConsoleService/GenerateSchemaSample")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_ConsoleService_GenerateSchemaSample_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_ConsoleService_GenerateSchemaSample_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) return nil } @@ -191,15 +238,34 @@ func RegisterConsoleServiceHandlerClient(ctx context.Context, mux *runtime.Serve } forward_ConsoleService_PublishMessage_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) + mux.Handle(http.MethodPost, pattern_ConsoleService_GenerateSchemaSample_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/redpanda.api.console.v1alpha1.ConsoleService/GenerateSchemaSample", runtime.WithHTTPPathPattern("/redpanda.api.console.v1alpha1.ConsoleService/GenerateSchemaSample")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_ConsoleService_GenerateSchemaSample_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_ConsoleService_GenerateSchemaSample_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) return nil } var ( - pattern_ConsoleService_ListMessages_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"redpanda.api.console.v1alpha1.ConsoleService", "ListMessages"}, "")) - pattern_ConsoleService_PublishMessage_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"redpanda.api.console.v1alpha1.ConsoleService", "PublishMessage"}, "")) + pattern_ConsoleService_ListMessages_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"redpanda.api.console.v1alpha1.ConsoleService", "ListMessages"}, "")) + pattern_ConsoleService_PublishMessage_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"redpanda.api.console.v1alpha1.ConsoleService", "PublishMessage"}, "")) + pattern_ConsoleService_GenerateSchemaSample_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"redpanda.api.console.v1alpha1.ConsoleService", "GenerateSchemaSample"}, "")) ) var ( - forward_ConsoleService_ListMessages_0 = runtime.ForwardResponseStream - forward_ConsoleService_PublishMessage_0 = runtime.ForwardResponseMessage + forward_ConsoleService_ListMessages_0 = runtime.ForwardResponseStream + forward_ConsoleService_PublishMessage_0 = runtime.ForwardResponseMessage + forward_ConsoleService_GenerateSchemaSample_0 = runtime.ForwardResponseMessage ) diff --git a/backend/pkg/protogen/redpanda/api/console/v1alpha1/console_service_grpc.pb.go b/backend/pkg/protogen/redpanda/api/console/v1alpha1/console_service_grpc.pb.go index f0aad34001..4a3f53319e 100644 --- a/backend/pkg/protogen/redpanda/api/console/v1alpha1/console_service_grpc.pb.go +++ b/backend/pkg/protogen/redpanda/api/console/v1alpha1/console_service_grpc.pb.go @@ -20,8 +20,9 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( - ConsoleService_ListMessages_FullMethodName = "/redpanda.api.console.v1alpha1.ConsoleService/ListMessages" - ConsoleService_PublishMessage_FullMethodName = "/redpanda.api.console.v1alpha1.ConsoleService/PublishMessage" + ConsoleService_ListMessages_FullMethodName = "/redpanda.api.console.v1alpha1.ConsoleService/ListMessages" + ConsoleService_PublishMessage_FullMethodName = "/redpanda.api.console.v1alpha1.ConsoleService/PublishMessage" + ConsoleService_GenerateSchemaSample_FullMethodName = "/redpanda.api.console.v1alpha1.ConsoleService/GenerateSchemaSample" ) // ConsoleServiceClient is the client API for ConsoleService service. @@ -34,6 +35,9 @@ type ConsoleServiceClient interface { ListMessages(ctx context.Context, in *ListMessagesRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ListMessagesResponse], error) // PublishMessage publishes message. PublishMessage(ctx context.Context, in *PublishMessageRequest, opts ...grpc.CallOption) (*PublishMessageResponse, error) + // GenerateSchemaSample renders a JSON skeleton for any Schema Registry-backed + // schema (Avro / Protobuf / JSON Schema). Dispatches by schema type server-side. + GenerateSchemaSample(ctx context.Context, in *GenerateSchemaSampleRequest, opts ...grpc.CallOption) (*GenerateSchemaSampleResponse, error) } type consoleServiceClient struct { @@ -73,6 +77,16 @@ func (c *consoleServiceClient) PublishMessage(ctx context.Context, in *PublishMe return out, nil } +func (c *consoleServiceClient) GenerateSchemaSample(ctx context.Context, in *GenerateSchemaSampleRequest, opts ...grpc.CallOption) (*GenerateSchemaSampleResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GenerateSchemaSampleResponse) + err := c.cc.Invoke(ctx, ConsoleService_GenerateSchemaSample_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // ConsoleServiceServer is the server API for ConsoleService service. // All implementations must embed UnimplementedConsoleServiceServer // for forward compatibility. @@ -83,6 +97,9 @@ type ConsoleServiceServer interface { ListMessages(*ListMessagesRequest, grpc.ServerStreamingServer[ListMessagesResponse]) error // PublishMessage publishes message. PublishMessage(context.Context, *PublishMessageRequest) (*PublishMessageResponse, error) + // GenerateSchemaSample renders a JSON skeleton for any Schema Registry-backed + // schema (Avro / Protobuf / JSON Schema). Dispatches by schema type server-side. + GenerateSchemaSample(context.Context, *GenerateSchemaSampleRequest) (*GenerateSchemaSampleResponse, error) mustEmbedUnimplementedConsoleServiceServer() } @@ -99,6 +116,9 @@ func (UnimplementedConsoleServiceServer) ListMessages(*ListMessagesRequest, grpc func (UnimplementedConsoleServiceServer) PublishMessage(context.Context, *PublishMessageRequest) (*PublishMessageResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method PublishMessage not implemented") } +func (UnimplementedConsoleServiceServer) GenerateSchemaSample(context.Context, *GenerateSchemaSampleRequest) (*GenerateSchemaSampleResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GenerateSchemaSample not implemented") +} func (UnimplementedConsoleServiceServer) mustEmbedUnimplementedConsoleServiceServer() {} func (UnimplementedConsoleServiceServer) testEmbeddedByValue() {} @@ -149,6 +169,24 @@ func _ConsoleService_PublishMessage_Handler(srv interface{}, ctx context.Context return interceptor(ctx, in, info, handler) } +func _ConsoleService_GenerateSchemaSample_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GenerateSchemaSampleRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ConsoleServiceServer).GenerateSchemaSample(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ConsoleService_GenerateSchemaSample_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ConsoleServiceServer).GenerateSchemaSample(ctx, req.(*GenerateSchemaSampleRequest)) + } + return interceptor(ctx, in, info, handler) +} + // ConsoleService_ServiceDesc is the grpc.ServiceDesc for ConsoleService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -160,6 +198,10 @@ var ConsoleService_ServiceDesc = grpc.ServiceDesc{ MethodName: "PublishMessage", Handler: _ConsoleService_PublishMessage_Handler, }, + { + MethodName: "GenerateSchemaSample", + Handler: _ConsoleService_GenerateSchemaSample_Handler, + }, }, Streams: []grpc.StreamDesc{ { diff --git a/backend/pkg/protogen/redpanda/api/console/v1alpha1/consolev1alpha1connect/console_service.connect.go b/backend/pkg/protogen/redpanda/api/console/v1alpha1/consolev1alpha1connect/console_service.connect.go index a9f10348ae..4889ac5f78 100644 --- a/backend/pkg/protogen/redpanda/api/console/v1alpha1/consolev1alpha1connect/console_service.connect.go +++ b/backend/pkg/protogen/redpanda/api/console/v1alpha1/consolev1alpha1connect/console_service.connect.go @@ -41,13 +41,17 @@ const ( // ConsoleServicePublishMessageProcedure is the fully-qualified name of the ConsoleService's // PublishMessage RPC. ConsoleServicePublishMessageProcedure = "/redpanda.api.console.v1alpha1.ConsoleService/PublishMessage" + // ConsoleServiceGenerateSchemaSampleProcedure is the fully-qualified name of the ConsoleService's + // GenerateSchemaSample RPC. + ConsoleServiceGenerateSchemaSampleProcedure = "/redpanda.api.console.v1alpha1.ConsoleService/GenerateSchemaSample" ) // These variables are the protoreflect.Descriptor objects for the RPCs defined in this package. var ( - consoleServiceServiceDescriptor = v1alpha1.File_redpanda_api_console_v1alpha1_console_service_proto.Services().ByName("ConsoleService") - consoleServiceListMessagesMethodDescriptor = consoleServiceServiceDescriptor.Methods().ByName("ListMessages") - consoleServicePublishMessageMethodDescriptor = consoleServiceServiceDescriptor.Methods().ByName("PublishMessage") + consoleServiceServiceDescriptor = v1alpha1.File_redpanda_api_console_v1alpha1_console_service_proto.Services().ByName("ConsoleService") + consoleServiceListMessagesMethodDescriptor = consoleServiceServiceDescriptor.Methods().ByName("ListMessages") + consoleServicePublishMessageMethodDescriptor = consoleServiceServiceDescriptor.Methods().ByName("PublishMessage") + consoleServiceGenerateSchemaSampleMethodDescriptor = consoleServiceServiceDescriptor.Methods().ByName("GenerateSchemaSample") ) // ConsoleServiceClient is a client for the redpanda.api.console.v1alpha1.ConsoleService service. @@ -56,6 +60,9 @@ type ConsoleServiceClient interface { ListMessages(context.Context, *connect.Request[v1alpha1.ListMessagesRequest]) (*connect.ServerStreamForClient[v1alpha1.ListMessagesResponse], error) // PublishMessage publishes message. PublishMessage(context.Context, *connect.Request[v1alpha1.PublishMessageRequest]) (*connect.Response[v1alpha1.PublishMessageResponse], error) + // GenerateSchemaSample renders a JSON skeleton for any Schema Registry-backed + // schema (Avro / Protobuf / JSON Schema). Dispatches by schema type server-side. + GenerateSchemaSample(context.Context, *connect.Request[v1alpha1.GenerateSchemaSampleRequest]) (*connect.Response[v1alpha1.GenerateSchemaSampleResponse], error) } // NewConsoleServiceClient constructs a client for the redpanda.api.console.v1alpha1.ConsoleService @@ -80,13 +87,20 @@ func NewConsoleServiceClient(httpClient connect.HTTPClient, baseURL string, opts connect.WithSchema(consoleServicePublishMessageMethodDescriptor), connect.WithClientOptions(opts...), ), + generateSchemaSample: connect.NewClient[v1alpha1.GenerateSchemaSampleRequest, v1alpha1.GenerateSchemaSampleResponse]( + httpClient, + baseURL+ConsoleServiceGenerateSchemaSampleProcedure, + connect.WithSchema(consoleServiceGenerateSchemaSampleMethodDescriptor), + connect.WithClientOptions(opts...), + ), } } // consoleServiceClient implements ConsoleServiceClient. type consoleServiceClient struct { - listMessages *connect.Client[v1alpha1.ListMessagesRequest, v1alpha1.ListMessagesResponse] - publishMessage *connect.Client[v1alpha1.PublishMessageRequest, v1alpha1.PublishMessageResponse] + listMessages *connect.Client[v1alpha1.ListMessagesRequest, v1alpha1.ListMessagesResponse] + publishMessage *connect.Client[v1alpha1.PublishMessageRequest, v1alpha1.PublishMessageResponse] + generateSchemaSample *connect.Client[v1alpha1.GenerateSchemaSampleRequest, v1alpha1.GenerateSchemaSampleResponse] } // ListMessages calls redpanda.api.console.v1alpha1.ConsoleService.ListMessages. @@ -99,6 +113,11 @@ func (c *consoleServiceClient) PublishMessage(ctx context.Context, req *connect. return c.publishMessage.CallUnary(ctx, req) } +// GenerateSchemaSample calls redpanda.api.console.v1alpha1.ConsoleService.GenerateSchemaSample. +func (c *consoleServiceClient) GenerateSchemaSample(ctx context.Context, req *connect.Request[v1alpha1.GenerateSchemaSampleRequest]) (*connect.Response[v1alpha1.GenerateSchemaSampleResponse], error) { + return c.generateSchemaSample.CallUnary(ctx, req) +} + // ConsoleServiceHandler is an implementation of the redpanda.api.console.v1alpha1.ConsoleService // service. type ConsoleServiceHandler interface { @@ -106,6 +125,9 @@ type ConsoleServiceHandler interface { ListMessages(context.Context, *connect.Request[v1alpha1.ListMessagesRequest], *connect.ServerStream[v1alpha1.ListMessagesResponse]) error // PublishMessage publishes message. PublishMessage(context.Context, *connect.Request[v1alpha1.PublishMessageRequest]) (*connect.Response[v1alpha1.PublishMessageResponse], error) + // GenerateSchemaSample renders a JSON skeleton for any Schema Registry-backed + // schema (Avro / Protobuf / JSON Schema). Dispatches by schema type server-side. + GenerateSchemaSample(context.Context, *connect.Request[v1alpha1.GenerateSchemaSampleRequest]) (*connect.Response[v1alpha1.GenerateSchemaSampleResponse], error) } // NewConsoleServiceHandler builds an HTTP handler from the service implementation. It returns the @@ -126,12 +148,20 @@ func NewConsoleServiceHandler(svc ConsoleServiceHandler, opts ...connect.Handler connect.WithSchema(consoleServicePublishMessageMethodDescriptor), connect.WithHandlerOptions(opts...), ) + consoleServiceGenerateSchemaSampleHandler := connect.NewUnaryHandler( + ConsoleServiceGenerateSchemaSampleProcedure, + svc.GenerateSchemaSample, + connect.WithSchema(consoleServiceGenerateSchemaSampleMethodDescriptor), + connect.WithHandlerOptions(opts...), + ) return "/redpanda.api.console.v1alpha1.ConsoleService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case ConsoleServiceListMessagesProcedure: consoleServiceListMessagesHandler.ServeHTTP(w, r) case ConsoleServicePublishMessageProcedure: consoleServicePublishMessageHandler.ServeHTTP(w, r) + case ConsoleServiceGenerateSchemaSampleProcedure: + consoleServiceGenerateSchemaSampleHandler.ServeHTTP(w, r) default: http.NotFound(w, r) } @@ -148,3 +178,7 @@ func (UnimplementedConsoleServiceHandler) ListMessages(context.Context, *connect func (UnimplementedConsoleServiceHandler) PublishMessage(context.Context, *connect.Request[v1alpha1.PublishMessageRequest]) (*connect.Response[v1alpha1.PublishMessageResponse], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("redpanda.api.console.v1alpha1.ConsoleService.PublishMessage is not implemented")) } + +func (UnimplementedConsoleServiceHandler) GenerateSchemaSample(context.Context, *connect.Request[v1alpha1.GenerateSchemaSampleRequest]) (*connect.Response[v1alpha1.GenerateSchemaSampleResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("redpanda.api.console.v1alpha1.ConsoleService.GenerateSchemaSample is not implemented")) +} diff --git a/backend/pkg/protogen/redpanda/api/console/v1alpha1/consolev1alpha1connect/console_service.connect.gw.go b/backend/pkg/protogen/redpanda/api/console/v1alpha1/consolev1alpha1connect/console_service.connect.gw.go index 46a611b65e..9194baf1f3 100644 --- a/backend/pkg/protogen/redpanda/api/console/v1alpha1/consolev1alpha1connect/console_service.connect.gw.go +++ b/backend/pkg/protogen/redpanda/api/console/v1alpha1/consolev1alpha1connect/console_service.connect.gw.go @@ -19,14 +19,16 @@ import ( // ConsoleServiceGatewayServer implements the gRPC server API for the ConsoleService service. type ConsoleServiceGatewayServer struct { v1alpha1.UnimplementedConsoleServiceServer - publishMessage connect_gateway.UnaryHandler[v1alpha1.PublishMessageRequest, v1alpha1.PublishMessageResponse] + publishMessage connect_gateway.UnaryHandler[v1alpha1.PublishMessageRequest, v1alpha1.PublishMessageResponse] + generateSchemaSample connect_gateway.UnaryHandler[v1alpha1.GenerateSchemaSampleRequest, v1alpha1.GenerateSchemaSampleResponse] } // NewConsoleServiceGatewayServer constructs a Connect-Gateway gRPC server for the ConsoleService // service. func NewConsoleServiceGatewayServer(svc ConsoleServiceHandler, opts ...connect_gateway.HandlerOption) *ConsoleServiceGatewayServer { return &ConsoleServiceGatewayServer{ - publishMessage: connect_gateway.NewUnaryHandler(ConsoleServicePublishMessageProcedure, svc.PublishMessage, opts...), + publishMessage: connect_gateway.NewUnaryHandler(ConsoleServicePublishMessageProcedure, svc.PublishMessage, opts...), + generateSchemaSample: connect_gateway.NewUnaryHandler(ConsoleServiceGenerateSchemaSampleProcedure, svc.GenerateSchemaSample, opts...), } } @@ -38,6 +40,10 @@ func (s *ConsoleServiceGatewayServer) PublishMessage(ctx context.Context, req *v return s.publishMessage(ctx, req) } +func (s *ConsoleServiceGatewayServer) GenerateSchemaSample(ctx context.Context, req *v1alpha1.GenerateSchemaSampleRequest) (*v1alpha1.GenerateSchemaSampleResponse, error) { + return s.generateSchemaSample(ctx, req) +} + // RegisterConsoleServiceHandlerGatewayServer registers the Connect handlers for the ConsoleService // "svc" to "mux". func RegisterConsoleServiceHandlerGatewayServer(mux *runtime.ServeMux, svc ConsoleServiceHandler, opts ...connect_gateway.HandlerOption) { diff --git a/backend/pkg/protogen/redpanda/api/console/v1alpha1/publish_messages.pb.go b/backend/pkg/protogen/redpanda/api/console/v1alpha1/publish_messages.pb.go index 1f727c731c..85e9896b2d 100644 --- a/backend/pkg/protogen/redpanda/api/console/v1alpha1/publish_messages.pb.go +++ b/backend/pkg/protogen/redpanda/api/console/v1alpha1/publish_messages.pb.go @@ -120,7 +120,8 @@ type PublishMessagePayloadOptions struct { Encoding PayloadEncoding `protobuf:"varint,1,opt,name=encoding,proto3,enum=redpanda.api.console.v1alpha1.PayloadEncoding" json:"encoding,omitempty"` // Payload encoding to use. Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` // Data. SchemaId *int32 `protobuf:"varint,9,opt,name=schema_id,json=schemaId,proto3,oneof" json:"schema_id,omitempty"` // Optional schema ID. - Index *int32 `protobuf:"varint,10,opt,name=index,proto3,oneof" json:"index,omitempty"` // Optional index. Useful for Protobuf messages. + Index *int32 `protobuf:"varint,10,opt,name=index,proto3,oneof" json:"index,omitempty"` // Deprecated single-index. Prefer index_path for Protobuf messages so nested types are addressable. + IndexPath []int32 `protobuf:"varint,11,rep,packed,name=index_path,json=indexPath,proto3" json:"index_path,omitempty"` // Optional message-index path for Protobuf. Each element selects the Nth nested MessageDescriptor; e.g. [0] = first top-level, [1, 0] = first nested message of the second top-level. Empty = first top-level. unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -183,6 +184,13 @@ func (x *PublishMessagePayloadOptions) GetIndex() int32 { return 0 } +func (x *PublishMessagePayloadOptions) GetIndexPath() []int32 { + if x != nil { + return x.IndexPath + } + return nil +} + // PublishMessageResponse is the response for PublishMessage call. type PublishMessageResponse struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -244,6 +252,106 @@ func (x *PublishMessageResponse) GetOffset() int64 { return 0 } +// GenerateSchemaSampleRequest asks for a zero-valued JSON skeleton for the schema identified +// by schema_id. The backend dispatches based on the schema's registered type (AVRO/PROTOBUF/JSON). +// index_path is only consulted for Protobuf and is the Confluent message-indexes path. +type GenerateSchemaSampleRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + SchemaId int32 `protobuf:"varint,1,opt,name=schema_id,json=schemaId,proto3" json:"schema_id,omitempty"` + IndexPath []int32 `protobuf:"varint,2,rep,packed,name=index_path,json=indexPath,proto3" json:"index_path,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GenerateSchemaSampleRequest) Reset() { + *x = GenerateSchemaSampleRequest{} + mi := &file_redpanda_api_console_v1alpha1_publish_messages_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GenerateSchemaSampleRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GenerateSchemaSampleRequest) ProtoMessage() {} + +func (x *GenerateSchemaSampleRequest) ProtoReflect() protoreflect.Message { + mi := &file_redpanda_api_console_v1alpha1_publish_messages_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GenerateSchemaSampleRequest.ProtoReflect.Descriptor instead. +func (*GenerateSchemaSampleRequest) Descriptor() ([]byte, []int) { + return file_redpanda_api_console_v1alpha1_publish_messages_proto_rawDescGZIP(), []int{3} +} + +func (x *GenerateSchemaSampleRequest) GetSchemaId() int32 { + if x != nil { + return x.SchemaId + } + return 0 +} + +func (x *GenerateSchemaSampleRequest) GetIndexPath() []int32 { + if x != nil { + return x.IndexPath + } + return nil +} + +// GenerateSchemaSampleResponse returns the JSON skeleton. +type GenerateSchemaSampleResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + SampleJson string `protobuf:"bytes,1,opt,name=sample_json,json=sampleJson,proto3" json:"sample_json,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GenerateSchemaSampleResponse) Reset() { + *x = GenerateSchemaSampleResponse{} + mi := &file_redpanda_api_console_v1alpha1_publish_messages_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GenerateSchemaSampleResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GenerateSchemaSampleResponse) ProtoMessage() {} + +func (x *GenerateSchemaSampleResponse) ProtoReflect() protoreflect.Message { + mi := &file_redpanda_api_console_v1alpha1_publish_messages_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GenerateSchemaSampleResponse.ProtoReflect.Descriptor instead. +func (*GenerateSchemaSampleResponse) Descriptor() ([]byte, []int) { + return file_redpanda_api_console_v1alpha1_publish_messages_proto_rawDescGZIP(), []int{4} +} + +func (x *GenerateSchemaSampleResponse) GetSampleJson() string { + if x != nil { + return x.SampleJson + } + return "" +} + var File_redpanda_api_console_v1alpha1_publish_messages_proto protoreflect.FileDescriptor var file_redpanda_api_console_v1alpha1_publish_messages_proto_rawDesc = []byte{ @@ -287,7 +395,7 @@ var file_redpanda_api_console_v1alpha1_publish_messages_proto_rawDesc = []byte{ 0x70, 0x61, 0x6e, 0x64, 0x61, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, - 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0xd3, + 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0xf2, 0x01, 0x0a, 0x1c, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x4a, 0x0a, 0x08, 0x65, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, @@ -299,35 +407,48 @@ var file_redpanda_api_console_v1alpha1_publish_messages_proto_rawDesc = []byte{ 0x20, 0x0a, 0x09, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x5f, 0x69, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x05, 0x48, 0x00, 0x52, 0x08, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x49, 0x64, 0x88, 0x01, 0x01, 0x12, 0x19, 0x0a, 0x05, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x05, - 0x48, 0x01, 0x52, 0x05, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x88, 0x01, 0x01, 0x42, 0x0c, 0x0a, 0x0a, - 0x5f, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x5f, 0x69, 0x64, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x69, - 0x6e, 0x64, 0x65, 0x78, 0x22, 0x69, 0x0a, 0x16, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x4d, - 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, - 0x0a, 0x05, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, - 0x6f, 0x70, 0x69, 0x63, 0x12, 0x21, 0x0a, 0x0c, 0x70, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, - 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0b, 0x70, 0x61, 0x72, 0x74, - 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, - 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x42, - 0xb5, 0x02, 0x0a, 0x21, 0x63, 0x6f, 0x6d, 0x2e, 0x72, 0x65, 0x64, 0x70, 0x61, 0x6e, 0x64, 0x61, - 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x61, - 0x6c, 0x70, 0x68, 0x61, 0x31, 0x42, 0x14, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x4d, 0x65, - 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x63, 0x67, - 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x72, 0x65, 0x64, 0x70, 0x61, 0x6e, - 0x64, 0x61, 0x2d, 0x64, 0x61, 0x74, 0x61, 0x2f, 0x63, 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65, 0x2f, - 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x67, 0x65, 0x6e, 0x2f, 0x72, 0x65, 0x64, 0x70, 0x61, 0x6e, 0x64, 0x61, 0x2f, 0x61, 0x70, - 0x69, 0x2f, 0x63, 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, - 0x61, 0x31, 0x3b, 0x63, 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, - 0x61, 0x31, 0xa2, 0x02, 0x03, 0x52, 0x41, 0x43, 0xaa, 0x02, 0x1d, 0x52, 0x65, 0x64, 0x70, 0x61, - 0x6e, 0x64, 0x61, 0x2e, 0x41, 0x70, 0x69, 0x2e, 0x43, 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65, 0x2e, - 0x56, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0xca, 0x02, 0x1d, 0x52, 0x65, 0x64, 0x70, 0x61, - 0x6e, 0x64, 0x61, 0x5c, 0x41, 0x70, 0x69, 0x5c, 0x43, 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65, 0x5c, - 0x56, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0xe2, 0x02, 0x29, 0x52, 0x65, 0x64, 0x70, 0x61, - 0x6e, 0x64, 0x61, 0x5c, 0x41, 0x70, 0x69, 0x5c, 0x43, 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65, 0x5c, - 0x56, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, - 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x20, 0x52, 0x65, 0x64, 0x70, 0x61, 0x6e, 0x64, 0x61, 0x3a, - 0x3a, 0x41, 0x70, 0x69, 0x3a, 0x3a, 0x43, 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65, 0x3a, 0x3a, 0x56, - 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x48, 0x01, 0x52, 0x05, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x88, 0x01, 0x01, 0x12, 0x1d, 0x0a, 0x0a, + 0x69, 0x6e, 0x64, 0x65, 0x78, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x05, + 0x52, 0x09, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x50, 0x61, 0x74, 0x68, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, + 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x5f, 0x69, 0x64, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x69, 0x6e, + 0x64, 0x65, 0x78, 0x22, 0x69, 0x0a, 0x16, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x4d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, + 0x05, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, + 0x70, 0x69, 0x63, 0x12, 0x21, 0x0a, 0x0c, 0x70, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, + 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0b, 0x70, 0x61, 0x72, 0x74, 0x69, + 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x22, 0x62, + 0x0a, 0x1b, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, + 0x53, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x24, 0x0a, + 0x09, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, + 0x42, 0x07, 0xba, 0x48, 0x04, 0x1a, 0x02, 0x20, 0x00, 0x52, 0x08, 0x73, 0x63, 0x68, 0x65, 0x6d, + 0x61, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x5f, 0x70, 0x61, 0x74, + 0x68, 0x18, 0x02, 0x20, 0x03, 0x28, 0x05, 0x52, 0x09, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x50, 0x61, + 0x74, 0x68, 0x22, 0x3f, 0x0a, 0x1c, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x53, 0x63, + 0x68, 0x65, 0x6d, 0x61, 0x53, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x5f, 0x6a, 0x73, 0x6f, + 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x4a, + 0x73, 0x6f, 0x6e, 0x42, 0xb5, 0x02, 0x0a, 0x21, 0x63, 0x6f, 0x6d, 0x2e, 0x72, 0x65, 0x64, 0x70, + 0x61, 0x6e, 0x64, 0x61, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65, + 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x42, 0x14, 0x50, 0x75, 0x62, 0x6c, 0x69, + 0x73, 0x68, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, + 0x01, 0x5a, 0x63, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x72, 0x65, + 0x64, 0x70, 0x61, 0x6e, 0x64, 0x61, 0x2d, 0x64, 0x61, 0x74, 0x61, 0x2f, 0x63, 0x6f, 0x6e, 0x73, + 0x6f, 0x6c, 0x65, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x2f, 0x70, 0x6b, 0x67, 0x2f, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x67, 0x65, 0x6e, 0x2f, 0x72, 0x65, 0x64, 0x70, 0x61, 0x6e, 0x64, + 0x61, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x63, 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65, 0x2f, 0x76, 0x31, + 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x3b, 0x63, 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65, 0x76, 0x31, + 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0xa2, 0x02, 0x03, 0x52, 0x41, 0x43, 0xaa, 0x02, 0x1d, 0x52, + 0x65, 0x64, 0x70, 0x61, 0x6e, 0x64, 0x61, 0x2e, 0x41, 0x70, 0x69, 0x2e, 0x43, 0x6f, 0x6e, 0x73, + 0x6f, 0x6c, 0x65, 0x2e, 0x56, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0xca, 0x02, 0x1d, 0x52, + 0x65, 0x64, 0x70, 0x61, 0x6e, 0x64, 0x61, 0x5c, 0x41, 0x70, 0x69, 0x5c, 0x43, 0x6f, 0x6e, 0x73, + 0x6f, 0x6c, 0x65, 0x5c, 0x56, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0xe2, 0x02, 0x29, 0x52, + 0x65, 0x64, 0x70, 0x61, 0x6e, 0x64, 0x61, 0x5c, 0x41, 0x70, 0x69, 0x5c, 0x43, 0x6f, 0x6e, 0x73, + 0x6f, 0x6c, 0x65, 0x5c, 0x56, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x5c, 0x47, 0x50, 0x42, + 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x20, 0x52, 0x65, 0x64, 0x70, 0x61, + 0x6e, 0x64, 0x61, 0x3a, 0x3a, 0x41, 0x70, 0x69, 0x3a, 0x3a, 0x43, 0x6f, 0x6e, 0x73, 0x6f, 0x6c, + 0x65, 0x3a, 0x3a, 0x56, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, } var ( @@ -342,21 +463,23 @@ func file_redpanda_api_console_v1alpha1_publish_messages_proto_rawDescGZIP() []b return file_redpanda_api_console_v1alpha1_publish_messages_proto_rawDescData } -var file_redpanda_api_console_v1alpha1_publish_messages_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_redpanda_api_console_v1alpha1_publish_messages_proto_msgTypes = make([]protoimpl.MessageInfo, 5) var file_redpanda_api_console_v1alpha1_publish_messages_proto_goTypes = []any{ (*PublishMessageRequest)(nil), // 0: redpanda.api.console.v1alpha1.PublishMessageRequest (*PublishMessagePayloadOptions)(nil), // 1: redpanda.api.console.v1alpha1.PublishMessagePayloadOptions (*PublishMessageResponse)(nil), // 2: redpanda.api.console.v1alpha1.PublishMessageResponse - (CompressionType)(0), // 3: redpanda.api.console.v1alpha1.CompressionType - (*KafkaRecordHeader)(nil), // 4: redpanda.api.console.v1alpha1.KafkaRecordHeader - (PayloadEncoding)(0), // 5: redpanda.api.console.v1alpha1.PayloadEncoding + (*GenerateSchemaSampleRequest)(nil), // 3: redpanda.api.console.v1alpha1.GenerateSchemaSampleRequest + (*GenerateSchemaSampleResponse)(nil), // 4: redpanda.api.console.v1alpha1.GenerateSchemaSampleResponse + (CompressionType)(0), // 5: redpanda.api.console.v1alpha1.CompressionType + (*KafkaRecordHeader)(nil), // 6: redpanda.api.console.v1alpha1.KafkaRecordHeader + (PayloadEncoding)(0), // 7: redpanda.api.console.v1alpha1.PayloadEncoding } var file_redpanda_api_console_v1alpha1_publish_messages_proto_depIdxs = []int32{ - 3, // 0: redpanda.api.console.v1alpha1.PublishMessageRequest.compression:type_name -> redpanda.api.console.v1alpha1.CompressionType - 4, // 1: redpanda.api.console.v1alpha1.PublishMessageRequest.headers:type_name -> redpanda.api.console.v1alpha1.KafkaRecordHeader + 5, // 0: redpanda.api.console.v1alpha1.PublishMessageRequest.compression:type_name -> redpanda.api.console.v1alpha1.CompressionType + 6, // 1: redpanda.api.console.v1alpha1.PublishMessageRequest.headers:type_name -> redpanda.api.console.v1alpha1.KafkaRecordHeader 1, // 2: redpanda.api.console.v1alpha1.PublishMessageRequest.key:type_name -> redpanda.api.console.v1alpha1.PublishMessagePayloadOptions 1, // 3: redpanda.api.console.v1alpha1.PublishMessageRequest.value:type_name -> redpanda.api.console.v1alpha1.PublishMessagePayloadOptions - 5, // 4: redpanda.api.console.v1alpha1.PublishMessagePayloadOptions.encoding:type_name -> redpanda.api.console.v1alpha1.PayloadEncoding + 7, // 4: redpanda.api.console.v1alpha1.PublishMessagePayloadOptions.encoding:type_name -> redpanda.api.console.v1alpha1.PayloadEncoding 5, // [5:5] is the sub-list for method output_type 5, // [5:5] is the sub-list for method input_type 5, // [5:5] is the sub-list for extension type_name @@ -377,7 +500,7 @@ func file_redpanda_api_console_v1alpha1_publish_messages_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_redpanda_api_console_v1alpha1_publish_messages_proto_rawDesc, NumEnums: 0, - NumMessages: 3, + NumMessages: 5, NumExtensions: 0, NumServices: 0, }, diff --git a/backend/pkg/schemasample/schemasample.go b/backend/pkg/schemasample/schemasample.go new file mode 100644 index 0000000000..0d0a5bc4e4 --- /dev/null +++ b/backend/pkg/schemasample/schemasample.go @@ -0,0 +1,537 @@ +// Copyright 2026 Redpanda Data, Inc. +// +// Use of this software is governed by the Business Source License +// included in the file https://github.com/redpanda-data/redpanda/blob/dev/licenses/bsl.md +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0 + +// Package schemasample renders zero-valued JSON skeletons for Avro, +// JSON Schema, and Protobuf schemas. The renderers are pure — they take +// already-resolved schema text or descriptors and don't touch the schema +// registry — so callers that fetch a schema separately can plug them in. +package schemasample + +import ( + "encoding/json" + "errors" + "fmt" + "strings" + + "github.com/bufbuild/protocompile/linker" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/types/dynamicpb" +) + +// Avro/JSON-Schema literal type names. Extracted as constants so goconst doesn't +// flag the repeated occurrences and to make typos compile-time errors. +const ( + avroTypeNull = "null" + avroTypeBool = "boolean" + avroTypeInt = "int" + avroTypeLong = "long" + avroTypeFloat = "float" + avroTypeDouble = "double" + avroTypeBytes = "bytes" + avroTypeString = "string" + avroTypeRecord = "record" + avroTypeEnum = "enum" + avroTypeFixed = "fixed" + avroTypeArray = "array" + avroTypeMap = "map" + jsonTypeObject = "object" + jsonTypeArray = "array" + jsonTypeString = "string" + jsonTypeInteger = "integer" + jsonTypeNumber = "number" + jsonTypeBool = "boolean" + jsonTypeNull = "null" +) + +// Avro renders a zero-valued JSON skeleton for an Avro schema. Unions +// containing null serialize as bare null; non-null unions wrap the first +// branch as {"": value}. Recursive types short-circuit to null +// when re-encountered. +func Avro(schemaText string) ([]byte, error) { + var raw any + if err := json.Unmarshal([]byte(schemaText), &raw); err != nil { + return nil, fmt.Errorf("failed to parse avro schema JSON: %w", err) + } + registry := map[string]any{} + collectAvroNamed(raw, "", registry) + val := avroSample(raw, "", registry, map[string]bool{}) + return json.MarshalIndent(val, "", " ") +} + +// JSONSchema renders a zero-valued JSON skeleton for a JSON Schema document. +// Supports the common subset used in schema-registry-attached schemas: +// object/array/string/integer/number/boolean/null, enum, const, oneOf/anyOf +// /allOf (first-branch), and $ref resolution against $defs / definitions +// (cycles short-circuit). +func JSONSchema(schemaText string) ([]byte, error) { + var root any + if err := json.Unmarshal([]byte(schemaText), &root); err != nil { + return nil, fmt.Errorf("failed to parse JSON schema: %w", err) + } + defs := collectJSONDefs(root) + val := jsonSchemaSample(root, defs, map[string]bool{}) + return json.MarshalIndent(val, "", " ") +} + +// Protobuf renders a zero-valued JSON skeleton for the Protobuf message +// located at indexPath inside rootFilename's nested message tree. An empty +// indexPath selects the first top-level message. +func Protobuf(files linker.Files, rootFilename string, indexPath []int) ([]byte, error) { + if files == nil { + return nil, errors.New("nil proto files") + } + rootFile := files.FindFileByPath(rootFilename) + if rootFile == nil { + return nil, fmt.Errorf("root proto file %q not found", rootFilename) + } + + path := indexPath + if len(path) == 0 { + path = []int{0} + } + desc, err := DescriptorByIndexPath(rootFile.Messages(), path) + if err != nil { + return nil, err + } + + msg := dynamicpb.NewMessage(desc) + return protojson.MarshalOptions{ + EmitDefaultValues: true, + Multiline: true, + Indent: " ", + Resolver: files.AsResolver(), + }.Marshal(msg) +} + +// DescriptorByIndexPath walks msgs by successive nested-message indices, +// returning the descriptor at the end of the path. Each path element selects +// a sibling at the current level; the next element descends into that +// sibling's nested messages. +func DescriptorByIndexPath(msgs protoreflect.MessageDescriptors, path []int) (protoreflect.MessageDescriptor, error) { + var current protoreflect.MessageDescriptor + siblings := msgs + for _, idx := range path { + if idx < 0 || idx >= siblings.Len() { + return nil, fmt.Errorf("message index %d out of range (have %d siblings)", idx, siblings.Len()) + } + current = siblings.Get(idx) + siblings = current.Messages() + } + if current == nil { + return nil, errors.New("index path resolved to no message descriptor") + } + return current, nil +} + +// --- AVRO ----------------------------------------------------------------- +// We walk the schema JSON directly rather than the compiled *avro.Schema +// because the latter doesn't expose its node tree. + +// stringField extracts a string keyed in m, returning "" when the key is +// missing or holds a non-string value. +func stringField(m map[string]any, key string) string { + if v, ok := m[key].(string); ok { + return v + } + return "" +} + +// avroFullName resolves the fully-qualified name of a named Avro type given +// its declared namespace (if any) and the enclosing namespace. +func avroFullName(name, declaredNS, enclosingNS string) string { + ns := declaredNS + if ns == "" { + ns = enclosingNS + } + if ns == "" { + return name + } + return ns + "." + name +} + +// collectAvroNamed walks the schema tree once to record fully-qualified names +// for record/enum/fixed types so later references can resolve them. Only +// fullnames are stored — short-name lookups happen at resolve time using +// enclosingNS to avoid cross-namespace collisions (e.g. `a.Status` vs `b.Status`). +func collectAvroNamed(node any, enclosingNS string, out map[string]any) { + switch v := node.(type) { + case []any: + for _, branch := range v { + collectAvroNamed(branch, enclosingNS, out) + } + case map[string]any: + collectAvroNamedFromMap(v, enclosingNS, out) + } +} + +func collectAvroNamedFromMap(v map[string]any, enclosingNS string, out map[string]any) { + t := stringField(v, "type") + switch t { + case avroTypeRecord, avroTypeEnum, avroTypeFixed: + name := stringField(v, "name") + declaredNS := stringField(v, "namespace") + ns := declaredNS + if ns == "" { + ns = enclosingNS + } + out[avroFullName(name, declaredNS, enclosingNS)] = v + if t == avroTypeRecord { + fields, ok := v["fields"].([]any) + if !ok { + return + } + for _, f := range fields { + fm, ok := f.(map[string]any) + if !ok { + continue + } + collectAvroNamed(fm["type"], ns, out) + } + } + case avroTypeArray: + collectAvroNamed(v["items"], enclosingNS, out) + case avroTypeMap: + collectAvroNamed(v["values"], enclosingNS, out) + } +} + +// avroLookup resolves a (possibly short) named reference using enclosingNS, +// falling back to the bare name. Returns the registered schema and whether +// it was found. +func avroLookup(name, enclosingNS string, registry map[string]any) (any, bool) { + if enclosingNS != "" { + if v, ok := registry[enclosingNS+"."+name]; ok { + return v, true + } + } + if v, ok := registry[name]; ok { + return v, true + } + return nil, false +} + +// avroSample emits a zero-valued sample for the node. visited tracks the +// fullnames currently being expanded so recursive types (`record A { next: A }`) +// don't blow the stack — when we re-encounter a name we return nil. +func avroSample(node any, enclosingNS string, registry map[string]any, visited map[string]bool) any { + switch v := node.(type) { + case string: + return avroSamplePrimitiveOrRef(v, enclosingNS, registry, visited) + case []any: + return avroSampleUnion(v, enclosingNS, registry, visited) + case map[string]any: + return avroSampleObject(v, enclosingNS, registry, visited) + } + return nil +} + +func avroSamplePrimitiveOrRef(name, enclosingNS string, registry map[string]any, visited map[string]bool) any { + switch name { + case avroTypeNull: + return nil + case avroTypeBool: + return false + case avroTypeInt, avroTypeLong: + return 0 + case avroTypeFloat, avroTypeDouble: + return 0.0 + case avroTypeBytes, avroTypeString: + return "" + } + // Named reference — guard against cycles. + ref, ok := avroLookup(name, enclosingNS, registry) + if !ok { + return "" + } + if rm, ok := ref.(map[string]any); ok { + full := avroFullName(stringField(rm, "name"), stringField(rm, "namespace"), enclosingNS) + if visited[full] { + return nil + } + visited[full] = true + defer delete(visited, full) + } + return avroSample(ref, enclosingNS, registry, visited) +} + +// avroSampleUnion implements Avro JSON union encoding: a null branch renders +// as bare null; otherwise the first branch is wrapped as {"": value}. +func avroSampleUnion(branches []any, enclosingNS string, registry map[string]any, visited map[string]bool) any { + for _, branch := range branches { + if s, ok := branch.(string); ok && s == avroTypeNull { + return nil + } + } + if len(branches) == 0 { + return nil + } + first := branches[0] + return map[string]any{ + avroBranchKey(first, enclosingNS): avroSample(first, enclosingNS, registry, visited), + } +} + +func avroSampleObject(v map[string]any, enclosingNS string, registry map[string]any, visited map[string]bool) any { + t := stringField(v, "type") + switch t { + case avroTypeRecord: + return avroSampleRecord(v, enclosingNS, registry, visited) + case avroTypeEnum: + return avroSampleEnum(v) + case avroTypeArray: + return []any{} + case avroTypeMap: + return map[string]any{} + case avroTypeFixed: + return "" + } + // Logical types on a primitive base, e.g. {"type":"long","logicalType":"timestamp-millis"}. + if lt, ok := v["logicalType"].(string); ok { + if val, ok := avroSampleLogicalType(lt); ok { + return val + } + } + // Inline type wrapper like {"type":"string"}. + if t != "" { + return avroSample(t, enclosingNS, registry, visited) + } + return nil +} + +func avroSampleRecord(v map[string]any, enclosingNS string, registry map[string]any, visited map[string]bool) any { + // Mark this record as being expanded so self-referential fields short-circuit. + full := avroFullName(stringField(v, "name"), stringField(v, "namespace"), enclosingNS) + if full != "" { + if visited[full] { + return nil + } + visited[full] = true + defer delete(visited, full) + } + + ns := stringField(v, "namespace") + if ns == "" { + ns = enclosingNS + } + out := map[string]any{} + fields, ok := v["fields"].([]any) + if !ok { + return out + } + for _, f := range fields { + fm, ok := f.(map[string]any) + if !ok { + continue + } + name := stringField(fm, "name") + if def, has := fm["default"]; has { + out[name] = def + continue + } + out[name] = avroSample(fm["type"], ns, registry, visited) + } + return out +} + +func avroSampleEnum(v map[string]any) any { + syms, ok := v["symbols"].([]any) + if !ok || len(syms) == 0 { + return "" + } + if def, has := v["default"]; has { + return def + } + return syms[0] +} + +func avroSampleLogicalType(lt string) (any, bool) { + switch lt { + case "decimal": + return "0", true + case "uuid": + return "00000000-0000-0000-0000-000000000000", true + case "date", "time-millis", "time-micros", "timestamp-millis", "timestamp-micros": + return 0, true + } + return nil, false +} + +// avroBranchKey returns the key Avro JSON encoding uses for a chosen union branch. +func avroBranchKey(branch any, enclosingNS string) string { + switch v := branch.(type) { + case string: + return v + case map[string]any: + t := stringField(v, "type") + // Named types take their fullname. + if t == avroTypeRecord || t == avroTypeEnum || t == avroTypeFixed { + return avroFullName(stringField(v, "name"), stringField(v, "namespace"), enclosingNS) + } + if t != "" { + return t + } + } + return "unknown" +} + +// --- JSON SCHEMA ---------------------------------------------------------- + +func collectJSONDefs(root any) map[string]any { + out := map[string]any{} + r, ok := root.(map[string]any) + if !ok { + return out + } + for _, key := range []string{"$defs", "definitions"} { + if m, ok := r[key].(map[string]any); ok { + for k, v := range m { + out[k] = v + } + } + } + return out +} + +// jsonSchemaSample emits a zero-valued sample for a JSON Schema node. visited +// tracks the $ref targets currently being expanded so cyclic `$defs` (e.g. a +// `Node` whose `next` points back at `Node`) don't blow the stack. +func jsonSchemaSample(node any, defs map[string]any, visited map[string]bool) any { + m, ok := node.(map[string]any) + if !ok { + return nil + } + if v, ok := resolveJSONRef(m, defs, visited); ok { + return v + } + if v, ok := pickConstOrEnum(m); ok { + return v + } + if def, has := m["default"]; has { + return def + } + if v, ok := pickJSONAlternative(m, defs, visited); ok { + return v + } + return sampleByJSONType(m, defs, visited) +} + +// resolveJSONRef handles local `$ref` lookups into `$defs` / `definitions`, +// honors JSON Pointer escaping (`~1` → `/`, `~0` → `~`), and short-circuits +// recursion when a ref is already being expanded. +func resolveJSONRef(m map[string]any, defs map[string]any, visited map[string]bool) (any, bool) { + ref, ok := m["$ref"].(string) + if !ok { + return nil, false + } + const p1, p2 = "#/$defs/", "#/definitions/" + var key string + switch { + case strings.HasPrefix(ref, p1): + key = ref[len(p1):] + case strings.HasPrefix(ref, p2): + key = ref[len(p2):] + default: + return nil, false + } + key = jsonPointerUnescape(key) + target, ok := defs[key] + if !ok { + return nil, false + } + if visited[ref] { + return nil, true + } + visited[ref] = true + defer delete(visited, ref) + return jsonSchemaSample(target, defs, visited), true +} + +func jsonPointerUnescape(token string) string { + // Per RFC 6901: ~1 → /, ~0 → ~. Order matters. + token = strings.ReplaceAll(token, "~1", "/") + token = strings.ReplaceAll(token, "~0", "~") + return token +} + +func pickConstOrEnum(m map[string]any) (any, bool) { + if c, has := m["const"]; has { + return c, true + } + if enum, has := m["enum"].([]any); has && len(enum) > 0 { + return enum[0], true + } + return nil, false +} + +func pickJSONAlternative(m map[string]any, defs map[string]any, visited map[string]bool) (any, bool) { + for _, key := range []string{"oneOf", "anyOf", "allOf"} { + alts, ok := m[key].([]any) + if !ok || len(alts) == 0 { + continue + } + return jsonSchemaSample(alts[0], defs, visited), true + } + return nil, false +} + +func sampleByJSONType(m map[string]any, defs map[string]any, visited map[string]bool) any { + switch tt := m["type"].(type) { + case []any: + // Type-list union — pick the first non-null type. + for _, choice := range tt { + s, ok := choice.(string) + if !ok || s == jsonTypeNull { + continue + } + return jsonSchemaSample(map[string]any{ + "type": s, + "properties": m["properties"], + "items": m["items"], + }, defs, visited) + } + return nil + case string: + return jsonSchemaZero(tt, m, defs, visited) + } + if _, has := m["properties"]; has { + return jsonSchemaZero(jsonTypeObject, m, defs, visited) + } + if _, has := m["items"]; has { + return jsonSchemaZero(jsonTypeArray, m, defs, visited) + } + return nil +} + +func jsonSchemaZero(t string, m map[string]any, defs map[string]any, visited map[string]bool) any { + switch t { + case jsonTypeObject: + out := map[string]any{} + if props, ok := m["properties"].(map[string]any); ok { + for k, v := range props { + out[k] = jsonSchemaSample(v, defs, visited) + } + } + return out + case jsonTypeArray: + if items, ok := m["items"].(map[string]any); ok { + return []any{jsonSchemaSample(items, defs, visited)} + } + return []any{} + case jsonTypeString: + return "" + case jsonTypeInteger, jsonTypeNumber: + return 0 + case jsonTypeBool: + return false + case jsonTypeNull: + return nil + } + return nil +} diff --git a/backend/pkg/schemasample/schemasample_test.go b/backend/pkg/schemasample/schemasample_test.go new file mode 100644 index 0000000000..af9cc4e85e --- /dev/null +++ b/backend/pkg/schemasample/schemasample_test.go @@ -0,0 +1,405 @@ +// Copyright 2026 Redpanda Data, Inc. +// +// Use of this software is governed by the Business Source License +// included in the file https://github.com/redpanda-data/redpanda/blob/dev/licenses/bsl.md +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0 + +package schemasample_test + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/reflect/protodesc" + "google.golang.org/protobuf/types/descriptorpb" + + "github.com/redpanda-data/console/backend/pkg/schemasample" +) + +// unmarshal decodes the renderer's JSON output into a Go value for structured +// assertions. Renderers always return MarshalIndent'd JSON, so this is safe. +func unmarshal(t *testing.T, b []byte) any { + t.Helper() + var v any + require.NoError(t, json.Unmarshal(b, &v)) + return v +} + +func TestAvro_Primitives(t *testing.T) { + cases := map[string]any{ + `"null"`: nil, + `"boolean"`: false, + `"int"`: float64(0), + `"long"`: float64(0), + `"float"`: float64(0), + `"double"`: float64(0), + `"bytes"`: "", + `"string"`: "", + } + for schema, want := range cases { + t.Run(schema, func(t *testing.T) { + out, err := schemasample.Avro(schema) + require.NoError(t, err) + assert.Equal(t, want, unmarshal(t, out)) + }) + } +} + +func TestAvro_Record(t *testing.T) { + schema := `{ + "type": "record", + "name": "User", + "fields": [ + {"name": "id", "type": "long"}, + {"name": "email", "type": "string"}, + {"name": "active", "type": "boolean", "default": true} + ] + }` + out, err := schemasample.Avro(schema) + require.NoError(t, err) + assert.Equal(t, map[string]any{ + "id": float64(0), + "email": "", + "active": true, // default honored + }, unmarshal(t, out)) +} + +func TestAvro_UnionWithNull(t *testing.T) { + // Avro JSON encoding: ["null", "string"] for a field renders as bare null + // (not {"string": ""}) because null is the implicit default for nullable. + schema := `{ + "type": "record", + "name": "Wrap", + "fields": [{"name": "maybe", "type": ["null", "string"]}] + }` + out, err := schemasample.Avro(schema) + require.NoError(t, err) + assert.Equal(t, map[string]any{"maybe": nil}, unmarshal(t, out)) +} + +func TestAvro_UnionWithoutNull(t *testing.T) { + // Non-null union wraps the first branch as {"": value}. + schema := `{ + "type": "record", + "name": "Wrap", + "fields": [{"name": "choice", "type": ["string", "long"]}] + }` + out, err := schemasample.Avro(schema) + require.NoError(t, err) + assert.Equal(t, map[string]any{ + "choice": map[string]any{"string": ""}, + }, unmarshal(t, out)) +} + +func TestAvro_Enum(t *testing.T) { + schema := `{ + "type": "enum", + "name": "Color", + "symbols": ["RED", "GREEN", "BLUE"] + }` + out, err := schemasample.Avro(schema) + require.NoError(t, err) + assert.Equal(t, "RED", unmarshal(t, out)) +} + +func TestAvro_EnumWithDefault(t *testing.T) { + schema := `{ + "type": "enum", + "name": "Color", + "symbols": ["RED", "GREEN"], + "default": "GREEN" + }` + out, err := schemasample.Avro(schema) + require.NoError(t, err) + assert.Equal(t, "GREEN", unmarshal(t, out)) +} + +func TestAvro_LogicalTypes(t *testing.T) { + cases := map[string]any{ + `{"type":"string","logicalType":"uuid"}`: "00000000-0000-0000-0000-000000000000", + `{"type":"bytes","logicalType":"decimal"}`: "0", + `{"type":"int","logicalType":"date"}`: float64(0), + `{"type":"long","logicalType":"timestamp-millis"}`: float64(0), + `{"type":"long","logicalType":"timestamp-micros"}`: float64(0), + } + for schema, want := range cases { + t.Run(schema, func(t *testing.T) { + out, err := schemasample.Avro(schema) + require.NoError(t, err) + assert.Equal(t, want, unmarshal(t, out)) + }) + } +} + +func TestAvro_RecursiveRef(t *testing.T) { + // A record that references itself by name. The renderer must short-circuit + // when it sees the same named type already on its expansion stack — else + // it'd recurse forever. + schema := `{ + "type": "record", + "name": "Node", + "fields": [ + {"name": "value", "type": "long"}, + {"name": "next", "type": ["null", "Node"]} + ] + }` + out, err := schemasample.Avro(schema) + require.NoError(t, err) + v, ok := unmarshal(t, out).(map[string]any) + require.True(t, ok) + assert.Equal(t, float64(0), v["value"]) + // next is the null branch of the union, so renders as nil. + assert.Nil(t, v["next"]) +} + +func TestAvro_NestedNamespaces(t *testing.T) { + // A record refers to a named type declared in the same namespace by short + // name. The renderer must resolve via enclosingNS. + schema := `{ + "type": "record", + "name": "Order", + "namespace": "shop", + "fields": [ + { + "name": "status", + "type": { + "type": "enum", + "name": "Status", + "symbols": ["OPEN", "CLOSED"] + } + }, + {"name": "previous_status", "type": "Status"} + ] + }` + out, err := schemasample.Avro(schema) + require.NoError(t, err) + assert.Equal(t, map[string]any{ + "status": "OPEN", + "previous_status": "OPEN", + }, unmarshal(t, out)) +} + +func TestAvro_ArrayAndMap(t *testing.T) { + schema := `{ + "type": "record", + "name": "Bag", + "fields": [ + {"name": "tags", "type": {"type": "array", "items": "string"}}, + {"name": "props", "type": {"type": "map", "values": "long"}} + ] + }` + out, err := schemasample.Avro(schema) + require.NoError(t, err) + assert.Equal(t, map[string]any{ + "tags": []any{}, + "props": map[string]any{}, + }, unmarshal(t, out)) +} + +func TestJSONSchema_Primitives(t *testing.T) { + cases := map[string]any{ + `{"type":"string"}`: "", + `{"type":"integer"}`: float64(0), + `{"type":"number"}`: float64(0), + `{"type":"boolean"}`: false, + `{"type":"null"}`: nil, + } + for schema, want := range cases { + t.Run(schema, func(t *testing.T) { + out, err := schemasample.JSONSchema(schema) + require.NoError(t, err) + assert.Equal(t, want, unmarshal(t, out)) + }) + } +} + +func TestJSONSchema_Object(t *testing.T) { + schema := `{ + "type": "object", + "properties": { + "name": {"type": "string"}, + "age": {"type": "integer"} + } + }` + out, err := schemasample.JSONSchema(schema) + require.NoError(t, err) + assert.Equal(t, map[string]any{ + "name": "", + "age": float64(0), + }, unmarshal(t, out)) +} + +func TestJSONSchema_Array(t *testing.T) { + schema := `{"type":"array","items":{"type":"string"}}` + out, err := schemasample.JSONSchema(schema) + require.NoError(t, err) + assert.Equal(t, []any{""}, unmarshal(t, out)) +} + +func TestJSONSchema_OneOfPicksFirst(t *testing.T) { + schema := `{ + "oneOf": [ + {"type": "string"}, + {"type": "integer"} + ] + }` + out, err := schemasample.JSONSchema(schema) + require.NoError(t, err) + assert.Equal(t, "", unmarshal(t, out)) +} + +func TestJSONSchema_Const(t *testing.T) { + out, err := schemasample.JSONSchema(`{"const": "pinned"}`) + require.NoError(t, err) + assert.Equal(t, "pinned", unmarshal(t, out)) +} + +func TestJSONSchema_EnumPicksFirst(t *testing.T) { + out, err := schemasample.JSONSchema(`{"enum": ["a", "b", "c"]}`) + require.NoError(t, err) + assert.Equal(t, "a", unmarshal(t, out)) +} + +func TestJSONSchema_DefaultWins(t *testing.T) { + out, err := schemasample.JSONSchema(`{"type":"string","default":"hello"}`) + require.NoError(t, err) + assert.Equal(t, "hello", unmarshal(t, out)) +} + +func TestJSONSchema_RefResolved(t *testing.T) { + schema := `{ + "$ref": "#/$defs/User", + "$defs": { + "User": { + "type": "object", + "properties": {"id": {"type": "integer"}} + } + } + }` + out, err := schemasample.JSONSchema(schema) + require.NoError(t, err) + assert.Equal(t, map[string]any{"id": float64(0)}, unmarshal(t, out)) +} + +func TestJSONSchema_RefCycleTerminates(t *testing.T) { + // Node refers to itself — the renderer must terminate cleanly. + schema := `{ + "$ref": "#/$defs/Node", + "$defs": { + "Node": { + "type": "object", + "properties": { + "value": {"type": "integer"}, + "next": {"$ref": "#/$defs/Node"} + } + } + } + }` + out, err := schemasample.JSONSchema(schema) + require.NoError(t, err) + v, ok := unmarshal(t, out).(map[string]any) + require.True(t, ok) + assert.Equal(t, float64(0), v["value"]) + // Cyclic ref short-circuits to null. + assert.Nil(t, v["next"]) +} + +func TestJSONSchema_RefJSONPointerEscape(t *testing.T) { + // JSON Pointer: ~1 → /, ~0 → ~. A def key containing '/' must be encoded + // as ~1 in the $ref. + schema := `{ + "$ref": "#/$defs/odd~1key", + "$defs": { + "odd/key": {"type": "string"} + } + }` + out, err := schemasample.JSONSchema(schema) + require.NoError(t, err) + assert.Equal(t, "", unmarshal(t, out)) +} + +func TestJSONSchema_DefinitionsAlias(t *testing.T) { + // JSON Schema draft-07 used "definitions" instead of "$defs"; the renderer + // should accept both. + schema := `{ + "$ref": "#/definitions/User", + "definitions": { + "User": {"type": "object", "properties": {"name": {"type": "string"}}} + } + }` + out, err := schemasample.JSONSchema(schema) + require.NoError(t, err) + assert.Equal(t, map[string]any{"name": ""}, unmarshal(t, out)) +} + +// TestDescriptorByIndexPath exercises the protobuf descriptor walker against a +// hand-built file with one nested level. Going through a real +// FileDescriptorProto rather than mocking exercises the same protoreflect +// surface the production caller uses. +func TestDescriptorByIndexPath(t *testing.T) { + str := func(s string) *string { return &s } + + fdp := &descriptorpb.FileDescriptorProto{ + Name: str("test.proto"), + Syntax: str("proto3"), + Package: str("test"), + MessageType: []*descriptorpb.DescriptorProto{ + { + Name: str("Outer"), + NestedType: []*descriptorpb.DescriptorProto{ + {Name: str("Inner")}, + {Name: str("Inner2")}, + }, + }, + {Name: str("Sibling")}, + }, + } + fd, err := protodesc.NewFile(fdp, nil) + require.NoError(t, err) + + t.Run("empty path is rejected by caller; loop alone returns nil descriptor", func(t *testing.T) { + // The function itself doesn't default empty path — that's the caller's + // (Protobuf) job. Empty path yields nil descriptor + error. + _, err := schemasample.DescriptorByIndexPath(fd.Messages(), nil) + require.Error(t, err) + }) + + t.Run("top-level by index 0", func(t *testing.T) { + desc, err := schemasample.DescriptorByIndexPath(fd.Messages(), []int{0}) + require.NoError(t, err) + assert.Equal(t, "Outer", string(desc.Name())) + }) + + t.Run("top-level by index 1", func(t *testing.T) { + desc, err := schemasample.DescriptorByIndexPath(fd.Messages(), []int{1}) + require.NoError(t, err) + assert.Equal(t, "Sibling", string(desc.Name())) + }) + + t.Run("nested descent", func(t *testing.T) { + desc, err := schemasample.DescriptorByIndexPath(fd.Messages(), []int{0, 1}) + require.NoError(t, err) + assert.Equal(t, "Inner2", string(desc.Name())) + }) + + t.Run("out-of-range at top level", func(t *testing.T) { + _, err := schemasample.DescriptorByIndexPath(fd.Messages(), []int{99}) + require.Error(t, err) + assert.Contains(t, err.Error(), "out of range") + }) + + t.Run("out-of-range nested", func(t *testing.T) { + _, err := schemasample.DescriptorByIndexPath(fd.Messages(), []int{0, 99}) + require.Error(t, err) + }) + + t.Run("negative index", func(t *testing.T) { + _, err := schemasample.DescriptorByIndexPath(fd.Messages(), []int{-1}) + require.Error(t, err) + }) +} diff --git a/backend/pkg/serde/protobuf_schema.go b/backend/pkg/serde/protobuf_schema.go index 97e4cceab0..1e5f84ca7c 100644 --- a/backend/pkg/serde/protobuf_schema.go +++ b/backend/pkg/serde/protobuf_schema.go @@ -91,7 +91,10 @@ func (d ProtobufSchemaSerde) DeserializePayload(ctx context.Context, record *kgo // the right proto type that shall be used for decoding the binary data. The index // path points us to the right type inside the main proto file. rootDescriptors := compiledProtoFiles.FindFileByPath(rootFilename).Messages() - messageDescriptor := messageDescriptorFromIndexPath(rootDescriptors, indexPath) + messageDescriptor, err := messageDescriptorFromIndexPath(rootDescriptors, indexPath) + if err != nil { + return &RecordPayload{}, fmt.Errorf("failed to resolve protobuf descriptor: %w", err) + } protoMessage := dynamicpb.NewMessage(messageDescriptor) err = proto.Unmarshal(binaryPayload, protoMessage) @@ -196,7 +199,10 @@ func (d ProtobufSchemaSerde) jsonToProtobufWire(ctx context.Context, jsonInput [ return nil, fmt.Errorf("failed getting proto files: %w", err) } rootDescriptors := compiledProtoFiles.FindFileByPath(rootFilename).Messages() - messageDescriptor := messageDescriptorFromIndexPath(rootDescriptors, indexPath) + messageDescriptor, err := messageDescriptorFromIndexPath(rootDescriptors, indexPath) + if err != nil { + return nil, fmt.Errorf("failed to resolve protobuf descriptor: %w", err) + } message := dynamicpb.NewMessage(messageDescriptor) o := protojson.UnmarshalOptions{ @@ -235,14 +241,20 @@ func indexPathFromDescriptor(descriptor protoreflect.MessageDescriptor) []int { } // messageDescriptorFromIndexPath recursively navigates through the descriptors -// to find the message descriptor specified by the given index path. -func messageDescriptorFromIndexPath(descriptors protoreflect.MessageDescriptors, indexPath []int) protoreflect.MessageDescriptor { - // Base case: return the descriptor if we've reached the last index. +// to find the message descriptor specified by the given index path. Returns an +// error when an index is negative or out of range so the caller can reject the +// request instead of panicking inside protoreflect.MessageDescriptors.Get. +func messageDescriptorFromIndexPath(descriptors protoreflect.MessageDescriptors, indexPath []int) (protoreflect.MessageDescriptor, error) { + if len(indexPath) == 0 { + return nil, errors.New("empty protobuf index path") + } + idx := indexPath[0] + if idx < 0 || idx >= descriptors.Len() { + return nil, fmt.Errorf("protobuf message index %d out of range (have %d siblings)", idx, descriptors.Len()) + } + descriptor := descriptors.Get(idx) if len(indexPath) == 1 { - return descriptors.Get(indexPath[0]) + return descriptor, nil } - - // Recursive case: continue to the next level of descriptors. - nextDescriptor := descriptors.Get(indexPath[0]) - return messageDescriptorFromIndexPath(nextDescriptor.Messages(), indexPath[1:]) + return messageDescriptorFromIndexPath(descriptor.Messages(), indexPath[1:]) } diff --git a/frontend/src/protogen/redpanda/api/console/v1alpha1/console_service-ConsoleService_connectquery.ts b/frontend/src/protogen/redpanda/api/console/v1alpha1/console_service-ConsoleService_connectquery.ts index 5634d75a3f..c9d79117f3 100644 --- a/frontend/src/protogen/redpanda/api/console/v1alpha1/console_service-ConsoleService_connectquery.ts +++ b/frontend/src/protogen/redpanda/api/console/v1alpha1/console_service-ConsoleService_connectquery.ts @@ -10,3 +10,11 @@ import { ConsoleService } from "./console_service_pb"; * @generated from rpc redpanda.api.console.v1alpha1.ConsoleService.PublishMessage */ export const publishMessage = ConsoleService.method.publishMessage; + +/** + * GenerateSchemaSample renders a JSON skeleton for any Schema Registry-backed + * schema (Avro / Protobuf / JSON Schema). Dispatches by schema type server-side. + * + * @generated from rpc redpanda.api.console.v1alpha1.ConsoleService.GenerateSchemaSample + */ +export const generateSchemaSample = ConsoleService.method.generateSchemaSample; diff --git a/frontend/src/protogen/redpanda/api/console/v1alpha1/console_service_pb.ts b/frontend/src/protogen/redpanda/api/console/v1alpha1/console_service_pb.ts index 1fe52dadd9..8078879b8f 100644 --- a/frontend/src/protogen/redpanda/api/console/v1alpha1/console_service_pb.ts +++ b/frontend/src/protogen/redpanda/api/console/v1alpha1/console_service_pb.ts @@ -7,14 +7,14 @@ import { fileDesc, serviceDesc } from "@bufbuild/protobuf/codegenv1"; import { file_redpanda_api_auth_v1_authorization } from "../../auth/v1/authorization_pb"; import type { ListMessagesRequestSchema, ListMessagesResponseSchema } from "./list_messages_pb"; import { file_redpanda_api_console_v1alpha1_list_messages } from "./list_messages_pb"; -import type { PublishMessageRequestSchema, PublishMessageResponseSchema } from "./publish_messages_pb"; +import type { GenerateSchemaSampleRequestSchema, GenerateSchemaSampleResponseSchema, PublishMessageRequestSchema, PublishMessageResponseSchema } from "./publish_messages_pb"; import { file_redpanda_api_console_v1alpha1_publish_messages } from "./publish_messages_pb"; /** * Describes the file redpanda/api/console/v1alpha1/console_service.proto. */ export const file_redpanda_api_console_v1alpha1_console_service: GenFile = /*@__PURE__*/ - fileDesc("CjNyZWRwYW5kYS9hcGkvY29uc29sZS92MWFscGhhMS9jb25zb2xlX3NlcnZpY2UucHJvdG8SHXJlZHBhbmRhLmFwaS5jb25zb2xlLnYxYWxwaGExMqACCg5Db25zb2xlU2VydmljZRKDAQoMTGlzdE1lc3NhZ2VzEjIucmVkcGFuZGEuYXBpLmNvbnNvbGUudjFhbHBoYTEuTGlzdE1lc3NhZ2VzUmVxdWVzdBozLnJlZHBhbmRhLmFwaS5jb25zb2xlLnYxYWxwaGExLkxpc3RNZXNzYWdlc1Jlc3BvbnNlIgiKph0ECAEQATABEocBCg5QdWJsaXNoTWVzc2FnZRI0LnJlZHBhbmRhLmFwaS5jb25zb2xlLnYxYWxwaGExLlB1Ymxpc2hNZXNzYWdlUmVxdWVzdBo1LnJlZHBhbmRhLmFwaS5jb25zb2xlLnYxYWxwaGExLlB1Ymxpc2hNZXNzYWdlUmVzcG9uc2UiCIqmHQQIAhABYgZwcm90bzM", [file_redpanda_api_auth_v1_authorization, file_redpanda_api_console_v1alpha1_list_messages, file_redpanda_api_console_v1alpha1_publish_messages]); + fileDesc("CjNyZWRwYW5kYS9hcGkvY29uc29sZS92MWFscGhhMS9jb25zb2xlX3NlcnZpY2UucHJvdG8SHXJlZHBhbmRhLmFwaS5jb25zb2xlLnYxYWxwaGExMrwDCg5Db25zb2xlU2VydmljZRKDAQoMTGlzdE1lc3NhZ2VzEjIucmVkcGFuZGEuYXBpLmNvbnNvbGUudjFhbHBoYTEuTGlzdE1lc3NhZ2VzUmVxdWVzdBozLnJlZHBhbmRhLmFwaS5jb25zb2xlLnYxYWxwaGExLkxpc3RNZXNzYWdlc1Jlc3BvbnNlIgiKph0ECAEQATABEocBCg5QdWJsaXNoTWVzc2FnZRI0LnJlZHBhbmRhLmFwaS5jb25zb2xlLnYxYWxwaGExLlB1Ymxpc2hNZXNzYWdlUmVxdWVzdBo1LnJlZHBhbmRhLmFwaS5jb25zb2xlLnYxYWxwaGExLlB1Ymxpc2hNZXNzYWdlUmVzcG9uc2UiCIqmHQQIAhABEpkBChRHZW5lcmF0ZVNjaGVtYVNhbXBsZRI6LnJlZHBhbmRhLmFwaS5jb25zb2xlLnYxYWxwaGExLkdlbmVyYXRlU2NoZW1hU2FtcGxlUmVxdWVzdBo7LnJlZHBhbmRhLmFwaS5jb25zb2xlLnYxYWxwaGExLkdlbmVyYXRlU2NoZW1hU2FtcGxlUmVzcG9uc2UiCIqmHQQIARABYgZwcm90bzM", [file_redpanda_api_auth_v1_authorization, file_redpanda_api_console_v1alpha1_list_messages, file_redpanda_api_console_v1alpha1_publish_messages]); /** * ConsoleService represents the Console API service. @@ -42,6 +42,17 @@ export const ConsoleService: GenService<{ input: typeof PublishMessageRequestSchema; output: typeof PublishMessageResponseSchema; }, + /** + * GenerateSchemaSample renders a JSON skeleton for any Schema Registry-backed + * schema (Avro / Protobuf / JSON Schema). Dispatches by schema type server-side. + * + * @generated from rpc redpanda.api.console.v1alpha1.ConsoleService.GenerateSchemaSample + */ + generateSchemaSample: { + methodKind: "unary"; + input: typeof GenerateSchemaSampleRequestSchema; + output: typeof GenerateSchemaSampleResponseSchema; + }, }> = /*@__PURE__*/ serviceDesc(file_redpanda_api_console_v1alpha1_console_service, 0); diff --git a/frontend/src/protogen/redpanda/api/console/v1alpha1/publish_messages_pb.ts b/frontend/src/protogen/redpanda/api/console/v1alpha1/publish_messages_pb.ts index 95143880bf..99aa2f97ca 100644 --- a/frontend/src/protogen/redpanda/api/console/v1alpha1/publish_messages_pb.ts +++ b/frontend/src/protogen/redpanda/api/console/v1alpha1/publish_messages_pb.ts @@ -13,7 +13,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file redpanda/api/console/v1alpha1/publish_messages.proto. */ export const file_redpanda_api_console_v1alpha1_publish_messages: GenFile = /*@__PURE__*/ - fileDesc("CjRyZWRwYW5kYS9hcGkvY29uc29sZS92MWFscGhhMS9wdWJsaXNoX21lc3NhZ2VzLnByb3RvEh1yZWRwYW5kYS5hcGkuY29uc29sZS52MWFscGhhMSKmAwoVUHVibGlzaE1lc3NhZ2VSZXF1ZXN0Ei0KBXRvcGljGAEgASgJQh66SBtyGRABGPkBMhJeW2EtekEtWjAtOS5fXC1dKiQSJgoMcGFydGl0aW9uX2lkGAIgASgFQhC6SA0aCyj///////////8BEkMKC2NvbXByZXNzaW9uGAMgASgOMi4ucmVkcGFuZGEuYXBpLmNvbnNvbGUudjFhbHBoYTEuQ29tcHJlc3Npb25UeXBlEhgKEHVzZV90cmFuc2FjdGlvbnMYBCABKAgSQQoHaGVhZGVycxgFIAMoCzIwLnJlZHBhbmRhLmFwaS5jb25zb2xlLnYxYWxwaGExLkthZmthUmVjb3JkSGVhZGVyEkgKA2tleRgGIAEoCzI7LnJlZHBhbmRhLmFwaS5jb25zb2xlLnYxYWxwaGExLlB1Ymxpc2hNZXNzYWdlUGF5bG9hZE9wdGlvbnMSSgoFdmFsdWUYByABKAsyOy5yZWRwYW5kYS5hcGkuY29uc29sZS52MWFscGhhMS5QdWJsaXNoTWVzc2FnZVBheWxvYWRPcHRpb25zIrIBChxQdWJsaXNoTWVzc2FnZVBheWxvYWRPcHRpb25zEkAKCGVuY29kaW5nGAEgASgOMi4ucmVkcGFuZGEuYXBpLmNvbnNvbGUudjFhbHBoYTEuUGF5bG9hZEVuY29kaW5nEgwKBGRhdGEYAiABKAwSFgoJc2NoZW1hX2lkGAkgASgFSACIAQESEgoFaW5kZXgYCiABKAVIAYgBAUIMCgpfc2NoZW1hX2lkQggKBl9pbmRleCJNChZQdWJsaXNoTWVzc2FnZVJlc3BvbnNlEg0KBXRvcGljGAEgASgJEhQKDHBhcnRpdGlvbl9pZBgCIAEoBRIOCgZvZmZzZXQYAyABKANiBnByb3RvMw", [file_buf_validate_validate, file_redpanda_api_console_v1alpha1_common]); + fileDesc("CjRyZWRwYW5kYS9hcGkvY29uc29sZS92MWFscGhhMS9wdWJsaXNoX21lc3NhZ2VzLnByb3RvEh1yZWRwYW5kYS5hcGkuY29uc29sZS52MWFscGhhMSKmAwoVUHVibGlzaE1lc3NhZ2VSZXF1ZXN0Ei0KBXRvcGljGAEgASgJQh66SBtyGRABGPkBMhJeW2EtekEtWjAtOS5fXC1dKiQSJgoMcGFydGl0aW9uX2lkGAIgASgFQhC6SA0aCyj///////////8BEkMKC2NvbXByZXNzaW9uGAMgASgOMi4ucmVkcGFuZGEuYXBpLmNvbnNvbGUudjFhbHBoYTEuQ29tcHJlc3Npb25UeXBlEhgKEHVzZV90cmFuc2FjdGlvbnMYBCABKAgSQQoHaGVhZGVycxgFIAMoCzIwLnJlZHBhbmRhLmFwaS5jb25zb2xlLnYxYWxwaGExLkthZmthUmVjb3JkSGVhZGVyEkgKA2tleRgGIAEoCzI7LnJlZHBhbmRhLmFwaS5jb25zb2xlLnYxYWxwaGExLlB1Ymxpc2hNZXNzYWdlUGF5bG9hZE9wdGlvbnMSSgoFdmFsdWUYByABKAsyOy5yZWRwYW5kYS5hcGkuY29uc29sZS52MWFscGhhMS5QdWJsaXNoTWVzc2FnZVBheWxvYWRPcHRpb25zIsYBChxQdWJsaXNoTWVzc2FnZVBheWxvYWRPcHRpb25zEkAKCGVuY29kaW5nGAEgASgOMi4ucmVkcGFuZGEuYXBpLmNvbnNvbGUudjFhbHBoYTEuUGF5bG9hZEVuY29kaW5nEgwKBGRhdGEYAiABKAwSFgoJc2NoZW1hX2lkGAkgASgFSACIAQESEgoFaW5kZXgYCiABKAVIAYgBARISCgppbmRleF9wYXRoGAsgAygFQgwKCl9zY2hlbWFfaWRCCAoGX2luZGV4Ik0KFlB1Ymxpc2hNZXNzYWdlUmVzcG9uc2USDQoFdG9waWMYASABKAkSFAoMcGFydGl0aW9uX2lkGAIgASgFEg4KBm9mZnNldBgDIAEoAyJNChtHZW5lcmF0ZVNjaGVtYVNhbXBsZVJlcXVlc3QSGgoJc2NoZW1hX2lkGAEgASgFQge6SAQaAiAAEhIKCmluZGV4X3BhdGgYAiADKAUiMwocR2VuZXJhdGVTY2hlbWFTYW1wbGVSZXNwb25zZRITCgtzYW1wbGVfanNvbhgBIAEoCWIGcHJvdG8z", [file_buf_validate_validate, file_redpanda_api_console_v1alpha1_common]); /** * PublishMessageRequest is the request for PublishMessage call. @@ -100,11 +100,18 @@ export type PublishMessagePayloadOptions = Message<"redpanda.api.console.v1alpha schemaId?: number; /** - * Optional index. Useful for Protobuf messages. + * Deprecated single-index. Prefer index_path for Protobuf messages so nested types are addressable. * * @generated from field: optional int32 index = 10; */ index?: number; + + /** + * Optional message-index path for Protobuf. Each element selects the Nth nested MessageDescriptor; e.g. [0] = first top-level, [1, 0] = first nested message of the second top-level. Empty = first top-level. + * + * @generated from field: repeated int32 index_path = 11; + */ + indexPath: number[]; }; /** @@ -143,3 +150,48 @@ export type PublishMessageResponse = Message<"redpanda.api.console.v1alpha1.Publ export const PublishMessageResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_redpanda_api_console_v1alpha1_publish_messages, 2); +/** + * GenerateSchemaSampleRequest asks for a zero-valued JSON skeleton for the schema identified + * by schema_id. The backend dispatches based on the schema's registered type (AVRO/PROTOBUF/JSON). + * index_path is only consulted for Protobuf and is the Confluent message-indexes path. + * + * @generated from message redpanda.api.console.v1alpha1.GenerateSchemaSampleRequest + */ +export type GenerateSchemaSampleRequest = Message<"redpanda.api.console.v1alpha1.GenerateSchemaSampleRequest"> & { + /** + * @generated from field: int32 schema_id = 1; + */ + schemaId: number; + + /** + * @generated from field: repeated int32 index_path = 2; + */ + indexPath: number[]; +}; + +/** + * Describes the message redpanda.api.console.v1alpha1.GenerateSchemaSampleRequest. + * Use `create(GenerateSchemaSampleRequestSchema)` to create a new message. + */ +export const GenerateSchemaSampleRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_redpanda_api_console_v1alpha1_publish_messages, 3); + +/** + * GenerateSchemaSampleResponse returns the JSON skeleton. + * + * @generated from message redpanda.api.console.v1alpha1.GenerateSchemaSampleResponse + */ +export type GenerateSchemaSampleResponse = Message<"redpanda.api.console.v1alpha1.GenerateSchemaSampleResponse"> & { + /** + * @generated from field: string sample_json = 1; + */ + sampleJson: string; +}; + +/** + * Describes the message redpanda.api.console.v1alpha1.GenerateSchemaSampleResponse. + * Use `create(GenerateSchemaSampleResponseSchema)` to create a new message. + */ +export const GenerateSchemaSampleResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_redpanda_api_console_v1alpha1_publish_messages, 4); + diff --git a/proto/redpanda/api/console/v1alpha1/console_service.proto b/proto/redpanda/api/console/v1alpha1/console_service.proto index 3e39df9bd7..dae377d645 100644 --- a/proto/redpanda/api/console/v1alpha1/console_service.proto +++ b/proto/redpanda/api/console/v1alpha1/console_service.proto @@ -23,4 +23,13 @@ service ConsoleService { api: API_KAFKA }; } + + // GenerateSchemaSample renders a JSON skeleton for any Schema Registry-backed + // schema (Avro / Protobuf / JSON Schema). Dispatches by schema type server-side. + rpc GenerateSchemaSample(GenerateSchemaSampleRequest) returns (GenerateSchemaSampleResponse) { + option (redpanda.api.auth.v1.authorization) = { + required_permission: PERMISSION_VIEW + api: API_KAFKA + }; + } } diff --git a/proto/redpanda/api/console/v1alpha1/publish_messages.proto b/proto/redpanda/api/console/v1alpha1/publish_messages.proto index 3edc43baaf..197575e01a 100644 --- a/proto/redpanda/api/console/v1alpha1/publish_messages.proto +++ b/proto/redpanda/api/console/v1alpha1/publish_messages.proto @@ -24,7 +24,8 @@ message PublishMessagePayloadOptions { PayloadEncoding encoding = 1; // Payload encoding to use. bytes data = 2; // Data. optional int32 schema_id = 9; // Optional schema ID. - optional int32 index = 10; // Optional index. Useful for Protobuf messages. + optional int32 index = 10; // Deprecated single-index. Prefer index_path for Protobuf messages so nested types are addressable. + repeated int32 index_path = 11; // Optional message-index path for Protobuf. Each element selects the Nth nested MessageDescriptor; e.g. [0] = first top-level, [1, 0] = first nested message of the second top-level. Empty = first top-level. } // PublishMessageResponse is the response for PublishMessage call. @@ -33,3 +34,16 @@ message PublishMessageResponse { int32 partition_id = 2; int64 offset = 3; } + +// GenerateSchemaSampleRequest asks for a zero-valued JSON skeleton for the schema identified +// by schema_id. The backend dispatches based on the schema's registered type (AVRO/PROTOBUF/JSON). +// index_path is only consulted for Protobuf and is the Confluent message-indexes path. +message GenerateSchemaSampleRequest { + int32 schema_id = 1 [(buf.validate.field).int32.gt = 0]; + repeated int32 index_path = 2; +} + +// GenerateSchemaSampleResponse returns the JSON skeleton. +message GenerateSchemaSampleResponse { + string sample_json = 1; +} From 216c6da3366ebf4928c576f832e6741ba6560368 Mon Sep 17 00:00:00 2001 From: Julin <142230457+c-julin@users.noreply.github.com> Date: Thu, 21 May 2026 19:21:16 +0100 Subject: [PATCH 2/3] fix(produce): backend review fixes (UX-1292) Addresses adversarial code review findings on PR #2461: - JSON tags on MessageTypeInfo so the REST subject-details response uses fullyQualifiedName/indexPath camelCase. Without tags Go encoded them as FullyQualifiedName/IndexPath, breaking the frontend message-type picker. - avroSamplePrimitiveOrRef no longer double-marks the visited flag. The duplicate-flag bug caused the first reference to a non-recursive named record to render as null instead of expanding the record's fields. - walkMessageTypes skips synthetic map-entry descriptors so map field types don't appear as selectable message types in the produce UI. - handleGetAllSchemas clamps Limit to 1000 (and defaults to 1000 when unset) so an unbounded request can't pull MB-sized payloads. - Regression test TestAvro_NonRecursiveRecordReference locks the visited-flag fix. --- backend/pkg/api/handle_schema_registry.go | 6 ++++ backend/pkg/console/proto_message_types.go | 6 ++++ backend/pkg/proto/service.go | 4 +-- backend/pkg/schemasample/schemasample.go | 11 ++----- backend/pkg/schemasample/schemasample_test.go | 32 +++++++++++++++++++ 5 files changed, 48 insertions(+), 11 deletions(-) diff --git a/backend/pkg/api/handle_schema_registry.go b/backend/pkg/api/handle_schema_registry.go index 7078fb3114..efd150d7db 100644 --- a/backend/pkg/api/handle_schema_registry.go +++ b/backend/pkg/api/handle_schema_registry.go @@ -520,6 +520,12 @@ func (api *API) handleGetAllSchemas() http.HandlerFunc { rest.SendRESTError(w, r, api.Logger, &rest.Error{Err: err, Status: http.StatusBadRequest, Message: err.Error()}) return } + // Defense in depth: cap unbounded fetches. A registry with thousands of schemas would + // otherwise stream MBs of schema text per request. + const maxSchemasLimit = 1000 + if opts.Limit == 0 || opts.Limit > maxSchemasLimit { + opts.Limit = maxSchemasLimit + } res, err := api.ConsoleSvc.GetAllSchemas(r.Context(), opts) if err != nil { diff --git a/backend/pkg/console/proto_message_types.go b/backend/pkg/console/proto_message_types.go index f30fea02ac..2b33862f75 100644 --- a/backend/pkg/console/proto_message_types.go +++ b/backend/pkg/console/proto_message_types.go @@ -46,6 +46,12 @@ func walkMessageTypes(msgs protoreflect.MessageDescriptors, prefix []int32, out for i := 0; i < msgs.Len(); i++ { md := msgs.Get(i) path := append(append([]int32(nil), prefix...), int32(i)) + // Synthetic map-entry descriptors (e.g. Foo.LabelsEntry for a map field) + // are not user-selectable message types — skip them but keep walking so real siblings + // keep their indices. + if md.IsMapEntry() { + continue + } *out = append(*out, proto.MessageTypeInfo{ FullyQualifiedName: string(md.FullName()), IndexPath: path, diff --git a/backend/pkg/proto/service.go b/backend/pkg/proto/service.go index 3b53e6b1ca..d7b26f8797 100644 --- a/backend/pkg/proto/service.go +++ b/backend/pkg/proto/service.go @@ -228,8 +228,8 @@ func (s *Service) GetMessageDescriptorForSchema(schemaID int, index []int) (prot // MessageTypeInfo pairs a Protobuf message's fully-qualified name with its Confluent wire-format // index path. type MessageTypeInfo struct { - FullyQualifiedName string - IndexPath []int32 + FullyQualifiedName string `json:"fullyQualifiedName"` + IndexPath []int32 `json:"indexPath"` } // SerializeJSONToConfluentProtobufMessage serialized the JSON message to confluent wrapped payload diff --git a/backend/pkg/schemasample/schemasample.go b/backend/pkg/schemasample/schemasample.go index 0d0a5bc4e4..cd61318a8a 100644 --- a/backend/pkg/schemasample/schemasample.go +++ b/backend/pkg/schemasample/schemasample.go @@ -245,19 +245,12 @@ func avroSamplePrimitiveOrRef(name, enclosingNS string, registry map[string]any, case avroTypeBytes, avroTypeString: return "" } - // Named reference — guard against cycles. + // Named reference — let avroSampleRecord own the visited-flag bookkeeping. Marking the flag + // here as well caused the first reference to a non-recursive record to render as nil. ref, ok := avroLookup(name, enclosingNS, registry) if !ok { return "" } - if rm, ok := ref.(map[string]any); ok { - full := avroFullName(stringField(rm, "name"), stringField(rm, "namespace"), enclosingNS) - if visited[full] { - return nil - } - visited[full] = true - defer delete(visited, full) - } return avroSample(ref, enclosingNS, registry, visited) } diff --git a/backend/pkg/schemasample/schemasample_test.go b/backend/pkg/schemasample/schemasample_test.go index af9cc4e85e..9abec3a859 100644 --- a/backend/pkg/schemasample/schemasample_test.go +++ b/backend/pkg/schemasample/schemasample_test.go @@ -157,6 +157,38 @@ func TestAvro_RecursiveRef(t *testing.T) { assert.Nil(t, v["next"]) } +func TestAvro_NonRecursiveRecordReference(t *testing.T) { + // A field whose type is a named record (not the enclosing one). The renderer must expand + // the referenced record's fields rather than short-circuiting to nil — regression test for + // a bug where avroSamplePrimitiveOrRef and avroSampleRecord both marked the visited flag, + // causing the first record reference to render as null. + schema := `{ + "type": "record", + "name": "Order", + "fields": [ + { + "name": "billing_address", + "type": { + "type": "record", + "name": "Address", + "fields": [ + {"name": "city", "type": "string"}, + {"name": "zip", "type": "string"} + ] + } + }, + {"name": "shipping_address", "type": "Address"} + ] + }` + out, err := schemasample.Avro(schema) + require.NoError(t, err) + v, ok := unmarshal(t, out).(map[string]any) + require.True(t, ok) + expected := map[string]any{"city": "", "zip": ""} + assert.Equal(t, expected, v["billing_address"]) + assert.Equal(t, expected, v["shipping_address"], "non-recursive record reference must expand, not short-circuit to nil") +} + func TestAvro_NestedNamespaces(t *testing.T) { // A record refers to a named type declared in the same namespace by short // name. The renderer must resolve via enclosingNS. From 5c6bc55b7b544b408e7c069a3ff4e832df110b7f Mon Sep 17 00:00:00 2001 From: Julin <142230457+c-julin@users.noreply.github.com> Date: Sun, 24 May 2026 10:36:22 +0100 Subject: [PATCH 3/3] fix(produce): address PR review nits (UX-1292) - drop redundant `i := i` loop alias (Go 1.22+ scopes per-iteration) - use `slices.Clone` for prefix copy in `walkMessageTypes` - shorten Avro/JSON-Schema constants block comment --- backend/pkg/console/proto_message_types.go | 3 ++- backend/pkg/console/schema_registry.go | 1 - backend/pkg/schemasample/schemasample.go | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/backend/pkg/console/proto_message_types.go b/backend/pkg/console/proto_message_types.go index 2b33862f75..4ef7c75838 100644 --- a/backend/pkg/console/proto_message_types.go +++ b/backend/pkg/console/proto_message_types.go @@ -13,6 +13,7 @@ import ( "context" "errors" "fmt" + "slices" "google.golang.org/protobuf/reflect/protoreflect" @@ -45,7 +46,7 @@ func (s *Service) protoMessageTypesByID(ctx context.Context, schemaID int) ([]pr func walkMessageTypes(msgs protoreflect.MessageDescriptors, prefix []int32, out *[]proto.MessageTypeInfo) { for i := 0; i < msgs.Len(); i++ { md := msgs.Get(i) - path := append(append([]int32(nil), prefix...), int32(i)) + path := append(slices.Clone(prefix), int32(i)) // Synthetic map-entry descriptors (e.g. Foo.LabelsEntry for a map field) // are not user-selectable message types — skip them but keep walking so real siblings // keep their indices. diff --git a/backend/pkg/console/schema_registry.go b/backend/pkg/console/schema_registry.go index c04a39ab14..70ed14ddaf 100644 --- a/backend/pkg/console/schema_registry.go +++ b/backend/pkg/console/schema_registry.go @@ -468,7 +468,6 @@ func (s *Service) populateProtoMessageTypes(ctx context.Context, subjectName str if schemas[i].Type != sr.TypeProtobuf { continue } - i := i grp.Go(func() error { types, err := s.protoMessageTypesByID(grpCtx, schemas[i].ID) if err != nil { diff --git a/backend/pkg/schemasample/schemasample.go b/backend/pkg/schemasample/schemasample.go index cd61318a8a..a2c1dd2fe7 100644 --- a/backend/pkg/schemasample/schemasample.go +++ b/backend/pkg/schemasample/schemasample.go @@ -25,8 +25,7 @@ import ( "google.golang.org/protobuf/types/dynamicpb" ) -// Avro/JSON-Schema literal type names. Extracted as constants so goconst doesn't -// flag the repeated occurrences and to make typos compile-time errors. +// Avro/JSON-Schema literal type names. const ( avroTypeNull = "null" avroTypeBool = "boolean"