Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions xff/config/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,29 @@ cc_test(
"@googletest//:gtest_main",
],
)

# The loader core: resolves the parsed layers (system [defaults] + user/project
# .xffrc) under the active --config selectors into a precedence-ordered, prov-
# enance-tagged flag list. No discovery (phase B-next) or gating (phase C) yet.
cc_library(
name = "config_cc",
srcs = ["config.cc"],
hdrs = ["config.h"],
visibility = ["//xff:__subpackages__"],
deps = [
":ini_cc",
":xffrc_cc",
],
)

cc_test(
name = "config_test",
size = "small",
srcs = ["config_test.cc"],
deps = [
":config_cc",
":xffrc_cc",
"@googletest//:gtest",
"@googletest//:gtest_main",
],
)
75 changes: 75 additions & 0 deletions xff/config/config.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// SPDX-FileCopyrightText: Copyright (c) The helly25 authors (helly25.com)
// SPDX-License-Identifier: Apache-2.0
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include "xff/config/config.h"

#include <string>
#include <string_view>
#include <vector>

#include "xff/config/ini.h"
#include "xff/config/xffrc.h"

namespace xff::config {
namespace {

bool Contains(const std::vector<std::string>& haystack, std::string_view needle) {
for (const std::string& item : haystack) {
if (item == needle) {
return true;
}
}
return false;
}

// An .xffrc line applies under the active --config selectors when its base is
// "common"/empty or names an active config, AND its config is empty or names one.
bool LineApplies(const RcLine& line, const std::vector<std::string>& configs) {
const bool base_ok = line.base.empty() || line.base == "common" || Contains(configs, line.base);
const bool config_ok = line.config.empty() || Contains(configs, line.config);
return base_ok && config_ok;
}

void AppendMatching(
std::vector<ResolvedFlag>& out,
const std::vector<RcLine>& lines,
const std::vector<std::string>& configs,
Source source) {
for (const RcLine& line : lines) {
if (!LineApplies(line, configs)) {
continue;
}
for (const std::string& flag : line.flags) {
out.push_back(ResolvedFlag{.flag = flag, .source = source});
}
}
}

} // namespace

std::vector<ResolvedFlag> ResolveConfig(const ConfigInputs& inputs) {
std::vector<ResolvedFlag> resolved;
if (inputs.no_config) {
return resolved; // pure CLI + built-ins; the system policy (read elsewhere) still bounds the run
}
for (const std::string& flag : inputs.system.defaults) { // global system defaults, lowest precedence
resolved.push_back(ResolvedFlag{.flag = flag, .source = Source::kSystem});
}
AppendMatching(resolved, inputs.user, inputs.configs, Source::kUser);
AppendMatching(resolved, inputs.project, inputs.configs, Source::kProject);
return resolved;
}

} // namespace xff::config
58 changes: 58 additions & 0 deletions xff/config/config.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// SPDX-FileCopyrightText: Copyright (c) The helly25 authors (helly25.com)
// SPDX-License-Identifier: Apache-2.0
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#ifndef XFF_CONFIG_CONFIG_H_
#define XFF_CONFIG_CONFIG_H_

#include <string>
#include <vector>

#include "xff/config/ini.h"
#include "xff/config/xffrc.h"

namespace xff::config {

// Provenance of a resolved setting: which layer contributed it. Resolution is
// last-non-unset-wins; kUnset is the "no override" sentinel and is never stored.
enum class Source { kUnset, kSystem, kUser, kProject, kCli };

// One resolved config flag plus the layer it came from.
struct ResolvedFlag {
std::string flag;
Source source;
};

// The parsed layers + active selectors fed to ResolveConfig. CLI flags are NOT
// here: the caller applies them last (highest precedence) after this resolution.
struct ConfigInputs {
SystemConfig system; // parsed /etc/xff.ini ([defaults] + [policy])
std::vector<RcLine> user; // parsed user .xffrc
std::vector<RcLine> project; // parsed project .xffrc (single file for now; cascade is phase E)
std::vector<std::string> configs; // active --config=NAME selectors (styles and/or named configs)
bool no_config = false; // --no-config: drop user+project layers and system defaults
};

// Resolves config-supplied flags, lowest precedence first (system [defaults] <
// user .xffrc < project .xffrc), each tagged with its Source; the caller appends
// CLI flags afterwards (they win). An .xffrc line contributes its flags when its
// base selector is empty/"common" or names an active --config, AND its config
// selector is empty or names an active --config. --no-config yields an empty
// result (pure CLI + built-ins); the system *policy* is never dropped (it is read
// elsewhere and bounds the run regardless). No capability gating yet (phase C).
std::vector<ResolvedFlag> ResolveConfig(const ConfigInputs& inputs);

} // namespace xff::config

