You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							569 lines
						
					
					
						
							19 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							569 lines
						
					
					
						
							19 KiB
						
					
					
				| package schema | |
| 
 | |
| import ( | |
| 	"bytes" | |
| 	"encoding/binary" | |
| 	"encoding/json" | |
| 	"fmt" | |
| 	"net/http" | |
| 	"net/http/httptest" | |
| 	"testing" | |
| 
 | |
| 	"github.com/linkedin/goavro/v2" | |
| 	"github.com/seaweedfs/seaweedfs/weed/pb/schema_pb" | |
| 	"github.com/stretchr/testify/assert" | |
| 	"github.com/stretchr/testify/require" | |
| ) | |
| 
 | |
| // TestSchemaDecodeEncode_Avro tests comprehensive Avro decode/encode workflow | |
| func TestSchemaDecodeEncode_Avro(t *testing.T) { | |
| 	// Create mock schema registry | |
| 	registry := createMockSchemaRegistryForDecodeTest(t) | |
| 	defer registry.Close() | |
| 
 | |
| 	manager, err := NewManager(ManagerConfig{ | |
| 		RegistryURL: registry.URL, | |
| 	}) | |
| 	require.NoError(t, err) | |
| 
 | |
| 	// Test data | |
| 	testCases := []struct { | |
| 		name       string | |
| 		schemaID   int32 | |
| 		schemaJSON string | |
| 		testData   map[string]interface{} | |
| 	}{ | |
| 		{ | |
| 			name:     "Simple User Record", | |
| 			schemaID: 1, | |
| 			schemaJSON: `{ | |
| 				"type": "record", | |
| 				"name": "User", | |
| 				"fields": [ | |
| 					{"name": "id", "type": "int"}, | |
| 					{"name": "name", "type": "string"}, | |
| 					{"name": "email", "type": ["null", "string"], "default": null} | |
| 				] | |
| 			}`, | |
| 			testData: map[string]interface{}{ | |
| 				"id":    int32(123), | |
| 				"name":  "John Doe", | |
| 				"email": map[string]interface{}{"string": "john@example.com"}, | |
| 			}, | |
| 		}, | |
| 		{ | |
| 			name:     "Complex Record with Arrays", | |
| 			schemaID: 2, | |
| 			schemaJSON: `{ | |
| 				"type": "record", | |
| 				"name": "Order", | |
| 				"fields": [ | |
| 					{"name": "order_id", "type": "string"}, | |
| 					{"name": "items", "type": {"type": "array", "items": "string"}}, | |
| 					{"name": "total", "type": "double"}, | |
| 					{"name": "metadata", "type": {"type": "map", "values": "string"}} | |
| 				] | |
| 			}`, | |
| 			testData: map[string]interface{}{ | |
| 				"order_id": "ORD-001", | |
| 				"items":    []interface{}{"item1", "item2", "item3"}, | |
| 				"total":    99.99, | |
| 				"metadata": map[string]interface{}{ | |
| 					"source":   "web", | |
| 					"campaign": "summer2024", | |
| 				}, | |
| 			}, | |
| 		}, | |
| 		{ | |
| 			name:     "Union Types", | |
| 			schemaID: 3, | |
| 			schemaJSON: `{ | |
| 				"type": "record", | |
| 				"name": "Event", | |
| 				"fields": [ | |
| 					{"name": "event_id", "type": "string"}, | |
| 					{"name": "payload", "type": ["null", "string", "int"]}, | |
| 					{"name": "timestamp", "type": "long"} | |
| 				] | |
| 			}`, | |
| 			testData: map[string]interface{}{ | |
| 				"event_id":  "evt-123", | |
| 				"payload":   map[string]interface{}{"int": int32(42)}, | |
| 				"timestamp": int64(1640995200000), | |
| 			}, | |
| 		}, | |
| 	} | |
| 
 | |
| 	for _, tc := range testCases { | |
| 		t.Run(tc.name, func(t *testing.T) { | |
| 			// Register schema in mock registry | |
| 			registerSchemaInMock(t, registry, tc.schemaID, tc.schemaJSON) | |
| 
 | |
| 			// Create Avro codec | |
| 			codec, err := goavro.NewCodec(tc.schemaJSON) | |
| 			require.NoError(t, err) | |
| 
 | |
| 			// Encode test data to Avro binary | |
| 			avroBinary, err := codec.BinaryFromNative(nil, tc.testData) | |
| 			require.NoError(t, err) | |
| 
 | |
| 			// Create Confluent envelope | |
| 			envelope := createConfluentEnvelope(tc.schemaID, avroBinary) | |
| 
 | |
| 			// Test decode | |
| 			decoded, err := manager.DecodeMessage(envelope) | |
| 			require.NoError(t, err) | |
| 			assert.Equal(t, uint32(tc.schemaID), decoded.SchemaID) | |
| 			assert.Equal(t, FormatAvro, decoded.SchemaFormat) | |
| 			assert.NotNil(t, decoded.RecordValue) | |
| 
 | |
| 			// Verify decoded fields match original data | |
| 			verifyDecodedFields(t, tc.testData, decoded.RecordValue.Fields) | |
| 
 | |
| 			// Test re-encoding (round-trip) | |
| 			reconstructed, err := manager.EncodeMessage(decoded.RecordValue, decoded.SchemaID, decoded.SchemaFormat) | |
| 			require.NoError(t, err) | |
| 
 | |
| 			// Verify reconstructed envelope | |
| 			assert.Equal(t, envelope[:5], reconstructed[:5]) // Magic byte + schema ID | |
|  | |
| 			// Decode reconstructed data to verify round-trip integrity | |
| 			decodedAgain, err := manager.DecodeMessage(reconstructed) | |
| 			require.NoError(t, err) | |
| 			assert.Equal(t, decoded.SchemaID, decodedAgain.SchemaID) | |
| 			assert.Equal(t, decoded.SchemaFormat, decodedAgain.SchemaFormat) | |
| 
 | |
| 			// // Verify fields are identical after round-trip | |
| 			// verifyRecordValuesEqual(t, decoded.RecordValue, decodedAgain.RecordValue) | |
| 		}) | |
| 	} | |
| } | |
| 
 | |
