ifw-daq 3.1.0
IFW Data Acquisition modules
Loading...
Searching...
No Matches
dictKeywordFormatter.cpp
Go to the documentation of this file.
1/**
2 * @file
3 * @copyright
4 * (c) Copyright ESO 2024
5 * All Rights Reserved
6 * ESO (eso.org) is an Intergovernmental Organisation, and therefore special legal conditions apply.
7 */
9
10#include <algorithm>
11#include <charconv>
12#include <cstdint>
13
14#include <boost/algorithm/string/join.hpp>
15
16namespace daq {
17using namespace daq::fits;
18
19namespace {
20
21template <class T>
22constexpr char const* TypeStr() noexcept;
23
24template <>
25constexpr char const* TypeStr<std::uint64_t>() noexcept {
26 return "uint64";
27}
28template <>
29constexpr char const* TypeStr<std::int64_t>() noexcept {
30 return "int64";
31}
32
33template <>
34constexpr char const* TypeStr<double>() noexcept {
35 return "double";
36}
37
38template <>
39constexpr char const* TypeStr<bool>() noexcept {
40 return "bool";
41}
42
43template <>
44constexpr char const* TypeStr<std::string>() noexcept {
45 return "string";
46}
47
48template <class T>
49constexpr auto DitDataType() noexcept -> core::dit::did::DataType;
50
51template <>
52constexpr auto DitDataType<std::uint64_t>() noexcept -> core::dit::did::DataType {
53 return core::dit::did::DataType::INTEGER;
54}
55
56template <>
57constexpr auto DitDataType<std::int64_t>() noexcept -> core::dit::did::DataType {
58 return core::dit::did::DataType::INTEGER;
59}
60
61template <>
62constexpr auto DitDataType<std::string>() noexcept -> core::dit::did::DataType {
63 return core::dit::did::DataType::STRING;
64}
65
66template <>
67constexpr auto DitDataType<bool>() noexcept -> core::dit::did::DataType {
68 return core::dit::did::DataType::BOOL;
69}
70
71/**
72 * FITS strings must be quoted with '' and any ' within must be escaped as `''`.
73 */
74void ValidateString(std::string_view name, std::string_view value) {
75 auto bad = [&](std::string_view reason) {
76 throw InvalidKeyword(
77 name, fmt::format("validating string value `{}` failed - {}", value, reason));
78 };
79 if (value.size() < 2u) {
80 bad("string not enclosed with `'`");
81 }
82 if (*value.begin() != '\'') {
83 bad("must begin with '");
84 }
85 if (*(value.end() - 1) != '\'') {
86 bad("must end with '");
87 }
88 // Check the content of {string} string within '{string}'
89 bool unescaped_quote = false;
90 for (auto c : value.substr(1, value.size() - 2)) {
91 if (unescaped_quote && c != '\'') {
92 bad("quote `'` must be escaped as `''`");
93 }
94 if (c == '\'') {
95 unescaped_quote = !unescaped_quote;
96 } else if (c < ' ' || c > '~') {
97 // Invalid character
98 bad(fmt::format("invalid character `{}`", c));
99 }
100 }
101
102 if (unescaped_quote) {
103 bad("quote `'` must be escaped as `''`");
104 }
105}
106
107void ValidateInteger(std::string_view name, std::string_view value) {
108 long int_value = 0;
109 auto [ptr, ec] = std::from_chars(value.begin(), value.end(), int_value);
110 if (ec != std::errc{}) {
111 // Could not parse full range or value is out of range
112 throw InvalidKeyword(name,
113 fmt::format("parsing base-10 integer value `{}` failed with {}",
114 value,
115 std::make_error_code(ec).message()));
116 } else if (ptr != value.end()) {
117 throw InvalidKeyword(
118 name,
119 fmt::format("parsing base-10 integer value `{}` failed as not all characters was "
120 "converted",
121 value));
122 }
123}
124
125auto FormatFloat(double value,
126 std::string_view format,
127 std::array<char, constants::RECORD_LENGTH>& buffer) -> std::string_view {
128 std::string fmt = std::string("%") + std::string(format);
129 auto sz = snprintf(buffer.data(), buffer.size(), fmt.c_str(), value);
130 return std::string_view(buffer.data(), sz);
131}
132
133auto ReformatFloat(std::string_view name,
134 std::string_view value,
135 std::string_view format,
136 std::array<char, constants::RECORD_LENGTH>& buffer) -> std::string_view {
137 double float_value = 0.0;
138 auto [ptr, ec] = std::from_chars(value.begin(), value.end(), float_value);
139 if (ec != std::errc{}) {
140 // Could not parse full range or value is out of range
141 throw InvalidKeyword(name,
142 fmt::format("parsing floating point value `{}` failed with {}",
143 value,
144 std::make_error_code(ec).message()));
145 } else if (ptr != value.end()) {
146 throw InvalidKeyword(
147 name,
148 fmt::format("parsing floating point value `{}` failed as not all characters was "
149 "converted",
150 value));
151 }
152 if (format.empty()) {
153 return {};
154 }
155 // Got a valid floating point value -> format it.
156 return FormatFloat(float_value, format, buffer);
157}
158
159auto Format(LiteralKeyword const& keyword, core::dit::did::Record const& record) -> LiteralKeyword {
160 using core::dit::did::DataType;
161 std::string_view logical_name = keyword.GetName().name;
162 std::array<char, constants::RECORD_LENGTH> value_buf;
163 auto const components = keyword.GetComponents();
164 auto new_components = components;
165
166 // Validate data type and reformat as necessary
167 switch (record.GetDataType()) {
168 case DataType::DOUBLE:
169 if (auto new_value =
170 ReformatFloat(logical_name, components.value, record.GetFormat(), value_buf);
171 !new_value.empty()) {
172 new_components.value = new_value;
173 }
174 break;
175 case DataType::BOOL:
176 // Must be `T` or `F`
177 if (components.value != "T" && components.value != "F") {
178 throw InvalidKeyword(
179 logical_name,
180 fmt::format("dictionary specifies value to be boolean but `{}` is not `T` or `F`",
181 components.value));
182 }
183 break;
184 case DataType::INTEGER:
185 ValidateInteger(logical_name, components.value);
186 case DataType::STRING:
187 // Must be `'[^']*'`
188 ValidateString(logical_name, components.value);
189 break;
190
191 case DataType::ALL: // GCOVR_EXCL_START
192 [[fallthrough]];
193 default:
194 // These are not really expected but if provided we won't do anything with it.
195 break;
196 // GCOVR_EXCL_STOP
197 };
198
199 // Add comment if none is provided
200 if (keyword.GetComponents().comment.empty() && !record.GetComment().empty()) {
201 new_components.comment = record.GetComment();
202 }
203
204 // Formatting also include insignificant whitespaces so we cannot simply compare the components
205 return Format(new_components);
206}
207
208auto CheckedFormat(KeywordNameView name,
209 double value,
210 std::optional<std::string> const& opt_comment,
211 core::dit::did::Record const& record) -> LiteralKeyword {
212 namespace did = core::dit::did;
213 if (record.GetDataType() != did::DataType::DOUBLE) {
214 throw InvalidKeyword(name.name,
215 fmt::format("dictionary specifies value to be {} but value is {}",
216 DataTypeToStr(record.GetDataType()),
217 TypeStr<decltype(value)>()));
218 }
219 auto comment = opt_comment.value_or(record.GetComment());
220 if (auto const& fmt = record.GetFormat(); !fmt.empty()) {
221 // Use user-provided format
222 std::array<char, constants::RECORD_LENGTH> buf;
223 return UntypedFormat(name, FormatFloat(value, fmt, buf), comment);
224 } else {
225 // No custom format, just use default.
226 return UntypedFormat(name, FormatFitsValue(name, value), comment);
227 }
228}
229
230template <class Trait>
231auto Format(BasicKeyword<Trait> const& keyword, core::dit::did::Record const& record)
232 -> LiteralKeyword {
233 // Handle special case of double reformatting
234 return std::visit(
235 [&](auto& value) -> LiteralKeyword {
236 using T = std::decay_t<decltype(value)>;
237 // In
238 if constexpr (std::is_same_v<T, double>) {
239 return CheckedFormat(keyword.GetName(), value, keyword.comment, record);
240 } else {
241 if (DitDataType<T>() != record.GetDataType()) {
242 throw InvalidKeyword(
243 keyword.GetName().name,
244 fmt::format("dictionary specifies value to be {} but value is {}",
245 core::dit::did::DataTypeToStr(record.GetDataType()),
246 TypeStr<T>()));
247 }
248 // Use default formatting
249 if (!keyword.comment && !record.GetComment().empty()) {
250 auto copy = keyword;
251 copy.comment = record.GetComment();
252 return daq::fits::Format(copy);
253 } else {
254 return daq::fits::Format(keyword);
255 }
256 }
257 },
258 keyword.value);
259}
260
261} // namespace
262
263void DictKeywordFormatter::Load(std::vector<std::string> const& dictionaries) try {
264 std::for_each(dictionaries.begin(), dictionaries.end(), [&](std::string const& path) {
265 m_did.Load(path);
266 });
267 std::copy(dictionaries.begin(), dictionaries.end(), std::back_inserter(m_dictionaries));
268} catch (...) {
269 char const* cfgpath = std::getenv("CFGPATH");
270 if (cfgpath == nullptr) {
271 cfgpath = "{unset}";
272 }
273 std::throw_with_nested(
274 std::runtime_error(fmt::format("Failed to load dictionaries using $CFGPATH={}", cfgpath)));
275}
276
278 auto name = GetKeywordName(keyword);
279 core::dit::did::Record record;
280 if (!m_did.LookUp(
281 std::string(name.name), record, true /* allow index subst */, false /* throw */)) {
282 throw UnknownKeyword(name.name, m_dictionaries);
283 }
284 return daq::Format(keyword, record);
285}
286auto DictKeywordFormatter::GetDictionaries() const noexcept -> std::vector<std::string> const& {
287 return m_dictionaries;
288}
290 return boost::algorithm::join(m_dictionaries, ", ");
291}
292
293auto Format(KeywordVariant const& keyword, core::dit::did::Record const& record) -> LiteralKeyword {
294 return std::visit([&](auto& kw) -> LiteralKeyword { return Format(kw, record); }, keyword);
295}
296} // namespace daq
auto GetDictionaries() const noexcept -> std::vector< std::string > const &
void Load(std::vector< std::string > const &dictionaries)
Load dictionaries or throw exception if not found.
auto GetDictionariesNames() const -> std::string
auto Format(fits::KeywordVariant const &keyword) const -> fits::LiteralKeyword override
Look up keyword from loaded dictionaries and format using Format(fits::KeywordVariant const&,...
Indicates keyword is invalid for some reason.
Definition: keyword.hpp:534
Represents the literal 80-character FITS keyword record.
Definition: keyword.hpp:129
constexpr KeywordNameView GetName() const &noexcept
Query logical keyword name.
Definition: keyword.hpp:190
Indicates keyword is unknown and cannot be formatted.
Definition: keyword.hpp:543
constexpr KeywordNameView GetKeywordName(EsoKeyword const &keyword) noexcept
Get keyword name from keyword.
Definition: keyword.hpp:436
LiteralKeyword Format(KeywordVariant const &keyword)
Definition: keyword.cpp:782
auto FormatFitsValue(KeywordNameView name, BasicKeywordBase::ValueType const &value) -> std::string
Format keyword value using built-in rules such that it can be used as a component when formatting a c...
Definition: keyword.cpp:717
auto UntypedFormat(KeywordNameView name, std::string_view value, std::string_view comment) -> LiteralKeyword
Untyped formatting where value already has been formatted correctly.
Definition: keyword.cpp:817
std::variant< ValueKeyword, EsoKeyword, LiteralKeyword > KeywordVariant
The different variants of keywords that are supported.
Definition: keyword.hpp:409
A type safe version of LiteralKeyword that consist of the three basic components of a FITS keyword ke...
Definition: keyword.hpp:275
std::string_view name
Definition: keyword.hpp:84