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

问题描述

最近,我的任务是解析一个 YAML 文件,其中包含一些自动计算的参数。我以前没有听说过“YAML”这个词,但老实说,我在互联网上找到了所有关于它的信息。

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

选择运行时表示的“复合”模式是因为 YAML 文本本身是一种“持久复合”。此运行时结构的草稿可能如下所示:

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是一个可以分配任何标量值的具体类:不同大小的整数和浮点数、字符串甚至原始的“N 字节”。此类已在我们的软件产品中使用多年。

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

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

标签: c++yamlyaml-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";
}

推荐阅读