| // TestSchemaDecodeEncode_JSONSchema tests JSON Schema decode/encode workflow | |
| func TestSchemaDecodeEncode_JSONSchema(t *testing.T) { | |
| 	registry := createMockSchemaRegistryForDecodeTest(t) | |
| 	defer registry.Close() | |
| 
 | |
| 	manager, err := NewManager(ManagerConfig{ | |
| 		RegistryURL: registry.URL, | |
| 	}) | |
| 	require.NoError(t, err) | |
| 
 | |
| 	testCases := []struct { | |
| 		name       string | |
| 		schemaID   int32 | |
| 		schemaJSON string | |
| 		testData   map[string]interface{} | |
| 	}{ | |
| 		{ | |
| 			name:     "Product Schema", | |
| 			schemaID: 10, | |
| 			schemaJSON: `{ | |
| 				"type": "object", | |
| 				"properties": { | |
| 					"product_id": {"type": "string"}, | |
| 					"name": {"type": "string"}, | |
| 					"price": {"type": "number"}, | |
| 					"in_stock": {"type": "boolean"}, | |
| 					"tags": { | |
| 						"type": "array", | |
| 						"items": {"type": "string"} | |
| 					} | |
| 				}, | |
| 				"required": ["product_id", "name", "price"] | |
| 			}`, | |
| 			testData: map[string]interface{}{ | |
| 				"product_id": "PROD-123", | |
| 				"name":       "Awesome Widget", | |
| 				"price":      29.99, | |
| 				"in_stock":   true, | |
| 				"tags":       []interface{}{"electronics", "gadget"}, | |
| 			}, | |
| 		}, | |
| 		{ | |
| 			name:     "Nested Object Schema", | |
| 			schemaID: 11, | |
| 			schemaJSON: `{ | |
| 				"type": "object", | |
| 				"properties": { | |
| 					"customer": { | |
| 						"type": "object", | |
| 						"properties": { | |
| 							"id": {"type": "integer"}, | |
| 							"name": {"type": "string"}, | |
| 							"address": { | |
| 								"type": "object", | |
| 								"properties": { | |
| 									"street": {"type": "string"}, | |
| 									"city": {"type": "string"}, | |
| 									"zip": {"type": "string"} | |
| 								} | |
| 							} | |
| 						} | |
| 					}, | |
| 					"order_date": {"type": "string", "format": "date"} | |
| 				} | |
| 			}`, | |
| 			testData: map[string]interface{}{ | |
| 				"customer": map[string]interface{}{ | |
| 					"id":   float64(456), // JSON numbers are float64 | |
| 					"name": "Jane Smith", | |
| 					"address": map[string]interface{}{ | |
| 						"street": "123 Main St", | |
| 						"city":   "Anytown", | |
| 						"zip":    "12345", | |
| 					}, | |
| 				}, | |
| 				"order_date": "2024-01-15", | |
| 			}, | |
| 		}, | |
| 	} | |
| 
 | |
| 	for _, tc := range testCases { | |
| 		t.Run(tc.name, func(t *testing.T) { | |
| 			// Register schema in mock registry | |
| 			registerSchemaInMock(t, registry, tc.schemaID, tc.schemaJSON) | |
| 
 | |
| 			// Encode test data to JSON | |
| 			jsonBytes, err := json.Marshal(tc.testData) | |
| 			require.NoError(t, err) | |
| 
 | |
| 			// Create Confluent envelope | |
| 			envelope := createConfluentEnvelope(tc.schemaID, jsonBytes) | |
| 
 | |
| 			// Test decode | |
| 			decoded, err := manager.DecodeMessage(envelope) | |
| 			require.NoError(t, err) | |
| 			assert.Equal(t, uint32(tc.schemaID), decoded.SchemaID) | |
| 			assert.Equal(t, FormatJSONSchema, decoded.SchemaFormat) | |
| 			assert.NotNil(t, decoded.RecordValue) | |
| 
 | |
| 			// Test encode back to Confluent envelope | |
| 			reconstructed, err := manager.EncodeMessage(decoded.RecordValue, decoded.SchemaID, decoded.SchemaFormat) | |
| 			require.NoError(t, err) | |
| 
 | |
| 			// Verify reconstructed envelope has correct header | |
| 			assert.Equal(t, envelope[:5], reconstructed[:5]) // Magic byte + schema ID | |
|  | |
| 			// Decode reconstructed data to verify round-trip integrity | |
| 			decodedAgain, err := manager.DecodeMessage(reconstructed) | |
| 			require.NoError(t, err) | |
| 			assert.Equal(t, decoded.SchemaID, decodedAgain.SchemaID) | |
| 			assert.Equal(t, decoded.SchemaFormat, decodedAgain.SchemaFormat) | |
| 
 | |
| 			// Verify fields are identical after round-trip | |
| 			verifyRecordValuesEqual(t, decoded.RecordValue, decodedAgain.RecordValue) | |
| 		}) | |
| 	} | |
| } | |
| 
 | |
