Implementacja Rust RFC 7396 - Łatka JSON Merge
Prędkość i niezawodność Rust sprawiają, że jest idealny do implementacji JSON Merge Patch, zgodnie z definicją w RFC 7396. Ta specyfikacja umożliwia efektywne i bezpieczne częściowe aktualizacje dokumentów JSON.
Daniel Gustaw
• 10 min read
JSON Merge Patch to znormalizowany algorytm do opisywania zmian w dokumencie JSON, traktując go jako zbiór nieuporządkowanych par klucz-wartość. Zdefiniowany w RFC 7396, JSON Merge Patch oferuje prosty i efektywny sposób aktualizacji dokumentów JSON w spójny sposób. W tym wpisie na blogu pokażemy, jak zaimplementować algorytm JSON Merge Patch w Rust, języku programowania systemowego, który kładzie nacisk na bezpieczeństwo, współbieżność i wydajność.
Przegląd JSON Merge Patch:
JSON Merge Patch to technika, która pozwala na wprowadzanie modyfikacji do dokumentu JSON poprzez stworzenie dokumentu “łaty”. Dokument łatki zawiera zestaw par klucz-wartość, które reprezentują zmiany wprowadzane do oryginalnego dokumentu. Algorytm przestrzega tych zasad:
- Jeśli dokument łatki zawiera klucz z wartością różną od null, para klucz-wartość jest dodawana do oryginalnego dokumentu. Jeśli klucz już istnieje, wartość jest zastępowana.
- Jeśli dokument łatki zawiera klucz z wartością null, klucz jest usuwany z oryginalnego dokumentu.
- Jeśli dokument łatki jest tablicą, oryginalny dokument jest zastępowany przez dokument łatki.
- Jeśli dokument łatki jest wartością prymitywną (np. łańcuch, liczba, lub boolean), oryginalny dokument jest zastępowany przez dokument łatki.
JSON Merge Patch jest szczególnie przydatny, gdy potrzebujesz dokonać częściowych aktualizacji w dokumencie JSON bez wysyłania całego dokumentu z powrotem na serwer. To lekkie rozwiązanie w porównaniu do bardziej złożonych formatów łatek, takich jak JSON Patch (RFC 6902).
Rozpoczęcie pracy z Rust:
Aby zaimplementować JSON Merge Patch w Rust, najpierw musimy założyć nowy projekt Rust i dodać paczkę serde_json
, która zapewnia wsparcie dla pracy z danymi JSON:
- Zainstaluj Rust, postępując zgodnie z instrukcjami na https://www.rust-lang.org/tools/install.
- Utwórz nowy projekt Rust, używając
cargo new json_merge_patch
. - Dodaj paczkę
serde_json
do swojego plikuCargo.toml
:
[dependencies]
serde_json = "1.0"
Implementacja JSON Merge Patch w Rust:
Stworzymy funkcję json_merge_patch
, która przyjmuje mutowalną referencję do oryginalnego dokumentu JSON (cel) oraz referencję do dokumentu poprawki. Funkcja zastosuje poprawkę do dokumentu celu zgodnie z zasadami opisanymi wcześniej.
use serde_json::Value;
pub fn json_merge_patch(target: &mut Value, patch: &Value) {
match patch {
Value::Object(patch_obj) => {
if !(target.is_object()
|| target.is_array() && patch_obj.keys().all(|key| key.parse::<usize>().is_ok()))
{
*target = Value::Object(serde_json::Map::new());
}
if let Value::Object(target_obj) = target {
for (key, value) in patch_obj {
if value.is_null() {
target_obj.remove(key);
} else {
let target_value =
target_obj.entry(key.clone()).or_insert_with(|| Value::Null);
json_merge_patch(target_value, value);
}
}
} else if let Value::Array(target_arr) = target {
for (key, value) in patch_obj {
if let Ok(index) = key.parse::<usize>() {
if value.is_null() && index < target_arr.len() {
target_arr.remove(index);
} else if index < target_arr.len() {
json_merge_patch(&mut target_arr[index], value);
} else {
// Handling the case where the index is greater than the current length of the target array
while target_arr.len() < index {
target_arr.push(Value::Null);
}
target_arr.push(value.clone());
}
}
}
}
}
Value::Array(patch_arr) => {
*target = serde_json::Value::Array(
patch_arr
.clone()
.into_iter()
.filter(|value| !value.is_null())
.collect(),
);
}
_ => *target = patch.clone(),
}
}
W tej implementacji najpierw sprawdzamy, czy dokument poprawki jest obiektem. Jeśli tak, iterujemy przez pary klucz-wartość w obiekcie poprawki i stosujemy odpowiednie aktualizacje do dokumentu docelowego. Jeśli dokument docelowy jest tablicą, obsługujemy przypadek, gdy obiekt poprawki ma klucze, które są ważnymi indeksami tablicy, i modyfikujemy docelową tablicę w odpowiedni sposób:
Jeśli wartość jest równa null, a indeks mieści się w granicach tablicy docelowej, usuń element w tym indeksie.
Jeśli indeks mieści się w granicach tablicy docelowej, zastosuj JSON Merge Patch rekurencyjnie.
Jeśli indeks jest większy niż obecna długość tablicy docelowej, wypełnij tablicę docelową wartościami null, aż osiągnie pożądany indeks, a następnie dołącz wartość z obiektu poprawki.
Jeśli dokument poprawki nie jest obiektem, po prostu zastępujemy dokument docelowy dokumentem poprawki.
Testowanie implementacji:
Teraz, gdy zaimplementowaliśmy algorytm JSON Merge Patch, przetestujmy go za pomocą kilku przykładów:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_json_merge_patch_should_merge_objects_and_override_field() {
let mut target = serde_json::from_str(r#"{"a": "b", "c": {"d": "e", "f": "g"}}"#).unwrap();
let patch = serde_json::from_str(r#"{"a": "z", "c": {"f": null}}"#).unwrap();
json_merge_patch(&mut target, &patch);
let expected: serde_json::Value =
serde_json::from_str(r#"{"a": "z", "c": {"d": "e"}}"#).unwrap();
assert_eq!(target, expected);
}
#[test]
fn test_json_merge_patch_should_override_field_in_object() {
let mut target = serde_json::from_str(r#"{"a": "b"}"#).unwrap();
let patch = serde_json::from_str(r#"{"a": "c"}"#).unwrap();
let expected: serde_json::Value = serde_json::from_str(r#"{"a": "c"}"#).unwrap();
json_merge_patch(&mut target, &patch);
assert_eq!(target, expected);
}
#[test]
fn test_json_merge_patch_should_add_field_to_object() {
let mut target = serde_json::from_str(r#"{"a": "b"}"#).unwrap();
let patch = serde_json::from_str(r#"{"b": "c"}"#).unwrap();
let expected: serde_json::Value = serde_json::from_str(r#"{"a": "b", "b": "c"}"#).unwrap();
json_merge_patch(&mut target, &patch);
assert_eq!(target, expected);
}
#[test]
fn test_json_merge_patch_should_remove_field_from_object() {
let mut target = serde_json::from_str(r#"{"a": "b", "b": "c"}"#).unwrap();
let patch = serde_json::from_str(r#"{"a": null}"#).unwrap();
let expected: serde_json::Value = serde_json::from_str(r#"{"b": "c"}"#).unwrap();
json_merge_patch(&mut target, &patch);
assert_eq!(target, expected);
}
#[test]
fn test_json_merge_patch_should_override_field_in_array() {
let mut target = serde_json::from_str(r#"{"a": ["b"]}"#).unwrap();
let patch = serde_json::from_str(r#"{"a": "c"}"#).unwrap();
let expected: serde_json::Value = serde_json::from_str(r#"{"a": "c"}"#).unwrap();
json_merge_patch(&mut target, &patch);
assert_eq!(target, expected);
}
#[test]
fn test_json_merge_patch_should_replace_array_with_scalar() {
let mut target = serde_json::from_str(r#"{"a": "c"}"#).unwrap();
let patch = serde_json::from_str(r#"{"a": ["b"]}"#).unwrap();
let expected: serde_json::Value = serde_json::from_str(r#"{"a": ["b"]}"#).unwrap();
json_merge_patch(&mut target, &patch);
assert_eq!(target, expected);
}
#[test]
fn test_json_merge_patch_should_merge_objects_in_object() {
let mut target = serde_json::from_str(r#"{"a": {"b": "c"}}"#).unwrap();
let patch = serde_json::from_str(r#"{"a": {"b": "d", "c": null}}"#).unwrap();
let expected: serde_json::Value = serde_json::from_str(r#"{"a": {"b": "d"}}"#).unwrap();
json_merge_patch(&mut target, &patch);
assert_eq!(target, expected);
}
#[test]
fn test_json_merge_patch_should_replace_array_with_value() {
let mut target = serde_json::from_str(r#"{"a": [{"b": "c"}]}"#).unwrap();
let patch = serde_json::from_str(r#"{"a": [1]}"#).unwrap();
let expected: serde_json::Value = serde_json::from_str(r#"{"a": [1]}"#).unwrap();
json_merge_patch(&mut target, &patch);
assert_eq!(target, expected);
}
#[test]
fn test_json_merge_patch_should_merge_nested_objects_and_remove_leaf_nodes() {
let mut target = serde_json::from_str(r#"{}"#).unwrap();
let patch = serde_json::from_str(r#"{"a": {"bb": {"ccc": null}}}"#).unwrap();
let expected: serde_json::Value = serde_json::from_str(r#"{"a": {"bb": {}}}"#).unwrap();
json_merge_patch(&mut target, &patch);
assert_eq!(target, expected);
}
#[test]
fn test_json_merge_patch_should_replace_scalar_with_scalar() {
let mut target = serde_json::from_str(r#"{"a": "b"}"#).unwrap();
let patch = serde_json::from_str(r#"["c"]"#).unwrap();
let expected: serde_json::Value = serde_json::from_str(r#"["c"]"#).unwrap();
json_merge_patch(&mut target, &patch);
assert_eq!(target, expected);
}
#[test]
fn test_json_merge_patch_should_replace_scalar_with_null() {
let mut target = serde_json::from_str(r#"{"a": "foo"}"#).unwrap();
let patch = serde_json::Value::Null;
json_merge_patch(&mut target, &patch);
assert_eq!(target, serde_json::Value::Null);
}
#[test]
fn test_json_merge_patch_should_replace_scalar_with_string() {
let mut target = serde_json::from_str(r#"{"a": "foo"}"#).unwrap();
let patch = serde_json::from_str(r#""bar""#).unwrap();
let expected: serde_json::Value = serde_json::from_str(r#""bar""#).unwrap();
json_merge_patch(&mut target, &patch);
assert_eq!(target, expected);
}
#[test]
fn test_json_merge_patch_should_merge_null_with_scalar() {
let mut target = serde_json::from_str(r#"{"e": null}"#).unwrap();
let patch = serde_json::from_str(r#"{"a": 1}"#).unwrap();
let expected: serde_json::Value = serde_json::from_str(r#"{"e": null, "a": 1}"#).unwrap();
json_merge_patch(&mut target, &patch);
assert_eq!(target, expected);
}
#[test]
fn test_json_merge_patch_should_replace_array_with_object() {
let mut target = serde_json::from_str(r#"{"a": []}"#).unwrap();
let patch = serde_json::from_str(r#"{"a": {"b": "c"}}"#).unwrap();
let expected: serde_json::Value = serde_json::from_str(r#"{"a": {"b": "c"}}"#).unwrap();
json_merge_patch(&mut target, &patch);
assert_eq!(target, expected);
}
#[test]
fn test_json_merge_patch_should_merge_objects_in_array() {
let mut target = serde_json::from_str(r#"[{"a": "b"}, {"c": "d"}]"#).unwrap();
let patch = serde_json::from_str(r#"{"1": {"e": "f"}}"#).unwrap();
let expected: serde_json::Value =
serde_json::from_str(r#"[{"a": "b"}, {"c": "d", "e": "f"}]"#).unwrap();
json_merge_patch(&mut target, &patch);
assert_eq!(target, expected);
}
#[test]
fn test_json_merge_patch_should_replace_object_with_array() {
let mut target = serde_json::from_str(r#"{"a": {"b": "c"}}"#).unwrap();
let patch = serde_json::from_str(r#"{"a": []}"#).unwrap();
let expected: serde_json::Value = serde_json::from_str(r#"{"a": []}"#).unwrap();
json_merge_patch(&mut target, &patch);
assert_eq!(target, expected);
}
#[test]
fn test_json_merge_patch_should_merge_arrays() {
let mut target = serde_json::from_str(r#"["a", "b"]"#).unwrap();
let patch = serde_json::from_str(r#"["c", "d"]"#).unwrap();
let expected: serde_json::Value = serde_json::from_str(r#"["c", "d"]"#).unwrap();
json_merge_patch(&mut target, &patch);
assert_eq!(target, expected);
}
#[test]
fn test_json_merge_patch_should_remove_key_from_object() {
let mut target = serde_json::from_str(r#"{"a": "b"}"#).unwrap();
let patch = serde_json::from_str(r#"{"a": null}"#).unwrap();
let expected: serde_json::Value = serde_json::from_str(r#"{}"#).unwrap();
json_merge_patch(&mut target, &patch);
assert_eq!(target, expected);
}
#[test]
fn test_json_merge_patch_should_remove_index_from_array() {
let mut target = serde_json::from_str(r#"["a", "b"]"#).unwrap();
let patch = serde_json::from_str(r#"{"1": null}"#).unwrap();
let expected: serde_json::Value = serde_json::from_str(r#"["a"]"#).unwrap();
json_merge_patch(&mut target, &patch);
assert_eq!(target, expected);
}
#[test]
fn test_json_merge_patch_should_remove_array_element() {
let mut target = serde_json::from_str(r#"[1, 2, 3]"#).unwrap();
let patch = serde_json::from_str(r#"[null, 2]"#).unwrap();
let expected: serde_json::Value = serde_json::from_str(r#"[2]"#).unwrap();
json_merge_patch(&mut target, &patch);
assert_eq!(target, expected);
}
}
W tym poście na blogu pokazaliśmy, jak zaimplementować algorytm JSON Merge Patch (RFC 7396) w Rust. Nasza implementacja jest zarówno wydajna, jak i łatwa do zrozumienia, a także może być używana do stosowania częściowych aktualizacji w dokumentach JSON w sposób spójny.
Użyliśmy biblioteki serde_json
, aby pracować z danymi JSON w Rust. Więcej informacji na temat serde_json
można znaleźć w oficjalnej dokumentacji: https://docs.serde.rs/serde_json/.
Pełny kod źródłowy tej implementacji, w tym testy, jest dostępny na GitHubie:
GitHub - gustawdaniel/json-merge-patch: Rust implementation of RFC 7396 - JSON Merge Patch
Czuj się swobodnie, aby eksplorować, wnosić wkład lub używać tego jako punktu wyjścia do własnych projektów.
Dzięki silnym gwarancjom Rust dotyczących bezpieczeństwa, współbieżności i wydajności, implementacja JSON Merge Patch i innych algorytmów związanych z JSON może być zarówno przyjemna, jak i niezawodna. Zachęcam do spróbowania Rust w swoim następnym projekcie, który wymaga przetwarzania JSON lub jakichkolwiek innych zadań związanych z programowaniem na poziomie systemu.
Other articles
You can find interesting also.
Wybrane składnie w JavaScript ES2020, ES2021 i ES2022
Nullish coalescing, Opcjonalne łańcuchowanie, Proxies, Pola prywatne, allSettled, BigInt, Dynamiczny import, replaceAll, Separatorzy numeryczni, matchAll, Przypisanie logiczne, Await na najwyższym poziomie
Daniel Gustaw
• 19 min read
Pulumi - Infrastruktura jako kod [ Digital Ocean ]
Za pomocą Pulumi możesz zdefiniować swoją infrastrukturę IT w pliku opisanym za pomocą twojego ulubionego języka programowania. Ten artykuł pokazuje, jak to zrobić.
Daniel Gustaw
• 10 min read
Instalacja odnawialnego certyfikatu TLS (certbot + apache na Ubuntu)
Jest wiele metod uzyskiwania certyfikatu pozwalającego szyfrować ruch http. Jedną z nich jest instalacja certbota i użycie go w zestawieniu z serwerem apache.
Daniel Gustaw
• 2 min read