Трудности десериализации XML с использованием Rust и Serde, где документ имеет необязательные подэлементы - PullRequest
1 голос
/ 23 декабря 2019

Я очень новичок в Rust и все еще пытаюсь научиться работать с ним. Это круто, но я явно что-то упускаю в упражнении, которое я дал себе. Для справки я использую rustc 1.39.0.

Я хотел попробовать написать простую программу для чтения XML из анализа кода MSBuild, которая выводит довольно простой XML. Я думаю, что проблема в том, что есть элемент (PATH), который обычно пуст, но иногда может содержать элементы под ним. Большая проблема в том, что я не умею работать с Rust (и я обычно не имею дело с XML), и я не совсем уверен, как правильно настроить структуры, необходимые для десериализации. Я использую Serde и quick_xml. Когда я установил PATH в качестве строки и работал с XML, в котором не было элемента SFA в PATH, мои тесты работали. Но как только я выяснил, как использовать этот тег предполагается , и соответственно обновил свои структуры, я постоянно получаю сообщение об ошибке:

thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Custom("missing field `FILEPATH`")', src\libcore\result.rs:1165:5

... даже если all дефектов в тестовом XML-файле имеют элемент SFA в PATH.

Все XML-файлы, с которыми я имею дело, выглядят так:

<?xml version="1.0" encoding="utf-8"?>
<DEFECTS>
  <DEFECT>
    <SFA>
      <FILEPATH>c:\projects\source\repos\defecttest\defecttest</FILEPATH>
      <FILENAME>source.cpp</FILENAME>
      <LINE>8</LINE>
      <COLUMN>5</COLUMN>
    </SFA>
    <DEFECTCODE>26496</DEFECTCODE>
    <DESCRIPTION>The variable 'y' is assigned only once, mark it as const (con.4).</DESCRIPTION>
    <FUNCTION>main</FUNCTION>
    <DECORATED>main</DECORATED>
    <FUNCLINE>6</FUNCLINE>
    <PATH></PATH>
  </DEFECT>
  <DEFECT>
    <SFA>
      <FILEPATH>c:\projects\source\repos\defecttest\defecttest</FILEPATH>
      <FILENAME>source.cpp</FILENAME>
      <LINE>9</LINE>
      <COLUMN>5</COLUMN>
    </SFA>
    <DEFECTCODE>26496</DEFECTCODE>
    <DESCRIPTION>The variable 'z' is assigned only once, mark it as const (con.4).</DESCRIPTION>
    <FUNCTION>main</FUNCTION>
    <DECORATED>main</DECORATED>
    <FUNCLINE>6</FUNCLINE>
    <PATH></PATH>
  </DEFECT>
</DEFECTS>

Во многих случаях PATH пусто, но в некоторых он содержит свой собственный элемент SFA:

  <DEFECT>
    <SFA>
      <FILEPATH>c:\projects\source\repos\defecttest\defecttest</FILEPATH>
      <FILENAME>source.cpp</FILENAME>
      <LINE>9</LINE>
      <COLUMN>5</COLUMN>
    </SFA>
    <DEFECTCODE>26496</DEFECTCODE>
    <DESCRIPTION>The variable 'z' is assigned only once, mark it as const (con.4).</DESCRIPTION>
    <FUNCTION>main</FUNCTION>
    <DECORATED>main</DECORATED>
    <FUNCLINE>6</FUNCLINE>
    <PATH>
      <SFA>
        <FILEPATH>c:\projects\source\repos\defecttest\defecttest</FILEPATH>
        <FILENAME>source.cpp</FILENAME>
        <LINE>12</LINE>
        <COLUMN>3</COLUMN>
      </SFA>
    </PATH>
  </DEFECT>

До того, как я это понял, все поля в структуре DEFECT были установлены в String. Это работает правильно, если предположить, что ни один из дефектов в XML-файле не имеет подэлементов в PATH. Когда я изменил его на SFA вместо String, он выдает ошибку отсутствующего поля, упомянутую выше. Пример кода, с которым я тестирую:

main.rs

extern crate quick_xml;
extern crate serde;

use std::default::Default;
use std::env;
use std::vec::Vec;

use quick_xml::de::from_str;
use serde::{Serialize, Deserialize};