| // TestSchemaDecodeEncode_Protobuf tests Protobuf decode/encode workflow | |
| func TestSchemaDecodeEncode_Protobuf(t *testing.T) { | |
| 	registry := createMockSchemaRegistryForDecodeTest(t) | |
| 	defer registry.Close() | |
| 
 | |
| 	manager, err := NewManager(ManagerConfig{ | |
| 		RegistryURL: registry.URL, | |
| 	}) | |
| 	require.NoError(t, err) | |
| 
 | |
| 	// Test that Protobuf text schema parsing and decoding works | |
| 	schemaID := int32(20) | |
| 	protoSchema := `syntax = "proto3"; message TestMessage { string name = 1; int32 id = 2; }` | |
| 
 | |
| 	// Register schema in mock registry | |
| 	registerSchemaInMock(t, registry, schemaID, protoSchema) | |
| 
 | |
| 	// Create a Protobuf message: name="test", id=123 | |
| 	protobufData := []byte{0x0a, 0x04, 0x74, 0x65, 0x73, 0x74, 0x10, 0x7b} | |
| 	envelope := createConfluentEnvelope(schemaID, protobufData) | |
| 
 | |
| 	// Test decode - should work with text .proto schema parsing | |
| 	decoded, err := manager.DecodeMessage(envelope) | |
| 
 | |
| 	// Should successfully decode now that text .proto parsing is implemented | |
| 	require.NoError(t, err) | |
| 	assert.NotNil(t, decoded) | |
| 	assert.Equal(t, uint32(schemaID), decoded.SchemaID) | |
| 	assert.Equal(t, FormatProtobuf, decoded.SchemaFormat) | |
| 	assert.NotNil(t, decoded.RecordValue) | |
| 
 | |
| 	// Verify the decoded fields | |
| 	assert.Contains(t, decoded.RecordValue.Fields, "name") | |
| 	assert.Contains(t, decoded.RecordValue.Fields, "id") | |
| } | |
| 
 | |
