cppfig 0.1.0
Modern C++20 compile-time type-safe configuration library
Loading...
Searching...
No Matches
conf.h
Go to the documentation of this file.
1#pragma once
2
3#include <cstddef>
4#include <cstdint>
5#include <istream>
6#include <map>
7#include <set>
8#include <sstream>
9#include <string>
10#include <string_view>
11#include <utility>
12#include <vector>
13
14#include "cppfig/status.h"
15#include "cppfig/value.h"
16
17namespace cppfig {
18
42
44 static auto Parse(std::istream& is) -> StatusOr<Value>
45 {
46 Value result = Value::Object();
47 std::string line;
48 int line_number = 0;
49
50 while (std::getline(is, line)) {
51 ++line_number;
52
53 auto trimmed = Trim(line);
54
55 // Skip empty lines and comments
56 if (trimmed.empty() || trimmed[0] == '#') {
57 continue;
58 }
59
60 // key = value
61 auto eq_pos = trimmed.find('=');
62 if (eq_pos == std::string::npos) {
63 return InvalidArgumentError("conf parse error: missing '=' on line " + std::to_string(line_number));
64 }
65
66 std::string key = Trim(trimmed.substr(0, eq_pos));
67 std::string value_str = Trim(trimmed.substr(eq_pos + 1));
68
69 // Reject keys SetAtPath would have to ignore, so a malformed file
70 // is reported with its line number rather than silently dropped.
71 if (Value::SplitPath(key).empty()) {
72 return InvalidArgumentError("conf parse error: ill-formed key '" + key + "' on line " + std::to_string(line_number));
73 }
74
75 result.SetAtPath(key, InferValue(value_str));
76 }
77
78 return result;
79 }
80
82 static auto ParseString(std::string_view str) -> StatusOr<Value>
83 {
84 std::istringstream stream { std::string(str) };
85 return Parse(stream);
86 }
87
89 static auto Stringify(const Value& data) -> std::string
90 {
91 std::vector<std::pair<std::string, const Value*>> leaves;
92 CollectLeaves(data, "", leaves);
93
94 std::ostringstream stream;
95 for (const auto& [path, val] : leaves) {
96 stream << path << " = " << ValueToString(*val) << '\n';
97 }
98
99 return stream.str();
100 }
101
116 static auto Merge(std::string_view existing, const Value& data) -> std::string
117 {
118 std::vector<std::pair<std::string, const Value*>> leaves;
119 CollectLeaves(data, "", leaves);
120
121 std::map<std::string, const Value*, std::less<>> by_path;
122 for (const auto& [path, val] : leaves) {
123 by_path.emplace(path, val);
124 }
125
126 std::set<std::string, std::less<>> updated;
127 std::ostringstream out;
128 std::istringstream in { std::string(existing) };
129 std::string line;
130
131 while (std::getline(in, line)) {
132 auto trimmed = Trim(line);
133 auto eq_pos = line.find('=');
134
135 // Comments, blank lines and anything without a separator are the
136 // user's; pass them through untouched.
137 if (trimmed.empty() || trimmed[0] == '#' || eq_pos == std::string::npos) {
138 out << line << '\n';
139 continue;
140 }
141
142 const std::string key = Trim(line.substr(0, eq_pos));
143 auto entry = by_path.find(key);
144 if (entry == by_path.end()) {
145 // A key the schema no longer has. Removing it is the user's
146 // call, not ours.
147 out << line << '\n';
148 continue;
149 }
150
151 updated.insert(key);
152
153 const std::string rendered = ValueToString(*entry->second);
154 if (InferValue(Trim(line.substr(eq_pos + 1))) == *entry->second) {
155 out << line << '\n'; // unchanged — keep the user's spacing
156 }
157 else {
158 out << line.substr(0, eq_pos + 1) << ' ' << rendered << '\n';
159 }
160 }
161
162 AppendMissing(out, leaves, updated);
163 return out.str();
164 }
165
166private:
168 static auto Trim(std::string_view sv) -> std::string
169 {
170 auto start = sv.find_first_not_of(" \t\r\n");
171 if (start == std::string_view::npos) {
172 return "";
173 }
174 auto end = sv.find_last_not_of(" \t\r\n");
175 return std::string(sv.substr(start, end - start + 1));
176 }
177
179 static auto InferValue(const std::string& str) -> Value
180 {
181 // Quoted string
182 if (str.size() >= 2 && str.front() == '"' && str.back() == '"') {
183 return { str.substr(1, str.size() - 2) };
184 }
185
186 // Empty → empty string
187 if (str.empty()) {
188 return { std::string("") };
189 }
190
191 // Boolean
192 if (str == "true" || str == "yes" || str == "on") {
193 return { true };
194 }
195 if (str == "false" || str == "no" || str == "off") {
196 return { false };
197 }
198
199 // Integer
200 try {
201 std::size_t pos = 0;
202 auto int_val = std::stoll(str, &pos);
203 if (pos == str.size()) {
204 return { static_cast<std::int64_t>(int_val) };
205 }
206 }
207 catch (...) {
208 }
209
210 // Double (only if it contains a decimal point or exponent)
211 if (str.find('.') != std::string::npos || str.find('e') != std::string::npos || str.find('E') != std::string::npos) {
212 try {
213 std::size_t pos = 0;
214 auto double_val = std::stod(str, &pos);
215 if (pos == str.size()) {
216 return { double_val };
217 }
218 }
219 catch (...) {
220 }
221 }
222
223 // Unquoted string
224 return { str };
225 }
226
228 static auto ValueToString(const Value& val) -> std::string
229 {
230 if (val.IsNull()) {
231 return "";
232 }
233 if (val.IsBoolean()) {
234 return val.Get<bool>() ? "true" : "false";
235 }
236 if (val.IsInteger()) {
237 return std::to_string(val.Get<std::int64_t>());
238 }
239 if (val.IsDouble()) {
240 std::ostringstream stream;
241 stream << val.Get<double>();
242 return stream.str();
243 }
244 if (val.IsString()) {
245 const auto& str = val.Get<std::string>();
246 // Quote if the string contains special characters or is empty
247 bool needs_quoting = str.empty() || str.front() == ' ' || str.back() == ' ' || str.front() == '"' || str.find_first_of("=#\n\r") != std::string::npos;
248 if (needs_quoting) {
249 return "\"" + str + "\"";
250 }
251 return str;
252 }
253 return "";
254 }
255
257 static void AppendMissing(std::ostringstream& out,
258 const std::vector<std::pair<std::string, const Value*>>& leaves,
259 const std::set<std::string, std::less<>>& updated)
260 {
261 bool wrote_header = false;
262 for (const auto& [path, val] : leaves) {
263 if (updated.contains(path)) {
264 continue;
265 }
266 if (!wrote_header) {
267 out << "\n# Added by cppfig\n";
268 wrote_header = true;
269 }
270 out << path << " = " << ValueToString(*val) << '\n';
271 }
272 }
273
275 static void CollectLeaves(const Value& node, const std::string& prefix, std::vector<std::pair<std::string, const Value*>>& leaves)
276 {
277 if (node.IsObject()) {
278 for (const auto& [key, val] : node.Items()) {
279 std::string path = prefix.empty() ? key : prefix + "." + key;
280 CollectLeaves(val, path, leaves);
281 }
282 }
283 else {
284 leaves.emplace_back(prefix, &node);
285 }
286 }
287};
288
289} // namespace cppfig
A value-or-error type, similar to std::expected (C++23).
Definition status.h:102
A self-contained, recursive value type for configuration data.
Definition value.h:32
void SetAtPath(std::string_view path, const Value &value)
Sets a value at a dot-separated path, creating intermediate objects.
Definition value.h:466
static auto Object() -> Value
Creates an empty object value.
Definition value.h:139
static auto SplitPath(std::string_view path) -> std::vector< std::string >
Splits a dot-separated path into its segments.
Definition value.h:413
C++20 compile-time type-safe configuration library.
Definition conf.h:17
auto InvalidArgumentError(std::string message) -> Status
Returns an InvalidArgument error status.
Definition status.h:63
Flat key-value .conf serializer — the default, zero-dependency serializer.
Definition conf.h:40
static auto Parse(std::istream &is) -> StatusOr< Value >
Parses a .conf stream into a Value tree.
Definition conf.h:44
static auto Merge(std::string_view existing, const Value &data) -> std::string
Rewrites an existing .conf document, preserving its layout.
Definition conf.h:116
static auto ParseString(std::string_view str) -> StatusOr< Value >
Parses a .conf string into a Value tree.
Definition conf.h:82
static auto Stringify(const Value &data) -> std::string
Converts a Value tree to flat key = value lines.
Definition conf.h:89