/*
 * Structs for the defect XML
 */

#[derive(Serialize, Deserialize, Debug)]
#[allow(non_snake_case)]
pub struct DEFECTS {
    #[serde(rename = "DEFECT", default)]
    pub defects: Vec<DEFECT>,
}

#[derive(Default, Serialize, Deserialize, Debug)]
#[allow(non_snake_case)]
pub struct DEFECT {
    #[serde(default)]
    pub SFA: SFA,
    pub DEFECTCODE: String,
    pub DESCRIPTION: String,
    pub FUNCTION: String,
    pub DECORATED: String,
    pub FUNCLINE: String,
    #[serde(default)]
    pub PATH: Vec<SFA>,
}

#[derive(Default, Serialize, Deserialize, Debug)]
#[allow(non_snake_case)]
pub struct SFA {
    pub FILEPATH: String,
    pub FILENAME: String,
    pub LINE: String,
    pub COLUMN: String,
}

/*
 * Main app code
 */

fn main() {
    // Expect the path to the XML file to be passed as the first and only argument
    let args: Vec<String> = env::args().collect();
    if args.len() != 2 {
        panic!("Invalid argument count. Specify a single file to process.");
    }

    let processing_file = &args[1];
    println!("Will attempt to process file: '{}'", &processing_file);

    // Try to load the contents of the file
    let file_content : String = match std::fs::read_to_string(&processing_file) {
        Ok(file_content) => file_content,
        Err(e) => {
            panic!("Failed to read file: '{}' -- {}", &processing_file, e);
        }
    };

    // Now, try to deserialize the XML we have in file_content
    let defect_list : DEFECTS = from_str(&file_content).unwrap();

    // Assuming the unwrap above didn't blow up, we should get a count here
    println!("Retrieved {} defects from file '{}'", defect_list.defects.len(), &processing_file);
}

Cargo.toml

[package]
name = "rust_xml_test"
version = "0.1.0"
authors = ["fny82"]
edition = "2018"

[dependencies]
quick-xml = { version = "0.17", features = [ "serialize" ] }
serde = { version = "1.0", features = [ "derive" ] }

Пример выходных данных

C:\Development\RustXmlTest>cargo run -- "c:\development\rustxmltest\test3.xml"
   Compiling rust_xml_test v0.1.0 (C:\Development\RustXmlTest)
    Finished dev [unoptimized + debuginfo] target(s) in 1.56s
     Running `target\debug\rust_xml_test.exe c:\development\rustxmltest\test3.xml`
Will attempt to process file: 'c:\development\rustxmltest\test3.xml'
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Custom("missing field `FILEPATH`")', src\libcore\result.rs:1165:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.
error: process didn't exit successfully: `target\debug\rust_xml_test.exe c:\development\rustxmltest\test3.xml` (exit code: 101)

Я уверен, что делаю что-то глупое, и отчасти это связано с тем, что я забегаю вперед в отношении объемазадачи по сравнению с моим текущим уровнем понимания работы с Rust. Может кто-нибудь помочь мне разобраться, что я пропускаю и что делаю неправильно?

Несколько связано: с тех пор я узнал, что могу использовать свойство rename, чтобы мои структуры соответствовали Rustсоглашения об именах, но пока я не хотел начинать возиться с этим, пока не заработал базовую функциональность.

---- EDIT ----

Для справки, с исправлением от @edwardw теперь работающий код:

extern crate quick_xml;
extern crate serde;

use std::default::Default;
use std::env;
use std::vec::Vec;

use quick_xml::de::from_str;
use serde::{Serialize, Deserialize};

/*
 * Structs for the defect XML
 */

#[derive(Serialize, Deserialize, Debug)]
#[allow(non_snake_case)]
pub struct DEFECTS {
    #[serde(rename = "DEFECT", default)]
    pub defects: Vec<DEFECT>,
}

#[derive(Default, Serialize, Deserialize, Debug)]
#[allow(non_snake_case)]
pub struct DEFECT {
    #[serde(default)]
    pub SFA: SFA,
    pub DEFECTCODE: String,
    pub DESCRIPTION: String,
    pub FUNCTION: String,
    pub DECORATED: String,
    pub FUNCLINE: String,
    pub PATH: PATH,
}