| // TestSchemaDecodeEncode_ErrorHandling tests various error conditions | |
| func TestSchemaDecodeEncode_ErrorHandling(t *testing.T) { | |
| 	registry := createMockSchemaRegistryForDecodeTest(t) | |
| 	defer registry.Close() | |
| 
 | |
| 	manager, err := NewManager(ManagerConfig{ | |
| 		RegistryURL: registry.URL, | |
| 	}) | |
| 	require.NoError(t, err) | |
| 
 | |
| 	t.Run("Invalid Confluent Envelope", func(t *testing.T) { | |
| 		// Too short envelope | |
| 		_, err := manager.DecodeMessage([]byte{0x00, 0x00}) | |
| 		assert.Error(t, err) | |
| 		assert.Contains(t, err.Error(), "message is not schematized") | |
| 
 | |
| 		// Wrong magic byte | |
| 		wrongMagic := []byte{0x01, 0x00, 0x00, 0x00, 0x01, 0x41, 0x42} | |
| 		_, err = manager.DecodeMessage(wrongMagic) | |
| 		assert.Error(t, err) | |
| 		assert.Contains(t, err.Error(), "message is not schematized") | |
| 	}) | |
| 
 | |
| 	t.Run("Schema Not Found", func(t *testing.T) { | |
| 		// Create envelope with non-existent schema ID | |
| 		envelope := createConfluentEnvelope(999, []byte("test")) | |
| 		_, err := manager.DecodeMessage(envelope) | |
| 		assert.Error(t, err) | |
| 		assert.Contains(t, err.Error(), "failed to get schema 999") | |
| 	}) | |
| 
 | |
| 	t.Run("Invalid Avro Data", func(t *testing.T) { | |
| 		schemaID := int32(100) | |
| 		schemaJSON := `{"type": "record", "name": "Test", "fields": [{"name": "id", "type": "int"}]}` | |
| 		registerSchemaInMock(t, registry, schemaID, schemaJSON) | |
| 
 | |
| 		// Create envelope with invalid Avro data that will fail decoding | |
| 		invalidAvroData := []byte{0xFF, 0xFF, 0xFF, 0xFF} // Invalid Avro binary data | |
| 		envelope := createConfluentEnvelope(schemaID, invalidAvroData) | |
| 		_, err := manager.DecodeMessage(envelope) | |
| 		assert.Error(t, err) | |
| 		assert.Contains(t, err.Error(), "failed to decode Avro") | |
| 	}) | |
| 
 | |
| 	t.Run("Invalid JSON Data", func(t *testing.T) { | |
| 		schemaID := int32(101) | |
| 		schemaJSON := `{"type": "object", "properties": {"name": {"type": "string"}}}` | |
| 		registerSchemaInMock(t, registry, schemaID, schemaJSON) | |
| 
 | |
| 		// Create envelope with invalid JSON data | |
| 		envelope := createConfluentEnvelope(schemaID, []byte("{invalid json")) | |
| 		_, err := manager.DecodeMessage(envelope) | |
| 		assert.Error(t, err) | |
| 		assert.Contains(t, err.Error(), "failed to decode") | |
| 	}) | |
| } | |
| 
 | |
