ParseOptions.hpp
Go to the documentation of this file.
1 // Distributed under the MIT License.
2 // See LICENSE.txt for details.
3 
4 /// \file
5 /// Defines classes and functions that handle parsing of input parameters.
6 
7 #pragma once
8 
9 #include <cstring>
10 #include <exception>
11 #include <iterator>
12 #include <map>
13 #include <ostream>
14 #include <sstream>
15 #include <string>
16 #include <unordered_map>
17 #include <unordered_set>
18 #include <utility>
19 #include <vector>
20 #include <yaml-cpp/yaml.h>
21 
22 #include "ErrorHandling/Assert.hpp"
23 #include "ErrorHandling/Error.hpp"
24 #include "Options/Options.hpp"
26 #include "Utilities/Overloader.hpp"
27 #include "Utilities/PrettyType.hpp"
28 #include "Utilities/Requires.hpp"
29 #include "Utilities/StdHelpers.hpp"
30 #include "Utilities/TypeTraits.hpp"
31 
32 // Defining methods as inline in a different header from the class
33 // definition is somewhat strange. It is done here to minimize the
34 // amount of code in the frequently-included Options.hpp file. The
35 // only external consumers of Option should be create_from_yaml
36 // specializations, and they should only be instantiated by code in
37 // this file. (Or explicitly instantiated in cpp files, which can
38 // include this file.)
39 
40 inline Option::Option(YAML::Node node, OptionContext context) noexcept
41  // clang-tidy: YAML::Node not movable (as of yaml-cpp-0.5.3)
42  : node_(std::make_unique<YAML::Node>(std::move(node))),
43  context_(std::move(context)) { // NOLINT
44  context_.line = node.Mark().line;
45  context_.column = node.Mark().column;
46  }
47 
48 inline Option::Option(OptionContext context) noexcept
49  : node_(std::make_unique<YAML::Node>()), context_(std::move(context)) {}
50 
51 inline const YAML::Node& Option::node() const noexcept { return *node_; }
52 inline const OptionContext& Option::context() const noexcept {
53  return context_;
54 }
55 
56 /// Append a line to the contained context.
57 inline void Option::append_context(const std::string& context) noexcept {
58  context_.append(context);
59 }
60 
61 inline void Option::set_node(YAML::Node node) noexcept {
62  // clang-tidy: YAML::Node not movable (as of yaml-cpp-0.5.3)
63  *node_ = std::move(node); // NOLINT
64  context_.line = node_->Mark().line;
65  context_.column = node_->Mark().column;
66 }
67 
68 template <typename T>
69 T Option::parse_as() const {
70  try {
71  // yaml-cpp's `as` method won't parse empty nodes, so we need to
72  // inline a bit of its logic.
73  Options_detail::wrap_create_types<T> result{};
74  if (YAML::convert<decltype(result)>::decode(node(), result)) {
75  return Options_detail::unwrap_create_types(std::move(result));
76  }
77  // clang-tidy: thrown exception is not nothrow copy constructible
78  throw YAML::BadConversion(node().Mark()); // NOLINT
79  } catch (const YAML::BadConversion& e) {
80  // This happens when trying to parse an empty value as a container
81  // with no entries.
82  if ((tt::is_a_v<std::vector, T> or
83  tt::is_std_array_of_size_v<0, T> or
84  tt::is_maplike_v<T>) and
85  node().IsNull()) {
86  return T{};
87  }
88  OptionContext error_context = context();
89  error_context.line = e.mark.line;
90  error_context.column = e.mark.column;
92  ss << "Failed to convert value to type "
93  << Options_detail::yaml_type<T>::value()
94  << ":";
95 
96  const std::string value_text = YAML::Dump(node());
97  if (value_text.find('\n') == std::string::npos) {
98  ss << " " << value_text;
99  } else {
100  // Indent each line of the value by two spaces and start on a new line
101  ss << "\n ";
102  for (char c : value_text) {
103  ss << c;
104  if (c == '\n') {
105  ss << " ";
106  }
107  }
108  }
109 
110  if (tt::is_a_v<std::vector, T> or tt::is_std_array_v<T>) {
111  ss << "\n\nNote: For sequences this can happen because the length of the "
112  "sequence specified\nin the input file is not equal to the length "
113  "expected by the code. Sequences in\nfiles can be denoted either as "
114  "a bracket enclosed list ([foo, bar]) or with each\nentry on a "
115  "separate line, indented and preceeded by a dash ( - foo).";
116  }
117  PARSE_ERROR(error_context, ss.str());
118  } catch (const Options_detail::propagate_context& e) {
119  OptionContext error_context = context();
120  // Avoid line numbers in the middle of the trace
121  error_context.line = -1;
122  error_context.column = -1;
123  PARSE_ERROR(error_context, e.message());
124  } catch (std::exception& e) {
125  ERROR("Unexpected exception: " << e.what());
126  }
127 }
128 
129 /// \ingroup OptionParsingGroup
130 /// \brief Class that handles parsing an input file
131 ///
132 /// Options must be given YAML data to parse before output can be
133 /// extracted. This can be done either from a file (parse_file
134 /// method), from a string (parse method), or, in the case of
135 /// recursive parsing, from an Option (parse method). The options
136 /// can then be extracted using the get method.
137 ///
138 /// \example
139 /// \snippet Test_Options.cpp options_example_scalar_struct
140 /// \snippet Test_Options.cpp options_example_scalar_parse
141 ///
142 /// \see the \ref dev_guide_option_parsing tutorial
143 ///
144 /// \tparam OptionList the list of option structs to parse
145 template <typename OptionList>
146 class Options {
147  public:
148  /// \param help_text an overall description of the options
149  explicit Options(std::string help_text) noexcept;
150 
151  /// Parse a string to obtain options and their values.
152  ///
153  /// \param options the string holding the YAML formatted options
154  void parse(const std::string& options) noexcept;
155 
156  /// Parse an Option to obtain options and their values.
157  void parse(const Option& options);
158 
159  /// Parse a file containing options
160  ///
161  /// \param file_name the path to the file to parse
162  void parse_file(const std::string& file_name) noexcept;
163 
164  /// Get the value of the specified option
165  ///
166  /// \tparam T the option to retrieve
167  /// \return the value of the option
168  template <typename T>
169  typename T::type get() const;
170 
171  /// Call a function with the specified options as arguments.
172  ///
173  /// \tparam TagList a typelist of options to pass
174  /// \return the result of the function call
175  template <typename TagList, typename F>
176  decltype(auto) apply(F&& func) const;
177 
178  /// Get the help string
179  std::string help() const noexcept;
180 
181  private:
183  "The OptionList template parameter to Options must be a "
184  "tmpl::list<...>.");
185  using opts_t = OptionList;
186  static constexpr int max_label_size_ = 20;
187  static constexpr size_t max_help_size_ = 56;
188 
189  void parse(const YAML::Node& node);
190 
191  //@{
192  /// Check that the size is not smaller than the lower bound
193  ///
194  /// \tparam T the option struct
195  /// \param t the value of the read in option
196  template <typename T,
198  nullptr>
199  void check_lower_bound_on_size(const typename T::type& t,
200  const OptionContext& context) const;
201  template <typename T,
203  nullptr>
204  constexpr void check_lower_bound_on_size(
205  const typename T::type& /*t*/,
206  const OptionContext& /*context*/) const noexcept {}
207  //@}
208 
209  //@{
210  /// Check that the size is not larger than the upper bound
211  ///
212  /// \tparam T the option struct
213  /// \param t the value of the read in option
214  template <typename T,
216  nullptr>
217  void check_upper_bound_on_size(const typename T::type& t,
218  const OptionContext& context) const;
219  template <typename T,
221  nullptr>
222  constexpr void check_upper_bound_on_size(
223  const typename T::type& /*t*/,
224  const OptionContext& /*context*/) const noexcept {}
225  //@}
226 
227  //@{
228  /// Returns the default value or errors if there is no default.
229  ///
230  /// \tparam T the option struct
231  template <typename T,
233  typename T::type get_default() const noexcept {
234  static_assert(
235  cpp17::is_same_v<decltype(T::default_value()), typename T::type>,
236  "Default value is not of the same type as the option.");
237  return T::default_value();
238  }
239  template <typename T,
241  [[noreturn]] typename T::type get_default() const {
242  PARSE_ERROR(context_,
243  "You did not specify the option '" << Options_detail::name<T>()
244  << "'.\n" << help());
245  }
246  //@}
247 
248  //@{
249  /// If the options has a lower bound, check it is satisfied.
250  ///
251  /// Note: Lower bounds are >=, not just >.
252  /// \tparam T the option struct
253  /// \param t the value of the read in option
254  template <typename T,
256  void check_lower_bound(const typename T::type& t,
257  const OptionContext& context) const;
258  template <typename T,
260  constexpr void check_lower_bound(
261  const typename T::type& /*t*/,
262  const OptionContext& /*context*/) const noexcept {}
263  //@}
264 
265  //@{
266  /// If the options has a upper bound, check it is satisfied.
267  ///
268  /// Note: Upper bounds are <=, not just <.
269  /// \tparam T the option struct
270  /// \param t the value of the read in option
271  template <
272  typename T,
274  void check_upper_bound(const typename T::type& t,
275  const OptionContext& context) const;
276  template <typename T,
278  constexpr void check_upper_bound(
279  const typename T::type& /*t*/,
280  const OptionContext& /*context*/) const noexcept {}
281  //@}
282 
283  //@{
284  /// Check that the default (if any) satisfies the bounds
285  ///
286  /// \tparam T the option struct
287  template <typename T,
288  Requires<Options_detail::has_default<T>::value> = nullptr>
289  void validate_default() const {
290  OptionContext context;
291  context.append("Checking DEFAULT value for " + Options_detail::name<T>());
292  const auto default_value = T::default_value();
293  check_lower_bound_on_size<T>(default_value, context);
294  check_upper_bound_on_size<T>(default_value, context);
295  check_lower_bound<T>(default_value, context);
296  check_upper_bound<T>(default_value, context);
297  }
298  template <typename T,
299  Requires<not Options_detail::has_default<T>::value> = nullptr>
300  constexpr void validate_default() const noexcept {}
301  //@}
302 
303  /// Get the help string for parsing errors
304  std::string parsing_help(const YAML::Node& options) const noexcept;
305 
306  /// Error message when failed to parse an input file.
307  [[noreturn]] void parser_error(const YAML::Exception& e) const noexcept;
308 
309  std::string help_text_{};
310  OptionContext context_{};
311  std::unordered_set<std::string> valid_names_{};
313 };
314 
315 template <typename OptionList>
317  : help_text_(std::move(help_text)),
318  valid_names_(
319  tmpl::for_each<opts_t>(Options_detail::create_valid_names{}).value) {
320  tmpl::for_each<opts_t>([](auto t) noexcept {
321  using T = typename decltype(t)::type;
322  const std::string label = Options_detail::name<T>();
323  ASSERT(label.size() < max_label_size_,
324  "The option name " << label << " is too long for nice formatting, "
325  "please shorten the name to be under " << max_label_size_
326  << " characters");
327  ASSERT(std::strlen(T::help) > 0,
328  "You must supply a help string of non-zero length for " << label);
329  ASSERT(std::strlen(T::help) < max_help_size_,
330  "Option help strings should be short and to the point. "
331  "The help string for " << label << " should be less than "
332  << max_help_size_ << " characters long.");
333  });
334 }
335 
336 template <typename OptionList>
337 void Options<OptionList>::parse_file(const std::string& file_name) noexcept {
338  context_.append("In " + file_name);
339  try {
340  parse(YAML::LoadFile(file_name));
341  } catch (YAML::BadFile& /*e*/) {
342  ERROR("Could not open the input file " << file_name);
343  } catch (const YAML::Exception& e) {
344  parser_error(e);
345  }
346 }
347 
348 template <typename OptionList>
349 void Options<OptionList>::parse(const std::string& options) noexcept {
350  context_.append("In string");
351  try {
352  parse(YAML::Load(options));
353  } catch (YAML::Exception& e) {
354  parser_error(e);
355  }
356 }
357 
358 template <typename OptionList>
359 void Options<OptionList>::parse(const Option& options) {
360  context_ = options.context();
361  parse(options.node());
362 }
363 
364 template <typename OptionList>
365 void Options<OptionList>::parse(const YAML::Node& node) {
366  if (not(node.IsMap() or node.IsNull())) {
367  PARSE_ERROR(context_,
368  "'" << node << "' does not look like options.\n"
369  << help());
370  }
371  for (const auto& name_and_value : node) {
372  const auto& name = name_and_value.first.as<std::string>();
373  const auto& value = name_and_value.second;
374  auto context = context_;
375  context.line = name_and_value.first.Mark().line;
376  context.column = name_and_value.first.Mark().column;
377 
378  // Check for invalid key
379  if (1 != valid_names_.count(name)) {
380  PARSE_ERROR(context,
381  "Option '" << name << "' is not a valid option.\n"
382  << parsing_help(node));
383  }
384 
385  // Check for duplicate key
386  if (0 != parsed_options_.count(name)) {
387  PARSE_ERROR(context,
388  "Option '" << name << "' specified twice.\n"
389  << parsing_help(node));
390  }
391  parsed_options_.emplace(name, value);
392  }
393 }
394 
395 template <typename OptionList>
396 template <typename T>
397 typename T::type Options<OptionList>::get() const {
398  static_assert(
399  not cpp17::is_same_v<tmpl::index_of<opts_t, T>, tmpl::no_such_type_>,
400  "Could not find requested option in the list of options provided. Did "
401  "you forget to add the option struct to the OptionList?");
402  const std::string label = Options_detail::name<T>();
403 
404  validate_default<T>();
405  if (0 == parsed_options_.count(label)) {
406  return get_default<T>();
407  }
408 
409  Option option(parsed_options_.find(label)->second, context_);
410  option.append_context("While parsing option " + label);
411 
412  auto t = option.parse_as<typename T::type>();
413 
414  check_lower_bound_on_size<T>(t, option.context());
415  check_upper_bound_on_size<T>(t, option.context());
416  check_lower_bound<T>(t, option.context());
417  check_upper_bound<T>(t, option.context());
418  return t;
419 }
420 
421 namespace Options_detail {
422 template <typename>
423 struct apply_helper;
424 
425 template <typename... Tags>
426 struct apply_helper<tmpl::list<Tags...>> {
427  template <typename Options, typename F>
428  static decltype(auto) apply(const Options& opts, F&& func) {
429  return func(opts.template get<Tags>()...);
430  }
431 };
432 } // namespace Options_detail
433 
434 // \cond
435 // Doxygen is confused by decltype(auto)
436 template <typename OptionList>
437 template <typename TagList, typename F>
438 decltype(auto) Options<OptionList>::apply(F&& func) const {
440  std::forward<F>(func));
441 }
442 // \endcond
443 
444 template <typename OptionList>
447  ss << "\n==== Description of expected options:\n" << help_text_;
448  if (tmpl::size<opts_t>::value > 0) {
449  ss << "\n\nOptions:\n"
450  << tmpl::for_each<opts_t>(Options_detail::print{max_label_size_}).value;
451  } else {
452  ss << "\n\n<No options>\n";
453  }
454  return ss.str();
455 }
456 
457 template <typename OptionList>
458 template <typename T,
461  const typename T::type& t, const OptionContext& context) const {
462  static_assert(cpp17::is_same_v<decltype(T::lower_bound_on_size()), size_t>,
463  "lower_bound_on_size() is not a size_t.");
464  if (t.size() < T::lower_bound_on_size()) {
465  PARSE_ERROR(context,
466  "Value must have at least " << T::lower_bound_on_size()
467  << " entries, but " << t.size() << " were given.\n"
468  << help());
469  }
470 }
471 
472 template <typename OptionList>
473 template <typename T,
476  const typename T::type& t, const OptionContext& context) const {
477  static_assert(cpp17::is_same_v<decltype(T::upper_bound_on_size()), size_t>,
478  "upper_bound_on_size() is not a size_t.");
479  if (t.size() > T::upper_bound_on_size()) {
480  PARSE_ERROR(context,
481  "Value must have at most " << T::upper_bound_on_size()
482  << " entries, but " << t.size() << " were given.\n"
483  << help());
484  }
485 }
486 
487 template <typename OptionList>
488 template <typename T, Requires<Options_detail::has_lower_bound<T>::value>>
490  const typename T::type& t, const OptionContext& context) const {
491  static_assert(cpp17::is_same_v<decltype(T::lower_bound()), typename T::type>,
492  "Lower bound is not of the same type as the option.");
493  static_assert(not cpp17::is_same_v<typename T::type, bool>,
494  "Cannot set a lower bound for a bool.");
495  if (t < T::lower_bound()) {
496  PARSE_ERROR(context,
497  "Value " << t << " is below the lower bound of "
498  << T::lower_bound() << ".\n"
499  << help());
500  }
501 }
502 
503 template <typename OptionList>
504 template <typename T, Requires<Options_detail::has_upper_bound<T>::value>>
506  const typename T::type& t, const OptionContext& context) const {
507  static_assert(cpp17::is_same_v<decltype(T::upper_bound()), typename T::type>,
508  "Upper bound is not of the same type as the option.");
509  static_assert(not cpp17::is_same_v<typename T::type, bool>,
510  "Cannot set an upper bound for a bool.");
511  if (t > T::upper_bound()) {
512  PARSE_ERROR(context,
513  "Value " << t << " is above the upper bound of "
514  << T::upper_bound() << ".\n"
515  << help());
516  }
517 }
518 
519 template <typename OptionList>
521  const YAML::Node& options) const noexcept {
523  // At top level this would dump the entire input file, which is very
524  // verbose and not very informative. At lower levels the result
525  // should be much shorter and may actually give useful context for
526  // what part of the file is being parsed.
527  if (not context_.top_level) {
528  os << "\n==== Parsing the option string:\n" << options << "\n";
529  }
530  os << help();
531  return os.str();
532 }
533 
534 template <typename OptionList>
535 [[noreturn]] void Options<OptionList>::parser_error(
536  const YAML::Exception& e) const noexcept {
537  auto context = context_;
538  context.line = e.mark.line;
539  context.column = e.mark.column;
540  // Inline the top_level branch of PARSE_ERROR to avoid warning that
541  // the other branch would call terminate. (Parser errors can only
542  // be generated at top level.)
543  ERROR("\n" << context <<
544  "Unable to correctly parse the input file because of a syntax error.\n"
545  "This is often due to placing a suboption on the same line as an "
546  "option, e.g.:\nDomainCreator: CreateInterval:\n IsPeriodicIn: "
547  "[false]\n\nShould be:\nDomainCreator:\n CreateInterval:\n "
548  "IsPeriodicIn: [true]\n\nSee an example input file for help.");
549 }
550 
551 template <typename T>
552 T create_from_yaml<T>::create(const Option& options) {
553  Options<typename T::options> parser(T::help);
554  parser.parse(options);
555  return parser.template apply<typename T::options>([&options](auto&&... args) {
556  return make_overloader(
557  [&options](std::true_type /*meta*/, auto&&... args2) {
558  return T(std::move(args2)..., options.context());
559  },
560  [](std::false_type /*meta*/, auto&&... args2) {
561  return T(std::move(args2)...);
562  })(cpp17::is_constructible_t<T, decltype(std::move(args))...,
563  const OptionContext&>{},
564  std::move(args)...);
565  });
566 }
567 
568 namespace YAML {
569 template <typename T>
570 struct convert<Options_detail::CreateWrapper<T>> {
571  /* clang-tidy: non-const reference parameter */
572  static bool decode(const Node& node,
573  Options_detail::CreateWrapper<T>& rhs) { /* NOLINT */
574  OptionContext context;
575  context.top_level = false;
576  context.append("While creating a " + pretty_type::short_name<T>());
577  Option options(node, std::move(context));
578  rhs =
579  Options_detail::CreateWrapper<T>{create_from_yaml<T>::create(options)};
580  return true;
581  }
582 };
583 } // namespace YAML
584 
585 // yaml-cpp doesn't handle C++11 types yet
586 template <typename K, typename V, typename H, typename P>
587 struct create_from_yaml<std::unordered_map<K, V, H, P>> {
588  static std::unordered_map<K, V, H, P> create(const Option& options) {
589  // This shared_ptr stuff is a hack to work around the inability to
590  // extract keys from maps before C++17. Once we require C++17
591  // this function and the conversion code for maps in
592  // OptionsDetails.hpp can be updated to use the map `extract`
593  // method and the shared_ptr conversion below can be removed.
594  std::map<std::shared_ptr<K>, V> ordered =
595  options.parse_as<std::map<std::shared_ptr<K>, V>>();
597  for (auto& kv : ordered) {
598  result.emplace(std::move(*kv.first), std::move(kv.second));
599  }
600  return result;
601  }
602 };
603 
604 // This is more of the hack for pre-C++17 unordered_maps
605 template <typename T>
606 struct create_from_yaml<std::shared_ptr<T>> {
607  static std::shared_ptr<T> create(const Option& options) {
608  return std::make_shared<T>(options.parse_as<T>());
609  }
610 };
611 
612 // This is more of the hack for pre-C++17 unordered_maps
613 namespace Options_detail {
614 template <typename T>
615 struct yaml_type<std::shared_ptr<T>> {
616  static std::string value() noexcept { return yaml_type<T>::value(); }
617 };
618 } // namespace Options_detail
619 
620 #include "Options/Factory.hpp"
Defines helper functions for the standard library.
The type that options are passed around as. Contains YAML node data and an OptionContext.
Definition: Options.hpp:103
std::string help() const noexcept
Get the help string.
Definition: ParseOptions.hpp:445
#define ERROR(m)
prints an error message to the standard error stream and aborts the program.
Definition: Error.hpp:35
T parse_as() const
Convert to an object of type T.
Definition: ParseOptions.hpp:69
Overloader< Fs... > make_overloader(Fs... fs)
Create Overloader<Fs...>, see Overloader for details.
Definition: Overloader.hpp:109
Used by the parser to create an object. The default action is to parse options using T::options...
Definition: Options.hpp:143
Defines helpers for the Options<T> class.
Defines classes and functions for making classes creatable from input files.
constexpr auto create(Args &&... args)
Create a new DataBox.
Definition: DataBox.hpp:1259
#define ASSERT(a, m)
Assert that an expression should be true.
Definition: Assert.hpp:49
Defines the type alias Requires.
constexpr auto apply(F &&f, const DataBox< BoxTags > &box, Args &&... args)
Apply the function f with argument Tags TagsList from DataBox box
Definition: DataBox.hpp:1595
Defines class template Factory.
#define PARSE_ERROR(context, m)
Like ERROR("\n" << (context) << m), but instead throws an exception that will be caught in a higher l...
Definition: Options.hpp:66
void append_context(const std::string &context) noexcept
Append a line to the contained context.
Definition: ParseOptions.hpp:57
void set_node(YAML::Node node) noexcept
Sets the node and updates the context&#39;s mark to correspond to it.
Definition: ParseOptions.hpp:61
int column
File column number (0 based)
Definition: Options.hpp:42
const YAML::Node & node() const noexcept
Definition: ParseOptions.hpp:51
constexpr bool is_same_v
Variable template for is_same.
Definition: TypeTraits.hpp:221
int line
File line number (0 based)
Definition: Options.hpp:40
Class that handles parsing an input file.
Definition: ParseOptions.hpp:146
void parse(const std::string &options) noexcept
Parse a string to obtain options and their values.
Definition: ParseOptions.hpp:349
Definition: DataBoxTag.hpp:29
void append(const std::string &c) noexcept
Append a line to the context. Automatically appends a colon.
Definition: Options.hpp:45
Check if type T is a template specialization of U
Definition: TypeTraits.hpp:536
Option(YAML::Node node, OptionContext context={}) noexcept
Definition: ParseOptions.hpp:40
Defines macro ASSERT.
Contains a pretty_type library to write types in a "pretty" format.
Options(std::string help_text) noexcept
Definition: ParseOptions.hpp:316
T::type get() const
Get the value of the specified option.
Definition: ParseOptions.hpp:397
Information about the nested operations being performed by the parser, for use in printing errors...
Definition: Options.hpp:35
decltype(auto) apply(F &&func) const
Call a function with the specified options as arguments.
Holds details of the implementation of Options.
Definition: Options.hpp:80
Definition: ParseOptions.hpp:568
typename Requires_detail::requires_impl< B >::template_error_type_failed_to_meet_requirements_on_template_parameters Requires
Express requirements on the template parameters of a function or class, replaces std::enable_if_t ...
Definition: Requires.hpp:67
Defines macro ERROR.
void parse_file(const std::string &file_name) noexcept
Parse a file containing options.
Definition: ParseOptions.hpp:337
Defines type traits, some of which are future STL type_traits header.