#endif // XFF_CONFIG_CONFIG_H_
91 changes: 91 additions & 0 deletions xff/config/config_test.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// SPDX-FileCopyrightText: Copyright (c) The helly25 authors (helly25.com)
// SPDX-License-Identifier: Apache-2.0
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include "xff/config/config.h"

#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "xff/config/xffrc.h"

namespace xff::config {
namespace {

using ::testing::IsEmpty;

struct ConfigTest : ::testing::Test {};

TEST_F(ConfigTest, NoConfigYieldsEmpty) {
ConfigInputs in;
in.system.defaults = {"--color=auto"};
in.user = ParseXffrc("common: --sort");
in.no_config = true;
EXPECT_THAT(ResolveConfig(in), IsEmpty());
}

TEST_F(ConfigTest, SystemDefaultsAreLowestPrecedence) {
ConfigInputs in;
in.system.defaults = {"--color=auto", "--threads=4"};
const auto resolved = ResolveConfig(in);
ASSERT_EQ(resolved.size(), 2U);
EXPECT_EQ(resolved[0].flag, "--color=auto");
EXPECT_EQ(resolved[0].source, Source::kSystem);
EXPECT_EQ(resolved[1].flag, "--threads=4");
}

TEST_F(ConfigTest, CommonAndBareLinesAlwaysApply) {
ConfigInputs in;
in.user = ParseXffrc("common: --color=never\n--sort");
const auto resolved = ResolveConfig(in);
ASSERT_EQ(resolved.size(), 2U);
EXPECT_EQ(resolved[0].flag, "--color=never");
EXPECT_EQ(resolved[0].source, Source::kUser);
EXPECT_EQ(resolved[1].flag, "--sort");
}

TEST_F(ConfigTest, BaseSelectorGatedByActiveConfig) {
ConfigInputs in;
in.user = ParseXffrc("xff: --feature=long-paths\nfind: --warn");
EXPECT_THAT(ResolveConfig(in), IsEmpty()); // no active --config -> neither base applies
in.configs = {"xff"};
const auto resolved = ResolveConfig(in);
ASSERT_EQ(resolved.size(), 1U);
EXPECT_EQ(resolved[0].flag, "--feature=long-paths");
}

TEST_F(ConfigTest, ConfigSelectorGatedByNamedConfig) {
ConfigInputs in;
in.user = ParseXffrc("xff:debug: --threads=1");
in.configs = {"xff"}; // style active, but not the :debug named config
EXPECT_THAT(ResolveConfig(in), IsEmpty());
in.configs = {"xff", "debug"};
const auto resolved = ResolveConfig(in);
ASSERT_EQ(resolved.size(), 1U);
EXPECT_EQ(resolved[0].flag, "--threads=1");
}

TEST_F(ConfigTest, LayerPrecedenceSystemThenUserThenProject) {
ConfigInputs in;
in.system.defaults = {"--color=auto"};
in.user = ParseXffrc("common: --sort");
in.project = ParseXffrc("common: --color=never");
const auto resolved = ResolveConfig(in);
ASSERT_EQ(resolved.size(), 3U);
EXPECT_EQ(resolved[0].source, Source::kSystem); // --color=auto
EXPECT_EQ(resolved[1].source, Source::kUser); // --sort
EXPECT_EQ(resolved[2].source, Source::kProject); // --color=never (later wins when applied)
}

} // namespace
} // namespace xff::config
Loading