| // TestSchemaDecodeEncode_CachePerformance tests caching behavior | |
| func TestSchemaDecodeEncode_CachePerformance(t *testing.T) { | |
| 	registry := createMockSchemaRegistryForDecodeTest(t) | |
| 	defer registry.Close() | |
| 
 | |
| 	manager, err := NewManager(ManagerConfig{ | |
| 		RegistryURL: registry.URL, | |
| 	}) | |
| 	require.NoError(t, err) | |
| 
 | |
| 	schemaID := int32(200) | |
| 	schemaJSON := `{"type": "record", "name": "CacheTest", "fields": [{"name": "value", "type": "string"}]}` | |
| 	registerSchemaInMock(t, registry, schemaID, schemaJSON) | |
| 
 | |
| 	// Create test data | |
| 	testData := map[string]interface{}{"value": "test"} | |
| 	codec, err := goavro.NewCodec(schemaJSON) | |
| 	require.NoError(t, err) | |
| 	avroBinary, err := codec.BinaryFromNative(nil, testData) | |
| 	require.NoError(t, err) | |
| 	envelope := createConfluentEnvelope(schemaID, avroBinary) | |
| 
 | |
| 	// First decode - should populate cache | |
| 	decoded1, err := manager.DecodeMessage(envelope) | |
| 	require.NoError(t, err) | |
| 
 | |
| 	// Second decode - should use cache | |
| 	decoded2, err := manager.DecodeMessage(envelope) | |
| 	require.NoError(t, err) | |
| 
 | |
| 	// Verify both results are identical | |
| 	assert.Equal(t, decoded1.SchemaID, decoded2.SchemaID) | |
| 	assert.Equal(t, decoded1.SchemaFormat, decoded2.SchemaFormat) | |
| 	verifyRecordValuesEqual(t, decoded1.RecordValue, decoded2.RecordValue) | |
| 
 | |
| 	// Check cache stats | |
| 	decoders, schemas, subjects := manager.GetCacheStats() | |
| 	assert.True(t, decoders > 0) | |
| 	assert.True(t, schemas > 0) | |
| 	assert.True(t, subjects >= 0) | |
| } | |
| 
 | |
| // Helper functions | |
|  | |
| func createMockSchemaRegistryForDecodeTest(t *testing.T) *httptest.Server { | |
| 	schemas := make(map[int32]string) | |
| 
 | |
| 	return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 		switch r.URL.Path { | |
| 		case "/subjects": | |
| 			w.WriteHeader(http.StatusOK) | |
| 			w.Write([]byte("[]")) | |
| 		default: | |
| 			// Handle schema requests like /schemas/ids/1 | |
| 			var schemaID int32 | |
| 			if n, err := fmt.Sscanf(r.URL.Path, "/schemas/ids/%d", &schemaID); n == 1 && err == nil { | |
| 				if schema, exists := schemas[schemaID]; exists { | |
| 					response := fmt.Sprintf(`{"schema": %q}`, schema) | |
| 					w.Header().Set("Content-Type", "application/json") | |
| 					w.WriteHeader(http.StatusOK) | |
| 					w.Write([]byte(response)) | |
| 				} else { | |
| 					w.WriteHeader(http.StatusNotFound) | |
| 					w.Write([]byte(`{"error_code": 40403, "message": "Schema not found"}`)) | |
| 				} | |
| 			} else if r.Method == "POST" && r.URL.Path == "/register-schema" { | |
| 				// Custom endpoint for test registration | |
| 				var req struct { | |
| 					SchemaID int32  `json:"schema_id"` | |
| 					Schema   string `json:"schema"` | |
| 				} | |
| 				if err := json.NewDecoder(r.Body).Decode(&req); err == nil { | |
| 					schemas[req.SchemaID] = req.Schema | |
| 					w.WriteHeader(http.StatusOK) | |
| 					w.Write([]byte(`{"success": true}`)) | |
| 				} else { | |
| 					w.WriteHeader(http.StatusBadRequest) | |
| 				} | |
| 			} else { | |
| 				w.WriteHeader(http.StatusNotFound) | |
| 			} | |
| 		} | |
| 	})) | |
| } | |
| 
 | |
