aboutsummaryrefslogblamecommitdiffstats
path: root/library/cpp/yconf/patcher/config_patcher.cpp
blob: 93b8649705f1485d1060f03f9a06337ebcac1ba8 (plain) (tree)









































































































































































































                                                                                                                 
#include "config_patcher.h"

#include <library/cpp/yconf/patcher/unstrict_config.h>

#include <library/cpp/json/json_reader.h>
#include <library/cpp/json/json_prettifier.h>

#include <util/generic/maybe.h>
#include <util/generic/ptr.h>
#include <util/generic/xrange.h>
#include <util/stream/file.h>
#include <util/string/subst.h>

#include <array>

namespace {
    class TWrapper {
    public:
        TWrapper(const TString& base, const NJson::TJsonValue& patch, const TString& prefix);

        void Patch();
        TString GetPatchedConfig() {
            Patch();
            return PatchedConfixText;
        }

    private:
        void Preprocess();
        void Postprocess();

    private:
        static const TString IncludeDirective;
        static const TString IncludeGuard;

    private:
        TString BaseConfigText;
        THolder<TUnstrictConfig> Config;
        NJson::TJsonValue JsonPatch;
        TString Prefix;
        TString PatchedConfixText;
        bool Patched;
        bool NeedsPostprocessing;
    };

    const TString TWrapper::IncludeDirective = "#include";
    const TString TWrapper::IncludeGuard = "__INCLUDE__ :";

    TWrapper::TWrapper(const TString& base, const NJson::TJsonValue& patch, const TString& prefix)
        : BaseConfigText(base)
        , JsonPatch(patch)
        , Prefix(prefix)
        , Patched(false)
        , NeedsPostprocessing(false)
    {
    }

    void TWrapper::Patch() {
        if (Patched)
            return;
        Preprocess();

        const NJson::TJsonValue::TMapType* values;
        if (!JsonPatch.GetMapPointer(&values))
            ythrow yexception() << "unable to get map pointer in the json patch.";
        for (const auto& value : *values) {
            Config->PatchEntry(value.first, value.second.GetStringRobust(), Prefix);
        }

        Postprocess();
        Patched = true;
    }

    void TWrapper::Preprocess() {
        TString parsed;
        size_t pos = BaseConfigText.find(IncludeDirective);
        if (pos != TString::npos) {
            NeedsPostprocessing = true;
            parsed = BaseConfigText.replace(pos, IncludeDirective.size(), IncludeGuard);
        } else {
            parsed = BaseConfigText;
        }
        Config.Reset(new TUnstrictConfig);
        if (!Config->ParseMemory(parsed.data())) {
            TString errors;
            Config->PrintErrors(errors);
            ythrow yexception() << "Can't parse config:" << errors;
        }
    }

    void TWrapper::Postprocess() {
        PatchedConfixText = Config->ToString();
        if (NeedsPostprocessing) {
            SubstGlobal(PatchedConfixText, IncludeGuard, IncludeDirective);
        }
    }

    void MakeDiff(
            NJson::TJsonValue& container,
            const TYandexConfig::Section* source,
            const TYandexConfig::Section* target,
            const TString& parentPrefix = TString()) {
        Y_VERIFY(target);
        const TString& prefix = parentPrefix ? (parentPrefix + ".") : parentPrefix;

        for (const auto& [name, value]: target->GetDirectives()) {
            const auto p = source ? source->GetDirectives().FindPtr(name) : nullptr;
            if (!p || TString(*p) != value) {
                container[prefix + name] = value;
            }
        }

        if (source) {
            for (const auto& [name, value] : source->GetDirectives()) {
                if (!target->GetDirectives().contains(name)) {
                    container[prefix + name] = "__remove__";
                }
            }
        }

        TMap<TCiString, std::array<TVector<const TYandexConfig::Section *>, 2>> alignedSections;

        auto fillSections = [&](const TYandexConfig::TSectionsMap& sections, size_t targetIndex) {
            for (const auto& [sectionName, section] : sections) {
                alignedSections[sectionName][targetIndex].push_back(section);
            }
        };

        if (source) {
            fillSections(source->GetAllChildren(), 0);
        }
        fillSections(target->GetAllChildren(), 1);

        for (const auto& [sectionName, pair]: alignedSections) {
            // Cannot use structured binding on std::array here because of a bug in MSVC.
            const auto& sourceSections = pair[0];
            const auto& targetSections = pair[1];
            const bool needsIndex = targetSections.size() > 1;

            if (targetSections.empty()) {
                Y_VERIFY(source);
                container[prefix + sectionName] = "__remove_all__";
            } else {
                for (const size_t i: xrange(targetSections.size())) {
                    MakeDiff(
                            container,
                            i < sourceSections.size() ? sourceSections[i] : nullptr,
                            targetSections[i],
                            !needsIndex ? prefix + sectionName : prefix + sectionName + '[' + ToString(i) + ']');
                }
                if (sourceSections.size() > targetSections.size()) {
                    container[
                        prefix +
                            sectionName +
                            '[' +
                            ToString(targetSections.size()) +
                            ':' +
                            ToString(sourceSections.size() - 1) +
                            ']'] = "__remove__";
                }
            }
        }

        if (target->GetAllChildren().empty() && target->GetDirectives().empty()) {
            container[prefix] = "__add_section__";
        }
    }
}

namespace NConfigPatcher {
    TString Patch(const TString& config, const TString& patch, const TOptions& options) {
        if (!patch) {
            return config;
        }

        NJson::TJsonValue parsedPatch;
        TStringInput ss(patch);
        if (!NJson::ReadJsonTree(&ss, true, &parsedPatch, true)) {
            ythrow yexception() << "Cannot parse patch as json";
        }
        return Patch(config, parsedPatch, options);
    }

    TString Patch(const TString& config, const NJson::TJsonValue& parsedPatch, const TOptions& options) {
        TWrapper patcher(config, parsedPatch, options.Prefix);
        return patcher.GetPatchedConfig();
    }

    TString Diff(const TString& sourceText, const TString& targetText) {
        TUnstrictConfig source;
        if (!source.ParseMemory(sourceText.data())) {
            throw yexception() << "Cannot parse source config";
        }

        TUnstrictConfig target;
        if (!target.ParseMemory(targetText.data())) {
            throw yexception() << "Cannot parse target config";
        }

        NJson::TJsonValue diff(NJson::JSON_MAP);
        MakeDiff(diff, source.GetRootSection(), target.GetRootSection());
        return NJson::PrettifyJson(diff.GetStringRobust());
    }
}