如何在不知道映射中的键和终端标量类型的情况下使用 yaml-cpp 库解析任意 yaml 文件?

How to parse arbitrary yaml file with yaml-cpp library without knowing keys in mappings and types of the terminal scalars?

最近,我接到了一个任务,要解析一个包含自动计算参数的 YAML 文件。我以前没听说过 "YAML" 这个词,但老实说,我在 Internet 上找到了所有关于它的资料。

我看到的所有解析示例都是基于他们事先知道解析的.yaml文件的内容。他们知道 yaml 映射中的键和终端标量的类型。但是对于我必须编写的组件,情况并非如此。该组件是一种中介,它只应将 .yaml 文件转换为复合 运行 时间结构,该结构将作为最终客户端的数据源。

选择 "composite" 模式表示 运行 时间是因为 YAML 文本本身是一种 "persistent composite"。此 运行 时间结构的草稿可能如下所示:

class CAnyAttribute{};

class CAttrBaseNode
{
    std::wstring    m_Name;
    std::string     m_Tag;
public:
    CAttrBaseNode(const std::wstring& Name) : m_Name(Name) {}

    std::wstring Name() const { return m_Name; }
    std::string Tag() const { return m_Tag; }
    void SetTag(const std::string& TagVal) { m_Tag = TagVal; }

    virtual ~CAttrBaseNode() {}

    virtual bool IsScalar() const = 0;

    virtual CAnyAttribute Value() const
        { return CAnyAttribute(); }
    virtual bool SetValue(const CAnyAttribute& Val)
        { return false; }

    virtual CAttrBaseNode* Child(int Idx) const
        { return 0; }       // Get node by index
    virtual CAttrBaseNode* Child(const std::wstring& Key) const
        { return 0; }       // Get node by key

    virtual bool AddChild(CAttrBaseNode* pNode, int Idx = -1)
        { return false; }   // Add node by index
};

class CAttrLeafNode : public CAttrBaseNode
{
    CAnyAttribute   m_Value;
public:
    CAttrLeafNode(const std::wstring& Name, const CAnyAttribute& Val) :
        CAttrBaseNode(Name), m_Value(Val) {}
    ~CAttrLeafNode() {}

    bool IsScalar() const override { return true; }

    CAnyAttribute Value() const override
        { return m_Value; }
    bool SetValue(const CAnyAttribute& Val) override
        { m_Value = Val; return true; }
};

class CAttrCompositeNode : public CAttrBaseNode
{
    std::vector<CAttrBaseNode*>     m_Children;
public:
    CAttrCompositeNode(const std::wstring& Name) : CAttrBaseNode(Name) {}
    ~CAttrCompositeNode() { for (auto& pChild : m_Children) delete pChild; }

    bool IsScalar() const override { return false; }

    CAttrBaseNode* Child(int Idx) const override
        {
            return (0 <= Idx && Idx < (int)m_Children.size()) ? m_Children[Idx] : 0;
        }
    CAttrBaseNode* Child(const std::wstring& Key) const override
        {
            auto it = std::find_if(m_Children.begin(), m_Children.end(),
                    [Key](CAttrBaseNode* pNode)->bool
                    { return pNode->Name() == Key; });
            return (it != m_Children.end()) ? *it : 0;
        }

    bool AddChild(CAttrBaseNode* pNode, int Idx = -1) override
        {
            if (pNode == 0 || Idx >= (int)m_Children.size())
                return false;
            if (Idx < 0)
                m_Children.push_back(pNode);
            else
                m_Children.insert(m_Children.begin() + Idx, pNode);
            return true;
        }
};

在此草案中,CAnyAttribute 是一个具体的 class,可以分配任何标量值:不同大小的整数和浮点数、字符串甚至原始 "N bytes"。此 class 已在我们的软件产品中使用多年。

我知道这整个任务可能看起来很奇怪,因为来自 yaml-cpp 解析器的 YAML::Node 本身就是 YAML 文本的 运行 时间表示。有两个原因。首先,YAML::Node 接口不是标准的,没有很好的文档化并且需要时间来理解。假设公司里很多程序员都要处理,这是很多时间。第二个也是更重要的一点,我们不想与某个特定的库紧密相关。

现在的问题是:如何将任意一个yaml文件解析成上面的运行时间结构?我们使用最新版本0.6的yaml-cpp库。

我找到了解决办法,希望对其他人也有用。这是:

void Scalar2AnyAttribute(const YAML::Node& ScalarNode, CAnyAttribute& AnyAttr)
{
    assert(ScalarNode.IsScalar());
    //
    // Parse the scalar value using the @flyx's advice.
    //
}

CAttrBaseNode* Translate(const YAML::Node& YNode, const std::string& Name = std::string())
{
    CAttrBaseNode* pRet = 0;

    switch (YNode.Type())
    {
    case YAML::NodeType::Null:
        pRet = new CAttrLeafNode(Name, CAnyAttribute());
        break;
    case YAML::NodeType::Scalar:
        {
            CAnyAttribute Value;
            Scalar2AnyAttribute(YNode, Value);
            pRet = new CAttrLeafNode(Name, Value);
        }
        break;
    case YAML::NodeType::Sequence:
        {
            CAttrCompositeNode* pComp = new CAttrCompositeNode(Name);
            for (YAML::const_iterator it = YNode.begin(); it != YNode.end(); ++it)
                pComp->AddChild(Translate(*it));
            pRet = pComp;
        }
        break;
    case YAML::NodeType::Map:
        {
            CAttrCompositeNode* pComp = new CAttrCompositeNode(Name);
            for (YAML::const_iterator it = YNode.begin(); it != YNode.end(); ++it)
            {
                std::string MappingKey = it->first.as<std::string>();
                pComp->AddChild(Translate(it->second, MappingKey));
            }
            pRet = pComp;
        }
        break;
    default:
        break;
    }

    if (pRet)
        pRet->SetTag(YNode.Tag());

    return pRet;
}

int main()
{
    std::string file("....\a.yaml");
    YAML::Node baseNode = YAML::LoadFile(file);
    CAttrBaseNode* pComposite = Translate(baseNode);
    // ...
    // Work with pComposite.
    // ...
    delete pComposite;
    std::cout << "Hello, my first translation from YAML!\n";
}