| func registerSchemaInMock(t *testing.T, registry *httptest.Server, schemaID int32, schema string) { | |
| 	reqBody := fmt.Sprintf(`{"schema_id": %d, "schema": %q}`, schemaID, schema) | |
| 	resp, err := http.Post(registry.URL+"/register-schema", "application/json", bytes.NewReader([]byte(reqBody))) | |
| 	require.NoError(t, err) | |
| 	defer resp.Body.Close() | |
| 	require.Equal(t, http.StatusOK, resp.StatusCode) | |
| } | |
| 
 | |
| func createConfluentEnvelope(schemaID int32, data []byte) []byte { | |
| 	envelope := make([]byte, 5+len(data)) | |
| 	envelope[0] = 0x00 // Magic byte | |
| 	binary.BigEndian.PutUint32(envelope[1:5], uint32(schemaID)) | |
| 	copy(envelope[5:], data) | |
| 	return envelope | |
| } | |
| 
 | |
| func verifyDecodedFields(t *testing.T, expected map[string]interface{}, actual map[string]*schema_pb.Value) { | |
| 	for key, expectedValue := range expected { | |
| 		actualValue, exists := actual[key] | |
| 		require.True(t, exists, "Field %s should exist", key) | |
| 
 | |
| 		switch v := expectedValue.(type) { | |
| 		case int32: | |
| 			// Check both Int32Value and Int64Value since Avro integers can be stored as either | |
| 			if actualValue.GetInt32Value() != 0 { | |
| 				assert.Equal(t, v, actualValue.GetInt32Value(), "Field %s should match", key) | |
| 			} else { | |
| 				assert.Equal(t, int64(v), actualValue.GetInt64Value(), "Field %s should match", key) | |
| 			} | |
| 		case string: | |
| 			assert.Equal(t, v, actualValue.GetStringValue(), "Field %s should match", key) | |
| 		case float64: | |
| 			assert.Equal(t, v, actualValue.GetDoubleValue(), "Field %s should match", key) | |
| 		case bool: | |
| 			assert.Equal(t, v, actualValue.GetBoolValue(), "Field %s should match", key) | |
| 		case []interface{}: | |
| 			listValue := actualValue.GetListValue() | |
| 			require.NotNil(t, listValue, "Field %s should be a list", key) | |
| 			assert.Equal(t, len(v), len(listValue.Values), "List %s should have correct length", key) | |
| 		case map[string]interface{}: | |
| 			// Check if this is an Avro union type (single key-value pair with type name) | |
| 			if len(v) == 1 { | |
| 				for unionType, unionValue := range v { | |
| 					// Handle Avro union types - they are now stored as records | |
| 					switch unionType { | |
| 					case "int": | |
| 						if intVal, ok := unionValue.(int32); ok { | |
| 							// Union values are now stored as records with the union type as field name | |
| 							recordValue := actualValue.GetRecordValue() | |
| 							require.NotNil(t, recordValue, "Field %s should be a union record", key) | |
| 							unionField := recordValue.Fields[unionType] | |
| 							require.NotNil(t, unionField, "Union field %s should exist", unionType) | |
| 							assert.Equal(t, intVal, unionField.GetInt32Value(), "Field %s should match", key) | |
| 						} | |
| 					case "string": | |
| 						if strVal, ok := unionValue.(string); ok { | |
| 							recordValue := actualValue.GetRecordValue() | |
| 							require.NotNil(t, recordValue, "Field %s should be a union record", key) | |
| 							unionField := recordValue.Fields[unionType] | |
| 							require.NotNil(t, unionField, "Union field %s should exist", unionType) | |
| 							assert.Equal(t, strVal, unionField.GetStringValue(), "Field %s should match", key) | |
| 						} | |
| 					case "long": | |
| 						if longVal, ok := unionValue.(int64); ok { | |
| 							recordValue := actualValue.GetRecordValue() | |
| 							require.NotNil(t, recordValue, "Field %s should be a union record", key) | |
| 							unionField := recordValue.Fields[unionType] | |
| 							require.NotNil(t, unionField, "Union field %s should exist", unionType) | |
| 							assert.Equal(t, longVal, unionField.GetInt64Value(), "Field %s should match", key) | |
| 						} | |
| 					default: | |
| 						// If not a recognized union type, treat as regular nested record | |
| 						recordValue := actualValue.GetRecordValue() | |
| 						require.NotNil(t, recordValue, "Field %s should be a record", key) | |
| 						verifyDecodedFields(t, v, recordValue.Fields) | |
| 					} | |
| 					break // Only one iteration for single-key map | |
| 				} | |
| 			} else { | |
| 				// Handle regular maps/objects | |
| 				recordValue := actualValue.GetRecordValue() | |
| 				require.NotNil(t, recordValue, "Field %s should be a record", key) | |
| 				verifyDecodedFields(t, v, recordValue.Fields) | |
| 			} | |
| 		} | |
| 	} | |
| } | |
| 
 | |
