355 lines
10 KiB
Markdown
355 lines
10 KiB
Markdown
# Настраиваемые функции сериализации JSON в LineDb
|
||
|
||
## Обзор
|
||
|
||
LineDb поддерживает настраиваемые функции сериализации и десериализации JSON. По умолчанию используется библиотека [go-json](https://github.com/goccy/go-json), но вы можете указать любые функции сериализации, соответствующие определенным сигнатурам.
|
||
|
||
## Функции по умолчанию
|
||
|
||
```go
|
||
// Функции сериализации по умолчанию (используют go-json)
|
||
func defaultJSONMarshal(v any) ([]byte, error) {
|
||
return json.Marshal(v)
|
||
}
|
||
|
||
func defaultJSONUnmarshal(data []byte, v any) error {
|
||
return json.Unmarshal(data, v)
|
||
}
|
||
```
|
||
|
||
## Настройка в опциях коллекции
|
||
|
||
Вы можете указать пользовательские функции сериализации в `JSONLFileOptions`:
|
||
|
||
```go
|
||
type JSONLFileOptions struct {
|
||
CollectionName string `json:"collectionName,omitempty"`
|
||
AllocSize int `json:"allocSize,omitempty"`
|
||
IndexedFields []string `json:"indexedFields,omitempty"`
|
||
EncryptKeyForLineDb string `json:"encryptKeyForLineDb,omitempty"`
|
||
SkipInvalidLines bool `json:"skipInvalidLines,omitempty"`
|
||
DecryptKey string `json:"decryptKey,omitempty"`
|
||
ConvertStringIdToNumber bool `json:"convertStringIdToNumber,omitempty"`
|
||
// Функции сериализации и десериализации JSON
|
||
JSONMarshal func(any) ([]byte, error) `json:"-"`
|
||
JSONUnmarshal func([]byte, any) error `json:"-"`
|
||
}
|
||
```
|
||
|
||
## Примеры использования
|
||
|
||
### 1. Использование стандартного encoding/json
|
||
|
||
```go
|
||
import "encoding/json"
|
||
|
||
// Создаем функции сериализации с использованием стандартного encoding/json
|
||
standardJSONMarshal := func(v any) ([]byte, error) {
|
||
return json.Marshal(v)
|
||
}
|
||
|
||
standardJSONUnmarshal := func(data []byte, v any) error {
|
||
return json.Unmarshal(data, v)
|
||
}
|
||
|
||
// Настройка в опциях инициализации
|
||
initOptions := &linedb.LineDbInitOptions{
|
||
Collections: []linedb.JSONLFileOptions{
|
||
{
|
||
CollectionName: "users",
|
||
JSONMarshal: standardJSONMarshal,
|
||
JSONUnmarshal: standardJSONUnmarshal,
|
||
},
|
||
},
|
||
}
|
||
```
|
||
|
||
### 2. Кастомные функции с метаданными
|
||
|
||
```go
|
||
import "encoding/json"
|
||
import "time"
|
||
|
||
// Создаем кастомные функции сериализации с дополнительной логикой
|
||
customJSONMarshal := func(v any) ([]byte, error) {
|
||
// Добавляем метаданные к сериализации
|
||
data := map[string]any{
|
||
"data": v,
|
||
"timestamp": time.Now().Unix(),
|
||
"version": "1.0",
|
||
}
|
||
return json.Marshal(data)
|
||
}
|
||
|
||
customJSONUnmarshal := func(data []byte, v any) error {
|
||
// Извлекаем данные из кастомного формата
|
||
var wrapper map[string]any
|
||
if err := json.Unmarshal(data, &wrapper); err != nil {
|
||
return err
|
||
}
|
||
|
||
// Извлекаем основную часть данных
|
||
if dataField, exists := wrapper["data"]; exists {
|
||
// Сериализуем обратно в JSON и десериализуем в целевой тип
|
||
dataBytes, err := json.Marshal(dataField)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return json.Unmarshal(dataBytes, v)
|
||
}
|
||
|
||
// Если нет поля data, пытаемся десериализовать как обычный JSON
|
||
return json.Unmarshal(data, v)
|
||
}
|
||
|
||
// Настройка в опциях инициализации
|
||
initOptions := &linedb.LineDbInitOptions{
|
||
Collections: []linedb.JSONLFileOptions{
|
||
{
|
||
CollectionName: "users",
|
||
JSONMarshal: customJSONMarshal,
|
||
JSONUnmarshal: customJSONUnmarshal,
|
||
},
|
||
},
|
||
}
|
||
```
|
||
|
||
### 3. Функции с шифрованием
|
||
|
||
```go
|
||
import (
|
||
"crypto/aes"
|
||
"crypto/cipher"
|
||
"crypto/rand"
|
||
"encoding/base64"
|
||
"encoding/json"
|
||
"fmt"
|
||
"io"
|
||
)
|
||
|
||
var encryptionKey = []byte("your-32-byte-encryption-key-here")
|
||
|
||
func encryptedJSONMarshal(v any) ([]byte, error) {
|
||
// Сначала сериализуем в JSON
|
||
jsonData, err := json.Marshal(v)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// Создаем AES cipher
|
||
block, err := aes.NewCipher(encryptionKey)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// Создаем GCM
|
||
gcm, err := cipher.NewGCM(block)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// Создаем nonce
|
||
nonce := make([]byte, gcm.NonceSize())
|
||
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// Шифруем данные
|
||
ciphertext := gcm.Seal(nonce, nonce, jsonData, nil)
|
||
|
||
// Кодируем в base64
|
||
return []byte(base64.StdEncoding.EncodeToString(ciphertext)), nil
|
||
}
|
||
|
||
func encryptedJSONUnmarshal(data []byte, v any) error {
|
||
// Декодируем из base64
|
||
ciphertext, err := base64.StdEncoding.DecodeString(string(data))
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// Создаем AES cipher
|
||
block, err := aes.NewCipher(encryptionKey)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// Создаем GCM
|
||
gcm, err := cipher.NewGCM(block)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// Извлекаем nonce
|
||
nonceSize := gcm.NonceSize()
|
||
if len(ciphertext) < nonceSize {
|
||
return fmt.Errorf("ciphertext too short")
|
||
}
|
||
|
||
nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
|
||
|
||
// Расшифровываем данные
|
||
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// Десериализуем JSON
|
||
return json.Unmarshal(plaintext, v)
|
||
}
|
||
|
||
// Настройка в опциях инициализации
|
||
initOptions := &linedb.LineDbInitOptions{
|
||
Collections: []linedb.JSONLFileOptions{
|
||
{
|
||
CollectionName: "secure_users",
|
||
JSONMarshal: encryptedJSONMarshal,
|
||
JSONUnmarshal: encryptedJSONUnmarshal,
|
||
},
|
||
},
|
||
}
|
||
```
|
||
|
||
### 4. ID всегда первое поле
|
||
|
||
```go
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"sort"
|
||
"strings"
|
||
)
|
||
|
||
func idFirstJSONMarshal(v any) ([]byte, error) {
|
||
if data, ok := v.(map[string]any); ok {
|
||
var parts []string
|
||
|
||
// Сначала добавляем id если он есть
|
||
if id, exists := data["id"]; exists {
|
||
idBytes, err := json.Marshal(id)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
parts = append(parts, fmt.Sprintf(`"id":%s`, string(idBytes)))
|
||
}
|
||
|
||
// Затем добавляем все остальные поля в алфавитном порядке
|
||
var keys []string
|
||
for key := range data {
|
||
if key != "id" {
|
||
keys = append(keys, key)
|
||
}
|
||
}
|
||
sort.Strings(keys)
|
||
|
||
for _, key := range keys {
|
||
valueBytes, err := json.Marshal(data[key])
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
parts = append(parts, fmt.Sprintf(`"%s":%s`, key, string(valueBytes)))
|
||
}
|
||
|
||
jsonStr := "{" + strings.Join(parts, ",") + "}"
|
||
return []byte(jsonStr), nil
|
||
}
|
||
|
||
// Если это не map, используем стандартную сериализацию
|
||
return json.Marshal(v)
|
||
}
|
||
|
||
func idFirstJSONUnmarshal(data []byte, v any) error {
|
||
return json.Unmarshal(data, v)
|
||
}
|
||
|
||
// Настройка в опциях инициализации
|
||
initOptions := &linedb.LineDbInitOptions{
|
||
Collections: []linedb.JSONLFileOptions{
|
||
{
|
||
CollectionName: "users",
|
||
JSONMarshal: idFirstJSONMarshal,
|
||
JSONUnmarshal: idFirstJSONUnmarshal,
|
||
},
|
||
},
|
||
}
|
||
```
|
||
|
||
## Требования к функциям
|
||
|
||
### JSONMarshal
|
||
|
||
- **Сигнатура**: `func(any) ([]byte, error)`
|
||
- **Назначение**: Сериализует данные в JSON
|
||
- **Возвращает**: Байты JSON и ошибку (если есть)
|
||
|
||
### JSONUnmarshal
|
||
|
||
- **Сигнатура**: `func([]byte, any) error`
|
||
- **Назначение**: Десериализует данные из JSON
|
||
- **Возвращает**: Ошибку (если есть)
|
||
|
||
## Совместимость
|
||
|
||
При использовании кастомных функций сериализации важно обеспечить совместимость:
|
||
|
||
1. **Обратная совместимость**: Убедитесь, что новые функции могут читать данные, записанные старыми функциями
|
||
2. **Версионирование**: Рассмотрите добавление версии в метаданные для будущих изменений
|
||
3. **Тестирование**: Всегда тестируйте функции сериализации с реальными данными
|
||
|
||
## Примеры файлов
|
||
|
||
### Стандартный JSON (go-json по умолчанию)
|
||
|
||
```json
|
||
{"id":1,"username":"user1","email":"user1@example.com"}
|
||
```
|
||
|
||
### Стандартный JSON (encoding/json)
|
||
|
||
```json
|
||
{"id":2,"username":"user2","email":"user2@example.com"}
|
||
```
|
||
|
||
### Кастомный JSON с метаданными
|
||
|
||
```json
|
||
{
|
||
"data": {"id":3,"username":"user3","email":"user3@example.com"},
|
||
"timestamp": 1754969076,
|
||
"version": "1.0"
|
||
}
|
||
```
|
||
|
||
### JSON с ID первым полем
|
||
|
||
```json
|
||
{"id":1,"createdAt":1754969435,"email":"user@example.com","isActive":true,"role":"user","username":"user1"}
|
||
```
|
||
|
||
## Запуск примера
|
||
|
||
```bash
|
||
cd examples/custom-json
|
||
go run main.go
|
||
```
|
||
|
||
Этот пример демонстрирует:
|
||
|
||
- Использование go-json (по умолчанию)
|
||
- Использование стандартного encoding/json
|
||
- Кастомные функции сериализации с метаданными
|
||
- Функции сериализации с ID первым полем
|
||
|
||
## Тестирование
|
||
|
||
```bash
|
||
cd examples/custom-json
|
||
go test -v
|
||
```
|
||
|
||
Тесты проверяют:
|
||
|
||
- Корректность сериализации/десериализации
|
||
- Работу с JSONLFile
|
||
- Обработку ошибок
|