#[derive(Default, Serialize, Deserialize, Debug)]
#[allow(non_snake_case)]
pub struct SFA {
    pub FILEPATH: String,
    pub FILENAME: String,
    pub LINE: String,
    pub COLUMN: String,
}

#[derive(Default, Serialize, Deserialize, Debug)]
#[allow(non_snake_case)]
pub struct PATH {
    pub SFA: Option<SFA>,
}

/*
 * Main app code
 */

fn main() {
    // Expect the path to the XML file to be passed as the first and only argument
    let args: Vec<String> = env::args().collect();
    if args.len() != 2 {
        panic!("Invalid argument count. Specify a single file to process.");
    }

    let processing_file = &args[1];
    println!("Will attempt to process file: '{}'", &processing_file);

    // Try to load the contents of the file
    let file_content : String = match std::fs::read_to_string(&processing_file) {
        Ok(file_content) => file_content,
        Err(e) => {
            panic!("Failed to read file: '{}' -- {}", &processing_file, e);
        }
    };

    // Now, try to deserialize the XML we have in file_content
    let defect_list : DEFECTS = from_str(&file_content).unwrap();

    // Assuming the unwrap above didn't blow up, we should get a count here
    println!("Retrieved {} defects from file '{}'", defect_list.defects.len(), &processing_file);
}

Пример:

C:\Development\RustXmlTest>cargo run -- "c:\development\rustxmltest\test1.xml"
   Compiling rust_xml_test v0.1.0 (C:\Development\RustXmlTest)
    Finished dev [unoptimized + debuginfo] target(s) in 1.66s
     Running `target\debug\rust_xml_test.exe c:\development\rustxmltest\test1.xml`
Will attempt to process file: 'c:\development\rustxmltest\test1.xml'
Retrieved 2 defects from file 'c:\development\rustxmltest\test1.xml'

, где test1.xml содержит:

<?xml version="1.0" encoding="utf-8"?>
<DEFECTS>
  <DEFECT>
    <SFA>
      <FILEPATH>c:\projects\source\repos\defecttest\defecttest</FILEPATH>
      <FILENAME>source.cpp</FILENAME>
      <LINE>8</LINE>
      <COLUMN>5</COLUMN>
    </SFA>
    <DEFECTCODE>26496</DEFECTCODE>
    <DESCRIPTION>The variable 'y' is assigned only once, mark it as const (con.4).</DESCRIPTION>
    <FUNCTION>main</FUNCTION>
    <DECORATED>main</DECORATED>
    <FUNCLINE>6</FUNCLINE>
    <PATH></PATH>
  </DEFECT>
  <DEFECT>
    <SFA>
      <FILEPATH>c:\projects\source\repos\defecttest\defecttest</FILEPATH>
      <FILENAME>source.cpp</FILENAME>
      <LINE>9</LINE>
      <COLUMN>5</COLUMN>
    </SFA>
    <DEFECTCODE>26496</DEFECTCODE>
    <DESCRIPTION>The variable 'z' is assigned only once, mark it as const (con.4).</DESCRIPTION>
    <FUNCTION>main</FUNCTION>
    <DECORATED>main</DECORATED>
    <FUNCLINE>6</FUNCLINE>
    <PATH>
      <SFA>
        <FILEPATH>c:\projects\source\repos\defecttest\defecttest</FILEPATH>
        <FILENAME>source.cpp</FILENAME>
        <LINE>12</LINE>
        <COLUMN>3</COLUMN>
      </SFA>
    </PATH>
  </DEFECT>
</DEFECTS>

1 Ответ

1 голос
/ 23 декабря 2019

PATH сам должен быть смоделирован как структура с одним необязательным полем. Это работает:

#[derive(Default, Serialize, Deserialize, Debug)]
#[allow(non_snake_case)]
pub struct DEFECT {
    #[serde(default)]
    pub SFA: SFA,
    pub DEFECTCODE: String,
    pub DESCRIPTION: String,
    pub FUNCTION: String,
    pub DECORATED: String,
    pub FUNCLINE: String,
    pub PATH: PATH,
}

#[derive(Default, Serialize, Deserialize, Debug)]
#[allow(non_snake_case)]
pub struct PATH {
    SFA: Option<SFA>,
}
...