| func verifyRecordValuesEqual(t *testing.T, expected, actual *schema_pb.RecordValue) { | |
| 	require.Equal(t, len(expected.Fields), len(actual.Fields), "Record should have same number of fields") | |
| 
 | |
| 	for key, expectedValue := range expected.Fields { | |
| 		actualValue, exists := actual.Fields[key] | |
| 		require.True(t, exists, "Field %s should exist", key) | |
| 
 | |
| 		// Compare values based on type | |
| 		switch expectedValue.Kind.(type) { | |
| 		case *schema_pb.Value_StringValue: | |
| 			assert.Equal(t, expectedValue.GetStringValue(), actualValue.GetStringValue()) | |
| 		case *schema_pb.Value_Int64Value: | |
| 			assert.Equal(t, expectedValue.GetInt64Value(), actualValue.GetInt64Value()) | |
| 		case *schema_pb.Value_DoubleValue: | |
| 			assert.Equal(t, expectedValue.GetDoubleValue(), actualValue.GetDoubleValue()) | |
| 		case *schema_pb.Value_BoolValue: | |
| 			assert.Equal(t, expectedValue.GetBoolValue(), actualValue.GetBoolValue()) | |
| 		case *schema_pb.Value_ListValue: | |
| 			expectedList := expectedValue.GetListValue() | |
| 			actualList := actualValue.GetListValue() | |
| 			require.Equal(t, len(expectedList.Values), len(actualList.Values)) | |
| 			for i, expectedItem := range expectedList.Values { | |
| 				verifyValuesEqual(t, expectedItem, actualList.Values[i]) | |
| 			} | |
| 		case *schema_pb.Value_RecordValue: | |
| 			verifyRecordValuesEqual(t, expectedValue.GetRecordValue(), actualValue.GetRecordValue()) | |
| 		} | |
| 	} | |
| } | |
| 
 | |
| func verifyValuesEqual(t *testing.T, expected, actual *schema_pb.Value) { | |
| 	switch expected.Kind.(type) { | |
| 	case *schema_pb.Value_StringValue: | |
| 		assert.Equal(t, expected.GetStringValue(), actual.GetStringValue()) | |
| 	case *schema_pb.Value_Int64Value: | |
| 		assert.Equal(t, expected.GetInt64Value(), actual.GetInt64Value()) | |
| 	case *schema_pb.Value_DoubleValue: | |
| 		assert.Equal(t, expected.GetDoubleValue(), actual.GetDoubleValue()) | |
| 	case *schema_pb.Value_BoolValue: | |
| 		assert.Equal(t, expected.GetBoolValue(), actual.GetBoolValue()) | |
| 	default: | |
| 		t.Errorf("Unsupported value type for comparison") | |
| 	} | |
| }
 |